15 Commits

Author SHA1 Message Date
734a27f2ca v0.5.3-pre.005-fix010 2026-08-14 14:40:36 +02:00
b6583bd9fb v0.5.3-pre.004 2026-08-12 14:31:48 +02:00
400ced4832 v0.5.3-pre.003 2026-08-12 11:00:59 +02:00
8448ad1079 v0.5.3-pre.002 2026-08-11 22:22:40 +02:00
01d78b5845 v0.5.3-pre.001 2026-08-11 20:04:41 +02:00
34674e8187 v0.5.2 2026-08-11 15:51:09 +02:00
279fd67cc0 v0.5.2-pre.007 2026-08-11 15:15:03 +02:00
56572cec40 v0.5.2-pre.006-fix-010 2026-08-11 12:03:35 +02:00
1ee1d0297d v0.5.2-pre.006-fix-004 2026-08-11 00:30:03 +02:00
066d969f5a v0.5.2-pre.006 2026-08-10 23:14:17 +02:00
e3acb8ceb1 v0.5.2-pre.005 2026-08-10 19:12:13 +02:00
b8e6751747 v0.5.2-pre.004 2026-08-10 16:43:19 +02:00
ef630e3123 v0.5.2-pre.003 2026-08-10 16:23:18 +02:00
eaa24e6f11 v0.5.2-pre.002 2026-08-10 15:06:38 +02:00
0bd5bbf1c4 v0.5.2-pre.001 2026-08-10 14:09:39 +02:00
486 changed files with 31338 additions and 7083 deletions

View File

@@ -17,6 +17,10 @@ KS_SECRET_POSTGRES_TEST_URL=postgres://solana:solana@localhost:5432/khadhroony_t
# Optional binary composition override for the desktop demo.
# KB_APP_DEMO_DESKTOP_CONFIG_PATH=config/kb-app-demo-desktop.default.config.json
# Optional backend-only password used by the Wallets demo when creating a native .kswallet.
# Never expose this value through HTML, TypeScript, Tauri payloads or logs.
# KB_SECRET_DEMO_WALLET_PASSWORD=replace-with-a-local-demo-password
# Optional direct logging configuration override. When absent, the desktop uses the path declared by its composition.
# KS_LOGGING_CONFIG_PATH=config/logging.config.json
# KS_LOGS_DIRECTORY=logs

View File

@@ -1,10 +1,45 @@
<!-- file: CHANGELOG.md -->
<!-- version: 17 -->
<!-- version: 19 -->
# CHANGELOG — khadhroony-bot3
Ce changelog décrit les évolutions fonctionnelles globales du projet. Il ne recense pas les prereleases, les correctifs `fix` ni le détail de chaque delta. Ces informations appartiennent aux changelogs des crates concernées.
## 0.5.2 — `ks-wallet` multi-wallet et stockage natif protégé
### Wallet natif et sécurité
- introduction du conteneur binaire versionné `<alias>.kswallet`, protégé par Argon2id v19 + XChaCha20-Poly1305 et publié atomiquement/no-clobber avec permissions privées ;
- séparation entre `WalletIdentity`, `WalletPassword`, `WalletFileHandle` et `UnlockedWallet`, sans getter public des bytes privés ;
- création, scan, lookup, inspection externe, unlock, signature et changement de mot de passe avec conservation exacte de la keypair/pubkey ;
- séparation des wallets persistants sous `wallets/` et des fixtures/keypairs temporaires sous `wallets/temporary/**` ;
- durcissement des erreurs, logs et `Debug` afin de ne pas exposer secret, password ou chemin local inutile.
### Migration et compatibilité
- migration non destructive du keypair JSON Solana legacy vers `.kswallet` ;
- inspection publique d'un fichier keypair externe sans import, avec retour limité à la pubkey et au format ;
- import/export testés de `SolanaCliJson` et du Base58 du keypair complet de 64 octets ;
- authentification obligatoire du mot de passe avant tout export secret ;
- refus des collisions d'alias/pubkey, symlinks, permissions ouvertes et écrasements silencieux ;
- exports opérateur du desktop confinés sous `data/wallets/` ;
- matrice documentée des formats Phantom, Solflare, Backpack, Trust Wallet et Base/Coinbase, avec Base58 comme seul adaptateur tiers de cette version.
### Configuration, scénarios et desktop
- `wallet_alias` optionnel/nullable par profil dans `ks-config`, sans mot de passe ni matériau secret ;
- résolution explicite d'un signer `.kswallet` dans les démos Devnet System/SPL/Metadata, sans fallback temporaire lorsqu'un alias persistant est choisi ;
- sélection session-only du wallet d'exécution depuis la fenêtre Wallets, indépendante de la configuration persistée et authentifiée côté backend ;
- inventaire/création/inspection/import/export des wallets et explorateur public SOL/tokens/historique par profil RPC dans `kb-app-demo-desktop`, via DTO applicatifs sûrs ;
- ajout de `ks-wallet-demo-scenarios`, consommateur externe réutilisable de `ks-wallet`, avec validation ordonnée A → B → rejet de A → ouverture/signature avec B → restauration vers A.
### Validation et suite
- `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --all-targets`, audit workspace et `cargo test --workspace` validés sur la prerelease de clôture `0.5.2-pre.007` après réconciliation documentaire finale ;
- validation runtime du signer persistant dans les démos Devnet avec `persistence="persistent"` et exports JSON/Base58 sous `data/wallets/` ;
- l'inventaire/migration des fixtures historiques et la régression de chargement de fixture Token-2022 sont explicitement reportés à `0.5.4` ;
- `0.5.3` est consacré à l'audit et la normalisation de `ks-store`.
## 0.5.1 — namespaces Khadhroony Solana et configuration sûre
### Namespace et ownership

View File

@@ -1,5 +1,5 @@
# file: Cargo.toml
# version: 54
# version: 74
[workspace]
resolver = "3"
@@ -14,11 +14,12 @@ members = [
"ks-onchain-transport",
"ks-store",
"ks-wallet",
"ks-wallet-demo-scenarios",
"kb-app-demo-desktop",
]
[workspace.package]
version = "0.5.1"
version = "0.5.3-pre.5"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3"
@@ -26,7 +27,7 @@ authors = ["SinuS von SifriduS <sinus@sasedev.net>"]
publish = false
[workspace.dependencies]
dotenvy = {version = "^0.15", features = ["clap", "cli"]}
dotenvy = { version = "^0.15", features = ["clap", "cli"] }
argon2 = { version = "^0.5", features = ["std", "zeroize"] }
async-trait = { version = "^0.1", features = [] }
base64 = { version = "^0.23", features = [] }
@@ -34,10 +35,11 @@ bytemuck = { version = "^1.25", features = ["derive"] }
borsh_0_10 = { package = "borsh", version = "^0.10" }
borsh = { version = "^1.7", features = ["ascii", "bson", "bytes", "default", "derive", "de_strict_order", "borsh-derive", "indexmap", "std", "rc"] }
bs58 = { version = "^0.5", features = ["cb58", "check", "default"] }
chacha20poly1305 = { version = "^0.11", features = ["std", "stream"] }
chacha20poly1305 = { version = "^0.11", features = ["getrandom", "rand_core", "zeroize"] }
chrono = { version = "^0.4", features = ["serde"] }
fs2 = { version = "^0.4", features = [] }
futures-util = { version = "^0.3", features = ["default", "futures-sink"] }
getrandom = { version = "^0.4", features = ["std", "sys_rng"] }
jsonschema = { version = "^0.49", features = [] }
mpl-token-metadata = { version = "^5.1", features = ["serde"] }
rand = { version = "^0.10", features = ["chacha", "serde", "std", "sys_rng"] }
@@ -87,7 +89,7 @@ spl-token-metadata-interface = { version = "^1.0", features = ["serde", "serde-t
spl-token-group-interface = { version = "^0.7", features = [] }
spl-token-interface = { version = "^3.0", features = [] }
spl-token-2022-interface = { version = "^3.1", features = ["serde"] }
sqlx = { version = "^0.9", features = ["bigdecimal", "chrono", "default", "derive", "json", "mysql", "postgres", "runtime-tokio", "sqlite", "tls-rustls", "uuid"] }
sqlx = { version = "^0.9", features = ["all-databases", "bigdecimal", "chrono", "default", "derive", "json", "runtime-tokio", "tls-rustls", "uuid"] }
tauri = { version = "^2.11", features = ["default", "tray-icon"] }
tauri-build = { version = "^2.6", features = [] }
tauri-plugin-tracing = { version = "^0.3", default-features = false, features = [] }

View File

@@ -1,5 +1,5 @@
<!-- file: README.md -->
<!-- version: 24 -->
<!-- version: 26 -->
# Khadhroony Bot3
@@ -7,7 +7,7 @@
## Architecture
Le workspace contient onze crates :
Le workspace contient douze crates :
- `ks-core` : erreurs et primitives transversales ;
- `ks-config` : chargement et validation de la configuration ;
@@ -19,12 +19,15 @@ Le workspace contient onze crates :
- `ks-onchain-transport` : transports RPC HTTP et WebSocket ;
- `ks-store` : contrats de stockage et adaptateur PostgreSQL ;
- `ks-wallet` : frontière de wallet et signataires ;
- `ks-wallet-demo-scenarios` : scénarios réutilisables de validation du cycle de vie et des formats wallet ;
- `kb-app-demo-desktop` : application Tauri de démonstration et validation opérateur.
La série `0.5.x` prépare la séparation explicite entre le domaine applicatif `khadhroony-bot` et les bibliothèques généralistes `khadhroony-solana`. Depuis `0.5.1`, les dix crates généralistes utilisent les noms `ks-*` / `ks_*`, tandis que `kb-app-demo-desktop` conserve son nom et reste une application du workspace Bot. Le workspace, le dépôt et le répertoire racine restent nommés `khadhroony-bot3` pendant cette migration et ne doivent pas être renommés avant `1.0` ou une version ultérieure explicitement dédiée. La politique de migration est définie dans [`docs/decisions/KHADHROONY_SOLANA_NAMESPACE_POLICY.md`](docs/decisions/KHADHROONY_SOLANA_NAMESPACE_POLICY.md).
La série `0.5.x` prépare la séparation explicite entre le domaine applicatif `khadhroony-bot` et les bibliothèques généralistes `khadhroony-solana`. Depuis `0.5.1`, les dix crates généralistes issues de la migration utilisent les noms `ks-*` / `ks_*`. `0.5.2` ajoute `ks-wallet-demo-scenarios` comme onzième crate généraliste, tandis que `kb-app-demo-desktop` conserve son nom et reste une application du workspace Bot. Le workspace, le dépôt et le répertoire racine restent nommés `khadhroony-bot3` pendant cette migration et ne doivent pas être renommés avant `1.0` ou une version ultérieure explicitement dédiée. La politique de migration est définie dans [`docs/decisions/KHADHROONY_SOLANA_NAMESPACE_POLICY.md`](docs/decisions/KHADHROONY_SOLANA_NAMESPACE_POLICY.md).
Depuis `0.5.1`, les contrats de configuration source/runtime susceptibles de contenir des secrets restent backend-only et ne sont ni sérialisables ni `Debug` par défaut. Les crates `ks-config` et `ks-lib` ne génèrent plus de bindings TS-RS ; les surfaces TypeScript/Tauri sont possédées par les applications via des DTO explicites.
Depuis `0.5.2`, `ks-wallet` gère plusieurs wallets persistants `.kswallet` protégés par mot de passe, la migration/import/export de keypairs Solana et une capacité de signature sans exposition du secret. Les validations réutilisables du cycle de mot de passe sont isolées dans `ks-wallet-demo-scenarios`. Le guide opérationnel est [`docs/guides/WALLETS.md`](docs/guides/WALLETS.md).
Références :
- [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) ;

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md -->
<!-- version: 38 -->
<!-- version: 44 -->
# ROADMAP — khadhroony-bot3
@@ -96,30 +96,49 @@ Les préfixes SQL historiques `kb_sol_*` restent volontairement inchangés jusqu
### 0.5.2 — `ks-wallet`
- auditer et normaliser les frontières identité, secret, déverrouillage et signature ;
- consolider les besoins multi-wallets et multi-profils ;
- revoir import/export, chiffrement, verrouillage, sauvegarde et restauration lorsque ces capacités sont retenues ;
- définir explicitement la migration du format keypair JSON `0.4.8` si un nouveau format persistant est adopté ;
- empêcher toute fuite de secret vers la configuration, les logs, Tauri ou les DTO publics ;
- conserver une API de signature réutilisable par les futurs exécuteurs sans couplage à un protocole particulier.
Version clôturée fonctionnellement : la frontière wallet Solana générale est désormais séparée entre identité publique, secret persistant, mot de passe et capacité de signature.
### 0.5.3 — audit et normalisation de `ks-store`
- plusieurs `.kswallet` persistants sont découverts et sélectionnés par alias dans `wallets/`, tandis que les fixtures/keypairs temporaires restent sous `wallets/temporary/**` ;
- le format natif v1 est binaire, versionné, protégé par Argon2id v19 + XChaCha20-Poly1305 et publié atomiquement/no-clobber avec permissions privées ;
- `WalletPassword` et `UnlockedWallet` bornent respectivement l'acquisition du mot de passe et la capacité de signature, tandis que `ks-lib` continue de dépendre uniquement d'une capacité `Signer` ;
- le changement de mot de passe conserve exactement la même keypair/pubkey et son cycle A → B → rejet de A → B → A est validé par `ks-wallet-demo-scenarios` ;
- le legacy Solana CLI JSON peut être inspecté ou migré sans destruction vers `.kswallet` ;
- `SolanaCliJson` et `SolanaPrivateKeyBase58` disposent d'import/export testés, tout export secret exigeant un mot de passe valide ;
- les exports opérateur issus du desktop sont confinés sous `data/wallets/` ;
- `ks-config` ne transporte qu'un `wallet_alias` optionnel et aucun secret ;
- `kb-app-demo-desktop` peut sélectionner en session un `.kswallet` par profil Devnet, prioritaire sur l'alias configuré, et les logs runtime ont confirmé `persistence="persistent"` avec le signer choisi ;
- les chemins locaux inutiles sont retirés des logs/erreurs/`Debug` du store legacy et les collisions concurrentes de création native sont couvertes ;
- les détails normatifs restent dans `docs/NATIVE_FORMAT.md`, `docs/WALLET_FORMAT_COMPATIBILITY.md`, `docs/guides/WALLETS.md` et `docs/validation/V0_5_2_WALLET_VALIDATION_REPORT.md`.
- auditer DTO, entités, repositories, migrations, index, requêtes, idempotence et provenance ;
- vérifier les champs temporels et séparer explicitement le temps blockchain du temps de persistance ;
- distinguer notamment `slot`, `block_time` ou timestamp de transaction des timestamps d'acquisition, d'insertion et de mise à jour en base ;
- normaliser la structure de `ks-store` avant l'arrivée des matérialisations trading ;
- migrer les tables Solana du préfixe historique `kb_sol_*` vers `k_sol_*` ;
- réserver `kb_*` aux éventuelles tables dont la responsabilité appartient réellement au domaine applicatif Bot ;
- profiter de la fenêtre pré-`0.6.x` pour reconstruire ou migrer proprement les bases Devnet/Mainnet au lieu de conserver des alias historiques sans valeur durable ;
- préparer les index et contrats nécessaires aux faits de création de pools, liquidité, réserves, swaps, évolution de prix et séries temporelles ;
- préparer le terrain pour routing, multi-pools et analyse croisée sans introduire prématurément des tables spécifiques à un DEX ;
- permettre des extractions et analyses efficaces pour les futurs consommateurs de trading et de recherche historique.
### 0.5.3 — audit, gel de schéma et abstraction de `ks-store`
Aucune table trading spécifique à Meteora, Raydium, Pump, Orca ou Jupiter n'est inventée avant définition des faits stables produits par les futurs matérialisateurs.
- conserver `ks-store` comme frontière de persistance **généraliste Solana** pour les faits matérialisés par `ks-lib`; le trading est la priorité court terme, pas le périmètre exclusif du stockage ;
- migrer les tables Solana du préfixe historique `kb_sol_*` vers `k_sol_*` et réserver `kb_*` au domaine réellement Bot ;
- auditer DTO, models, repositories, migrations, index, requêtes, idempotence et provenance ;
- vérifier table par table que le contrat structurel est complet **avant gel** : colonnes, types, nullabilité, clés, contraintes, provenance, replay et temporalités ;
- corriger pendant `0.5.3` les omissions structurelles déjà identifiées, notamment la conservation du temps on-chain/`block_time` lorsqu'il existe et qu'il est pertinent ;
- considérer après clôture les tables stabilisées comme des contrats à ne plus remodeler : les nouvelles fonctionnalités doivent préférer de nouvelles tables reliées aux anciennes ;
- distinguer explicitement `slot`, `block_time` ou timestamp de transaction des timestamps d'acquisition, d'insertion, de mise à jour et de matérialisation ;
- mettre `ks-store` aux conventions du workspace avant l'extension massive des matérialisations ;
- supprimer des autres crates toute dépendance aux fonctions/modules/types PostgreSQL : elles consomment uniquement les DTO/models et opérations génériques de `ks-store` ;
- encapsuler dans `ks-store` la sélection du backend, l'interprétation des paramètres de connexion, la création des pools/connexions et les implémentations spécifiques ;
- conserver PostgreSQL comme backend opérationnel actuel tout en définissant une frontière réimplémentable plus tard avec MySQL, SQLite, RocksDB, Oracle ou un autre moteur sans modifier les consommateurs ;
- profiter de la fenêtre pré-`0.6.x` pour reconstruire ou migrer proprement les bases Devnet/Mainnet avant de figer les contrats ;
- auditer les modèles canoniques par **nature de fait** : core/transactions, comptes/balances/lifecycle, token/SPL, metadata, staking/vote, administration/autorités, programmes, audit/compliance, risques, trading/marchés et autres domaines justifiés par les Program IDs couverts ;
- faire des matérialiseurs la frontière de normalisation des comptes/événements/champs/unités spécifiques à chaque programme vers les DTO/models canoniques de `ks-store` ;
- pour la priorité trading, préparer les contrats canoniques nécessaires aux marchés/paires, pools de liquidité/AMM, order books lorsque leurs invariants ne peuvent pas être unifiés proprement, réserves, positions, swaps/trades, prix, volumes et séries temporelles ;
- autoriser de nouvelles tables génériques futures, par exemple candles/OHLC ou des tables dédiées par concept `liquidity_pool` / `order_book`, lorsque leur contrat est suffisamment durable ;
- conserver le Program ID/protocole/version comme provenance sans en faire le propriétaire par défaut du schéma ;
- permettre des extractions et analyses efficaces pour les futurs consommateurs de trading, metadata, recherche historique et autres usages Solana.
Le schéma ne doit pas être organisé en familles de tables par protocole ou Program ID. `meteora_*`, `raydium_*`, `pump_*`, `orca_*`, `jupiter_*`, etc. illustrent ce qui doit être évité : les différences protocolaires sont normalisées par les matérialiseurs. Lorsqu'une séparation est nécessaire, elle est définie par un concept/fait réellement distinct et durable, qu'il concerne le trading, les metadata, le staking ou un autre domaine.
### 0.5.4 — scénarios, exécuteurs et validations
- réconcilier le cas de decode CPI Metaplex Token Metadata observé en `0.5.3-pre.004` (`transfer`, chemin `2/0`, diagnostic `transfer account 9 lacks required signer/writable privileges`) afin de déterminer si le contrat daccounts du décodeur doit accepter les privilèges runtime observés ; ce point relève du décodeur/scénario, pas de `ks-store` ;
- diagnostiquer et corriger la régression observée dans `demo_execution_spl_token_2022` lors du chargement de fixture (`unable to read configured Token-2022 fixture`) après séparation des racines wallet/fixtures ;
- inventorier non destructivement les keypairs historiques sous `wallets/temporary/**`, extraire les pubkeys des formats supportés et préparer une migration sélective des seuls signers devant devenir persistants ;
- garder toute classification on-chain éventuelle chez le consommateur/scénario, à partir de la pubkey et du profil réseau, sans dépendance `ks-wallet -> ks-onchain-transport` ni rôle métier inventé depuis les bytes du secret ;
- auditer les scénarios encore déclarés ou assemblés directement dans `kb-app-demo-desktop` ;
- déplacer toute logique de scénario réutilisable vers `ks-pipeline-demo-scenarios` et laisser le desktop comme adaptateur UI/Tauri ;
- comparer les décodeurs existants aux exécuteurs disponibles et identifier les exécuteurs réellement manquants ;

View File

@@ -1,5 +1,5 @@
<!-- file: config/README.md -->
<!-- version: 25 -->
<!-- version: 27 -->
# Configuration locale
@@ -90,6 +90,7 @@ Actuellement :
- `logging.config.json.logs_directory` vaut `${KS_LOGS_DIRECTORY:-logs}` ;
- `wallet.config.json.wallets_directory` vaut `${KS_WALLETS_DIRECTORY:-wallets}`.
- un profil wallet peut sélectionner un wallet natif persistant avec `wallet_alias`; le champ est optionnel/non sensible et ne contient jamais de mot de passe ni de matériau privé.
Ces valeurs peuvent être remplacées par l'environnement ou `.env` sans dupliquer un chemin dans tous les profils.
@@ -110,7 +111,7 @@ Les defaults portent les timeouts, capacités de channels et `auto_reconnect`. U
## Store
`store.config.json` possède la sélection et les paramètres PostgreSQL/SQLite. Le split reste structurel : aucune migration SQL n'est introduite par `0.5.1`.
`store.config.json` possède la sélection du backend et un objet `backend_options` propre au profil sélectionné. `ks-config` conserve ce bloc opaque après composition/résolution ; `ks-store` seul interprète les champs PostgreSQL actuels (`url`, pool, timeout, auto-initialisation). Ajouter un futur moteur ne doit donc pas ajouter ses paramètres au contrat runtime commun tant qu'il n'est pas sélectionné.
## Frontière source/runtime/public/diagnostic

View File

@@ -34,14 +34,34 @@
}
],
"target_filters": [
{
"target": "h2",
"level": "warn"
},
{
"target": "hyper",
"level": "warn"
},
{
"target": "hyper_util",
"level": "warn"
},
{
"target": "reqwest",
"level": "warn"
},
{
"target": "rustls",
"level": "warn"
},
{
"target": "rustls_platform_verifier",
"level": "warn"
},
{
"target": "sqlx",
"level": "warn"
},
{
"target": "tao",
"level": "warn"

View File

@@ -6,19 +6,11 @@
"database": {
"enabled": true,
"backend": "postgres",
"postgres": {
"backend_options": {
"url": "${KS_SECRET_POSTGRES_DEVNET_URL}",
"max_connections": 8,
"connect_timeout_ms": 5000,
"auto_initialize_schema": true
},
"sqlite": {
"path": "data/local.sqlite",
"create_if_missing": true,
"busy_timeout_ms": 5000,
"max_connections": 1,
"auto_initialize_schema": true,
"use_wal": true
}
}
}

View File

@@ -67,6 +67,7 @@
"get_recent_performance_samples",
"get_recent_prioritization_fees",
"get_signature_statuses",
"get_signatures_for_address",
"get_slot",
"get_slot_leader",
"get_slot_leaders",

View File

@@ -6,9 +6,10 @@
"name": "local_devnet",
"directory": "temporary/local_devnet",
"cluster": "devnet",
"temporary_wallet_enabled": true,
"temporary_wallet_alias": "local-devnet-operator",
"temporary_wallet_persist": true
"wallet_alias": "example-operator",
"temporary_wallet_enabled": false,
"temporary_wallet_alias": "example-temporary-disabled",
"temporary_wallet_persist": false
}
]
}

View File

@@ -1168,14 +1168,34 @@
}
],
"target_filters": [
{
"target": "h2",
"level": "warn"
},
{
"target": "hyper",
"level": "warn"
},
{
"target": "hyper_util",
"level": "warn"
},
{
"target": "reqwest",
"level": "warn"
},
{
"target": "rustls",
"level": "warn"
},
{
"target": "rustls_platform_verifier",
"level": "warn"
},
{
"target": "sqlx",
"level": "warn"
},
{
"target": "tao",
"level": "warn"
@@ -2352,14 +2372,34 @@
}
],
"target_filters": [
{
"target": "h2",
"level": "warn"
},
{
"target": "hyper",
"level": "warn"
},
{
"target": "hyper_util",
"level": "warn"
},
{
"target": "reqwest",
"level": "warn"
},
{
"target": "rustls",
"level": "warn"
},
{
"target": "rustls_platform_verifier",
"level": "warn"
},
{
"target": "sqlx",
"level": "warn"
},
{
"target": "tao",
"level": "warn"
@@ -3536,14 +3576,34 @@
}
],
"target_filters": [
{
"target": "h2",
"level": "warn"
},
{
"target": "hyper",
"level": "warn"
},
{
"target": "hyper_util",
"level": "warn"
},
{
"target": "reqwest",
"level": "warn"
},
{
"target": "rustls",
"level": "warn"
},
{
"target": "rustls_platform_verifier",
"level": "warn"
},
{
"target": "sqlx",
"level": "warn"
},
{
"target": "tao",
"level": "warn"

View File

@@ -59,85 +59,17 @@
"required": [
"enabled",
"backend",
"postgres",
"sqlite"
"backend_options"
],
"properties": {
"enabled": {
"type": "boolean"
},
"backend": {
"type": "string",
"enum": [
"postgres",
"sqlite"
]
},
"postgres": {
"$ref": "#/$defs/postgres"
},
"sqlite": {
"$ref": "#/$defs/sqlite"
}
}
},
"postgres": {
"type": "object",
"additionalProperties": false,
"required": [
"url",
"max_connections",
"connect_timeout_ms",
"auto_initialize_schema"
],
"properties": {
"url": {
"$ref": "#/$defs/non_empty_string"
},
"max_connections": {
"type": "integer",
"minimum": 1
},
"connect_timeout_ms": {
"type": "integer",
"minimum": 1
},
"auto_initialize_schema": {
"type": "boolean"
}
}
},
"sqlite": {
"type": "object",
"additionalProperties": false,
"required": [
"path",
"create_if_missing",
"busy_timeout_ms",
"max_connections",
"auto_initialize_schema",
"use_wal"
],
"properties": {
"path": {
"$ref": "#/$defs/non_empty_string"
},
"create_if_missing": {
"type": "boolean"
},
"busy_timeout_ms": {
"type": "integer",
"minimum": 1
},
"max_connections": {
"type": "integer",
"minimum": 1
},
"auto_initialize_schema": {
"type": "boolean"
},
"use_wal": {
"type": "boolean"
"backend_options": {
"type": "object"
}
}
},
@@ -465,6 +397,7 @@
"additionalProperties": false,
"required": [
"wallet_dir",
"temporary_wallet_dir",
"cluster",
"temporary_wallet_enabled",
"temporary_wallet_alias",
@@ -474,9 +407,21 @@
"wallet_dir": {
"$ref": "#/$defs/non_empty_string"
},
"temporary_wallet_dir": {
"$ref": "#/$defs/non_empty_string"
},
"cluster": {
"$ref": "#/$defs/non_empty_string"
},
"wallet_alias": {
"type": [
"string",
"null"
],
"minLength": 1,
"maxLength": 64,
"pattern": "^[A-Za-z0-9][A-Za-z0-9_-]*$"
},
"temporary_wallet_enabled": {
"type": "boolean"
},

View File

@@ -31,85 +31,17 @@
"required": [
"enabled",
"backend",
"postgres",
"sqlite"
"backend_options"
],
"properties": {
"enabled": {
"type": "boolean"
},
"backend": {
"type": "string",
"enum": [
"postgres",
"sqlite"
]
},
"postgres": {
"$ref": "#/$defs/postgres"
},
"sqlite": {
"$ref": "#/$defs/sqlite"
}
}
},
"postgres": {
"type": "object",
"additionalProperties": false,
"required": [
"url",
"max_connections",
"connect_timeout_ms",
"auto_initialize_schema"
],
"properties": {
"url": {
"$ref": "#/$defs/non_empty_string"
},
"max_connections": {
"type": "integer",
"minimum": 1
},
"connect_timeout_ms": {
"type": "integer",
"minimum": 1
},
"auto_initialize_schema": {
"type": "boolean"
}
}
},
"sqlite": {
"type": "object",
"additionalProperties": false,
"required": [
"path",
"create_if_missing",
"busy_timeout_ms",
"max_connections",
"auto_initialize_schema",
"use_wal"
],
"properties": {
"path": {
"$ref": "#/$defs/non_empty_string"
},
"create_if_missing": {
"type": "boolean"
},
"busy_timeout_ms": {
"type": "integer",
"minimum": 1
},
"max_connections": {
"type": "integer",
"minimum": 1
},
"auto_initialize_schema": {
"type": "boolean"
},
"use_wal": {
"type": "boolean"
"backend_options": {
"type": "object"
}
}
},

View File

@@ -11,7 +11,8 @@
],
"properties": {
"wallets_directory": {
"$ref": "#/$defs/non_empty_string"
"$ref": "#/$defs/non_empty_string",
"description": "Root directory dedicated to persistent native .kswallet containers."
},
"default_profile": {
"$ref": "#/$defs/non_empty_string"
@@ -45,11 +46,21 @@
"$ref": "#/$defs/non_empty_string"
},
"directory": {
"$ref": "#/$defs/non_empty_string"
"$ref": "#/$defs/non_empty_string",
"description": "Profile-specific temporary wallet and fixture directory, relative to wallets_directory unless absolute."
},
"cluster": {
"$ref": "#/$defs/non_empty_string"
},
"wallet_alias": {
"type": [
"string",
"null"
],
"minLength": 1,
"maxLength": 64,
"pattern": "^[A-Za-z0-9][A-Za-z0-9_-]*$"
},
"temporary_wallet_enabled": {
"type": "boolean"
},

View File

@@ -6,19 +6,11 @@
"database": {
"enabled": true,
"backend": "postgres",
"postgres": {
"backend_options": {
"url": "${KS_SECRET_POSTGRES_DEVNET_URL}",
"max_connections": 8,
"connect_timeout_ms": 5000,
"auto_initialize_schema": true
},
"sqlite": {
"path": "data/local.sqlite",
"create_if_missing": true,
"busy_timeout_ms": 5000,
"max_connections": 1,
"auto_initialize_schema": true,
"use_wal": true
}
}
},
@@ -27,19 +19,11 @@
"database": {
"enabled": true,
"backend": "postgres",
"postgres": {
"backend_options": {
"url": "${KS_SECRET_POSTGRES_MAINNET_URL}",
"max_connections": 16,
"connect_timeout_ms": 5000,
"auto_initialize_schema": true
},
"sqlite": {
"path": "data/mainnet-import.sqlite",
"create_if_missing": true,
"busy_timeout_ms": 5000,
"max_connections": 1,
"auto_initialize_schema": true,
"use_wal": true
}
}
},
@@ -48,19 +32,11 @@
"database": {
"enabled": true,
"backend": "postgres",
"postgres": {
"backend_options": {
"url": "${KS_SECRET_POSTGRES_MAINNET_URL}",
"max_connections": 16,
"connect_timeout_ms": 5000,
"auto_initialize_schema": true
},
"sqlite": {
"path": "data/mainnet.sqlite",
"create_if_missing": true,
"busy_timeout_ms": 5000,
"max_connections": 1,
"auto_initialize_schema": true,
"use_wal": true
}
}
}

View File

@@ -67,6 +67,7 @@
"get_recent_performance_samples",
"get_recent_prioritization_fees",
"get_signature_statuses",
"get_signatures_for_address",
"get_slot",
"get_slot_leader",
"get_slot_leaders",

View File

@@ -6,6 +6,7 @@
"name": "local_devnet",
"directory": "temporary/local_devnet",
"cluster": "devnet",
"wallet_alias": null,
"temporary_wallet_enabled": true,
"temporary_wallet_alias": "local-devnet-operator",
"temporary_wallet_persist": true
@@ -14,6 +15,7 @@
"name": "mainnet_research",
"directory": "temporary/mainnet_research",
"cluster": "mainnet-beta",
"wallet_alias": null,
"temporary_wallet_enabled": false,
"temporary_wallet_alias": "mainnet_research-disabled",
"temporary_wallet_persist": false
@@ -22,6 +24,7 @@
"name": "mainnet",
"directory": "temporary/mainnet",
"cluster": "mainnet-beta",
"wallet_alias": null,
"temporary_wallet_enabled": false,
"temporary_wallet_alias": "mainnet-disabled",
"temporary_wallet_persist": false

View File

@@ -1,5 +1,5 @@
<!-- file: docs/DEVNET_EXECUTION_GUIDE.md -->
<!-- version: 26 -->
<!-- version: 28 -->
# Guide dexécution Devnet
@@ -30,7 +30,7 @@ Les commandes réutilisables sont centralisées en section 4. Chaque scénario r
- le wallet temporaire persistant nest jamais copié dans les logs ou les preuves ;
- le financement est réalisé par un faucet Web Devnet, pas par `solana airdrop`.
Lorsque `database.postgres.auto_initialize_schema` vaut `true`, louverture dune fenêtre Devnet crée les tables manquantes. Lorsquil vaut `false`, le schéma doit être préparé avant louverture.
Depuis `0.5.3-pre.002`, le desktop ne lit plus une section PostgreSQL concrète. Le profil sélectionne `database.backend` et transporte ses paramètres dans `database.backend_options`, interprétés uniquement par `ks-store`. Lorsque loption backend `auto_initialize_schema` vaut `true`, louverture persistante du `Store` peut initialiser le schéma du backend actif. Lorsquelle vaut `false`, le modèle de stockage doit déjà être disponible.
## 3. Organisation des terminaux
@@ -81,7 +81,7 @@ Attendre avant tout scénario :
- le démarrage de Vite ;
- le lancement du binaire `kb-app-demo-desktop` ;
- louverture de la fenêtre ;
- le message PostgreSQL indiquant que les 13 tables sont prêtes ;
- le résumé store indiquant que le backend actif est joignable et que les modèles logiques attendus sont prêts ;
- la résolution correcte du profil `local_devnet`.
Ne pas exécuter les commandes `C06` ou `C07` dans ce terminal.
@@ -103,6 +103,8 @@ Arrêter avec `Ctrl+C`, puis relancer `T01`.
cd ~/Projects/khadhroony-bot3 && set -o pipefail && export KS_DEVNET_PROFILE="local_devnet" && export KS_DEVNET_RPC_URL="https://api.devnet.solana.com" && export KS_DEVNET_VALIDATION_DIR="/tmp/devnet-validation/current" && export KS_DEVNET_WALLET="$PWD/wallets/temporary/local_devnet/local-devnet-operator.json" && mkdir -p "$KS_DEVNET_VALIDATION_DIR" && printf 'Profil : %s\n' "$KS_DEVNET_PROFILE" && printf 'RPC Devnet : %s\n' "$KS_DEVNET_RPC_URL" && printf 'Preuves : %s\n' "$KS_DEVNET_VALIDATION_DIR" && printf 'Wallet : %s\n' "$KS_DEVNET_WALLET";
```
`KS_DEVNET_WALLET` reste ici un **fichier Solana CLI JSON explicite** utilisé par les commandes externes historiques. Un fichier natif `.kswallet` ne doit jamais être fourni directement à `solana-keygen`, `solana` ou `spl-token`. Lorsqu'un opérateur part d'un wallet natif, il doit produire volontairement un export `SolanaCliJson` via `ks-wallet`, l'utiliser pour la campagne bornée, puis gérer explicitement ce fichier privé. Aucun export automatique n'est déclenché par les scénarios.
### C02 — Capturer les versions CLI
```bash
@@ -246,6 +248,8 @@ cargo run -p ks-pipeline-demo-scenarios --bin ks-pipeline-demo-scenarios-cli --
Lorsque `--rpc-url` et `--wallet` sont omis, la commande utilise respectivement `KS_DEVNET_RPC_URL` et `KS_DEVNET_WALLET`. Lorsque `--wallet-dir` est omis, elle utilise le répertoire parent du wallet.
`--wallet` / `KS_DEVNET_WALLET` acceptent uniquement le fichier keypair compatible Solana CLI. La commande refuse explicitement l'extension `.kswallet` afin d'empêcher qu'un conteneur natif protégé soit traité comme un keypair CLI.
La commande :
- crée silencieusement les keypairs manquants avec des permissions `0600` ;
@@ -326,7 +330,7 @@ Un scénario nest validé que si le replay ne produit ni échec fonctionnel n
1. Dans le terminal B, exécuter `C01`, `C02`, `C03` et `C04`.
2. Dans le terminal B, exécuter `C05` pour repartir dune base propre.
3. Dans le terminal A, exécuter `T01`.
4. Attendre la création des 13 tables et la disponibilité de la fenêtre.
4. Attendre que le résumé store confirme la disponibilité des modèles logiques attendus, puis la disponibilité de la fenêtre.
5. Dans la fenêtre Configuration, vérifier que `local_devnet` utilise la base Devnet.
6. Dans le terminal B, exécuter le contrôle de tables de `C05`.
7. Financer la pubkey par un faucet Web Devnet si le solde est insuffisant.

215
docs/NATIVE_FORMAT.md Normal file
View File

@@ -0,0 +1,215 @@
<!-- file: docs/NATIVE_FORMAT.md -->
<!-- version: 4 -->
# Format natif `.kswallet` — version 1
## Statut
Ce document fixe le codec binaire de la version `1` introduit par `0.5.2-pre.003` et activé pour les wallets persistants protégés dans `0.5.2-pre.004`.
`pre.004` conserve exactement ce layout et implémente l'encodage, la publication atomique, la dérivation Argon2id, le chiffrement/déchiffrement XChaCha20-Poly1305, l'ouverture authentifiée et le changement de mot de passe.
Un fichier natif porte le nom :
```text
<alias>.kswallet
```
Tous les entiers multi-octets sont encodés en **little-endian**.
## Objectifs du format v1
Le conteneur doit :
- être identifiable sans heuristique ;
- distinguer clairement sa version et ses algorithmes ;
- exposer uniquement l'identité publique nécessaire avant unlock ;
- borner toutes les longueurs et paramètres avant allocation ou KDF ;
- authentifier avec l'AEAD toutes les métadonnées influençant identité et déchiffrement ;
- permettre une publication atomique sans écrasement silencieux ;
- rester incompatible par construction avec le JSON keypair Solana legacy.
Le caractère binaire du format ne constitue pas une protection cryptographique. La confidentialité du secret est fournie par l'AEAD après dérivation d'une clé depuis le mot de passe.
## Layout binaire v1
Le header fixe occupe **72 octets**.
| Offset | Taille | Champ | Contrat v1 |
|------------:|---------:|--------------------|------------------------------------------------|
| `0` | `8` | magic | ASCII `KSWALLET` |
| `8` | `2` | format version | `1` |
| `10` | `2` | header length | `72 + alias_length` |
| `12` | `2` | flags | `0` |
| `14` | `1` | KDF id | `1` = Argon2id |
| `15` | `1` | KDF version | `0x13` = Argon2 v19 |
| `16` | `1` | AEAD id | `1` = XChaCha20-Poly1305 |
| `17` | `1` | salt length | `16` |
| `18` | `1` | nonce length | `24` |
| `19` | `1` | alias length | `1..64` |
| `20` | `4` | reserved | quatre octets `0` |
| `24` | `4` | Argon2 memory KiB | borné, voir ci-dessous |
| `28` | `4` | Argon2 passes | borné, voir ci-dessous |
| `32` | `4` | Argon2 parallelism | borné, voir ci-dessous |
| `36` | `4` | ciphertext length | `80` |
| `40` | `32` | Solana pubkey | 32 octets bruts |
| `72` | variable | alias | ASCII validé par `WalletAlias` |
| après alias | `16` | salt | sel Argon2id |
| après salt | `24` | nonce | nonce XChaCha20-Poly1305 |
| après nonce | `80` | ciphertext | keypair 64 octets protégé + tag AEAD 16 octets |
Aucun octet supplémentaire n'est accepté après le ciphertext déclaré.
## Identité publique et payload secret
L'alias et la pubkey sont disponibles avant unlock afin de permettre :
- le scan et le lookup ;
- l'affichage d'une identité non sensible ;
- la détection des collisions ;
- le choix du wallet par un consommateur.
Le secret Ed25519 n'est jamais placé dans ce header. Le plaintext protégé de la version `1` est exactement le keypair Solana brut de **64 octets** déjà caractérisé par le format CLI legacy. XChaCha20-Poly1305 ajoute un tag de 16 octets : le ciphertext v1 est donc exactement de **80 octets**. Toute évolution de la structure secrète nécessite une nouvelle version du format.
Le nom `<alias>.kswallet` et l'alias encodé doivent correspondre exactement. Une divergence est une erreur et ne déclenche aucun fallback vers le legacy.
Avant déverrouillage, cette identité est **déclarative** : le codec peut vérifier sa structure, mais pas le tag AEAD sans mot de passe. `unlock()` et `unlock_file()` authentifient le conteneur puis vérifient que la pubkey dérivée du keypair déchiffré correspond exactement à cette pubkey déclarée avant de créer `UnlockedWallet`.
## KDF v1
Le KDF sélectionné est **Argon2id version 19**.
Le profil d'écriture par défaut retenu est :
```text
memory = 65536 KiB (64 MiB)
passes = 3
parallelism = 4 lanes
salt = 16 bytes
output key = 32 bytes
```
Ce profil correspond au second profil recommandé par RFC 9106 pour les environnements à mémoire contrainte et produit la clé de 256 bits requise par l'AEAD choisi.
Le codec accepte uniquement des paramètres dans les bornes suivantes :
```text
memory = 65536..262144 KiB
passes = 3..10
parallelism = 1..8
```
La mémoire déclarée doit en plus respecter la relation Argon2 `m >= 8 * p` et être un multiple de `4 * p` afin que la valeur encodée corresponde sans ambiguïté au nombre de blocs réellement retenu.
Ces bornes servent deux objectifs distincts :
- refuser un conteneur v1 plus faible que le plancher retenu pour `ks-wallet` ;
- empêcher qu'un fichier hostile impose des coûts KDF arbitrairement élevés lors d'une ouverture authentifiée.
## AEAD v1
L'AEAD sélectionné est **XChaCha20-Poly1305** :
```text
key = 32 bytes
nonce = 24 bytes
tag = 16 bytes
```
Chaque nouvelle protection du payload génère un sel et un nonce neufs depuis la source aléatoire système. Une modification de mot de passe produit donc un nouveau sel et un nouveau nonce avant rechiffrement de la même keypair.
Ce choix appartient au format natif `ks-wallet`, pas à un format Solana externe. La documentation RustCrypto signale qu'il n'existe pas de spécification XChaCha20-Poly1305 autoritative unique, tout en documentant des implémentations interopérables et l'audit de la crate. Le layout v1 fixe donc explicitement l'algorithme et ses tailles au lieu de dépendre d'une convention implicite.
## Données authentifiées
Le contrat v1 réserve comme **AEAD associated data** l'intégralité des octets précédant le ciphertext :
```text
fixed header
+ alias
+ salt
+ nonce
```
Le champ `ciphertext_length` appartient donc lui aussi aux données authentifiées.
Ainsi, une altération de la version, des IDs crypto, des paramètres KDF, de la pubkey, de l'alias, du sel, du nonce ou de la longueur du ciphertext fait échouer l'ouverture authentifiée.
## Bornes du codec
Le codec v1 impose :
```text
alias <= 64 bytes
plaintext keypair = 64 bytes
ciphertext + tag = 80 bytes
native file total = 193..256 bytes
```
La taille du fichier est vérifiée à partir des métadonnées du fichier ouvert **avant** l'allocation du buffer de lecture. Après lecture des octets attendus, le lecteur vérifie également qu'aucun octet supplémentaire n'est apparu pendant l'opération. La somme exacte des longueurs déclarées doit être égale à la taille réelle ; truncation, croissance concurrente et trailing bytes sont refusés.
Les flags inconnus, IDs KDF/AEAD inconnus, version Argon2 inconnue et octets reserved non nuls sont refusés.
## Permissions et publication
Sous Unix :
```text
wallet directory : 0700
.kswallet file : 0600
```
La publication implémentée pour produire un conteneur v1 suit cette séquence :
1. encoder entièrement le conteneur ;
2. créer dans le même répertoire un fichier temporaire privé avec `create_new` ;
3. écrire le contenu complet ;
4. `sync_all` du fichier temporaire ;
5. publier sans remplacement par hard-link vers `<alias>.kswallet` ;
6. synchroniser le répertoire parent sous Unix ;
7. supprimer le nom temporaire ;
8. synchroniser à nouveau le répertoire parent sous Unix.
L'utilisation d'un hard-link de même répertoire garantit que la destination existante n'est jamais remplacée silencieusement. Si le filesystem ne permet pas cette publication, l'opération échoue au lieu de revenir à une écriture destructive.
## Non-divulgation
Les structures internes contenant sel, nonce ou ciphertext :
- ne sont pas une API crate-root publique ;
- n'implémentent pas `Debug` automatiquement ;
- ne sont pas sérialisées vers Tauri ;
- ne doivent pas apparaître dans les erreurs ou logs.
`WalletFileHandle` ne projette que :
- alias ;
- pubkey déclarée ;
- version du format ;
- identité persistante non sensible.
Le chemin local reste conservé en interne pour permettre l'ouverture ultérieure du fichier sélectionné.
## Implémentation `pre.004`
`pre.004` complète le codec v1 sans modifier son layout :
- encodage et publication atomique/no-clobber des créations ;
- dérivation Argon2id dans une tâche bloquante dédiée ;
- chiffrement/déchiffrement des 64 octets du keypair avec XChaCha20-Poly1305 ;
- authentification du préfixe complet comme AAD ;
- vérification stricte de la pubkey après déchiffrement ;
- création de `UnlockedWallet` seulement après authentification réussie ;
- changement de mot de passe avec nouvelle protection du même keypair ;
- zéroïsation des mots de passe, clés dérivées, plaintexts temporaires et buffers ciphertext possédés par `ks-wallet` lorsque leur durée de vie se termine ;
- activation du feature `zeroize` du cipher pour effacer son état interne à la destruction.
La keypair runtime est possédée par `solana_keypair::Keypair`; sa dépendance `ed25519-dalek` active par défaut la zéroïsation de `SigningKey`.
La migration et les adaptateurs d'import/export ne font pas partie du format wire v1 et restent la tranche suivante. Toute modification incompatible du layout défini ici doit utiliser une nouvelle version de format plutôt qu'une heuristique de décodage.
## Références de conception v1
- RFC 9106 — Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications, notamment le second profil recommandé Argon2id `t=3`, `p=4`, `m=64 MiB`, sel 128 bits et sortie 256 bits.
- Documentation RustCrypto `argon2` 0.5 — dérivation de clé Argon2id et paramètres explicites.
- Documentation RustCrypto `chacha20poly1305` 0.11 — contrat `XChaCha20Poly1305`, clé 32 octets, nonce 24 octets et tag 16 octets, ainsi que la note de statut de spécification XChaCha.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/README.md -->
<!-- version: 33 -->
<!-- version: 37 -->
# Documentation active de Khadhroony Bot3
@@ -52,6 +52,8 @@ Les audits et rapports de travail `0.4.8-pre.*` sont archivés sous `../olddocs/
- [`IDL_TO_KB_LIB_NOMENCLATURE.md`](IDL_TO_KB_LIB_NOMENCLATURE.md) ;
- [`MISSING_PROGRAM_IDLS.md`](MISSING_PROGRAM_IDLS.md) ;
- [`OPERATION_NAMING_CONVENTION.md`](OPERATION_NAMING_CONVENTION.md) ;
- [`NATIVE_FORMAT.md`](NATIVE_FORMAT.md) : format binaire natif versionné de `ks-wallet` ;
- [`WALLET_FORMAT_COMPATIBILITY.md`](WALLET_FORMAT_COMPATIBILITY.md) : matrice des formats dimport/export wallet Solana et décisions dadaptation ;
- [`IDEA_REMINDERS.md`](IDEA_REMINDERS.md).
Les idées ne rejoignent un `TODO.md` quaprès confirmation, attribution et reformulation en tâche vérifiable.
@@ -68,7 +70,7 @@ Elles peuvent être chargées directement par les tests unitaires ou dintégr
## 7. Documentation par crate
Les onze crates possèdent `README.md`, `TODO.md`, `USAGE.md` et `CHANGELOG.md` :
Les douze crates possèdent `README.md`, `TODO.md`, `USAGE.md` et `CHANGELOG.md` :
- [`ks-core`](../ks-core/README.md) ;
- [`ks-config`](../ks-config/README.md) ;
@@ -80,6 +82,7 @@ Les onze crates possèdent `README.md`, `TODO.md`, `USAGE.md` et `CHANGELOG.md`
- [`ks-onchain-transport`](../ks-onchain-transport/README.md) ;
- [`ks-store`](../ks-store/README.md) ;
- [`ks-wallet`](../ks-wallet/README.md) ;
- [`ks-wallet-demo-scenarios`](../ks-wallet-demo-scenarios/README.md) ;
- [`kb-app-demo-desktop`](../kb-app-demo-desktop/README.md).
## 8. Guides
@@ -89,13 +92,15 @@ Les onze crates possèdent `README.md`, `TODO.md`, `USAGE.md` et `CHANGELOG.md`
- [RPC, backfill et WebSocket](guides/RPC_BACKFILL_AND_WEBSOCKET.md) ;
- [Extraction Core, replay et matérialisation](guides/REPLAY_CORE_EXTRACTION_AND_MATERIALIZATION.md) ;
- [PostgreSQL et stockage](guides/POSTGRES_STORAGE.md) ;
- [Validation Devnet](guides/DEVNET_VALIDATION.md).
- [Validation Devnet](guides/DEVNET_VALIDATION.md) ;
- [Wallets, sécurité et opérations](guides/WALLETS.md).
## 9. Rapports de validation actifs
- [Validation Metaplex Token Metadata 0.4.7](validation/V0_4_7_METAPLEX_TOKEN_METADATA_VALIDATION_REPORT.md) ;
- [Validation Metadata on-chain et clôture 0.4.8](validation/V0_4_8_METADATA_VALIDATION_REPORT.md) ;
- [Validation de fondation et clôture 0.5.1](validation/V0_5_1_FOUNDATION_VALIDATION_REPORT.md) ;
- [Validation wallet et clôture 0.5.2](validation/V0_5_2_WALLET_VALIDATION_REPORT.md) ;
- [`validation/WEBSOCKET_MAINNET_RESEARCH_VALIDATION_REPORT.md`](validation/WEBSOCKET_MAINNET_RESEARCH_VALIDATION_REPORT.md) ;
- [`validation/MAINNET_RESEARCH_BACKFILL_VALIDATION_SCENARIO.md`](validation/MAINNET_RESEARCH_BACKFILL_VALIDATION_SCENARIO.md).
@@ -103,10 +108,10 @@ Les preuves détaillées de `0.4.8-pre.*` restent accessibles sous `../olddocs/a
## 10. Plans de version actifs
Les plans temporaires `0.5.0` et `0.5.1` sont clôturés et archivés sous `../olddocs/archivekbot3/docs/plans/`.
Les plans temporaires `0.5.0`, `0.5.1` et `0.5.2` sont clôturés et archivés sous `../olddocs/archivekbot3/docs/plans/`.
Le plan détaillé de `0.5.2` sera créé dans sa première prerelease conformément au cycle de développement ; aucun plan temporaire `0.5.2` nest préécrit avant cet inventaire.
Le plan détaillé de `0.5.3` sera créé par `0.5.3-pre.001` après audit de `ks-store`.
## 11. Prompt de reprise
- [`Prompt actif 0.5.2`](../prompts/031_v0_5_2_ks_wallet_restructuring.md).
- [`Prompt actif 0.5.3`](../prompts/032_v0_5_3_ks_store_normalization.md).

View File

@@ -0,0 +1,116 @@
<!-- file: docs/WALLET_FORMAT_COMPATIBILITY.md -->
<!-- version: 2 -->
# Compatibilité import/export des wallets Solana
## 1. Statut
Cette matrice est établie pour `khadhroony-bot3 0.5.2-pre.005` et borne les adaptateurs secrets de `ks-wallet`.
Elle ne prétend pas inventorier tous les wallets existants. Elle couvre les formats obligatoires du projet et les cibles explicitement retenues par le plan `0.5.2` : Solana CLI, Phantom, Solflare, Backpack, Trust Wallet et Base / Coinbase Wallet. Coinbase Developer Platform est distingué de Base app car son API de custody/import n'est pas le contrat utilisateur de Base app.
Règles transversales :
- tout export contenant le secret exige d'abord le mot de passe valide du `.kswallet` ;
- aucun adaptateur ne synthétise une recovery phrase à partir d'une keypair arbitraire ;
- un format n'est implémenté que si sa représentation est suffisamment précise pour permettre un round-trip et une validation de pubkey ;
- les exports secrets sont écrits dans un fichier privé, atomiquement et sans écrasement silencieux ;
- les sources d'import sont lues comme fichiers secrets privés et ne sont jamais loguées.
Sources vérifiées le **2026-08-10**.
## 2. Formats implémentés dans `0.5.2-pre.005`
| Format `ks-wallet` | Représentation | Import | Export | Cible de référence |
|--------------------------|-----------------------------------------------|:------:|:------:|---------------------------------------------|
| `SolanaCliJson` | tableau JSON contenant exactement 64 octets | oui | oui | Solana CLI / `solana-keygen` |
| `SolanaPrivateKeyBase58` | Base58 du keypair Solana complet de 64 octets | oui | oui | Phantom ; compatibilité Solflare documentée |
`SolanaPrivateKeyBase58` est volontairement un nom de format générique et non `PhantomPrivateKey` : le wire représente la keypair Solana, pas un conteneur propriétaire Phantom. Phantom sert de cible de référence parce que la documentation Solana identifie explicitement la private key copiée depuis Phantom comme **Base58**, et Phantom documente l'import d'une private key Solana. La référence officielle `@solana/web3.js` impose par ailleurs **64 octets** à `Keypair.fromSecretKey()` ; l'adaptateur `ks-wallet` encode donc exactement son keypair Solana complet de 64 octets en Base58 et refuse une seed brute de 32 octets.
Solflare documente de son côté le transfert d'une private key affichée par Phantom vers son propre écran `Import private key`. Cette compatibilité permet d'utiliser le même export Base58 sans créer un second codec de marque identique.
La version `pre.005` accepte uniquement la forme Base58 du **keypair complet de 64 octets**. Elle n'accepte pas une seed brute de 32 octets et ne dérive aucune keypair depuis une mnemonic.
## 3. Matrice des cibles étudiées
| Cible | Surface officielle observée | Encodage exploitable depuis une keypair arbitraire | Décision `0.5.2-pre.005` |
|-------------------------------|-------------------------------------------------------------------------|----------------------------------------------------|-----------------------------------------------------------------|
| Solana CLI / `solana-keygen` | fichier keypair JSON, tableau d'octets | oui, 64 octets | **implémenté import + export** |
| Phantom | import/export d'une private key Solana | oui, Base58 selon documentation Solana | **implémenté via `SolanaPrivateKeyBase58`** |
| Solflare | import d'une private key copiée depuis Phantom ; export de private key | oui par compatibilité documentée avec Phantom | même wire Base58 ; pas de codec de marque séparé |
| Solflare Keystore | import d'un fichier keystore protégé | format exact non suffisamment spécifié ici | TODO version ultérieure non déterminée |
| Backpack | import avancé par private key ou recovery phrase | représentation Solana exacte non spécifiée | TODO version ultérieure non déterminée |
| Trust Wallet | export de private keys ; restauration pouvant utiliser des private keys | représentation Solana exacte non spécifiée | TODO version ultérieure non déterminée |
| Base app / ex-Coinbase Wallet | restauration et backup principalement documentés par recovery phrase | pas de contrat arbitraire keypair -> même wallet | **aucune mnemonic synthétique ; pas d'adaptateur** |
| Coinbase Developer Platform | import Solana Base58 ou tableau brut 32 octets ; export API | oui, mais API de plateforme distincte de Base app | information de compatibilité ; pas un adaptateur wallet `0.5.2` |
## 4. Sources officielles
### Solana CLI JSON
La documentation Solana décrit le fichier keypair par défaut `~/.config/solana/id.json` comme un **JSON array of bytes**. Le format legacy déjà caractérisé dans `ks-wallet` fixe son wire actuel à 64 octets et `pre.005` conserve cette représentation en import/export.
- Solana — `Create a Token Account`: <https://solana.com/docs/tokens/extensions/confidential-transfer/create-token-account>
- Solana — `Deploying Programs`: <https://solana.com/docs/programs/deploying>
### Phantom / Base58
Phantom documente l'import d'une private key pour une adresse unique et l'export de la private key par réseau. Le template officiel Solana `ZK Compression Airdrop` précise que la private key obtenue par `Phantom -> Settings -> Show Private Key` est **Base58 encoded**.
- Phantom — `Sign in to or import an existing Phantom wallet`: <https://help.phantom.com/hc/en-us/articles/15079894392851-Sign-in-to-or-import-an-existing-Phantom-wallet>
- Phantom — `View your recovery phrase or private keys in Phantom`: <https://help.phantom.com/hc/en-us/articles/25334064171795-View-your-recovery-phrase-or-private-keys-in-Phantom>
- Solana — `ZK Compression Airdrop`: <https://solana.com/developers/templates/zk-compression-airdrop>
- Solana Foundation — `@solana/web3.js Keypair`: <https://solana-foundation.github.io/solana-web3.js/v1.x/classes/Keypair.html>
### Solflare
Solflare documente l'import d'une private key copiée depuis Phantom, ainsi que l'export de la private key d'un compte. Cette documentation établit la compatibilité du wire de private key entre les deux wallets, sans nécessiter un second format `ks-wallet`.
- Solflare — `How to Import a Wallet using a Private Key on Solflare`: <https://help.solflare.com/en/articles/6462529-how-to-import-a-wallet-using-a-private-key-on-solflare-desktop>
- Solflare — `How to Export Your Private Key on Solflare`: <https://help.solflare.com/en/articles/6150516-how-to-export-your-private-key-on-solflare-mobile>
- Solflare — `Importing a Keystore File into Your Solflare Wallet`: <https://help.solflare.com/en/articles/9351932-importing-a-keystore-file-into-your-solflare-wallet>
### Backpack
Backpack documente un import avancé par `Private key`, mais la page consultée ne fixe pas suffisamment l'encodage Solana du secret pour créer un codec testé sans hypothèse supplémentaire.
- Backpack — `Import/Recover Wallet`: <https://support.backpack.exchange/wallet/get-started/import-recover-wallet>
### Trust Wallet
Trust Wallet documente l'export de private keys et indique qu'une restauration peut utiliser des private keys stockées séparément. Les sources consultées ne fixent toutefois pas le wire Solana précis attendu par l'import utilisateur.
- Trust Wallet — `How to Backup Your Recovery Phrase and Export Private Keys in Trust Wallet`: <https://trustwallet.com/blog/product/how-to-backup-your-recovery-phrase-and-export-private-keys-in-trust-wallet>
- Trust Wallet — `Backing Up and Restoring Your Wallet: Step-by-Step Guide`: <https://trustwallet.com/blog/security/backing-up-and-restoring-your-wallet-step-by-step-guide>
### Base app et Coinbase Developer Platform
Base app documente principalement la recovery phrase pour backup/restauration. `ks-wallet` ne possède pas la mnemonic/seed d'origine d'une keypair arbitraire et ne doit donc pas en fabriquer une.
Coinbase Developer Platform possède un autre contrat : son API Solana accepte une private key Base58, explicitement citée comme pouvant venir de Phantom, ou un tableau brut de 32 octets. Ce contrat est utile pour confirmer l'existence du wire Base58 mais ne doit pas être confondu avec l'interface utilisateur Base app.
- Base / Coinbase Help — `Base wallet recovery phrase`: <https://help.coinbase.com/wallet/managing-account/wallet-recovery-phrase>
- Coinbase Developer Platform — `Import & Export`: <https://docs.cdp.coinbase.com/wallets/using-wallets/import-and-export>
## 5. Report après `0.5.2-pre.005`
Les adaptations suivantes sont volontairement reportées vers une version ultérieure non déterminée :
- vérifier et, si pertinent, implémenter le format exact `Private key` de Backpack pour Solana ;
- vérifier le wire d'import Solana de Trust Wallet avant tout codec ;
- caractériser le keystore Solflare uniquement si un format stable et suffisamment spécifié est disponible ;
- réévaluer Base app si une future documentation officielle expose un import de keypair arbitraire préservant directement la même pubkey ;
- ajouter d'autres wallets uniquement à partir d'un format privé explicitement documenté et testable.
Ces reports ne bloquent pas `0.5.2` : le format Solana CLI obligatoire et l'adaptateur tiers Base58 de référence sont couverts dans `pre.005`.
## Emplacement des exports de démonstration
`ks-wallet` accepte un chemin de destination explicite fourni par son consommateur. La démo desktop `0.5.2` borne volontairement ses exports opérateur au répertoire :
```text
data/wallets/
```
Cette convention appartient à `kb-app-demo-desktop`; elle ne devient pas une dépendance ou une constante générale de `ks-wallet`.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/architecture/CRATE_MAP.md -->
<!-- version: 7 -->
<!-- version: 8 -->
# Carte des crates
@@ -17,9 +17,10 @@
| `ks-onchain-transport` | bibliothèque | transports RPC HTTP/WebSocket et pools dendpoints | README/TODO/USAGE/CHANGELOG présents |
| `ks-store` | bibliothèque | contrats de stockage et adaptateur PostgreSQL | nom `ks-*` actif ; normalisation `0.5.3` |
| `ks-wallet` | bibliothèque | wallet temporaire et frontière de signataire | nom `ks-*` actif ; refonte `0.5.2` |
| `ks-wallet-demo-scenarios` | bibliothèque | scénarios de validation réutilisables du cycle de vie et des formats wallet | ajoutée en `0.5.2` |
| `kb-app-demo-desktop` | bibliothèque + binaire | application de démonstration Tauri | documentée ; réconciliation en `0.5.4` |
La table décrit les noms physiques stabilisés par `0.5.1`. La migration a renommé les dix crates généralistes en `ks-core`, `ks-config`, `ks-lib`, `ks-logging`, `ks-program-ids`, `ks-pipeline`, `ks-pipeline-demo-scenarios`, `ks-onchain-transport`, `ks-store` et `ks-wallet`. `kb-app-demo-desktop` conserve son nom parce quil appartient au domaine applicatif Bot. Voir [`../decisions/KHADHROONY_SOLANA_NAMESPACE_POLICY.md`](../decisions/KHADHROONY_SOLANA_NAMESPACE_POLICY.md).
La table décrit les noms physiques stabilisés par `0.5.1`. La migration a renommé les dix crates généralistes en `ks-core`, `ks-config`, `ks-lib`, `ks-logging`, `ks-program-ids`, `ks-pipeline`, `ks-pipeline-demo-scenarios`, `ks-onchain-transport`, `ks-store` et `ks-wallet`. `0.5.2` ajoute ensuite `ks-wallet-demo-scenarios` sans modifier cette histoire de migration. `kb-app-demo-desktop` conserve son nom parce quil appartient au domaine applicatif Bot. Voir [`../decisions/KHADHROONY_SOLANA_NAMESPACE_POLICY.md`](../decisions/KHADHROONY_SOLANA_NAMESPACE_POLICY.md).
## 2. Consolidations principales depuis bot2
@@ -30,7 +31,8 @@ La migration a regroupé de nombreuses anciennes crates dans des frontières plu
- les responsabilités de pipeline ont été réunies dans `ks-pipeline` ;
- les transports Solana sont réunis dans `ks-onchain-transport` ;
- lapplication et sa bibliothèque sont réunies dans `kb-app-demo-desktop` ;
- les scénarios réutilisables ont été extraits dans `ks-pipeline-demo-scenarios`.
- les scénarios réutilisables ont été extraits dans `ks-pipeline-demo-scenarios` ;
- les validations wallet réutilisables sont isolées dans `ks-wallet-demo-scenarios` à partir de `0.5.2`.
Cette carte nest pas une table de compatibilité exhaustive des anciennes crates. Les correspondances historiques détaillées seront synthétisées dans la documentation de migration et laudit dalignement.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/architecture/STORAGE_ARCHITECTURE.md -->
<!-- version: 5 -->
<!-- version: 9 -->
# Architecture du stockage
@@ -7,49 +7,172 @@
`ks-store` réunit :
- la façade publique backend-agnostique `Store` et ses options d'ouverture ;
- les contrats store-neutral ;
- les DTO et entités persistées ;
- la pagination et les rapports de santé ;
- les DTO et entités persistés ;
- la pagination, le replay et les rapports de santé ;
- les traits de repositories ;
- limplémentation PostgreSQL ;
- les migrations, requêtes et mécanismes de replay associés.
- les résumés runtime/init sanitisés ;
- l'implémentation PostgreSQL privée ;
- les ressources de schéma, maintenance et requêtes appartenant au backend actif.
La consolidation remplace lancien découpage entre plusieurs crates de stockage sans supprimer la séparation interne entre contrats et adaptateur PostgreSQL.
Depuis `0.5.3-pre.002`, les consommateurs ne construisent plus un backend concret. `ks-store` possède la connexion et le dispatch interne ; `PostgresStore`, `PgPool` et SQLx ne traversent plus sa frontière publique.
Depuis `0.5.3-pre.003`, PostgreSQL construit directement le nouveau baseline `k_sol_*` à partir de ressources SQL atomiques ; les migrations historiques `0001` à `0004` ne constituent plus la chaîne active.
## 2. Frontières
### 2.1 Contrats store-neutral
Les contrats ne doivent pas dépendre des détails SQL lorsquune abstraction stable est suffisante. Ils décrivent les entrées, sorties, identifiants, pages et erreurs nécessaires aux consommateurs.
Les contrats ne dépendent pas des détails SQL lorsqu'une abstraction stable est suffisante. Ils décrivent les entrées, sorties, identifiants, pages, diagnostics et erreurs nécessaires aux consommateurs. `StoreOpenOptions` transporte un code backend et un objet d'options opaque ; seul `ks-store` interprète le moteur sélectionné.
Le résumé runtime expose des modèles logiques et des compteurs d'objets par catégorie sans publier les noms physiques. PostgreSQL expose actuellement des compteurs `table` et `index`, tandis qu'un futur backend peut utiliser d'autres catégories sans modifier le desktop.
### 2.2 Adaptateur PostgreSQL
Le module PostgreSQL possède :
- la connexion et linitialisation ;
- lapplication idempotente des migrations ;
- la validation de ses options ;
- la connexion et l'initialisation ;
- l'orchestrateur transactionnel global du baseline ;
- les ressources SQL `include_str!` ;
- les requêtes typées ;
- les diagnostics ;
- les opérations de replay et de sélection de candidats.
- les diagnostics physiques ;
- les opérations de replay et de sélection de candidats ;
- le target de tracing canonique `ks-store`, enrichi des champs `backend="postgres"`, `domain="ks-store.pg"` et `action`.
Les détails physiques restent privés. Les erreurs et résumés traversant la façade ne doivent pas divulguer de DSN, password ou option sensible.
### 2.3 Modèles partagés
Les modèles métier communs restent dans `ks-lib` lorsquils dépassent la seule persistance. `ks-store` ne doit pas créer une seconde définition concurrente dun contrat partagé.
Les modèles métier communs restent dans `ks-lib` lorsqu'ils dépassent la seule persistance. `ks-store` ne doit pas créer une seconde définition concurrente d'un contrat partagé.
## 3. Catégories de données
## 3. Niveaux de persistance
Le stockage couvre plusieurs niveaux :
Le stockage distingue quatre niveaux durables :
- données brutes acquises ;
- transactions et instructions canoniques ;
- événements de décodage et diagnostics ;
- matérialisations ;
- états de campagne et candidats de replay ;
- informations opérationnelles et de santé.
- **N1** — acquisition et raw canonique ;
- **N2** — Core Solana canonique ;
- **N3** — decode/materialization générique et journaux versionnés ;
- **N4** — projections spécialisées/queryables additives.
Les noms de tables, contrats de replay et APIs publiques sont documentés dans `ks-store/USAGE.md` à partir des exports et migrations actuels.
N1/N2/N3 constituent la fondation candidate au gel de `0.5.3`. Le gel formel n'est déclaré qu'après snapshot et validations de réconciliation ; après clôture, leur évolution normale doit être fortement contrainte. N4 reste volontairement plus évolutif.
## 4. Propriétés attendues
Les replays N1 -> N2, N2 -> N3 et N3 -> N4 doivent pouvoir être exécutés indépendamment et de manière idempotente.
## 4. Baseline N1-N3 de `pre.003`
Le baseline PostgreSQL comporte **16 tables**.
### 4.1 N1
- `k_sol_raw_transactions` ;
- `k_sol_obs_transaction_observations` ;
- `k_sol_obs_account_observations`.
`k_sol_raw_transactions` conserve le document canonique source-independent et `block_time` lorsqu'il est fourni par Solana. Les observations d'acquisition restent séparées de ce document canonique.
`k_sol_obs_account_observations` réserve un input générique pour les acquisitions d'état de compte : identité du compte, slot/context, bytes exacts, hash et provenance d'acquisition. Cette structure n'est liée à aucun décodeur ou IDL.
### 4.2 N2 Core
- `k_sol_core_transactions` ;
- `k_sol_core_account_keys` ;
- `k_sol_core_instructions` ;
- `k_sol_core_inner_instructions` ;
- `k_sol_core_logs` ;
- `k_sol_core_balance_changes` ;
- `k_sol_core_return_data` ;
- `k_sol_core_account_states`.
`k_sol_core_transactions` porte l'ancre transactionnelle et `block_time`.
Les instructions top-level et CPI restent physiquement séparées. Les deux portent `stack_height` lorsque disponible ; les CPI conservent leur parent immédiat reconstructible. Elles doivent converger vers des capacités lifecycle/replay équivalentes sans imposer un stockage physique unique.
`k_sol_core_return_data` évite de perdre en N2 la `returnData` déjà présente dans la transaction canonique N1.
`k_sol_core_account_states` fournit le pendant Core des observations de comptes et conserve les bytes exacts ainsi que leur provenance N1.
### 4.3 N3 et transverse
- `k_sol_ops_processing_ledger` ;
- `k_sol_decode_events` ;
- `k_sol_decode_coverage_declarations` ;
- `k_sol_decode_coverage_observations` ;
- `k_sol_mat_outputs`.
`k_sol_mat_outputs` est le journal générique durable des sorties de matérialisation. Les projections métier N4 ne le remplacent pas.
`k_sol_decode_events` réserve une provenance de schéma nullable (`kind/id/version/hash`) pour les mécanismes de décodage pouvant dépendre d'un schéma externe. Les décodeurs compilés actuels la laissent vide.
## 5. Readiness des futurs décodeurs
Le canari future-decoder ne doit pas introduire Anchor dans le schéma. Il vérifie que les faits Solana génériques suffisants sont conservés :
- bytes d'instruction et comptes ordonnés ;
- propriétés signer/writable résolubles ;
- instructions top-level et CPI ;
- `stack_height` et parent CPI ;
- logs ordonnés ;
- `returnData` ;
- observations/états bruts de comptes ;
- temporalité et provenance ;
- identité/version/hash d'un schéma externe lorsqu'un décodeur en utilise un.
La conséquence de `pre.003` est l'ajout de trois contrats génériques au-dessus des 13 tables historiques : observation de compte, état Core de compte et return data Core. Le baseline candidat au gel passe donc à 16 tables.
Les métriques transactionnelles comme fee, rewards, compute units et cost units restent disponibles dans N1. Elles ne sont pas requises pour ce canari et ne justifient pas une extension de l'ancre Core en `pre.003`; une future projection additive pourra les rendre queryables si nécessaire.
## 6. Ressources PostgreSQL
Les ressources actives sont classées sous :
```text
ks-store/migrations/postgres/
schema/
tables/
constraints/
indexes/
maintenance/
truncate/
drop/
```
Principes :
- une instruction SQL par fichier ;
- un `include_str!` privé par ressource ;
- aucun DDL dupliqué dans une chaîne Rust ;
- tables avant contraintes, contraintes avant index ;
- initialisation complète dans une transaction PostgreSQL unique avec advisory lock ;
- `DROP`/`TRUNCATE` séparés de l'auto-init ;
- refus explicite d'un schéma contenant encore des objets `kb_sol_*`.
Après la revue repositories/index de `pre.004`, le baseline contient **240 ressources SQL atomiques** et **79 index attendus** (63 explicites + 16 index de clés primaires). Ces nombres décrivent l'implémentation PostgreSQL actuelle, pas une API publique gelée.
## 7. Replay, invalidation et pagination (`pre.004`)
`pre.004` ne modifie pas les 16 tables ; il ferme les chemins de repository au-dessus delles.
### 7.1 Instructions top-level et CPI
Les deux catégories restent physiquement séparées. La lecture de replay construit une union logique avec un scope explicite et un ordre stable `(slot, signature, scope_rank, instruction_path)`. Le curseur de page reprend cette clé ; aucun `OFFSET` nappartient au contrat durable.
### 7.2 Invalidation descendante
Un remplacement N2 Core dune signature invalide dans la même transaction ses descendants N3 decode/coverage/materialization et les ledgers concernés. Un remplacement decode invalide ses sorties de matérialisation pour lidentité exacte decoder name/version/input. Les préfixes de ledger sont comparés littéralement, sans `LIKE`, afin que `_` ne puisse pas agir comme wildcard.
Le lifecycle instructionnel est ensuite recomposé depuis les descendants réellement courants. Une ancienne matérialisation produite par une version antérieure du decoder ne suffit pas à déclarer le nouvel input matérialisé.
### 7.3 Account observation -> Core account state
Le flux N1 -> N2 réservé en `pre.003` devient opérationnel. La sélection est bornée et version-aware ; la présence dun ancien Core state ne masque pas une nouvelle version du normalizer. `lamports` et `rent_epoch` conservent tout le domaine `u64` via `NUMERIC(20,0)` et les conversions vers `BIGINT` échouent explicitement hors domaine signé.
### 7.4 Bornes et instrumentation
Les lectures interactives publiques sont plafonnées à 500 lignes. Les campagnes de processing Core/decode/account-state utilisent des batches distincts plafonnés à 1 000. Les opérations PostgreSQL réécrites tracent `action`, durée, lignes et outcome sous `ks-store`/`ks-store.pg`, sans SQL brut ni valeur bindée.
## 8. Propriétés attendues
- initialisation idempotente ;
- pagination bornée ;
@@ -57,12 +180,12 @@ Les noms de tables, contrats de replay et APIs publiques sont documentés dans `
- absence de double effet lors des replays ;
- validation stricte des entrées ;
- erreurs explicites ;
- séparation entre données brutes, résultats de décodage et matérialisations.
- séparation entre acquisition/raw, Core, decode/materialization et projections N4 ;
- absence de dépendance de N1/N2 à une technologie de décodeur ;
- conservation des faits nécessaires à un redécodage historique.
La série `0.5.3` normalisera cette fondation avant larrivée des protocoles trading. Elle doit notamment distinguer `slot`, `block_time` et les timestamps dacquisition/persistance, normaliser la structure interne de `ks-store`, vérifier les champs et index manquants et préparer les futures matérialisations trading, routing et multi-pools sans perdre les contrats de provenance et didempotence.
Les index non uniques restent des optimisations physiques évolutives. Les noms/colonnes/types/nullabilités/PK/FK/uniques/checks/sémantiques du baseline N1-N3 seront figés séparément lors de la tranche de gel.
La même migration remplace le préfixe historique des tables Solana `kb_sol_*` par `k_sol_*`. La base peut encore être reconstruite ou migrée proprement avant `0.6.x`, il nest donc pas cessaire de conserver indéfiniment lancien préfixe. Le préfixe `kb_*` est réservé aux éventuelles tables dont la responsabilité appartient réellement au domaine applicatif Bot, pas aux faits Solana simplement consommés par le bot.
## 9. Données de test
## 5. Données de test
Les fixtures privées, bases locales et preuves temporaires ne font pas partie des livraisons. Les matrices contractuelles partagées restent sous `test-fixtures/contract-matrices/` lorsquelles sont nécessaires aux tests.
Les fixtures privées, bases locales et preuves temporaires ne font pas partie des livraisons. Les matrices contractuelles partagées restent sous `test-fixtures/contract-matrices/` lorsqu'elles sont nécessaires aux tests.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/decisions/KHADHROONY_SOLANA_NAMESPACE_POLICY.md -->
<!-- version: 8 -->
<!-- version: 9 -->
# Politique de namespace Khadhroony Solana
@@ -7,7 +7,7 @@
Cette décision est adoptée pendant la clôture de `0.5.0` et devient la cible normative des migrations de fondation `0.5.1` à `0.5.3`.
À partir de `0.5.1-pre.002`, les dix crates Solana généralistes sont physiquement nommées `ks-*` et leurs identifiants Rust `ks_*`. `0.5.1-pre.003` migre également les identités techniques `kb-lib.*` vers `ks-lib-*`. `0.5.1-pre.004` migre les variables possédées par Khadhroony Solana vers `KS_*` et réserve explicitement `KB_*` aux composants réellement possédés par Khadhroony Bot. Le namespace SQL `kb_sol_*` reste temporairement présent jusqu'à `0.5.3`.
À partir de `0.5.1-pre.002`, les dix crates Solana généralistes sont physiquement nommées `ks-*` et leurs identifiants Rust `ks_*`. `0.5.1-pre.003` migre également les identités techniques `kb-lib.*` vers `ks-lib-*`. `0.5.1-pre.004` migre les variables possédées par Khadhroony Solana vers `KS_*` et réserve explicitement `KB_*` aux composants réellement possédés par Khadhroony Bot. Le namespace SQL historique `kb_sol_*` est remplacé par `k_sol_*` dans le baseline PostgreSQL de `0.5.3-pre.003`.
Le renommage des bibliothèques internes ne renomme pas le workspace, le dépôt ni le répertoire racine : ils restent `khadhroony-bot3` pendant toute la série pré-`1.0`. Un éventuel renommage du workspace racine est explicitement hors périmètre de `0.5.x` et ne doit intervenir qu'en `1.0` ou ultérieurement sur décision dédiée.
@@ -108,7 +108,7 @@ Les segments métier internes (`solana`, `spl`, protocoles, surfaces et opérati
La normalisation SQL appartient à `0.5.3` avec `ks-store`.
Les tables génériques représentant des faits Solana doivent migrer du préfixe actuel `kb_sol_*` vers :
Depuis `0.5.3-pre.003`, le baseline PostgreSQL actif des faits Solana utilise :
```text
k_sol_*
@@ -124,7 +124,7 @@ kb_*
Une table ne reçoit pas `kb_*` simplement parce qu'elle est consommée par le bot. Elle doit contenir des données dont la responsabilité appartient réellement au domaine applicatif Bot et non à la blockchain ou aux bibliothèques Solana généralistes.
La migration `kb_sol_*``k_sol_*` doit être traitée avec les contrats temporels, de provenance, d'idempotence, d'index et de replay de `0.5.3`, et non comme un remplacement textuel isolé.
La reconstruction `kb_sol_*``k_sol_*` de `0.5.3-pre.003` est traitée avec les contrats temporels, de provenance, d'idempotence, d'index et de replay ; l'ancien namespace reste uniquement un marqueur détecté/refusé et une référence historique.
## 8. Ordre des migrations

View File

@@ -1,5 +1,5 @@
<!-- file: docs/guides/CONFIGURATION.md -->
<!-- version: 10 -->
<!-- version: 11 -->
# Guide de configuration
@@ -130,7 +130,7 @@ Les classes actuelles couvrent le RPC WS standard et le RPC WS à capacité plus
## Wallet et politique d'exécution
Les propriétés de stockage/identité du wallet appartiennent à `wallet.config.json`.
Les propriétés de stockage/identité du wallet appartiennent à `wallet.config.json`. Un profil peut définir le champ optionnel `wallet_alias` pour sélectionner un wallet natif persistant par alias. Son omission ou `null` signifie quaucun wallet persistant nest sélectionné. Le champ est non sensible et ne contient jamais de mot de passe, de keypair ou de chemin de fichier individuel.
Les autorisations :

View File

@@ -1,69 +1,163 @@
<!-- file: docs/guides/POSTGRES_STORAGE.md -->
<!-- version: 3 -->
<!-- version: 7 -->
# Guide PostgreSQL et contrats de stockage
## Objectif
`ks-store` consolide les contrats de stockage Core, raw, decode et PostgreSQL de bot2 dans une crate unique.
`ks-store` porte la frontière de stockage généraliste. PostgreSQL reste le backend opérationnel actif, mais il est encapsulé derrière la façade publique backend-agnostique `Store`.
## Connexion
Les consommateurs ne construisent plus `PostgresStore`, `PostgresStoreOptions` ni un pool SQLx. Ils transmettent le backend sélectionné et ses options résolues à `StoreOpenOptions`; `ks-store` valide et interprète ensuite les paramètres PostgreSQL.
```rust
let options = match ks_store::PostgresStoreOptions::new(
database_url,
10,
10_000,
let options = match ks_store::StoreOpenOptions::new(
true,
"postgres",
serde_json::json!({
"url": database_url,
"max_connections": 10,
"connect_timeout_ms": 10_000,
"auto_initialize_schema": true
}),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store = match ks_store::PostgresStore::connect(options).await {
let store = match ks_store::Store::open(options).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
```
Toujours utiliser `masked_dsn()` dans les diagnostics.
Le descripteur public éventuellement affiché provient du résumé runtime et est déjà masqué par `ks-store`. Le DSN brut et les options backend ne doivent jamais être propagés dans les DTO, erreurs applicatives ou logs normaux.
## Domaines
- raw : acquisitions et observations ;
- Core : transactions, instructions, comptes et contexte normalisés ;
- decode : ledger, observations décodées et matérialisations ;
- replay : candidats et résumés bornés.
- N1 raw/acquisition : transactions canoniques, observations de transactions et observations génériques de comptes ;
- N2 Core : transactions, clés, top-level, CPI, logs, variations de balances, return data et états de comptes ;
- N3/transverse : ledger, observations décodées, couverture et `k_sol_mat_outputs` ;
- replay/diagnostic : sélections et résumés bornés au-dessus de ces contrats.
## Migrations
## Baseline `0.5.3-pre.003`
Les migrations sont idempotentes et ordonnées. Une nouvelle migration ne doit pas modifier rétroactivement une migration déjà publiée.
`pre.003` reconstruit directement le schéma sous `k_sol_*`. Les bases Devnet/Mainnet/test historiques sont considérées reconstructibles ; aucune migration in-place depuis `kb_sol_*` n'est fournie.
Le comportement attendu à l'ouverture est :
1. base vide + auto-init : création du baseline complet ;
2. base `k_sol_*` déjà conforme : vérification/idempotence ;
3. base contenant encore des objets `kb_sol_*` : refus non destructif et demande de reconstruction explicite ;
4. `DROP`/`TRUNCATE` : opérations de maintenance distinctes, jamais déclenchées implicitement par `Store::open()`.
Le baseline candidat au gel comprend 16 tables N1-N3.
## Ressources SQL
Les ressources actives sont sous :
```text
ks-store/migrations/postgres/
schema/
tables/
constraints/
indexes/
maintenance/
truncate/
drop/
```
Chaque fichier contient une instruction SQL. Le code PostgreSQL privé utilise `include_str!` et l'orchestrateur global exécute le schéma dans une transaction unique avec advisory lock.
Il n'existe plus trois initialiseurs raw/Core/decode indépendants : **l'initialisation du schéma est globale**.
Le corpus `pre.003` contient actuellement :
- 16 créations de tables ;
- 129 ressources de contraintes ;
- 59 créations d'index explicites ;
- 16 ressources `TRUNCATE` ;
- 16 ressources `DROP` ;
- quatre index supplémentaires justifiés par le replay/invalidation `pre.004` ;
- soit **240 ressources SQL**.
PostgreSQL attend désormais **79 index** au total dans son diagnostic : 63 index explicites plus les 16 index de clé primaire créés par PostgreSQL.
Les index non uniques restent des optimisations physiques et ne font pas partie du gel logique N1-N3.
## Repositories `pre.004`
La tranche `pre.004` conserve les 16 tables et ferme les comportements suivants :
- replay logique commun top-level/CPI avec scope explicite et pagination keyset ;
- lifecycle symétrique top-level/CPI ;
- invalidation N2 Core -> N3 lors dun remplacement de signature ;
- invalidation decode -> matérialisation pour lidentité decoder exacte ;
- normalisation versionnée `k_sol_obs_account_observations` -> `k_sol_core_account_states` ;
- limites interactives à 500 et batches de processing à 1 000 ;
- traces fonctionnelles `TRACE ks-store` avec durée/lignes/outcome, sans bind values.
Les quatre nouveaux index couvrent lordre de replay CPI et les recherches dinvalidation/descendants dans coverage/materialization. Leur utilité finale doit encore être confirmée par des plans PostgreSQL réels avant le gel ; les index non uniques restent une optimisation physique.
## Statut du schéma
La façade n'utilise plus `_sqlx_migrations` comme preuve du baseline actuel. Le statut est dérivé du contrat réellement observé :
- aucune ressource connue : non initialisé ;
- toutes les ressources attendues : contrat courant ;
- présence partielle : drift/partial.
Le résumé d'initialisation expose des compteurs de modèles, tables et index sans publier leurs noms physiques.
## Compatibilité des identités de processeurs
Depuis `0.5.1-pre.003`, les identités techniques produites par `ks-lib` utilisent `ks-lib-decoder.*`, `ks-lib-executor.*` et `ks-lib-materializer.*`. Une base alimentée avant cette migration peut encore contenir des valeurs `kb-lib.*` dans les contrats de provenance/replay.
Depuis `0.5.1-pre.003`, les identités techniques produites par `ks-lib` utilisent `ks-lib-decoder.*`, `ks-lib-executor.*` et `ks-lib-materializer.*`.
Aucune migration SQL automatique de ces valeurs n'est ajoutée en `0.5.1` : avant `0.6.x`, une base conservée doit soit être reconstruite proprement, soit faire l'objet d'une migration de données explicitement contrôlée avant reprise du replay. Il ne faut pas exploiter durablement un même corpus avec les anciennes et nouvelles identités mélangées. Les noms de tables `kb_sol_*` restent inchangés jusqu'au chantier `0.5.3`.
Comme les bases `0.5.2` doivent être reconstruites pour le nouveau baseline `0.5.3`, les anciennes identités `kb-lib.*` ne sont pas migrées automatiquement dans ce chantier.
## Core readiness
`pre.003` ajoute les éléments génériques nécessaires au futur redécodage :
- `block_time` sur raw/Core transaction ;
- `stack_height` sur top-level/CPI ;
- parent CPI immédiat reconstructible ;
- `k_sol_core_return_data` ;
- `k_sol_obs_account_observations` ;
- `k_sol_core_account_states` ;
- provenance de schéma optionnelle sur les observations décodées.
Ces contrats sont indépendants d'Anchor/IDL. Aucun parser Anchor ni table `anchor_*` n'appartient à `0.5.3`.
## Repositories
Les traits publics séparent le contrat de limplémentation PostgreSQL. Les opérations de lecture utilisent des filtres et paginations bornés.
Les traits publics et la façade `Store` décrivent des opérations fonctionnelles indépendantes du moteur. PostgreSQL implémente ces contrats à l'intérieur de `ks-store`; aucune crate consommatrice ne reçoit `PgPool`, un type SQLx ou une requête concrète.
Le schéma des observations/états de comptes est introduit avant gel en `pre.003`; son ingestion/replay fonctionnel est raccordé dans la tranche repositories/replay suivante.
## Diagnostics
La surface publique expose :
- health snapshot ;
- migration snapshot ;
- backend diagnostics ;
- diagnostics des tables raw, Core et decode ;
- validation des noms de tables.
- schema/migration snapshot générique ;
- backend descriptor masqué ;
- résumé de configuration sanitisé ;
- résumé d'initialisation/vérification par modèle logique ;
- diagnostics de ressources sans nom physique d'objet.
Les noms de tables, index, contraintes et le SQL restent des détails backend-internes. Les traces PostgreSQL utilisent l'unique `target: crate::TRACING_TARGET` (`ks-store`) avec les champs structurés `backend="postgres"`, `domain="ks-store.pg"` et un `action` précis.
## Invariants
- aucune donnée canonique ne doit être dupliquée sans justification ;
- les écritures rejouables doivent être idempotentes ;
- la progression de campagne doit rester cohérente avec les lignes effectivement traitées ;
- les requêtes dynamiques nacceptent que des identifiants validés ;
- les erreurs PostgreSQL restent distinctes des erreurs de contrat.
- les requêtes dynamiques n'acceptent que des identifiants validés ;
- les détails PostgreSQL utiles au diagnostic restent backend-internes ;
- les erreurs traversant la façade publique ne doivent jamais divulguer DSN, password ou options sensibles ;
- un nouveau décodeur doit pouvoir exploiter N1/N2/N3 sans remodelage normal de ces niveaux.
## Références

324
docs/guides/WALLETS.md Normal file
View File

@@ -0,0 +1,324 @@
<!-- file: docs/guides/WALLETS.md -->
<!-- version: 2 -->
# Wallets, sécurité et opérations
## 1. Objet
Ce guide décrit le contrat opérationnel de `ks-wallet` à partir de `0.5.2`.
`ks-wallet` possède :
- le stockage local des wallets Solana persistants ;
- la protection par mot de passe ;
- l'ouverture authentifiée ;
- la capacité de signature ;
- les imports/exports de formats secrets explicitement supportés.
Il ne possède pas :
- la politique d'autorisation d'une transaction ;
- les plafonds de dépense ;
- le routage RPC ;
- la classification on-chain d'une pubkey ;
- les DTO frontend d'une application Tauri.
## 2. Modèle de données
### 2.1 Identité publique
Une identité publique contient uniquement :
- alias ;
- pubkey ;
- persistance temporaire ou persistante.
Une identité publique ne contient jamais le secret, le mot de passe, le ciphertext ou un chemin local.
### 2.2 Wallet temporaire
`TemporaryWallet::generate()` crée un keypair en mémoire. Il ne nécessite ni mot de passe ni fichier.
Cette surface reste adaptée aux tests synthétiques et signers jetables. Les fixtures/keypairs persistées uniquement pour les campagnes historiques restent sous `wallets/temporary/**`; elles ne doivent pas être confondues avec les `.kswallet` persistants de `wallets/`.
### 2.3 Wallet persistant
Le format natif est :
```text
<alias>.kswallet
```
Le layout exact est défini dans [`../NATIVE_FORMAT.md`](../NATIVE_FORMAT.md).
Le store automatique de `WalletManager` :
- scanne uniquement son répertoire configuré ;
- ne descend pas récursivement ;
- ignore les extensions étrangères ;
- refuse symlinks et fichiers non réguliers ;
- exige des permissions privées sous Unix ;
- valide le conteneur complet avant de retourner un handle.
## 3. Protection cryptographique
Le format v1 utilise :
| Élément | Contrat v1 |
| ----------------- | ---------------------------------- |
| KDF | Argon2id v19 |
| Profil d'écriture | 64 MiB, 3 passes, 4 lanes |
| Sel | 16 octets |
| Clé dérivée | 32 octets |
| AEAD | XChaCha20-Poly1305 |
| Nonce | 24 octets |
| Plaintext | keypair Solana exacte de 64 octets |
| Ciphertext + tag | 80 octets |
| AAD | header + alias + sel + nonce |
Les paramètres lus sont bornés avant l'exécution du KDF.
La pubkey présente dans le header n'est pas considérée comme authentifiée avant l'unlock. Après déchiffrement, `ks-wallet` reconstruit le keypair et vérifie que sa pubkey correspond exactement au header avant de produire une capacité de signature.
## 4. Mot de passe et cycle de vie
`WalletPassword` :
- prend possession de sa chaîne ;
- n'est pas clonable ;
- n'est pas sérialisable ;
- possède un `Debug` redacted ;
- zéroïse la valeur qu'il possède à la destruction.
`WalletManager::create()` crée un wallet natif et retourne `UnlockedWallet`.
`WalletManager::unlock()` ouvre un wallet du store par alias.
`WalletManager::unlock_file()` ouvre un `WalletFileHandle` obtenu par inspection explicite d'un fichier externe.
`UnlockedWallet` :
- possède le keypair authentifié ;
- n'est pas clonable ;
- expose `Signer` / `Signer + Sync` ;
- n'expose pas les 64 octets privés ;
- est verrouillé par consommation explicite avec `lock()` ou par fin de portée.
Un changement de mot de passe ne change pas la keypair. Il produit un nouveau sel et un nouveau nonce, puis reprotège exactement la même identité Solana.
Une `UnlockedWallet` déjà remise à un consommateur n'est pas révoquée rétroactivement par un changement de mot de passe. Son propriétaire doit la détruire ou appeler `lock()`.
## 5. Écriture et permissions
Les nouveaux `.kswallet` sont publiés sans écrasement silencieux.
Sous Unix :
- répertoire wallet : `0700` ;
- fichier `.kswallet` : `0600` ;
- exports secrets : `0600`.
Les écritures natives utilisent un fichier temporaire privé dans le même répertoire, `sync_all`, puis une publication atomique/no-clobber. Le changement de mot de passe remplace le fichier après authentification de l'ancien secret.
Les collisions concurrentes de création d'un même alias doivent produire exactement un gagnant et conserver une destination native valide.
## 6. Configuration
`wallet.config.json` contient une racine globale `wallets_directory` et des profils.
Un profil peut sélectionner :
```json
{
"wallet_alias": "operator"
}
```
ou laisser :
```json
{
"wallet_alias": null
}
```
L'alias est une sélection non sensible. Aucun mot de passe n'est stocké dans `ks-config`.
Lorsqu'un alias persistant est configuré, un consommateur ne doit pas retomber silencieusement sur un wallet temporaire : il doit acquérir explicitement le mot de passe et appeler l'API d'unlock.
## 7. Migration legacy
Le legacy historique est :
```text
<alias>.json
```
avec le tableau JSON des 64 octets du keypair Solana.
`WalletManager::migrate_legacy()` :
1. lit la source legacy privée ;
2. valide le keypair ;
3. crée `<alias>.kswallet` ;
4. vérifie que la pubkey est identique ;
5. conserve la source JSON intacte.
La suppression du legacy reste une décision explicite de l'opérateur après validation.
`TemporaryWalletStore` reste disponible pour les scénarios historiques. Ses erreurs, logs et `Debug` ne doivent pas projeter les chemins locaux, même si ses getters explicites `directory()` et `wallet_path()` restent nécessaires aux outils de fixtures.
## 8. Import et export
Les formats secrets actuellement supportés sont :
| Format | Import | Export | Mot de passe requis à l'export |
| ---------------------------------------------- | :----: | :----: | :----------------------------: |
| `WalletTransferFormat::SolanaCliJson` | oui | oui | oui |
| `WalletTransferFormat::SolanaPrivateKeyBase58` | oui | oui | oui |
La matrice détaillée est dans [`../WALLET_FORMAT_COMPATIBILITY.md`](../WALLET_FORMAT_COMPATIBILITY.md).
L'import refuse :
- source non régulière ou symlink ;
- permissions trop ouvertes sous Unix ;
- format invalide ;
- keypair de taille incorrecte ;
- alias déjà occupé ;
- pubkey déjà gérée sous un autre alias natif.
L'export authentifie d'abord le `.kswallet`, puis seulement après crée la destination. Il n'écrase jamais silencieusement un fichier existant.
## 9. Keypair, wallet, mint et authority
Une keypair Solana ne contient pas son rôle métier.
Le même type de secret Ed25519 peut servir comme :
- payer/opérateur ;
- mint authority ;
- freeze authority ;
- clé utilisée lors de la création d'un mint ;
- recipient possédant lui-même un signer ;
- autre authority ou signer de fixture.
Il est donc incorrect de décider « ce fichier est un mint et non un wallet » uniquement à partir du tableau JSON de 64 octets.
La future inspection de `wallets/temporary/**` prévue avec `0.5.4` doit produire un inventaire local factuel :
```text
format détecté
keypair valide / invalide / format non supporté
pubkey si valide
```
Un consommateur peut ensuite interroger un réseau donné avec cette pubkey et classifier l'état actuel de l'adresse, par exemple :
- compte absent ;
- compte système ;
- mint SPL Token ;
- mint Token-2022 ;
- compte token ;
- programme ou autre compte.
Cette classification reste réseau-dépendante et ne prouve pas tous les rôles historiques du signer.
`ks-wallet` ne doit pas dépendre de `ks-onchain-transport` pour réaliser cette classification.
## 10. Desktop de démonstration
`kb-app-demo-desktop` possède ses propres DTO Tauri et ne sérialise pas les types secrets de `ks-wallet`.
La fenêtre Wallets permet :
- inventaire des `.kswallet` de `wallets/` ;
- création d'un wallet natif avec un secret backend-only ;
- inspection d'un `.kswallet` externe ;
- inspection/import d'un keypair Solana CLI JSON ou Base58 sans exposer son secret au frontend ;
- export secret authentifié vers `data/wallets/` avec nom de fichier borné et sans chemin arbitraire côté frontend ;
- sélection explicite d'un profil RPC pour les lectures publiques ;
- consultation du solde SOL, des comptes SPL Token/Token-2022, des signatures récentes et du détail `getTransaction` ;
- sélection session-only du `.kswallet` utilisé par les démos Devnet.
Le mot de passe de démonstration est lu uniquement côté backend depuis :
```text
KB_SECRET_DEMO_WALLET_PASSWORD
```
La sélection runtime du wallet d'exécution est authentifiée avant activation et ne réécrit pas `wallet.config.json`. L'ordre de résolution est :
```text
override session demo_wallet
-> wallet_alias du profil
-> wallet temporaire uniquement si aucun alias persistant n'est sélectionné
```
Lorsqu'un alias persistant est effectif, les adaptateurs Devnet System/SPL/Metadata utilisent une capacité `UnlockedWallet` et ne retombent pas silencieusement sur le JSON temporaire. La validation opérateur `0.5.2` a observé `persistence="persistent"` avec la pubkey sélectionnée.
Le profil RPC choisi dans la fenêtre Wallets ne modifie ni le profil actif global ni le store wallet actif ; il borne uniquement les lectures on-chain de cette fenêtre.
### 10.1 Répertoires opérateur
La convention du desktop est :
```text
wallets/
<alias>.kswallet
temporary/<profil>/...
data/wallets/
<exports secrets explicites>
```
`wallets/` est la racine persistante native. `wallets/temporary/**` appartient aux fixtures/keypairs historiques. `data/wallets/` contient les exports explicites destinés à des outils externes et ne doit pas être rescanné automatiquement comme store natif.
### 10.2 Validation du changement de mot de passe
Le changement de mot de passe n'est volontairement pas exposé dans le frontend. La crate `ks-wallet-demo-scenarios` valide ce cycle comme consommateur externe de l'API publique :
```text
A -> B
A rejeté
B ouvre et signe
B -> A
A ouvre et signe
```
Le test utilise un `.kswallet` synthétique dans un répertoire temporaire privé et ne modifie aucun wallet réel de l'opérateur.
## 11. Non-divulgation
Ne jamais placer un secret wallet dans :
- `wallet.config.json` ;
- `KS_PUBLIC_*` ou `KB_PUBLIC_*` ;
- un payload Tauri ;
- un DTO TS-RS généraliste ;
- un log ;
- un message d'erreur normal ;
- un `Debug` automatique d'un type sensible.
Les chemins locaux ne sont pas des secrets cryptographiques, mais ils restent des informations internes et ne doivent pas apparaître inutilement dans les logs, erreurs et DTO fonctionnels.
## 12. Modèle de menace borné
`0.5.2` protège le secret persistant contre la lecture directe du fichier sans mot de passe et impose des permissions privées ainsi qu'un chiffrement authentifié.
Cette version ne prétend pas protéger contre :
- une machine déjà compromise pendant qu'un wallet est déverrouillé ;
- un processus disposant des mêmes droits utilisateur et capable de lire la mémoire ;
- la saisie volontaire du mot de passe dans un programme malveillant ;
- la copie volontaire d'un `.kswallet` et de son mot de passe vers une autre installation compatible ;
- hardware wallet, Ledger, KMS ou HSM.
## 13. Références
- [`../../ks-wallet/README.md`](../../ks-wallet/README.md)
- [`../../ks-wallet/USAGE.md`](../../ks-wallet/USAGE.md)
- [`../../ks-wallet-demo-scenarios/README.md`](../../ks-wallet-demo-scenarios/README.md)
- [`../NATIVE_FORMAT.md`](../NATIVE_FORMAT.md)
- [`../WALLET_FORMAT_COMPATIBILITY.md`](../WALLET_FORMAT_COMPATIBILITY.md)
- [`../validation/V0_5_2_WALLET_VALIDATION_REPORT.md`](../validation/V0_5_2_WALLET_VALIDATION_REPORT.md)

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/RULES_RUST.md -->
<!-- version: 8 -->
<!-- version: 9 -->
# Règles Rust générales
@@ -101,6 +101,7 @@ implicit_saturating_sub = "allow"
## Visibilité et API de crate
- Aucun `pub mod` n'est autorisé. Les modules restent privés et l'API est constituée exclusivement par des réexports explicites depuis le point dentrée de la crate.
- Aucun `pub(in ...)` ni `pub(super)` n'est autorisé : un élément partagé au-delà de son module est `pub(crate)`, réexporté au crate-root, puis consommé via `crate::Item`; sinon il reste strictement privé.
- Un élément `pub` inaccessible depuis le point d'entrée de sa crate est une erreur de conception, pas un simple avertissement.
- Un élément `pub(crate)` utilisé hors de son module est réexporté au niveau du point d'entrée de la crate.
- Les chemins internes de modules ne font pas partie de l'API stable.

View File

@@ -0,0 +1,185 @@
<!-- file: docs/validation/V0_5_2_WALLET_VALIDATION_REPORT.md -->
<!-- version: 3 -->
# Validation `ks-wallet` — clôture 0.5.2
## 1. Périmètre
`0.5.2` transforme `ks-wallet` en frontière Solana réutilisable pour :
- wallets temporaires en mémoire ;
- plusieurs wallets persistants accessibles par alias ;
- conteneur natif `.kswallet` binaire, versionné et protégé par mot de passe ;
- capacité de signature sans exposition des bytes privés ;
- migration/inspection legacy ;
- import/export Solana CLI JSON et Base58 ;
- sélection non sensible depuis `ks-config` ;
- validation intégrée indépendante de Tauri via `ks-wallet-demo-scenarios` ;
- intégration opérateur desktop via DTO applicatifs sûrs.
La version ne transforme pas `ks-wallet` en client RPC et ne migre pas en masse les keypairs de fixtures sous `wallets/temporary/**`.
## 2. Organisation des données
La séparation validée est :
```text
wallets/
<alias>.kswallet
temporary/<profil>/...
data/wallets/
<exports JSON/Base58 explicites>
```
- `wallets/` : store natif persistant ;
- `wallets/temporary/**` : fixtures/keypairs historiques ou jetables ;
- `data/wallets/` : exports secrets explicites destinés à des outils externes.
Le desktop n'utilise pas `data/wallets/` comme store natif automatique.
## 3. Format natif et sécurité
Le format v1 validé utilise :
| Contrat | Valeur |
|-------------------|------------------------------------|
| Extension | `.kswallet` |
| Magic | `KSWALLET` |
| KDF | Argon2id v19 |
| Profil par défaut | 64 MiB / 3 passes / 4 lanes |
| AEAD | XChaCha20-Poly1305 |
| Sel | 16 octets |
| Nonce | 24 octets |
| Plaintext | keypair Solana exacte de 64 octets |
| Ciphertext + tag | 80 octets |
| AAD | header + alias + sel + nonce |
| Répertoire Unix | `0700` |
| Fichier Unix | `0600` |
Le codec rejette versions/algorithmes inconnus, réserves non nulles, troncatures, suffixes, longueurs incorrectes et paramètres KDF hors bornes. La pubkey du header n'est considérée authentifiée qu'après déchiffrement et comparaison avec la pubkey réellement dérivée du keypair.
La prerelease de clôture retire également les chemins locaux inutiles du `Debug`, des logs et des erreurs du `TemporaryWalletStore` legacy et ajoute un canari externe de non-divulgation.
## 4. Cycle de vie du mot de passe
Les surfaces validées comprennent :
- création native ;
- unlock par alias ;
- unlock d'un fichier externe inspecté explicitement ;
- signature avec `UnlockedWallet` ;
- `lock()` par consommation ;
- changement de mot de passe avec conservation exacte de l'alias/pubkey ;
- refus de l'ancien mot de passe ;
- altération des données authentifiées refusée avant capacité de signature ;
- refus d'écrasement natif ;
- test concurrent de création d'un même alias avec un seul gagnant et une destination valide.
`ks-wallet-demo-scenarios` valide le cycle ordonné suivant dans un seul test indépendant de Tauri :
```text
A -> B
A doit échouer
B ouvre et signe
B -> A
A restauré ouvre et signe
```
La fixture est synthétique et temporaire ; aucun wallet réel de l'opérateur n'est modifié.
## 5. Migration et transferts
Les tests externes valident :
- migration legacy `<alias>.json` non destructive ;
- conservation exacte de la pubkey ;
- inspection d'un fichier de transfert sans import ;
- import/export `SolanaCliJson` ;
- import/export `SolanaPrivateKeyBase58` ;
- refus des sources invalides ;
- collision de pubkey ;
- export impossible avec un mauvais mot de passe ;
- permissions privées ;
- absence d'écrasement silencieux.
Aucune recovery phrase n'est synthétisée depuis une keypair arbitraire.
La validation opérateur a confirmé les exports :
```text
data/wallets/local-devnet-operator2.json
data/wallets/local-devnet-operator2.txt
```
## 6. Configuration et signer Devnet
`wallet.config.json` sépare `wallets_directory` de `profile.directory` :
```text
wallet_dir = wallets
temporary_wallet_dir = wallets/temporary/<profil>
```
Un profil peut définir `wallet_alias`, sans mot de passe. Le desktop peut aussi définir un override session-only par profil Devnet. La résolution effective est runtime override -> alias configuré -> temporaire seulement lorsqu'aucun alias persistant n'est sélectionné.
La validation opérateur a confirmé que les démos Devnet utilisent bien le `.kswallet` sélectionné avec une trace `persistence="persistent"`, au lieu du JSON temporaire.
## 7. Desktop Wallets
La fenêtre Wallets couvre : inventaire, création, inspection externe, inspection/import de keypairs, export borné sous `data/wallets/`, sélection de profil RPC, balance SOL, comptes SPL Token/Token-2022, signatures récentes, détail de transaction et sélection session-only du wallet d'exécution.
Les secrets restent backend-only via `KB_SECRET_DEMO_WALLET_PASSWORD`; aucun password n'entre dans HTML, TypeScript ou les DTO Tauri.
## 8. Validation workspace — prerelease de clôture `0.5.2-pre.007`
L'opérateur a validé :
```bash
cargo fmt --all
cargo check --workspace
cargo clippy --all-targets
python3 scripts/audit_rust_workspace_rules.py
cargo test --workspace
```
Résultats :
- `cargo check --workspace` : succès ;
- `cargo clippy --all-targets` : succès sans warning ;
- audit Rust général : clean ;
- export completeness : `0 candidate(s)` ;
- audit Khadhroony workspace : clean ;
- `cargo test --workspace` : succès sur toutes les crates et doc-tests ;
- `kb-app-demo-desktop` : 185 tests ;
- `ks-config` : 35 tests ;
- `ks-lib` : 606 tests ;
- `ks-logging` : 25 tests ;
- `ks-onchain-transport` : 124 tests ;
- `ks-pipeline` : 109 tests ;
- `ks-pipeline-demo-scenarios` : 109 tests + 2 tests CLI ;
- `ks-program-ids` : 6 tests ;
- `ks-store` : 84 tests ;
- `ks-wallet` : 22 unitaires + 8 legacy + 4 password natif + 2 API publique + 6 transfert ;
- `ks-wallet-demo-scenarios` : 1 test d'intégration password ;
- doc-tests : succès.
La `pre.007` de clôture, incluant les canaris de non-divulgation legacy, le test concurrent natif et la réconciliation documentaire finale, a été rejouée avec la campagne standard et validée intégralement. Le delta final `0.5.2` ne doit plus introduire de fonctionnalité : il borne le passage de version et la réconciliation documentaire de release.
## 9. Reports explicites
### `0.5.4`
- diagnostiquer/réparer la régression `unable to read configured Token-2022 fixture` observée après la séparation des racines wallet/fixtures ;
- scanner/inventorier bornément `wallets/temporary/**` ;
- extraire format/validité/pubkey sans inventer de rôle métier ;
- migrer sélectivement les signers devant devenir persistants ;
- laisser toute classification on-chain au consommateur, sans dépendance `ks-wallet -> ks-onchain-transport`.
### Version ultérieure non déterminée
- nouveaux formats Backpack, Trust Wallet, Solflare Keystore ou autres uniquement lorsqu'un wire suffisamment spécifié peut être validé et testé.
## 10. Statut
**Prerelease de clôture `0.5.2-pre.007` validée intégralement. La release `0.5.2` est prête pour le passage de version final, suivi de la campagne standard de validation avant commit/tag.**

View File

@@ -1,8 +1,114 @@
<!-- file: kb-app-demo-desktop/CHANGELOG.md -->
<!-- version: 52 -->
<!-- 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 ;
- ne réalise aucune refonte fonctionnelle de lUI Store : celle-ci reste réservée à la tranche consommateurs `pre.005`.
## `0.5.3-pre.003`
- adapte les vues de replay/journaux aux contrats N3 `MaterializedOutput*` et au journal `k_sol_mat_outputs` sans réintroduire de dépendance PostgreSQL dans les DTO desktop ;
- neutralise les derniers libellés visibles de sources physiques dans la fenêtre de candidats replay : l'UI décrit désormais les faits Core/logiques plutôt que les noms de tables PostgreSQL ;
- conserve la refonte fonctionnelle complète des compteurs/modèles du nouveau baseline pour la tranche consommateurs prévue par le plan.
## `0.5.3-pre.002-delta-fix-004`
- aligne les versions Tauri/Vite de l'application sur `0.5.3` dans `package.json` et `tauri.conf.json` ;
- remplace le groupe visible `SQL` par `Store` dans le menu principal et renomme les entrées backend-spécifiques en `Raw store` / `Core store` ;
- retire les titres de fenêtres `PostgreSQL ...` des surfaces `demo_store_*` et renomme le titre de la page de replay pour rester backend-agnostique ;
- réconcilie les rustdocs, les bindings TS-RS et la documentation d'usage des surfaces desktop afin qu'ils décrivent la façade `Store` et non PostgreSQL/SQL ;
- aligne les messages de préparation Devnet et le texte d'extraction Core sur les ressources/store génériques au lieu de tables ou transactions PostgreSQL.
## `0.5.3-pre.002-delta-fix-003`
- corrige `demo_core_extraction_execute`, qui reçoit déjà `&AppState` et ne doit donc pas appeler `state.inner()` avant de réutiliser le `Store` partagé ;
- rend `store_open_options_from_profile` strictement privé à `demo_store_common` et supprime son réexport crate-root devenu inutilisé.
## `0.5.3-pre.002-delta-fix-002`
- renomme les modules, commandes Tauri, fenêtres, pages HTML, TypeScript et DTO de `demo_sql_*` vers `demo_store_*` afin que la nomenclature ne dépende plus dun backend SQL ;
- conserve la refonte fonctionnelle finale des tableaux/compteurs pour `pre.005`.
## `0.5.3-pre.002`
- conserve un `ks_store::Store` backend-agnostique dans `AppState` via `OnceCell` et réutilise ce handle dans les commandes de backfill, extraction Core, replay et scénarios d'exécution ;
- remplace les payloads SQL/configuration dépendants de PostgreSQL par des descripteurs de connexion sanitisés et des diagnostics de ressources/modèles génériques ;
- adapte le splash à la vérification du store par modèle logique et compteurs de ressources, sans afficher les noms physiques des tables/index ;
- remplace le vocabulaire replay `outer` par `top-level` dans les nouveaux contrats UI, tout en conservant temporairement les noms techniques des fenêtres `demo_store_*` jusqu'à leur refonte finale.
## `0.5.2-pre.006-delta-fix-009`
- corrige les adaptateurs Tauri Devnet qui transmettaient directement `tauri::State<AppState>` aux helpers wallet/projection attendant `&AppState`; les commandes empruntent désormais explicitement `&state` pour System, SPL et les trois familles Metadata ;
- ne modifie ni la sélection runtime du `.kswallet`, ni les chemins dexport `data/wallets/`, ni les contrats de configuration introduits par les fixes précédents.
## `0.5.2-pre.006-delta-fix-008`
- ajoute dans `demo_wallet` une sélection session-only du `.kswallet` d'exécution par profil Devnet ; l'alias est authentifié côté backend avant activation, ne réécrit pas `wallet.config.json` et devient prioritaire sur `wallet_alias` pour les démos suivantes ;
- propage cette sélection runtime à tous les adaptateurs desktop System, Memo, ATA, SPL Token, Token-2022 et Metadata, tout en conservant le fallback temporaire uniquement lorsqu'aucun alias runtime ou configuré n'existe ;
- contraint les exports secrets de la démo à `./data/wallets/`, remplace le chemin arbitraire par un nom de fichier borné, impose l'extension du format et sécurise le répertoire en `0700` sur Unix ;
- affiche explicitement les alias configuré, runtime et effectif afin que la validation Devnet puisse confirmer le passage de `persistence="temporary"` à `persistence="persistent"`.
## `0.5.2-pre.006-delta-fix-005`
- résout côté backend le signer Devnet sélectionné avant les démos System, Memo, ATA, SPL Token, Token-2022, Solana Program Metadata, Token-2022 Metadata ainsi que toutes les campagnes Metaplex qualifiées, y compris leurs parcours multi-signers ;
- utilise `KB_SECRET_DEMO_WALLET_PASSWORD` uniquement lorsque `wallet_alias` sélectionne un `.kswallet`, sans transmettre le password à Tauri ou TypeScript ;
- conserve les répertoires de fixtures sous `temporary_wallet_dir` tandis que `demo_wallet` inventorie et importe désormais dans la racine persistante `wallet_dir`.
## `0.5.2-pre.006-delta-fix-004`
- complète la fenêtre `Wallets` avec inspection publique, import et export des deux formats `ks-wallet` réellement supportés : Solana CLI JSON 64 octets et keypair Base58 64 octets ;
- garde le password hors Tauri IPC : création, import et export utilisent uniquement `KB_SECRET_DEMO_WALLET_PASSWORD` côté backend ;
- permet d'inspecter un ancien fichier keypair sans le convertir, d'en extraire uniquement la pubkey puis de l'envoyer directement vers l'explorateur Devnet/Mainnet ;
- peuple les sélecteurs de formats depuis l'inventaire exposé par `ks-wallet` afin d'éviter une liste frontend indépendante du backend ;
- aligne visuellement `Profil RPC`, `Adresse publique` et `Transactions`, puis affiche le profil/cluster sélectionné sous la ligne de contrôles ;
- prépare la validation suivante où les démos d'exécution Devnet devront consommer une capacité `.kswallet` explicite au lieu de recréer directement leur wallet JSON temporaire.
## `0.5.2-pre.006-delta-fix-003`
- ajoute dans `Wallets` un sélecteur explicite de profil RPC pour lexplorateur on-chain ; le profil actif reste la valeur initiale mais Devnet/Mainnet peuvent être interrogés sans redémarrer lapplication ni modifier `active_profile` ;
- route `getBalance`, `getTokenAccountsByOwner`, `getSignaturesForAddress` et `getTransaction` à travers le profil sélectionné et non plus obligatoirement à travers le pool HTTP actif ;
- borne la commande RPC dédiée à la démo wallet aux trois lectures génériques nécessaires aux tokens et à lhistorique, tandis que le solde garde son adaptateur typé ;
- conserve la gestion locale des `.kswallet` sur le profil applicatif actif : la sélection RPC de lexplorateur ne déplace ni ne recrée les fichiers wallet.
## `0.5.2-pre.006-delta-fix-002`
- complète la fenêtre `Wallets` avec la création de `.kswallet` dans le store du profil actif ; le password est lu uniquement côté backend depuis `KB_SECRET_DEMO_WALLET_PASSWORD` et ne traverse jamais HTML, TypeScript ou les arguments Tauri ;
- ajoute un explorateur public on-chain réutilisant le transport HTTP configuré : solde SOL exact, comptes SPL Token et Token-2022, signatures récentes impliquant ladresse et détail `getTransaction` via le JsonViewer commun ;
- étend le rôle `http_queries` Devnet avec `get_signatures_for_address` afin que lexplorateur fonctionne sur `local_devnet` comme sur les profils Mainnet déjà dotés des rôles dhistorique ;
- ajoute des DTO TS-RS applicatifs sûrs, des canaris frontend/backend de non-divulgation et des tests statiques de câblage pour les nouvelles commandes.
## `0.5.2-pre.006-delta-fix-001`
- ajoute la fenêtre frontend `demo_wallet.html` et son contrôleur `demo_wallet.ts` sur le même gabarit Bootstrap/Simplebar que les autres démonstrations ;
- ajoute `open_demo_wallet_window`, l'entrée `Wallets` dans le menu `Démos` de `main.html`, l'entrée Vite multipage et la capability Tauri `demo_wallet` ;
- permet de rafraîchir l'inventaire sûr des `.kswallet` et d'inspecter explicitement un fichier externe par chemin sans afficher ce chemin dans le résultat ;
- ajoute un test statique de câblage frontend afin que la présence du backend `demo_wallet` ne puisse plus être validée sans fenêtre réellement ouvrable.
## `0.5.2-pre.006`
- ajoute des DTO TS-RS possédés par le desktop pour l'inventaire des wallets natifs : alias, pubkey déclarée, version et état de sélection uniquement ;
- ajoute des commandes Tauri lecture seule pour lister le store configuré et inspecter un `.kswallet` sélectionné explicitement hors store, sans retourner son chemin ;
- expose l'alias persistant sélectionné uniquement dans le diagnostic wallet borné et ajoute des canaris interdisant password, ciphertext, nonce, sel et chemins de stockage dans les DTO.
## `0.5.1-pre.008`
- Fix: remplace les snapshots `ks-onchain-transport` retournés directement par les commandes Tauri HTTP/WS par des DTO desktop sanitisés, fait supprimer lURL des snapshots backend et supprime `endpointUrl` des payloads dexécution/statut et ajoute des canaris empêchant toute URL résolue de traverser le frontend. Supprime également le chemin local du fixture Token-2022 des payloads fonctionnels et des erreurs affichées.

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/README.md -->
<!-- version: 15 -->
<!-- version: 24 -->
# kb-app-demo-desktop
@@ -8,8 +8,9 @@
## Fonctionnalités
- configuration et diagnostics de logging ;
- fenêtre Wallets dédiée : inventaire sûr des identités `.kswallet`, création dun conteneur persistant via un secret backend-only, sélection session-only du wallet d'exécution Devnet, inspection/import/export des formats keypair supportés sous `data/wallets/`, extraction sûre de pubkey depuis un keypair legacy, inspection explicite dun `.kswallet` externe et exploration publique du solde SOL, des comptes SPL Token / Token-2022 et des transactions récentes sur un profil RPC explicitement sélectionnable ;
- transport HTTP JSON-RPC et WebSocket ;
- diagnostics PostgreSQL ;
- diagnostics de stockage backend-agnostiques ;
- backfill, extraction Core et replay, avec enregistrement générique de Solana Program Metadata ;
- démonstrations Solana Core, SPL Memo, SPL Token, ATA et Token-2022 ;
- panneau Metadata avec parcours séparés Metaplex Token Metadata, Token-2022 Token Metadata et Solana Program Metadata, préparation du profil et de la base, simulation RPC réelle, soumission explicitement confirmée et affichage des postconditions ;
@@ -19,6 +20,8 @@
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 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.
Depuis `0.5.1-pre.008`, TS-RS appartient à cette frontière applicative : `ks-config` et `ks-lib` ne génèrent plus de bindings TypeScript. Lorsqu'un type généraliste doit être présenté au frontend, le desktop construit un DTO/wrapper dédié.

View File

@@ -1,10 +1,11 @@
<!-- file: kb-app-demo-desktop/TODO.md -->
<!-- version: 16 -->
<!-- version: 23 -->
# TODO — kb-app-demo-desktop
## Évolutions générales
- [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: 15 -->
<!-- version: 25 -->
# Utilisation de kb-app-demo-desktop
@@ -7,6 +7,71 @@
Lapplication expose une interface opérateur pour diagnostiquer, simuler et valider les couches de bot3.
## Surface wallet sûre
Le backend desktop peut lister les `.kswallet` du répertoire configuré, créer un wallet natif protégé, inspecter un fichier sélectionné explicitement ailleurs et explorer létat on-chain dune adresse publique. Les DTO frontend ne contiennent jamais de matériau secret. Aucun password, chemin interne du store, sel, nonce, ciphertext ou byte privé nest renvoyé par Tauri.
La fenêtre `Wallets`, accessible depuis le menu `Démos` de `main`, fournit maintenant sept surfaces :
- inventaire des wallets natifs du profil actif ;
- sélection session-only du `.kswallet` utilisé par les démos Devnet, sans réécrire la configuration ;
- création dun nouveau `.kswallet` dans le store du profil actif ;
- inspection et import d'un keypair externe dans un format supporté ;
- export authentifié d'un `.kswallet` vers un format supporté ;
- explorateur public on-chain ;
- inspection structurale dun `.kswallet` explicite situé hors du store.
### Créer un wallet natif depuis la démo
Le mot de passe de création ne doit pas traverser le frontend ou Tauri IPC. La démo lit uniquement côté backend le secret applicatif :
```bash
export KB_SECRET_DEMO_WALLET_PASSWORD='mot-de-passe-de-demo'
cargo tauri dev -c kb-app-demo-desktop/tauri.conf.json
```
Le secret peut aussi être placé dans le fichier `.env` local chargé par `ks-config`. Le vrai `.env` reste non versionné et ne doit jamais être archivé. LUI ne reçoit quun booléen indiquant si ce secret backend est configuré.
La création demande uniquement lalias. Elle crée `<alias>.kswallet` dans le répertoire wallet résolu du profil actif et ne modifie pas automatiquement `wallet_alias` dans la configuration. Un changement de valeur de `KB_SECRET_DEMO_WALLET_PASSWORD` avant une autre session permet dutiliser un autre mot de passe pour de nouveaux wallets ; ce secret de démonstration nest pas un mot de passe global du format `.kswallet`.
### Inspecter, importer et exporter les formats keypair
Les sélecteurs de format sont alimentés par `ks-wallet` et exposent actuellement :
- `Solana CLI keypair JSON (64 octets)` ;
- `Solana private key Base58 (64 octets)`.
L'action **Inspecter la keypair** ne crée aucun `.kswallet` : le backend valide le fichier avec le codec choisi et ne renvoie que la pubkey. Cette pubkey peut être envoyée directement à l'explorateur on-chain, ce qui permet par exemple de contrôler un ancien `wallets/temporary/local_devnet/*.json` avant de décider de le migrer.
L'action **Importer en .kswallet** demande en plus un alias destination et utilise `KB_SECRET_DEMO_WALLET_PASSWORD` uniquement côté backend. La source reste inchangée. L'export fonctionne dans le sens inverse : choix d'un alias natif, du format et d'un nom de fichier ; le backend authentifie le `.kswallet` avec le même secret de démonstration, refuse d'écraser un fichier existant et contraint la destination à `./data/wallets/`. Le répertoire d'export privé est créé avec des permissions `0700` sur Unix et `ks-wallet` conserve les fichiers exportés en `0600`.
Un keypair JSON de 64 octets ne contient pas son rôle métier. L'inspection peut dire « keypair Solana valide » et donner sa pubkey, mais elle ne peut pas décider localement si cette clé a servi de wallet opérateur, mint, authority ou autre fixture. Cette classification nécessite le contexte du scénario ou une lecture on-chain séparée.
### Sélectionner le `.kswallet` utilisé par les démos Devnet
La configuration persistante continue d'utiliser `wallet_alias`, mais la fenêtre Wallets permet également de choisir un alias pour un profil Devnet uniquement pendant la session desktop. Cette sélection est authentifiée immédiatement avec `KB_SECRET_DEMO_WALLET_PASSWORD`, ne réécrit aucun JSON et s'applique aux exécutions System, Memo, ATA, SPL Token, Token-2022 et Metadata suivantes.
Le resolver applique l'ordre suivant :
1. override session desktop pour le profil, lorsqu'il existe ;
2. `wallet_alias` du profil ;
3. wallet temporaire historique uniquement si les deux précédents sont absents.
Un override valide ne peut donc pas retomber silencieusement sur `temporary_wallet_alias`. Le log `ks-pipeline-demo-scenarios.wallet` doit alors afficher `persistence="persistent"` avec l'alias et la pubkey sélectionnés.
### Explorer une adresse sur un profil RPC sélectionné
Lexplorateur on-chain ne déverrouille aucun wallet : seule la pubkey publique est nécessaire. Le bouton `Explorer` dun wallet inventorié remplit ladresse automatiquement ; toute autre pubkey Solana peut également être saisie.
Les lectures utilisent le transport HTTP du profil choisi dans le sélecteur `Profil RPC` de la fenêtre Wallets. Le profil actif est sélectionné par défaut, mais `local_devnet`, `mainnet_research` ou tout autre profil HTTP résolu peut être choisi sans redémarrer lapplication :
- `getBalance` pour le solde SOL exact en lamports ;
- `getTokenAccountsByOwner` pour les comptes SPL Token classique et Token-2022 ;
- `getSignaturesForAddress` pour les signatures récentes des transactions qui référencent ladresse ;
- `getTransaction` à la demande pour afficher le détail JSON dune signature.
La liste est donc intitulée **transactions impliquant ladresse** : elle ne prétend pas que le wallet a initié ou signé chaque transaction retournée. Le profil `local_devnet` interroge Devnet ; `mainnet_research` ou `mainnet` interrogent Mainnet Beta. La sélection utilisée par lexplorateur est locale à cette fenêtre : elle ne modifie pas `active_profile`, la configuration du workspace ni le profil utilisé par les autres démos.
## Lancer le desktop
Depuis la racine du workspace :
@@ -51,14 +116,23 @@ Les autres fenêtres sont ouvertes par les commandes Tauri dédiées :
- backfill ;
- HTTP ;
- WebSocket ;
- diagnostics SQL ;
- diagnostics store ;
- candidats replay ;
- configuration ;
- wallets ;
- extraction Core ;
- decode replay ;
- 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.
@@ -68,6 +142,7 @@ Les commandes Tauri constituent linterface applicative frontend/backend. Elle
```typescript
import { invoke } from "@tauri-apps/api/core";
await invoke("open_demo_wallet_window");
await invoke("open_demo_http_window");
await invoke("open_demo_ws_window");
await invoke("open_demo_decode_replay_window");
@@ -244,22 +319,24 @@ Linventaire contient cinq variantes `Create -> Mint`, puis collection `Verify
Dans la colonne de résultats, `Préflight et opérations`, `Simulation et confirmation` et `Postconditions et preuves` sont présentés dans un accordéon commun. La carte `Exigences` reste sous cet accordéon et le journal dexécution reste sous la grille, sur toute la largeur.
### Diagnostics PostgreSQL
### Diagnostics du store
```typescript
const diagnostics = await invoke(
"load_demo_sql_diag"
"load_demo_store_diag"
);
const rawTables = await invoke(
"load_demo_sql_pg_raw"
"load_demo_store_raw"
);
const coreTables = await invoke(
"load_demo_sql_pg_core"
"load_demo_store_core"
);
console.log(diagnostics, rawTables, coreTables);
```
Les commandes/fenêtres historiques `demo_sql_*` sont renommées en `demo_store_*` dès `pre.002`, et leurs payloads sont backend-agnostiques : `connectionDescriptor`, `resources`, `backend`, `namespace` et compteurs logiques. Le frontend n'accède plus à un DSN PostgreSQL ni à des noms physiques de tables. La tranche consommateurs de `0.5.3` doit finaliser la présentation tabulaire de `demo_store_*`, aligner `demo_config` et exploiter les compteurs tables/index du nouveau baseline dans le splash.
## Capabilities Tauri
Les fenêtres autorisées sont listées dans `capabilities/default.json`. La capability active doit inclure au minimum les permissions Tauri Core et tracing nécessaires aux fenêtres déclarées.
@@ -283,7 +360,7 @@ Toute nouvelle fenêtre ou commande sensible doit être auditée avant ajout de
- tests de System Transfer et Memo ;
- tests ATA, SPL Token et Token-2022 ;
- tests de backfill, extraction Core et decode replay ;
- tests de diagnostics SQL et exports CSV ;
- tests de diagnostics store et exports CSV ;
- tests de journalisation frontend ;
- test du verrou détat applicatif et de la séquence splash.
@@ -301,7 +378,7 @@ Depuis la fenêtre principale, choisir **Exécution Devnet → Exécution Metada
- de choisir un scénario synthétique ou un contrat de campagne Devnet ;
- de sélectionner le profil Devnet ;
- de préparer et vérifier la base PostgreSQL du profil ;
- de préparer et vérifier le store persistant du profil ;
- dafficher les contrats, contrôles de préflight et preuves attendues dans le JSON viewer commun ;
- de restaurer le scénario sélectionné lors de la réouverture.
@@ -311,7 +388,7 @@ Les scénarios synthétiques restent disponibles pour inspecter les contrats san
### Campagnes Metaplex qualifiées
Ouvrir **Metaplex Token Metadata -> Campagnes Devnet qualifiées**, sélectionner un profil persistant et préparer PostgreSQL. Le sélecteur présente 11 campagnes : cinq familles `Create -> Mint`, collection, Print/Burn, lifecycle pNFT, escrow, maintenance et Use. Après confirmation opérateur, le desktop appelle le runner correspondant et affiche :
Ouvrir **Metaplex Token Metadata -> Campagnes Devnet qualifiées**, sélectionner un profil persistant et préparer le store du profil. Le sélecteur présente 11 campagnes : cinq familles `Create -> Mint`, collection, Print/Burn, lifecycle pNFT, escrow, maintenance et Use. Après confirmation opérateur, le desktop appelle le runner correspondant et affiche :
- la fixture et son setup ;
- les exécutions confirmées et les probes indisponibles dans leur ordre réel ;
@@ -322,7 +399,7 @@ La surface générique par opération reste une API avancée et ne doit pas êtr
### Campagne Token-2022 Token Metadata
Ouvrir laccordéon **Token-2022 Token Metadata**, sélectionner un profil Devnet persistant et préparer sa base PostgreSQL. Le panneau affiche le contrat fixe de cinq opérations :
Ouvrir laccordéon **Token-2022 Token Metadata**, sélectionner un profil Devnet persistant et préparer son store persistant. Le panneau affiche le contrat fixe de cinq opérations :
```text
Initialize -> UpdateField -> Emit -> RemoveKey -> UpdateAuthority

View File

@@ -6,15 +6,16 @@
"splash",
"main",
"demo_config",
"demo_wallet",
"demo_http",
"demo_ws",
"demo_backfill",
"demo_core_extraction",
"demo_decode_replay",
"demo_sql_diag",
"demo_sql_pg_raw",
"demo_sql_pg_core",
"demo_sql_replay_candidates",
"demo_store_diag",
"demo_store_raw",
"demo_store_core",
"demo_store_replay_candidates",
"demo_execution_solana_core",
"demo_execution_spl",
"demo_execution_metadata"

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/demo_core_extraction.html -->
<!-- version: 10 -->
<!-- version: 11 -->
<!DOCTYPE html>
<html lang="fr">
@@ -39,7 +39,7 @@
<div class="card shadow-sm border-0">
<div class="card-body">
<h1 class="h3 card-title">Extraction transactionnelle canonical vers core</h1>
<p class="text-body-secondary mb-0">Chaque transaction canonique sélectionnée est validée, normalisée dans les tables core et enregistrée dans le ledger de traitement au sein d'une seule transaction PostgreSQL.</p>
<p class="text-body-secondary mb-0">Chaque transaction canonique sélectionnée est validée, normalisée dans les tables core et enregistrée dans le ledger de traitement au sein d'une seule transaction de stockage atomique.</p>
</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/demo_decode_replay.html -->
<!-- version: 8 -->
<!-- version: 10 -->
<!DOCTYPE html>
<html lang="fr">
@@ -61,7 +61,7 @@
</div>
<div class="col-12 col-lg-6">
<label class="form-label" for="decodeReplaySignaturesTextarea">Signatures explicites, une par ligne</label>
<textarea id="decodeReplaySignaturesTextarea" class="form-control font-monospace" rows="7" placeholder="Copier depuis demo_sql_replay_candidates"></textarea>
<textarea id="decodeReplaySignaturesTextarea" class="form-control font-monospace" rows="7" placeholder="Copier depuis demo_store_replay_candidates"></textarea>
</div>
<div class="col-12 col-lg-6">
<label class="form-label" for="decodeReplayPathsTextarea">Instruction paths exacts, un par ligne</label>
@@ -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_sql_pg_core.html -->
<!-- version: 6 -->
<!-- file: kb-app-demo-desktop/frontend/demo_store_core.html -->
<!-- version: 9 -->
<!DOCTYPE html>
<html lang="fr">
@@ -8,7 +8,7 @@
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<link rel="icon" href="imgs/logo.png">
<link rel="stylesheet" href="sass/main.scss">
<title>Khadhroony Bot3 — PostgreSQL core store</title>
<title>Khadhroony Bot3 — Core store diagnostics</title>
</head>
<body>
@@ -17,7 +17,7 @@
<div class="container-fluid px-4 my-0">
<div class="navbar-brand d-flex align-items-center">
<img alt="Logo" src="imgs/logo.png" class="app-logo">
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot3 — <span class="badge text-bg-primary">PostgreSQL core store</span></span>
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot3 — <span class="badge text-bg-primary">Core store diagnostics</span></span>
</div>
</div>
</nav>
@@ -32,15 +32,15 @@
<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>
<dd class="col-sm-9"><code id="sqlCoreStatus">Chargement...</code></dd>
<dd class="col-sm-9"><code id="storeCoreStatus">Chargement...</code></dd>
<dt class="col-sm-3">Profil</dt>
<dd class="col-sm-9"><code id="sqlCoreProfile"></code></dd>
<dt class="col-sm-3">DSN</dt>
<dd class="col-sm-9"><code id="sqlCoreDsn"></code></dd>
<dd class="col-sm-9"><code id="storeCoreProfile"></code></dd>
<dt class="col-sm-3">Connexion</dt>
<dd class="col-sm-9"><code id="storeCoreConnection"></code></dd>
</dl>
</div>
</div>
@@ -49,13 +49,13 @@
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5 card-title mb-3">Tables core</h2>
<h2 class="h5 card-title mb-3">Ressources Core</h2>
<div class="table-responsive app-table-full-width">
<table class="table table-sm align-middle mb-0" id="sqlCoreTable">
<table class="table table-sm align-middle mb-0" id="storeCoreTable">
<thead>
<tr>
<th>Table</th>
<th>Domaine</th>
<th>Ressource</th>
<th>Modèle</th>
<th>Rôle</th>
<th>Existe</th>
<th>Lignes</th>
@@ -64,7 +64,7 @@
<th>Dernière insertion</th>
</tr>
</thead>
<tbody id="sqlCoreTableBody"></tbody>
<tbody id="storeCoreTableBody"></tbody>
</table>
</div>
</div>
@@ -75,7 +75,7 @@
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5 card-title mb-3">Payload JSON</h2>
<div id="sqlCoreJson" class="app-config-box app-json-viewer mb-0">{"status":"loading","message":"Chargement des données SQL."}</div>
<div id="storeCoreJson" class="app-config-box app-json-viewer mb-0">{"status":"loading","message":"Chargement des données du store."}</div>
</div>
</div>
</div>
@@ -91,7 +91,7 @@
</div>
</div>
</footer>
<script type="module" src="ts/demo_sql_pg_core.ts" defer></script>
<script type="module" src="ts/demo_store_core.ts" defer></script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/demo_sql_diag.html -->
<!-- version: 5 -->
<!-- file: kb-app-demo-desktop/frontend/demo_store_diag.html -->
<!-- version: 8 -->
<!DOCTYPE html>
<html lang="fr">
@@ -8,7 +8,7 @@
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<link rel="icon" href="imgs/logo.png">
<link rel="stylesheet" href="sass/main.scss">
<title>Khadhroony Bot3 — SQL diagnostics</title>
<title>Khadhroony Bot3 — Store diagnostics</title>
</head>
<body>
@@ -17,7 +17,7 @@
<div class="container-fluid px-4 my-0">
<div class="navbar-brand d-flex align-items-center">
<img alt="Logo" src="imgs/logo.png" class="app-logo">
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot3 — <span class="badge text-bg-primary">SQL diagnostics</span></span>
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot3 — <span class="badge text-bg-primary">Store diagnostics</span></span>
</div>
</div>
</nav>
@@ -31,29 +31,29 @@
<div class="card shadow-sm border-0">
<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">Diagnostic PostgreSQL</h1>
<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">
<dt class="col-sm-3">Statut</dt>
<dd class="col-sm-9"><code id="sqlDiagStatus">Chargement...</code></dd>
<dd class="col-sm-9"><code id="storeDiagStatus">Chargement...</code></dd>
<dt class="col-sm-3">Fichier config</dt>
<dd class="col-sm-9"><code id="sqlDiagConfigPath"></code></dd>
<dd class="col-sm-9"><code id="storeDiagConfigPath"></code></dd>
<dt class="col-sm-3">Profil</dt>
<dd class="col-sm-9"><code id="sqlDiagProfile"></code></dd>
<dd class="col-sm-9"><code id="storeDiagProfile"></code></dd>
<dt class="col-sm-3">Backend</dt>
<dd class="col-sm-9"><code id="sqlDiagBackend"></code></dd>
<dt class="col-sm-3">DSN</dt>
<dd class="col-sm-9"><code id="sqlDiagDsn"></code></dd>
<dd class="col-sm-9"><code id="storeDiagBackend"></code></dd>
<dt class="col-sm-3">Connexion</dt>
<dd class="col-sm-9"><code id="storeDiagConnection"></code></dd>
<dt class="col-sm-3">Schema courant</dt>
<dd class="col-sm-9"><code id="sqlDiagSchema"></code></dd>
<dd class="col-sm-9"><code id="storeDiagSchema"></code></dd>
<dt class="col-sm-3">Healthcheck</dt>
<dd class="col-sm-9"><code id="sqlDiagHealth"></code></dd>
<dd class="col-sm-9"><code id="storeDiagHealth"></code></dd>
<dt class="col-sm-3">Migrations</dt>
<dd class="col-sm-9"><code id="sqlDiagMigration"></code></dd>
<dd class="col-sm-9"><code id="storeDiagMigration"></code></dd>
</dl>
</div>
</div>
@@ -62,13 +62,13 @@
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5 card-title mb-3">Tables raw/core connues</h2>
<h2 class="h5 card-title mb-3">Ressources store connues</h2>
<div class="table-responsive app-table-full-width">
<table class="table table-sm align-middle mb-0" id="sqlDiagTable">
<table class="table table-sm align-middle mb-0" id="storeDiagTable">
<thead>
<tr>
<th>Table</th>
<th>Domaine</th>
<th>Ressource</th>
<th>Modèle</th>
<th>Rôle</th>
<th>Existe</th>
<th>Lignes</th>
@@ -77,7 +77,7 @@
<th>Dernière insertion</th>
</tr>
</thead>
<tbody id="sqlDiagTableBody"></tbody>
<tbody id="storeDiagTableBody"></tbody>
</table>
</div>
</div>
@@ -88,7 +88,7 @@
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5 card-title mb-3">Payload JSON</h2>
<div id="sqlDiagJson" class="app-config-box app-json-viewer mb-0">{"status":"loading","message":"Chargement des données SQL."}</div>
<div id="storeDiagJson" class="app-config-box app-json-viewer mb-0">{"status":"loading","message":"Chargement des données du store."}</div>
</div>
</div>
</div>
@@ -104,7 +104,7 @@
</div>
</div>
</footer>
<script type="module" src="ts/demo_sql_diag.ts" defer></script>
<script type="module" src="ts/demo_store_diag.ts" defer></script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/demo_sql_pg_raw.html -->
<!-- version: 7 -->
<!-- file: kb-app-demo-desktop/frontend/demo_store_raw.html -->
<!-- version: 10 -->
<!DOCTYPE html>
<html lang="fr">
@@ -8,7 +8,7 @@
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<link rel="icon" href="imgs/logo.png">
<link rel="stylesheet" href="sass/main.scss">
<title>Khadhroony Bot3 — PostgreSQL canonical acquisition</title>
<title>Khadhroony Bot3 — Raw store diagnostics</title>
</head>
<body>
@@ -17,7 +17,7 @@
<div class="container-fluid px-4 my-0">
<div class="navbar-brand d-flex align-items-center">
<img alt="Logo" src="imgs/logo.png" class="app-logo">
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot3 — <span class="badge text-bg-primary">PostgreSQL canonical acquisition</span></span>
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot3 — <span class="badge text-bg-primary">Raw store diagnostics</span></span>
</div>
</div>
</nav>
@@ -32,15 +32,15 @@
<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>
<dd class="col-sm-9"><code id="sqlRawStatus">Chargement...</code></dd>
<dd class="col-sm-9"><code id="storeRawStatus">Chargement...</code></dd>
<dt class="col-sm-3">Profil</dt>
<dd class="col-sm-9"><code id="sqlRawProfile"></code></dd>
<dt class="col-sm-3">DSN</dt>
<dd class="col-sm-9"><code id="sqlRawDsn"></code></dd>
<dd class="col-sm-9"><code id="storeRawProfile"></code></dd>
<dt class="col-sm-3">Connexion</dt>
<dd class="col-sm-9"><code id="storeRawConnection"></code></dd>
</dl>
</div>
</div>
@@ -51,11 +51,11 @@
<div class="card-body">
<h2 class="h5 card-title mb-3">Transactions canoniques et observations</h2>
<div class="table-responsive app-table-full-width">
<table class="table table-sm align-middle mb-0" id="sqlRawTable">
<table class="table table-sm align-middle mb-0" id="storeRawTable">
<thead>
<tr>
<th>Table</th>
<th>Domaine</th>
<th>Ressource</th>
<th>Modèle</th>
<th>Rôle</th>
<th>Existe</th>
<th>Lignes</th>
@@ -64,7 +64,7 @@
<th>Dernière insertion</th>
</tr>
</thead>
<tbody id="sqlRawTableBody"></tbody>
<tbody id="storeRawTableBody"></tbody>
</table>
</div>
</div>
@@ -75,7 +75,7 @@
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5 card-title mb-3">Payload JSON</h2>
<div id="sqlRawJson" class="app-config-box app-json-viewer mb-0">{"status":"loading","message":"Chargement des données SQL."}</div>
<div id="storeRawJson" class="app-config-box app-json-viewer mb-0">{"status":"loading","message":"Chargement des données du store."}</div>
</div>
</div>
</div>
@@ -91,7 +91,7 @@
</div>
</div>
</footer>
<script type="module" src="ts/demo_sql_pg_raw.ts" defer></script>
<script type="module" src="ts/demo_store_raw.ts" defer></script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/demo_sql_replay_candidates.html -->
<!-- version: 11 -->
<!-- file: kb-app-demo-desktop/frontend/demo_store_replay_candidates.html -->
<!-- version: 18 -->
<!DOCTYPE html>
<html lang="fr">
@@ -8,7 +8,7 @@
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<link rel="icon" href="imgs/logo.png">
<link rel="stylesheet" href="sass/main.scss">
<title>Khadhroony Bot3 — Candidats de replay SQL</title>
<title>Khadhroony Bot3 — Candidats de replay du store</title>
</head>
<body>
@@ -17,7 +17,7 @@
<div class="container-fluid px-4 my-0">
<div class="navbar-brand d-flex align-items-center">
<img alt="Logo" src="imgs/logo.png" class="app-logo">
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot3 — <span class="badge text-bg-primary">SQL replay candidates</span></span>
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot3 — <span class="badge text-bg-primary">Store replay candidates</span></span>
</div>
<div class="ms-auto d-flex align-items-center gap-2">
<span id="replayCandidateProfileBadge" class="badge text-bg-info">Profil —</span>
@@ -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 sans SQL libre. 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">DSN :</span>
<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>
@@ -68,7 +68,7 @@
<section class="tab-pane fade show active" id="transactionsTabPane" role="tabpanel" aria-labelledby="transactionsTabButton" tabindex="0">
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<h2 class="h5 card-title">Filtres PostgreSQL bornés</h2>
<h2 class="h5 card-title">Filtres store bornés</h2>
<div class="row g-3 align-items-end">
<div class="col-12 col-xl-4">
<label for="transactionSignatureContainsInput" class="form-label">Signature contient</label>
@@ -110,8 +110,8 @@
<div class="col-6 col-md-3 col-xl-2">
<label for="transactionProgramScopeSelect" class="form-label">Portée programme</label>
<select id="transactionProgramScopeSelect" class="form-select">
<option value="any">outer + inner + logs</option>
<option value="outer">outer uniquement</option>
<option value="any">top-level + inner + logs</option>
<option value="top_level">top-level uniquement</option>
<option value="inner">inner uniquement</option>
<option value="logs">logs liés uniquement</option>
</select>
@@ -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="100000" 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="100000" 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="100000" 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,7 +285,15 @@
<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="small text-body-secondary">Source : <code>kb_sol_core_balance_changes.mint</code>.</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>
<div class="btn-group flex-wrap" role="group" aria-label="Actions mints">
@@ -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="100000" 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,7 +347,15 @@
<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="small text-body-secondary">Source : <code>kb_sol_core_balance_changes.owner</code>.</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>
<div class="btn-group flex-wrap" role="group" aria-label="Actions owners">
@@ -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="100000" 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,7 +409,15 @@
<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="small text-body-secondary">Source : <code>kb_sol_core_account_keys.account_key</code>.</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>
<div class="btn-group flex-wrap" role="group" aria-label="Actions comptes">
@@ -441,7 +461,7 @@
</div>
</div>
</footer>
<script type="module" src="ts/demo_sql_replay_candidates.ts" defer></script>
<script type="module" src="ts/demo_store_replay_candidates.ts" defer></script>
</body>
</html>

View File

@@ -0,0 +1,331 @@
<!-- file: kb-app-demo-desktop/frontend/demo_wallet.html -->
<!-- version: 5 -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<link rel="icon" href="imgs/logo.png">
<link rel="stylesheet" href="sass/main.scss">
<title>Khadhroony Bot3 — Wallets</title>
</head>
<body>
<header class="app-header">
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
<div class="container-fluid px-4 my-0">
<div class="navbar-brand d-flex align-items-center">
<img alt="Logo" src="imgs/logo.png" class="app-logo">
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot3 — <span class="badge text-bg-primary">Wallets</span></span>
</div>
</div>
</nav>
</header>
<main class="app-main">
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
<div class="container-fluid px-4 py-4">
<div class="row g-4 justify-content-center">
<div class="col-12 col-xl-7">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
<div>
<h1 class="h3 card-title mb-1">Wallets natifs configurés</h1>
<p class="text-body-secondary mb-0">Inventaire des fichiers <code>.kswallet</code> du profil actif.</p>
</div>
<button id="refreshWalletInventoryButton" class="btn btn-primary" type="button">Rafraîchir</button>
</div>
<dl class="row mb-3">
<dt class="col-sm-3">Profil</dt>
<dd class="col-sm-9"><code id="walletProfile"></code></dd>
<dt class="col-sm-3">Cluster</dt>
<dd class="col-sm-9"><code id="walletCluster"></code></dd>
<dt class="col-sm-3">Statut</dt>
<dd class="col-sm-9"><code id="walletInventoryStatus">Chargement...</code></dd>
<dt class="col-sm-3">Alias sélectionné</dt>
<dd class="col-sm-9"><code id="walletSelectedAlias"></code></dd>
<dt class="col-sm-3">Wallets trouvés</dt>
<dd class="col-sm-9"><code id="walletInventoryCount">0</code></dd>
</dl>
<div class="border rounded p-3 mb-3">
<h2 class="h6 mb-2">Wallet d'exécution des démos Devnet</h2>
<p class="small text-body-secondary mb-3">La sélection est propre à cette session desktop. Elle ne modifie pas <code>wallet.config.json</code> et remplace <code>wallet_alias</code> uniquement pour les démos du profil Devnet choisi.</p>
<div class="row g-3 align-items-end">
<div class="col-12 col-lg-4">
<label for="walletExecutionProfileSelect" class="form-label">Profil Devnet</label>
<select id="walletExecutionProfileSelect" class="form-select"></select>
</div>
<div class="col-12 col-lg-5">
<label for="walletExecutionAliasSelect" class="form-label">Wallet d'exécution</label>
<select id="walletExecutionAliasSelect" class="form-select"></select>
</div>
<div class="col-12 col-lg-3">
<button id="applyWalletExecutionSelectionButton" class="btn btn-outline-primary w-100" type="button">Appliquer</button>
</div>
</div>
<dl class="row small mb-0 mt-3">
<dt class="col-sm-4">Configuration</dt>
<dd class="col-sm-8"><code id="walletExecutionConfiguredAlias"></code></dd>
<dt class="col-sm-4">Override session</dt>
<dd class="col-sm-8"><code id="walletExecutionRuntimeAlias"></code></dd>
<dt class="col-sm-4">Effectif</dt>
<dd class="col-sm-8"><code id="walletExecutionEffectiveAlias">temporaire</code></dd>
<dt class="col-sm-4">Statut</dt>
<dd class="col-sm-8"><code id="walletExecutionSelectionStatus">En attente</code></dd>
</dl>
</div>
<div class="table-responsive app-table-full-width">
<table class="table table-sm align-middle mb-0" id="walletInventoryTable">
<thead>
<tr>
<th>Alias</th>
<th>Pubkey déclarée</th>
<th>Format</th>
<th>Exécution</th>
<th>Action</th>
</tr>
</thead>
<tbody id="walletInventoryTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-12 col-xl-5">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h2 class="h5 card-title mb-3">Créer un wallet natif</h2>
<p class="text-body-secondary mb-2">Le mot de passe ne traverse jamais Tauri IPC. Cette démo utilise le secret backend <code>KB_SECRET_DEMO_WALLET_PASSWORD</code>.</p>
<div class="mb-3">
<span class="fw-semibold">Secret backend :</span>
<code id="walletCreateSecretStatus" class="ms-1"></code>
</div>
<label for="createWalletAliasInput" class="form-label">Alias</label>
<input id="createWalletAliasInput" class="form-control font-monospace" type="text" autocomplete="off" maxlength="64" placeholder="operator-devnet">
<div class="d-flex flex-wrap gap-2 mt-3">
<button id="createWalletButton" class="btn btn-primary" type="button">Créer</button>
</div>
<div class="mt-3">
<span class="fw-semibold">Statut :</span>
<code id="walletCreateStatus" class="ms-1">En attente</code>
</div>
<p class="small text-body-secondary mb-0 mt-3">La création n'écrit pas automatiquement <code>wallet_alias</code> dans la configuration. Elle ajoute seulement le nouveau conteneur au store du profil actif.</p>
</div>
</div>
</div>
<div class="col-12 col-xl-7">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h2 class="h5 card-title mb-2">Importer ou inspecter une keypair externe</h2>
<p class="text-body-secondary">Le fichier source reste inchangé. L'inspection valide la keypair et ne retourne que sa pubkey ; l'import crée ensuite un nouveau <code>.kswallet</code> dans le store du profil actif.</p>
<label for="walletImportSourcePathInput" class="form-label">Chemin source</label>
<input id="walletImportSourcePathInput" class="form-control font-monospace" type="text" autocomplete="off" placeholder="/chemin/vers/local-devnet-operator.json">
<div class="row g-3 mt-1">
<div class="col-12 col-md-6">
<label for="walletImportFormatSelect" class="form-label">Format source</label>
<select id="walletImportFormatSelect" class="form-select"></select>
</div>
<div class="col-12 col-md-6">
<label for="walletImportAliasInput" class="form-label">Alias destination</label>
<input id="walletImportAliasInput" class="form-control font-monospace" type="text" autocomplete="off" maxlength="64" placeholder="local-devnet-operator">
</div>
</div>
<div class="d-flex flex-wrap gap-2 mt-3">
<button id="inspectWalletTransferButton" class="btn btn-outline-primary" type="button">Inspecter la keypair</button>
<button id="importWalletTransferButton" class="btn btn-primary" type="button">Importer en .kswallet</button>
<button id="exploreWalletTransferButton" class="btn btn-outline-secondary" type="button" disabled>Explorer la pubkey</button>
</div>
<div class="mt-3">
<span class="fw-semibold">Pubkey détectée :</span>
<code id="walletTransferPublicKey" class="ms-1 text-break"></code>
</div>
<div class="mt-2">
<span class="fw-semibold">Statut :</span>
<code id="walletTransferStatus" class="ms-1">En attente</code>
</div>
</div>
</div>
</div>
<div class="col-12 col-xl-5">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h2 class="h5 card-title mb-2">Exporter un wallet natif</h2>
<p class="text-body-secondary">L'export exige le même secret backend que la création, refuse d'écraser un fichier existant et écrit uniquement dans <code>./data/wallets/</code>.</p>
<label for="walletExportAliasSelect" class="form-label">Wallet</label>
<select id="walletExportAliasSelect" class="form-select"></select>
<label for="walletExportFormatSelect" class="form-label mt-3">Format d'export</label>
<select id="walletExportFormatSelect" class="form-select"></select>
<label for="walletExportFileNameInput" class="form-label mt-3">Nom du fichier</label>
<div class="input-group">
<span id="walletExportDirectory" class="input-group-text font-monospace">data/wallets/</span>
<input id="walletExportFileNameInput" class="form-control font-monospace" type="text" autocomplete="off" maxlength="128" placeholder="operator.json">
</div>
<div class="d-flex flex-wrap gap-2 mt-3">
<button id="exportWalletTransferButton" class="btn btn-primary" type="button">Exporter</button>
</div>
<div class="mt-3">
<span class="fw-semibold">Statut :</span>
<code id="walletExportStatus" class="ms-1">En attente</code>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
<div>
<h2 class="h4 card-title mb-1">Explorateur on-chain</h2>
<p class="text-body-secondary mb-0">Lecture publique du solde SOL, des comptes SPL Token / Token-2022 et de l'historique récent impliquant l'adresse sur le profil RPC sélectionné.</p>
</div>
<button id="loadWalletOnchainButton" class="btn btn-primary" type="button">Charger l'état on-chain</button>
</div>
<div class="row g-3 align-items-end">
<div class="col-12 col-lg-3">
<label for="walletOnchainProfileSelect" class="form-label">Profil RPC</label>
<select id="walletOnchainProfileSelect" class="form-select"></select>
</div>
<div class="col-12 col-lg-7">
<label for="walletAddressInput" class="form-label">Adresse publique</label>
<input id="walletAddressInput" class="form-control font-monospace" type="text" autocomplete="off" placeholder="Pubkey Solana">
</div>
<div class="col-12 col-lg-2">
<label for="walletSignatureLimitInput" class="form-label">Transactions</label>
<input id="walletSignatureLimitInput" class="form-control" type="number" min="1" max="100" value="20">
</div>
</div>
<div class="small text-body-secondary mt-2">Profil sélectionné : <code id="walletOnchainSelectedProfile"></code> · Cluster : <code id="walletOnchainCluster"></code></div>
<div class="row g-3 mt-2">
<div class="col-12 col-md-4">
<div class="border rounded p-3 h-100">
<div class="small text-body-secondary">SOL</div>
<div class="fs-4 fw-semibold"><span id="walletSolBalance"></span> SOL</div>
<div class="small"><code id="walletLamportBalance"></code> lamports</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="border rounded p-3 h-100">
<div class="small text-body-secondary">Comptes token</div>
<div class="fs-4 fw-semibold" id="walletTokenCount"></div>
<div class="small" id="walletTokenStatus">En attente</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="border rounded p-3 h-100">
<div class="small text-body-secondary">Transactions récentes</div>
<div class="fs-4 fw-semibold" id="walletTransactionCount"></div>
<div class="small" id="walletTransactionStatus">En attente</div>
</div>
</div>
</div>
<div class="mt-3">
<span class="fw-semibold">Statut global :</span>
<code id="walletOnchainStatus" class="ms-1">En attente</code>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5 card-title mb-3">Tokens détenus</h2>
<div class="table-responsive app-table-full-width">
<table class="table table-sm align-middle mb-0">
<thead>
<tr>
<th>Programme</th>
<th>Mint</th>
<th>Compte token</th>
<th>Montant</th>
<th>Raw</th>
</tr>
</thead>
<tbody id="walletTokensTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5 card-title mb-3">Transactions impliquant l'adresse</h2>
<div class="table-responsive app-table-full-width">
<table class="table table-sm align-middle mb-0">
<thead>
<tr>
<th>Signature</th>
<th>Slot</th>
<th>Date</th>
<th>Statut</th>
<th>Confirmation</th>
<th>Action</th>
</tr>
</thead>
<tbody id="walletTransactionsTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5 card-title mb-3">Détail de transaction</h2>
<div id="walletTransactionJson" class="app-config-box app-json-viewer mb-0">{"status":"idle","message":"Sélectionner une transaction."}</div>
</div>
</div>
</div>
<div class="col-12 col-xl-5">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h2 class="h5 card-title mb-3">Inspecter un fichier externe</h2>
<p class="text-body-secondary">Le chemin saisi est transmis au backend uniquement pour cette inspection et n'est pas retourné dans le résultat.</p>
<label for="walletExternalPathInput" class="form-label">Chemin du fichier <code>.kswallet</code></label>
<input id="walletExternalPathInput" class="form-control font-monospace" type="text" autocomplete="off" placeholder="/chemin/vers/operator.kswallet">
<div class="d-flex flex-wrap gap-2 mt-3">
<button id="inspectWalletFileButton" class="btn btn-primary" type="button">Inspecter</button>
<button id="clearWalletInspectionButton" class="btn btn-outline-secondary" type="button">Effacer</button>
</div>
<div class="mt-3">
<span class="fw-semibold">Statut :</span>
<code id="walletInspectionStatus" class="ms-1">En attente</code>
</div>
</div>
</div>
</div>
<div class="col-12 col-xl-7">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h2 class="h5 card-title mb-3">Identité inspectée</h2>
<div id="walletInspectionJson" class="app-config-box app-json-viewer mb-0">{"status":"idle","message":"Aucun fichier inspecté."}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<footer class="app-footer bg-dark text-light">
<div class="container h-100 d-flex align-items-center">
<div class="row flex-grow-1 align-items-center">
<div class="col-12 text-center text-small my-1 my-md-0">&copy; 2026 SASEDEV</div>
</div>
</div>
</footer>
<script type="module" src="ts/demo_wallet.ts" defer></script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/main.html -->
<!-- version: 11 -->
<!-- version: 14 -->
<!DOCTYPE html>
<html lang="fr">
@@ -26,6 +26,7 @@
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a id="openDemoConfigLink" class="dropdown-item" href="#">Configuration</a></li>
<li><a id="openDemoWalletLink" class="dropdown-item" href="#">Wallets</a></li>
<li>
<hr class="dropdown-divider">
</li>
@@ -47,12 +48,12 @@
<hr class="dropdown-divider">
</li>
<li>
<h6 class="dropdown-header">SQL</h6>
<h6 class="dropdown-header">Store</h6>
</li>
<li><a id="openDemoSqlDiagLink" class="dropdown-item" href="#">SQL diagnostics</a></li>
<li><a id="openDemoSqlPgRawLink" class="dropdown-item" href="#">SQL PostgreSQL raw</a></li>
<li><a id="openDemoSqlPgCoreLink" class="dropdown-item" href="#">SQL PostgreSQL core</a></li>
<li><a id="openDemoSqlReplayCandidatesLink" class="dropdown-item" href="#">SQL replay candidates</a></li>
<li><a id="openDemoStoreDiagLink" class="dropdown-item" href="#">Store diagnostics</a></li>
<li><a id="openDemoStoreRawLink" class="dropdown-item" href="#">Raw store</a></li>
<li><a id="openDemoStoreCoreLink" class="dropdown-item" href="#">Core store</a></li>
<li><a id="openDemoStoreReplayCandidatesLink" class="dropdown-item" href="#">Store replay candidates</a></li>
<li>
<hr class="dropdown-divider">
</li>

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_core_extraction.ts
// version: 6
// version: 7
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -127,7 +127,7 @@ async function cancelCoreExtraction(): Promise<void> {
}
async function openDiagnostics(command: "open_demo_sql_pg_raw_window" | "open_demo_sql_pg_core_window" | "open_demo_sql_replay_candidates_window", label: string): Promise<void> {
async function openDiagnostics(command: "open_demo_store_raw_window" | "open_demo_store_core_window" | "open_demo_store_replay_candidates_window", label: string): Promise<void> {
try {
await invoke(command);
appendLog({ timestamp: new Date().toISOString(), level: "info", message: `${label} ouvert.` });
@@ -166,13 +166,13 @@ document.addEventListener("DOMContentLoaded", () => {
});
});
element<HTMLButtonElement>("#openRawDiagnosticsButton").addEventListener("click", () => {
void openDiagnostics("open_demo_sql_pg_raw_window", "Diagnostic raw");
void openDiagnostics("open_demo_store_raw_window", "Diagnostic raw");
});
element<HTMLButtonElement>("#openCoreDiagnosticsButton").addEventListener("click", () => {
void openDiagnostics("open_demo_sql_pg_core_window", "Diagnostic core");
void openDiagnostics("open_demo_store_core_window", "Diagnostic core");
});
element<HTMLButtonElement>("#openReplayCandidatesButton").addEventListener("click", () => {
void openDiagnostics("open_demo_sql_replay_candidates_window", "Sélecteur de candidats replay");
void openDiagnostics("open_demo_store_replay_candidates_window", "Sélecteur de candidats replay");
});
element<HTMLButtonElement>("#cancelCoreExtractionButton").addEventListener("click", () => {
void cancelCoreExtraction();

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_decode_replay.ts
// version: 7
// 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);
@@ -204,7 +223,7 @@ async function loadDiagnostics(): Promise<void> {
try {
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", "Invoke demo_decode_replay_diagnostics");
const diagnostics = await invoke<DemoDecodeDiagnosticsPayload>("demo_decode_replay_diagnostics");
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_diagnostics completed tables=${diagnostics.tables.length} coverage=${diagnostics.coverage.length}`);
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_diagnostics completed resources=${diagnostics.resources.length} coverage=${diagnostics.coverage.length}`);
element<HTMLTextAreaElement>("#decodeReplayDiagnosticsOutput").value = JSON.stringify(diagnostics, null, 2);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
@@ -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,24 +284,54 @@ 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 openWindow(command: "open_demo_sql_pg_core_window" | "open_demo_sql_replay_candidates_window"): Promise<void> {
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}`);
await invoke(command);
@@ -357,9 +406,16 @@ 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>("#openReplayCandidatesFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_sql_replay_candidates_window"));
element<HTMLButtonElement>("#openCoreDiagnosticsFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_sql_pg_core_window"));
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));
void loadOptions().catch(caughtError => {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);

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_solana_core.ts
// version: 10
// version: 11
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -20,18 +20,18 @@ const logLines: string[] = [];
let running = false;
let profileStoreReady = false;
let profileOptions: DemoExecutionSolanaCoreProfileOption[] = [];
function element<T extends HTMLElement>(selector: string): T { const value=document.querySelector<T>(selector); if(!value){throw new Error(`Missing UI element: ${selector}`);} return value; }
function integerValue(selector: string): number { const value=Number.parseInt(element<HTMLInputElement>(selector).value,10); if(!Number.isFinite(value)||value<0){throw new Error(`Valeur numérique invalide pour ${selector}`);} return value; }
function selectedProfile(): DemoExecutionSolanaCoreProfileOption|null { const name=element<HTMLSelectElement>("#executionProfileSelect").value; return profileOptions.find(profile=>profile.name===name)??null; }
function appendLog(payload: DemoExecutionSolanaCoreProgressPayload|{timestamp:string;level:string;stage:string;message:string;signature?:string|null}):void { const signature=payload.signature?` signature=${payload.signature}`:""; logLines.push(`${payload.timestamp} ${payload.level.toUpperCase()} [${payload.stage}] ${payload.message}${signature}`); const output=element<HTMLTextAreaElement>("#executionLogOutput"); output.value=logLines.join("\n"); output.scrollTop=output.scrollHeight; }
function setRunning(value:boolean):void { running=value; const profile=selectedProfile(); element<HTMLButtonElement>("#simulateExecutionButton").disabled=value||!profile||!profileStoreReady; element<HTMLButtonElement>("#submitExecutionButton").disabled=value||!profile||!profileStoreReady||!profile.sendEnabled; element<HTMLButtonElement>("#generateRecipientButton").disabled=value; element<HTMLButtonElement>("#cancelExecutionButton").disabled=!value; const badge=element<HTMLElement>("#executionStatusBadge"); badge.textContent=value?"Exécution en cours":"Prêt"; badge.className=value?"badge text-bg-warning":"badge text-bg-success"; }
function updateProfileHelp():void { const profile=selectedProfile(); const help=element<HTMLElement>("#executionProfileHelp"); if(!profile){help.textContent="Aucun profil Devnet compatible.";return;} help.textContent=`wallet=${profile.walletAlias}, spendMax=${profile.maxSpendLamports}, airdropMax=${profile.maxAirdropLamports}, send=${profile.sendEnabled?"enabled":"disabled"}`; element<HTMLInputElement>("#executionAirdropInput").max=String(profile.maxAirdropLamports); element<HTMLInputElement>("#executionLamportsInput").max=String(profile.maxSpendLamports); setRunning(running); }
function buildRequest(submit:boolean):DemoExecutionSolanaCoreRequest { const recipient=element<HTMLInputElement>("#executionRecipientInput").value.trim(); if(recipient.length===0){throw new Error("Le destinataire est obligatoire.");} return {profileName:element<HTMLSelectElement>("#executionProfileSelect").value,recipient,lamports:integerValue("#executionLamportsInput"),airdropLamports:integerValue("#executionAirdropInput"),submit,operatorConfirmed:element<HTMLInputElement>("#executionOperatorConfirmedInput").checked,forcePostValidationReplay:element<HTMLInputElement>("#executionForceReplayInput").checked,materializeAfterDecode:element<HTMLInputElement>("#executionMaterializeInput").checked}; }
function titledJson(title:string,value:string):string { try { const parsed:unknown=JSON.parse(value); if(parsed!==null&&typeof parsed==="object"&&!Array.isArray(parsed)){return JSON.stringify({title,...parsed},null,2);} return JSON.stringify({title,value:parsed},null,2); } catch { return JSON.stringify({title,status:"invalid_json",raw:value},null,2); } }
function displaySummary(summary:DemoExecutionSolanaCoreSummaryPayload):void { element<HTMLTextAreaElement>("#executionSummaryOutput").value=JSON.stringify({title:"Résumé",...summary},null,2); element<HTMLTextAreaElement>("#executionPlanOutput").value=titledJson("Plan exact",summary.planJson); element<HTMLTextAreaElement>("#executionSimulationOutput").value=titledJson("Simulation exacte",summary.simulationJson); element<HTMLTextAreaElement>("#executionDiagnosticsOutput").value=titledJson("Confirmation et replay",summary.diagnosticsJson); }
function displayExecutionFailure(message:string,submit:boolean):void { element<HTMLTextAreaElement>("#executionSummaryOutput").value=JSON.stringify({title:"Résumé",status:"error",message},null,2); element<HTMLTextAreaElement>("#executionPlanOutput").value=JSON.stringify({title:"Plan exact",status:"unavailable",reason:"preflight_failed",message:"Aucun plan exécutable na été produit.",error:message},null,2); element<HTMLTextAreaElement>("#executionSimulationOutput").value=JSON.stringify({title:"Simulation exacte",status:"not_started",reason:"preflight_failed",message:"La simulation na pas été lancée.",error:message},null,2); element<HTMLTextAreaElement>("#executionDiagnosticsOutput").value=JSON.stringify({title:"Confirmation et replay",status:"not_started",reason:"execution_failed_before_submission",message:submit?"La signature, lenvoi, la confirmation et le replay nont pas été exécutés.":"La confirmation et le replay ne sappliquent pas à cette simulation refusée.",error:message},null,2); appendLog({timestamp:new Date().toISOString(),level:"error",stage:"failed",message}); }
async function execute(submit:boolean):Promise<void>{if(running){return;}try{const request=buildRequest(submit);if(submit&&!request.operatorConfirmed){const message="La confirmation opérateur explicite est obligatoire avant signature et envoi.";displayExecutionFailure(message,submit);frontendWarn("kb-app-demo-desktop.frontend.demo_execution_solana_core","Submission blocked: operator confirmation missing");return;}setRunning(true);const summary=await invoke<DemoExecutionSolanaCoreSummaryPayload>("demo_execution_solana_core_execute",{request});displaySummary(summary);}catch(caughtError){const message=caughtError instanceof Error?caughtError.message:String(caughtError);displayExecutionFailure(message,submit);frontendError("kb-app-demo-desktop.frontend.demo_execution_solana_core",`Execution failed: ${message}`);}finally{setRunning(false);}}
function element<T extends HTMLElement>(selector: string): T { const value = document.querySelector<T>(selector); if (!value) { throw new Error(`Missing UI element: ${selector}`); } return value; }
function integerValue(selector: string): number { const value = Number.parseInt(element<HTMLInputElement>(selector).value, 10); if (!Number.isFinite(value) || value < 0) { throw new Error(`Valeur numérique invalide pour ${selector}`); } return value; }
function selectedProfile(): DemoExecutionSolanaCoreProfileOption | null { const name = element<HTMLSelectElement>("#executionProfileSelect").value; return profileOptions.find(profile => profile.name === name) ?? null; }
function appendLog(payload: DemoExecutionSolanaCoreProgressPayload | { timestamp: string; level: string; stage: string; message: string; signature?: string | null }): void { const signature = payload.signature ? ` signature=${payload.signature}` : ""; logLines.push(`${payload.timestamp} ${payload.level.toUpperCase()} [${payload.stage}] ${payload.message}${signature}`); const output = element<HTMLTextAreaElement>("#executionLogOutput"); output.value = logLines.join("\n"); output.scrollTop = output.scrollHeight; }
function setRunning(value: boolean): void { running = value; const profile = selectedProfile(); element<HTMLButtonElement>("#simulateExecutionButton").disabled = value || !profile || !profileStoreReady; element<HTMLButtonElement>("#submitExecutionButton").disabled = value || !profile || !profileStoreReady || !profile.sendEnabled; element<HTMLButtonElement>("#generateRecipientButton").disabled = value; element<HTMLButtonElement>("#cancelExecutionButton").disabled = !value; const badge = element<HTMLElement>("#executionStatusBadge"); badge.textContent = value ? "Exécution en cours" : "Prêt"; badge.className = value ? "badge text-bg-warning" : "badge text-bg-success"; }
function updateProfileHelp(): void { const profile = selectedProfile(); const help = element<HTMLElement>("#executionProfileHelp"); if (!profile) { help.textContent = "Aucun profil Devnet compatible."; return; } help.textContent = `wallet=${profile.walletAlias}, spendMax=${profile.maxSpendLamports}, airdropMax=${profile.maxAirdropLamports}, send=${profile.sendEnabled ? "enabled" : "disabled"}`; element<HTMLInputElement>("#executionAirdropInput").max = String(profile.maxAirdropLamports); element<HTMLInputElement>("#executionLamportsInput").max = String(profile.maxSpendLamports); setRunning(running); }
function buildRequest(submit: boolean): DemoExecutionSolanaCoreRequest { const recipient = element<HTMLInputElement>("#executionRecipientInput").value.trim(); if (recipient.length === 0) { throw new Error("Le destinataire est obligatoire."); } return { profileName: element<HTMLSelectElement>("#executionProfileSelect").value, recipient, lamports: integerValue("#executionLamportsInput"), airdropLamports: integerValue("#executionAirdropInput"), submit, operatorConfirmed: element<HTMLInputElement>("#executionOperatorConfirmedInput").checked, forcePostValidationReplay: element<HTMLInputElement>("#executionForceReplayInput").checked, materializeAfterDecode: element<HTMLInputElement>("#executionMaterializeInput").checked }; }
function titledJson(title: string, value: string): string { try { const parsed: unknown = JSON.parse(value); if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) { return JSON.stringify({ title, ...parsed }, null, 2); } return JSON.stringify({ title, value: parsed }, null, 2); } catch { return JSON.stringify({ title, status: "invalid_json", raw: value }, null, 2); } }
function displaySummary(summary: DemoExecutionSolanaCoreSummaryPayload): void { element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify({ title: "Résumé", ...summary }, null, 2); element<HTMLTextAreaElement>("#executionPlanOutput").value = titledJson("Plan exact", summary.planJson); element<HTMLTextAreaElement>("#executionSimulationOutput").value = titledJson("Simulation exacte", summary.simulationJson); element<HTMLTextAreaElement>("#executionDiagnosticsOutput").value = titledJson("Confirmation et replay", summary.diagnosticsJson); }
function displayExecutionFailure(message: string, submit: boolean): void { element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify({ title: "Résumé", status: "error", message }, null, 2); element<HTMLTextAreaElement>("#executionPlanOutput").value = JSON.stringify({ title: "Plan exact", status: "unavailable", reason: "preflight_failed", message: "Aucun plan exécutable na été produit.", error: message }, null, 2); element<HTMLTextAreaElement>("#executionSimulationOutput").value = JSON.stringify({ title: "Simulation exacte", status: "not_started", reason: "preflight_failed", message: "La simulation na pas été lancée.", error: message }, null, 2); element<HTMLTextAreaElement>("#executionDiagnosticsOutput").value = JSON.stringify({ title: "Confirmation et replay", status: "not_started", reason: "execution_failed_before_submission", message: submit ? "La signature, lenvoi, la confirmation et le replay nont pas été exécutés." : "La confirmation et le replay ne sappliquent pas à cette simulation refusée.", error: message }, null, 2); appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "failed", message }); }
async function execute(submit: boolean): Promise<void> { if (running) { return; } try { const request = buildRequest(submit); if (submit && !request.operatorConfirmed) { const message = "La confirmation opérateur explicite est obligatoire avant signature et envoi."; displayExecutionFailure(message, submit); frontendWarn("kb-app-demo-desktop.frontend.demo_execution_solana_core", "Submission blocked: operator confirmation missing"); return; } setRunning(true); const summary = await invoke<DemoExecutionSolanaCoreSummaryPayload>("demo_execution_solana_core_execute", { request }); displaySummary(summary); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); displayExecutionFailure(message, submit); frontendError("kb-app-demo-desktop.frontend.demo_execution_solana_core", `Execution failed: ${message}`); } finally { setRunning(false); } }
async function prepareSelectedProfile():Promise<void>{const profile=selectedProfile();profileStoreReady=false;setRunning(running);if(!profile){return;}appendLog({timestamp:new Date().toISOString(),level:"info",stage:"store_prepare",message:`Vérification PostgreSQL du profil ${profile.name}.`});try{const readiness=await invoke<DemoExecutionDevnetStoreReadinessPayload>("demo_execution_devnet_prepare_profile",{profileName:profile.name});profileStoreReady=true;appendLog({timestamp:new Date().toISOString(),level:"info",stage:"store_ready",message:`PostgreSQL prêt: ${readiness.existingTablesAfter}/${readiness.expectedTables} tables, créées=${readiness.createdTables}, autoInit=${readiness.autoInitializeSchema}.`});}catch(caughtError){const message=caughtError instanceof Error?caughtError.message:String(caughtError);frontendError("kb-app-demo-desktop.frontend.demo_execution_solana_core",`Devnet store preparation failed: ${message}`);}finally{setRunning(running);}}
async function loadOptions():Promise<void>{const options=await invoke<DemoExecutionSolanaCoreOptionsPayload>("demo_execution_solana_core_options");profileOptions=options.profiles;const select=element<HTMLSelectElement>("#executionProfileSelect");select.replaceChildren();for(const profile of profileOptions){const option=document.createElement("option");option.value=profile.name;option.textContent=profile.name;option.selected=profile.name===options.defaultProfileName;select.append(option);}element<HTMLInputElement>("#executionLamportsInput").value=String(options.defaultTransferLamports);updateProfileHelp();setRunning(options.running);await prepareSelectedProfile();}
document.addEventListener("DOMContentLoaded",async()=>{installFrontendConsoleBridge("kb-app-demo-desktop.frontend.demo_execution_solana_core");frontendDebug("kb-app-demo-desktop.frontend.demo_execution_solana_core","Solana Core execution window loaded");await listen<DemoExecutionSolanaCoreProgressPayload>("demo-execution-solana-core-progress",event=>appendLog(event.payload));element<HTMLSelectElement>("#executionProfileSelect").addEventListener("change",()=>{updateProfileHelp();void prepareSelectedProfile();});element<HTMLButtonElement>("#generateRecipientButton").addEventListener("click",async()=>{const payload=await invoke<DemoExecutionSolanaCoreGeneratedRecipientPayload>("demo_execution_solana_core_generate_recipient");element<HTMLInputElement>("#executionRecipientInput").value=payload.publicKey;});element<HTMLButtonElement>("#simulateExecutionButton").addEventListener("click",()=>void execute(false));element<HTMLButtonElement>("#submitExecutionButton").addEventListener("click",()=>void execute(true));element<HTMLButtonElement>("#cancelExecutionButton").addEventListener("click",()=>void invoke("demo_execution_solana_core_cancel"));element<HTMLTextAreaElement>("#executionLogOutput").addEventListener("app-log-clear",()=>{logLines.length=0;});try{await loadOptions();}catch(caughtError){const message=caughtError instanceof Error?caughtError.message:String(caughtError);frontendError("kb-app-demo-desktop.frontend.demo_execution_solana_core",`Options loading failed: ${message}`);}});
async function prepareSelectedProfile(): Promise<void> { const profile = selectedProfile(); profileStoreReady = false; setRunning(running); if (!profile) { return; } appendLog({ timestamp: new Date().toISOString(), level: "info", stage: "store_prepare", message: `Vérification du store du profil ${profile.name}.` }); try { const readiness = await invoke<DemoExecutionDevnetStoreReadinessPayload>("demo_execution_devnet_prepare_profile", { profileName: profile.name }); 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) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); frontendError("kb-app-demo-desktop.frontend.demo_execution_solana_core", `Devnet store preparation failed: ${message}`); } finally { setRunning(running); } }
async function loadOptions(): Promise<void> { const options = await invoke<DemoExecutionSolanaCoreOptionsPayload>("demo_execution_solana_core_options"); profileOptions = options.profiles; const select = element<HTMLSelectElement>("#executionProfileSelect"); select.replaceChildren(); for (const profile of profileOptions) { const option = document.createElement("option"); option.value = profile.name; option.textContent = profile.name; option.selected = profile.name === options.defaultProfileName; select.append(option); } element<HTMLInputElement>("#executionLamportsInput").value = String(options.defaultTransferLamports); updateProfileHelp(); setRunning(options.running); await prepareSelectedProfile(); }
document.addEventListener("DOMContentLoaded", async () => { installFrontendConsoleBridge("kb-app-demo-desktop.frontend.demo_execution_solana_core"); frontendDebug("kb-app-demo-desktop.frontend.demo_execution_solana_core", "Solana Core execution window loaded"); await listen<DemoExecutionSolanaCoreProgressPayload>("demo-execution-solana-core-progress", event => appendLog(event.payload)); element<HTMLSelectElement>("#executionProfileSelect").addEventListener("change", () => { updateProfileHelp(); void prepareSelectedProfile(); }); element<HTMLButtonElement>("#generateRecipientButton").addEventListener("click", async () => { const payload = await invoke<DemoExecutionSolanaCoreGeneratedRecipientPayload>("demo_execution_solana_core_generate_recipient"); element<HTMLInputElement>("#executionRecipientInput").value = payload.publicKey; }); element<HTMLButtonElement>("#simulateExecutionButton").addEventListener("click", () => void execute(false)); element<HTMLButtonElement>("#submitExecutionButton").addEventListener("click", () => void execute(true)); element<HTMLButtonElement>("#cancelExecutionButton").addEventListener("click", () => void invoke("demo_execution_solana_core_cancel")); element<HTMLTextAreaElement>("#executionLogOutput").addEventListener("app-log-clear", () => { logLines.length = 0; }); try { await loadOptions(); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); frontendError("kb-app-demo-desktop.frontend.demo_execution_solana_core", `Options loading failed: ${message}`); } });

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_execution_spl.ts
// version: 10
// 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");
@@ -896,11 +985,13 @@ async function prepareSelectedProfile(): Promise<void> {
if (!profile) {
return;
}
appendLog({ timestamp: new Date().toISOString(), level: "info", stage: "store_prepare", message: `Vérification PostgreSQL du profil ${profile.name}.` });
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: `PostgreSQL prêt: ${readiness.existingTablesAfter}/${readiness.expectedTables} tables, créées=${readiness.createdTables}, autoInit=${readiness.autoInitializeSchema}.` });
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) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb-app-demo-desktop.frontend.demo_execution_spl", `Devnet store preparation failed: ${message}`);
@@ -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

@@ -1,60 +0,0 @@
// file: kb-app-demo-desktop/frontend/ts/demo_sql_diag.ts
// version: 3
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import { jsonText, renderJsonViewer, renderTables, setText } from "./demo_sql_tables";
import type { DemoSqlDiagPayload } from "./bindings/kb_app_demo_desktop/demo_sql_diag/DemoSqlDiagPayload";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
let latestPayload: DemoSqlDiagPayload | null = null;
async function refreshDiagnostics(): Promise<void> {
setText("#sqlDiagStatus", "Chargement...");
try {
const payload = await invoke<DemoSqlDiagPayload>("load_demo_sql_diag");
setText("#sqlDiagConfigPath", payload.configPath);
setText("#sqlDiagProfile", payload.activeProfileName);
setText("#sqlDiagBackend", payload.backend);
setText("#sqlDiagDsn", payload.maskedDsn);
setText("#sqlDiagSchema", payload.currentSchema ?? "—");
setText("#sqlDiagHealth", payload.healthStatus);
setText("#sqlDiagMigration", payload.migrationStatus);
setText("#sqlDiagStatus", "OK");
renderTables("#sqlDiagTable", "#sqlDiagTableBody", payload.tables);
latestPayload = payload;
renderJsonViewer("#sqlDiagJson", payload);
frontendDebug("kb_app_demo_desktop.frontend.demo_sql_diag", "SQL diagnostics refreshed");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#sqlDiagStatus", `Erreur : ${message}`);
latestPayload = null;
renderJsonViewer("#sqlDiagJson", { error: message });
frontendError("kb_app_demo_desktop.frontend.demo_sql_diag", `SQL diagnostics loading failed: ${message}`);
}
}
async function copyDiagnostics(): Promise<void> {
if (latestPayload) {
await navigator.clipboard.writeText(jsonText(latestPayload));
}
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo_desktop.frontend.demo_sql_diag");
frontendDebug("kb_app_demo_desktop.frontend.demo_sql_diag", "SQL 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", () => {
void refreshDiagnostics();
});
document.querySelector<HTMLButtonElement>("#copySqlDiagButton")?.addEventListener("click", () => {
void copyDiagnostics();
});
void refreshDiagnostics();
});

View File

@@ -1,42 +0,0 @@
// file: kb-app-demo-desktop/frontend/ts/demo_sql_pg_core.ts
// version: 4
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import { renderJsonViewer, renderTables, setText } from "./demo_sql_tables";
import type { DemoSqlPgCorePayload } from "./bindings/kb_app_demo_desktop/demo_sql_pg_core/DemoSqlPgCorePayload";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
async function refreshCore(): Promise<void> {
setText("#sqlCoreStatus", "Chargement...");
try {
const payload = await invoke<DemoSqlPgCorePayload>("load_demo_sql_pg_core");
setText("#sqlCoreProfile", payload.activeProfileName);
setText("#sqlCoreDsn", payload.maskedDsn);
setText("#sqlCoreStatus", "OK");
renderTables("#sqlCoreTable", "#sqlCoreTableBody", payload.tables);
renderJsonViewer("#sqlCoreJson", payload);
frontendDebug("kb_app_demo_desktop.frontend.demo_sql_pg_core", "PostgreSQL core diagnostics refreshed");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#sqlCoreStatus", `Erreur : ${message}`);
renderJsonViewer("#sqlCoreJson", { error: message });
frontendError("kb_app_demo_desktop.frontend.demo_sql_pg_core", `PostgreSQL core diagnostics loading failed: ${message}`);
}
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo_desktop.frontend.demo_sql_pg_core");
frontendDebug("kb_app_demo_desktop.frontend.demo_sql_pg_core", "PostgreSQL core 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", () => {
void refreshCore();
});
void refreshCore();
});

View File

@@ -1,42 +0,0 @@
// file: kb-app-demo-desktop/frontend/ts/demo_sql_pg_raw.ts
// version: 4
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import { renderJsonViewer, renderTables, setText } from "./demo_sql_tables";
import type { DemoSqlPgRawPayload } from "./bindings/kb_app_demo_desktop/demo_sql_pg_raw/DemoSqlPgRawPayload";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
async function refreshRaw(): Promise<void> {
setText("#sqlRawStatus", "Chargement...");
try {
const payload = await invoke<DemoSqlPgRawPayload>("load_demo_sql_pg_raw");
setText("#sqlRawProfile", payload.activeProfileName);
setText("#sqlRawDsn", payload.maskedDsn);
setText("#sqlRawStatus", "OK");
renderTables("#sqlRawTable", "#sqlRawTableBody", payload.tables);
renderJsonViewer("#sqlRawJson", payload);
frontendDebug("kb_app_demo_desktop.frontend.demo_sql_pg_raw", "PostgreSQL raw diagnostics refreshed");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#sqlRawStatus", `Erreur : ${message}`);
renderJsonViewer("#sqlRawJson", { error: message });
frontendError("kb_app_demo_desktop.frontend.demo_sql_pg_raw", `PostgreSQL raw diagnostics loading failed: ${message}`);
}
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo_desktop.frontend.demo_sql_pg_raw");
frontendDebug("kb_app_demo_desktop.frontend.demo_sql_pg_raw", "PostgreSQL raw 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", () => {
void refreshRaw();
});
void refreshRaw();
});

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

@@ -0,0 +1,42 @@
// file: kb-app-demo-desktop/frontend/ts/demo_store_core.ts
// version: 7
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import { renderJsonViewer, renderTables, setText } from "./demo_store_tables";
import type { DemoStoreCorePayload } from "./bindings/kb_app_demo_desktop/demo_store_core/DemoStoreCorePayload";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
async function refreshCore(): Promise<void> {
setText("#storeCoreStatus", "Chargement...");
try {
const payload = await invoke<DemoStoreCorePayload>("load_demo_store_core");
setText("#storeCoreProfile", payload.activeProfileName);
setText("#storeCoreConnection", payload.connectionDescriptor);
setText("#storeCoreStatus", "OK");
renderTables("#storeCoreTable", "#storeCoreTableBody", payload.resources);
renderJsonViewer("#storeCoreJson", payload);
frontendDebug("kb_app_demo_desktop.frontend.demo_store_core", "Core store diagnostics refreshed");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#storeCoreStatus", `Erreur : ${message}`);
renderJsonViewer("#storeCoreJson", { error: message });
frontendError("kb_app_demo_desktop.frontend.demo_store_core", `Core store diagnostics loading failed: ${message}`);
}
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo_desktop.frontend.demo_store_core");
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>("#refreshStoreCoreButton")?.addEventListener("click", () => {
void refreshCore();
});
void refreshCore();
});

View File

@@ -0,0 +1,60 @@
// file: kb-app-demo-desktop/frontend/ts/demo_store_diag.ts
// version: 6
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import { jsonText, renderJsonViewer, renderTables, setText } from "./demo_store_tables";
import type { DemoStoreDiagPayload } from "./bindings/kb_app_demo_desktop/demo_store_diag/DemoStoreDiagPayload";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
let latestPayload: DemoStoreDiagPayload | null = null;
async function refreshDiagnostics(): Promise<void> {
setText("#storeDiagStatus", "Chargement...");
try {
const payload = await invoke<DemoStoreDiagPayload>("load_demo_store_diag");
setText("#storeDiagConfigPath", payload.configPath);
setText("#storeDiagProfile", payload.activeProfileName);
setText("#storeDiagBackend", payload.backend);
setText("#storeDiagConnection", payload.connectionDescriptor);
setText("#storeDiagSchema", payload.namespace ?? "—");
setText("#storeDiagHealth", payload.healthStatus);
setText("#storeDiagMigration", payload.migrationStatus);
setText("#storeDiagStatus", "OK");
renderTables("#storeDiagTable", "#storeDiagTableBody", payload.resources);
latestPayload = payload;
renderJsonViewer("#storeDiagJson", payload);
frontendDebug("kb_app_demo_desktop.frontend.demo_store_diag", "Store diagnostics refreshed");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#storeDiagStatus", `Erreur : ${message}`);
latestPayload = null;
renderJsonViewer("#storeDiagJson", { error: message });
frontendError("kb_app_demo_desktop.frontend.demo_store_diag", `Store diagnostics loading failed: ${message}`);
}
}
async function copyDiagnostics(): Promise<void> {
if (latestPayload) {
await navigator.clipboard.writeText(jsonText(latestPayload));
}
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo_desktop.frontend.demo_store_diag");
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>("#refreshStoreDiagButton")?.addEventListener("click", () => {
void refreshDiagnostics();
});
document.querySelector<HTMLButtonElement>("#copyStoreDiagButton")?.addEventListener("click", () => {
void copyDiagnostics();
});
void refreshDiagnostics();
});

View File

@@ -0,0 +1,42 @@
// file: kb-app-demo-desktop/frontend/ts/demo_store_raw.ts
// version: 7
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import { renderJsonViewer, renderTables, setText } from "./demo_store_tables";
import type { DemoStoreRawPayload } from "./bindings/kb_app_demo_desktop/demo_store_raw/DemoStoreRawPayload";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
async function refreshRaw(): Promise<void> {
setText("#storeRawStatus", "Chargement...");
try {
const payload = await invoke<DemoStoreRawPayload>("load_demo_store_raw");
setText("#storeRawProfile", payload.activeProfileName);
setText("#storeRawConnection", payload.connectionDescriptor);
setText("#storeRawStatus", "OK");
renderTables("#storeRawTable", "#storeRawTableBody", payload.resources);
renderJsonViewer("#storeRawJson", payload);
frontendDebug("kb_app_demo_desktop.frontend.demo_store_raw", "Raw store diagnostics refreshed");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#storeRawStatus", `Erreur : ${message}`);
renderJsonViewer("#storeRawJson", { error: message });
frontendError("kb_app_demo_desktop.frontend.demo_store_raw", `Raw store diagnostics loading failed: ${message}`);
}
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo_desktop.frontend.demo_store_raw");
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>("#refreshStoreRawButton")?.addEventListener("click", () => {
void refreshRaw();
});
void refreshRaw();
});

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_sql_replay_candidates.ts
// version: 9
// file: kb-app-demo-desktop/frontend/ts/demo_store_replay_candidates.ts
// version: 14
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -10,14 +10,27 @@ 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 type { DemoSqlReplayEntityRequest } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRequest";
import type { DemoSqlReplayEntityRow } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRow";
import type { DemoSqlReplayKnownProgramOption } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayKnownProgramOption";
import type { DemoSqlReplayOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayOptionsPayload";
import type { DemoSqlReplayProgramRequest } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRequest";
import type { DemoSqlReplayProgramRow } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRow";
import type { DemoSqlReplayTransactionRequest } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRequest";
import type { DemoSqlReplayTransactionRow } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRow";
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";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
@@ -30,7 +43,7 @@ type EntityViewConfig = {
pluralLabel: string;
tableSelector: string;
containsSelector: string;
limitSelector: string;
blockControls: StoreBlockControls;
summarySelector: string;
loadButtonSelector: string;
resetButtonSelector: string;
@@ -40,7 +53,7 @@ type EntityViewConfig = {
fileName: string;
};
const tracingTarget = "kb_app_demo_desktop.frontend.demo_sql_replay_candidates";
const tracingTarget = "kb_app_demo_desktop.frontend.demo_store_replay_candidates";
const entityViewConfigs: EntityViewConfig[] = [
{
kind: "mint",
@@ -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",
@@ -88,13 +116,37 @@ const entityViewConfigs: EntityViewConfig[] = [
fileName: "replay_account_keys.csv",
},
];
let transactionTable: Api<DemoSqlReplayTransactionRow> | null = null;
let programTable: Api<DemoSqlReplayProgramRow> | null = null;
let mintTable: Api<DemoSqlReplayEntityRow> | null = null;
let ownerTable: Api<DemoSqlReplayEntityRow> | null = null;
let accountTable: Api<DemoSqlReplayEntityRow> | null = null;
let transactionTable: Api<DemoStoreReplayTransactionRow> | null = null;
let programTable: Api<DemoStoreReplayProgramRow> | null = null;
let mintTable: Api<DemoStoreReplayEntityRow> | null = null;
let ownerTable: Api<DemoStoreReplayEntityRow> | null = null;
let accountTable: Api<DemoStoreReplayEntityRow> | null = null;
let busyOperationCount = 0;
let maximumReplayCandidateLimit = 100_000;
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) {
@@ -199,7 +233,7 @@ function badgeDisplay(value: unknown, type: string): string {
return `<span class="badge ${className}">${escapeHtml(text)}</span>`;
}
function coreStatusText(row: DemoSqlReplayTransactionRow): string {
function coreStatusText(row: DemoStoreReplayTransactionRow): string {
if (!row.hasCoreTransaction) {
return "absent";
}
@@ -209,7 +243,7 @@ function coreStatusText(row: DemoSqlReplayTransactionRow): string {
return "présent";
}
function coreDisplay(_value: unknown, type: string, row: DemoSqlReplayTransactionRow): string | number {
function coreDisplay(_value: unknown, type: string, row: DemoStoreReplayTransactionRow): string | number {
if (type === "display") {
if (!row.hasCoreTransaction) {
return '<span class="badge text-bg-secondary">absent</span>';
@@ -228,7 +262,7 @@ function coreDisplay(_value: unknown, type: string, row: DemoSqlReplayTransactio
return row.hasCoreTransaction ? 1 : 2;
}
function occurrenceRatioDisplay(_value: unknown, type: string, row: DemoSqlReplayEntityRow): string | number {
function occurrenceRatioDisplay(_value: unknown, type: string, row: DemoStoreReplayEntityRow): string | number {
const ratio = row.transactionCount > 0 ? row.occurrenceCount / row.transactionCount : 0;
if (type === "display" || type === "filter") {
return ratio.toFixed(2);
@@ -236,7 +270,7 @@ function occurrenceRatioDisplay(_value: unknown, type: string, row: DemoSqlRepla
return ratio;
}
function slotSpanDisplay(_value: unknown, type: string, row: DemoSqlReplayEntityRow): string | number {
function slotSpanDisplay(_value: unknown, type: string, row: DemoStoreReplayEntityRow): string | number {
const span = Math.max(0, row.maxSlot - row.minSlot);
if (type === "display" || type === "filter") {
return String(span);
@@ -258,8 +292,8 @@ function commonLanguage(emptyTable: string, infoLabel: string, zeroRecords: stri
};
}
function createTransactionTable(): Api<DemoSqlReplayTransactionRow> {
return new DataTable<DemoSqlReplayTransactionRow>("#transactionCandidateTable", {
function createTransactionTable(): Api<DemoStoreReplayTransactionRow> {
return new DataTable<DemoStoreReplayTransactionRow>("#transactionCandidateTable", {
data: [],
columns: [
{
@@ -277,15 +311,15 @@ function createTransactionTable(): Api<DemoSqlReplayTransactionRow> {
{ data: "ledgerStatus", render: badgeDisplay },
{ data: "processorVersion", defaultContent: "—", render: codeDisplay },
{ data: "attemptCount" },
{ data: "outerInstructionCount" },
{ data: "topLevelInstructionCount" },
{ data: "innerInstructionCount" },
{ data: "outerProgramCount" },
{ data: "topLevelProgramCount" },
{ data: "innerProgramCount" },
{ data: "updatedAt" },
],
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",
@@ -296,8 +330,8 @@ function createTransactionTable(): Api<DemoSqlReplayTransactionRow> {
});
}
function createProgramTable(): Api<DemoSqlReplayProgramRow> {
return new DataTable<DemoSqlReplayProgramRow>("#programCandidateTable", {
function createProgramTable(): Api<DemoStoreReplayProgramRow> {
return new DataTable<DemoStoreReplayProgramRow>("#programCandidateTable", {
data: [],
columns: [
{
@@ -311,7 +345,7 @@ function createProgramTable(): Api<DemoSqlReplayProgramRow> {
{ data: "programCode", defaultContent: "—", render: codeDisplay },
{ data: "programId", render: copyableCodeDisplay("le program ID") },
{ data: "transactionCount" },
{ data: "outerInstructionCount" },
{ data: "topLevelInstructionCount" },
{ data: "innerInstructionCount" },
{ data: "logCount" },
{ data: "minSlot" },
@@ -319,7 +353,7 @@ function createProgramTable(): Api<DemoSqlReplayProgramRow> {
],
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é",
@@ -330,8 +364,8 @@ function createProgramTable(): Api<DemoSqlReplayProgramRow> {
});
}
function createEntityTable(config: EntityViewConfig): Api<DemoSqlReplayEntityRow> {
return new DataTable<DemoSqlReplayEntityRow>(config.tableSelector, {
function createEntityTable(config: EntityViewConfig): Api<DemoStoreReplayEntityRow> {
return new DataTable<DemoStoreReplayEntityRow>(config.tableSelector, {
data: [],
columns: [
{
@@ -352,7 +386,7 @@ function createEntityTable(config: EntityViewConfig): Api<DemoSqlReplayEntityRow
],
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é`,
@@ -363,7 +397,7 @@ function createEntityTable(config: EntityViewConfig): Api<DemoSqlReplayEntityRow
});
}
function entityTable(kind: EntityKindCode): Api<DemoSqlReplayEntityRow> | null {
function entityTable(kind: EntityKindCode): Api<DemoStoreReplayEntityRow> | null {
if (kind === "mint") {
return mintTable;
}
@@ -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(): DemoSqlReplayTransactionRequest {
function buildTransactionBaseRequest(): TransactionBaseRequest {
const entityKind = optionalInputValue("#transactionEntityKindSelect");
const entityValue = optionalInputValue("#transactionEntityValueInput");
if ((entityKind === null) !== (entityValue === null)) {
@@ -441,11 +497,31 @@ function buildTransactionRequest(): DemoSqlReplayTransactionRequest {
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<DemoSqlReplayTransactionRow[]>("load_demo_sql_replay_transactions", { request });
replaceRows(transactionTable, rows);
element<HTMLElement>("#transactionResultSummary").textContent = `${rows.length} transaction(s) chargée(s) depuis PostgreSQL.`;
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: DemoSqlReplayProgramRequest = {
programIdContains: optionalInputValue("#programContainsInput"),
limit: boundedPositiveIntegerValue("#programLimitInput"),
const request: DemoStoreReplayProgramRequest = {
...programBaseRequest,
cursor,
blockIndex,
};
const rows = await invoke<DemoSqlReplayProgramRow[]>("load_demo_sql_replay_programs", { request });
replaceRows(programTable, rows);
element<HTMLElement>("#programResultSummary").textContent = `${rows.length} programme(s) chargé(s), avec occurrences outer, 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: DemoSqlReplayEntityRequest = {
entityKind: config.kind,
entityValueContains: optionalInputValue(config.containsSelector),
limit: boundedPositiveIntegerValue(config.limitSelector),
const request: DemoStoreReplayEntityRequest = {
...baseRequest,
cursor,
blockIndex,
};
const rows = await invoke<DemoSqlReplayEntityRow[]>("load_demo_sql_replay_entities", { request });
replaceRows(table, rows);
element<HTMLElement>(config.summarySelector).textContent = `${rows.length} ${config.pluralLabel} chargé(s) depuis PostgreSQL.`;
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.`);
@@ -567,7 +738,7 @@ async function saveCsv(fileName: string, content: string, rowCount: number): Pro
if (rowCount === 0) {
throw new Error("Aucune ligne à exporter.");
}
const path = await invoke<string>("export_demo_sql_replay_csv", { fileName, content });
const path = await invoke<string>("export_demo_store_replay_csv", { fileName, content });
const badge = element<HTMLElement>("#replayCandidateStatusBadge");
badge.textContent = `${rowCount} ligne(s) exportée(s) : ${path}`;
badge.className = "badge text-bg-success";
@@ -577,8 +748,8 @@ async function saveCsv(fileName: string, content: string, rowCount: number): Pro
async function exportTransactionsCsv(): Promise<void> {
const rows = transactionRowsForExport();
const content = csvContent(
["signature", "slot", "raw_processing_state", "retention_state", "core_status", "ledger_status", "processor_version", "attempt_count", "outer_instruction_count", "inner_instruction_count", "outer_program_count", "inner_program_count", "updated_at"],
rows.map(row => [row.signature, row.slot, row.rawProcessingState, row.retentionState, coreStatusText(row), row.ledgerStatus, row.processorVersion, row.attemptCount, row.outerInstructionCount, row.innerInstructionCount, row.outerProgramCount, row.innerProgramCount, row.updatedAt]),
["signature", "slot", "raw_processing_state", "retention_state", "core_status", "ledger_status", "processor_version", "attempt_count", "top_level_instruction_count", "inner_instruction_count", "top_level_program_count", "inner_program_count", "updated_at"],
rows.map(row => [row.signature, row.slot, row.rawProcessingState, row.retentionState, coreStatusText(row), row.ledgerStatus, row.processorVersion, row.attemptCount, row.topLevelInstructionCount, row.innerInstructionCount, row.topLevelProgramCount, row.innerProgramCount, row.updatedAt]),
);
await saveCsv("replay_transactions.csv", content, rows.length);
}
@@ -586,8 +757,8 @@ async function exportTransactionsCsv(): Promise<void> {
async function exportProgramsCsv(): Promise<void> {
const rows = programRowsForExport();
const content = csvContent(
["program_code", "program_id", "transaction_count", "outer_instruction_count", "inner_instruction_count", "log_count", "min_slot", "max_slot"],
rows.map(row => [row.programCode, row.programId, row.transactionCount, row.outerInstructionCount, row.innerInstructionCount, row.logCount, row.minSlot, row.maxSlot]),
["program_code", "program_id", "transaction_count", "top_level_instruction_count", "inner_instruction_count", "log_count", "min_slot", "max_slot"],
rows.map(row => [row.programCode, row.programId, row.transactionCount, row.topLevelInstructionCount, row.innerInstructionCount, row.logCount, row.minSlot, row.maxSlot]),
);
await saveCsv("replay_programs.csv", content, rows.length);
}
@@ -601,21 +772,21 @@ async function exportEntityCsv(config: EntityViewConfig): Promise<void> {
await saveCsv(config.fileName, content, rows.length);
}
function transactionRowsForExport(): DemoSqlReplayTransactionRow[] {
function transactionRowsForExport(): DemoStoreReplayTransactionRow[] {
if (transactionTable === null) {
return [];
}
return selectedOrFilteredRows(transactionTable);
}
function programRowsForExport(): DemoSqlReplayProgramRow[] {
function programRowsForExport(): DemoStoreReplayProgramRow[] {
if (programTable === null) {
return [];
}
return selectedOrFilteredRows(programTable);
}
function entityRowsForExport(kind: EntityKindCode): DemoSqlReplayEntityRow[] {
function entityRowsForExport(kind: EntityKindCode): DemoStoreReplayEntityRow[] {
const table = entityTable(kind);
if (table === null) {
return [];
@@ -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> {
@@ -660,21 +831,17 @@ async function openCoreExtraction(): Promise<void> {
}
async function loadOptions(): Promise<void> {
const options = await invoke<DemoSqlReplayOptionsPayload>("demo_sql_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.");
const options = await invoke<DemoStoreReplayOptionsPayload>("demo_store_replay_options");
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.maskedDsn;
element<HTMLElement>("#replayCandidateMaximumLimit").textContent = String(options.maximumLimit);
document.querySelectorAll<HTMLInputElement>("input[type=number][max]").forEach(input => {
input.max = String(options.maximumLimit);
});
element<HTMLElement>("#replayCandidateDsn").textContent = options.connectionDescriptor;
element<HTMLElement>("#replayCandidateBlockSize").textContent = `${options.blockSize} lignes`;
populateKnownPrograms(options.knownPrograms);
}
function populateKnownPrograms(programs: DemoSqlReplayKnownProgramOption[]): void {
function populateKnownPrograms(programs: DemoStoreReplayKnownProgramOption[]): void {
const select = element<HTMLSelectElement>("#knownProgramSelect");
const datalist = element<HTMLDataListElement>("#knownProgramDatalist");
select.replaceChildren(new Option("Tous les programmes connus", ""));
@@ -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,19 +943,52 @@ 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, "SQL replay candidate browser loaded");
frontendDebug(tracingTarget, "Store replay candidate browser loaded");
transactionTable = createTransactionTable();
programTable = createProgramTable();
mintTable = createEntityTable(entityViewConfigs[0]);
@@ -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,10 +1,10 @@
// file: kb-app-demo-desktop/frontend/ts/demo_sql_tables.ts
// version: 4
// file: kb-app-demo-desktop/frontend/ts/demo_store_tables.ts
// version: 6
import DataTable, { type Api } from "datatables.net-bs5";
import "datatables.net-bs5/css/dataTables.bootstrap5.css";
import { jsonText, renderJsonViewer } from "./json_viewer.ts";
import type { DemoSqlTableSnapshot } from "./bindings/kb_app_demo_desktop/demo_sql/DemoSqlTableSnapshot";
import type { DemoStoreResourceSnapshot } from "./bindings/kb_app_demo_desktop/demo_store/DemoStoreResourceSnapshot";
const tableInstances = new Map<string, Api>();
@@ -15,7 +15,7 @@ export function setText(selector: string, value: string): void {
}
}
export function renderTables(tableSelector: string, bodySelector: string, tables: DemoSqlTableSnapshot[]): void {
export function renderTables(tableSelector: string, bodySelector: string, resources: DemoStoreResourceSnapshot[]): void {
const existing = tableInstances.get(tableSelector);
if (existing) {
existing.destroy();
@@ -26,16 +26,16 @@ export function renderTables(tableSelector: string, bodySelector: string, tables
return;
}
tbody.textContent = "";
for (const table of tables) {
for (const resource of resources) {
const row = document.createElement("tr");
row.appendChild(cell(table.tableName, true));
row.appendChild(cell(table.domain, false));
row.appendChild(cell(table.role, false));
row.appendChild(cell(table.exists ? "oui" : "non", false));
row.appendChild(cell(table.rowCount === null ? "—" : String(table.rowCount), false));
row.appendChild(cell(table.minSlot === null ? "—" : String(table.minSlot), false));
row.appendChild(cell(table.maxSlot === null ? "—" : String(table.maxSlot), false));
row.appendChild(cell(table.latestCreatedAt ?? "—", false));
row.appendChild(cell(resource.resourceCode, true));
row.appendChild(cell(resource.modelCode, false));
row.appendChild(cell(resource.role, false));
row.appendChild(cell(resource.available ? "oui" : "non", false));
row.appendChild(cell(resource.recordCount === null ? "—" : String(resource.recordCount), false));
row.appendChild(cell(resource.minSlot === null ? "—" : String(resource.minSlot), false));
row.appendChild(cell(resource.maxSlot === null ? "—" : String(resource.maxSlot), false));
row.appendChild(cell(resource.latestCreatedAt ?? "—", false));
tbody.appendChild(row);
}
const tableElement = document.querySelector<HTMLTableElement>(tableSelector);
@@ -48,12 +48,12 @@ export function renderTables(tableSelector: string, bodySelector: string, tables
lengthMenu: [10, 25, 50, 100],
order: [[0, "asc"]],
language: {
emptyTable: "Aucune table disponible",
info: "Tables _START_ à _END_ sur _TOTAL_",
infoEmpty: "Aucune table",
lengthMenu: "Afficher _MENU_ tables",
emptyTable: "Aucune ressource disponible",
info: "Ressources _START_ à _END_ sur _TOTAL_",
infoEmpty: "Aucune ressource",
lengthMenu: "Afficher _MENU_ ressources",
search: "Rechercher :",
zeroRecords: "Aucune table correspondante",
zeroRecords: "Aucune ressource correspondante",
},
}));
}

View File

@@ -0,0 +1,855 @@
// file: kb-app-demo-desktop/frontend/ts/demo_wallet.ts
// version: 5
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log.ts";
import { renderJsonViewer } from "./json_viewer.ts";
import type { DemoWalletBalancePayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletBalancePayload.ts";
import type { DemoWalletCreateRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletCreateRequest.ts";
import type { DemoWalletExportRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletExportRequest.ts";
import type { DemoWalletExecutionProfilePayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletExecutionProfilePayload.ts";
import type { DemoWalletExecutionSelectionRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletExecutionSelectionRequest.ts";
import type { DemoWalletIdentityPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletIdentityPayload.ts";
import type { DemoWalletImportRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletImportRequest.ts";
import type { DemoWalletInventoryPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletInventoryPayload.ts";
import type { DemoWalletOnchainProfilePayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletOnchainProfilePayload.ts";
import type { DemoWalletRpcExecutionPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletRpcExecutionPayload.ts";
import type { DemoWalletRpcRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletRpcRequest.ts";
import type { DemoWalletTransferFormatPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletTransferFormatPayload.ts";
import type { DemoWalletTransferInspectionPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletTransferInspectionPayload.ts";
import type { DemoWalletTransferInspectionRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletTransferInspectionRequest.ts";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
interface ParsedTokenAccount {
program: string;
mint: string;
tokenAccount: string;
uiAmount: string;
rawAmount: string;
decimals: number;
}
interface ParsedSignatureInfo {
signature: string;
slot: number;
err: unknown | null;
memo: string | null;
blockTime: number | null;
confirmationStatus: string | null;
}
let walletInventory: DemoWalletInventoryPayload | null = null;
let inspectedTransferPublicKey = "";
function setText(selector: string, value: string): void {
const element = document.querySelector<HTMLElement>(selector);
if (element) {
element.textContent = value;
}
}
function inputValue(selector: string): string {
return document.querySelector<HTMLInputElement>(selector)?.value.trim() ?? "";
}
function selectedOnchainProfile(): string {
return document.querySelector<HTMLSelectElement>("#walletOnchainProfileSelect")?.value.trim() ?? "";
}
function selectedOnchainProfilePayload(): DemoWalletOnchainProfilePayload | null {
const profile = selectedOnchainProfile();
if (!walletInventory || profile.length === 0) {
return null;
}
return walletInventory.onchain_profiles.find(candidate => candidate.name === profile) ?? null;
}
function updateOnchainClusterLabel(): void {
const selected = selectedOnchainProfilePayload();
setText("#walletOnchainSelectedProfile", selected?.name ?? "—");
setText("#walletOnchainCluster", selected?.cluster ?? "—");
}
function renderOnchainProfiles(profiles: ReadonlyArray<DemoWalletOnchainProfilePayload>, activeProfile: string): void {
const select = document.querySelector<HTMLSelectElement>("#walletOnchainProfileSelect");
if (!select) {
return;
}
const previous = select.value;
select.replaceChildren();
for (const profile of profiles) {
const option = document.createElement("option");
option.value = profile.name;
option.textContent = `${profile.name}${profile.cluster}${profile.active ? " (actif)" : ""}`;
select.appendChild(option);
}
if (profiles.some(profile => profile.name === previous)) {
select.value = previous;
} else if (profiles.some(profile => profile.name === activeProfile)) {
select.value = activeProfile;
} else if (profiles.length > 0) {
select.value = profiles[0].name;
}
updateOnchainClusterLabel();
}
function selectedExecutionProfilePayload(): DemoWalletExecutionProfilePayload | null {
const profile = document.querySelector<HTMLSelectElement>("#walletExecutionProfileSelect")?.value.trim() ?? "";
if (!walletInventory || profile.length === 0) {
return null;
}
return walletInventory.execution_profiles.find(candidate => candidate.name === profile) ?? null;
}
function renderExecutionProfiles(profiles: ReadonlyArray<DemoWalletExecutionProfilePayload>): void {
const select = document.querySelector<HTMLSelectElement>("#walletExecutionProfileSelect");
if (!select) {
return;
}
const previous = select.value;
select.replaceChildren();
for (const profile of profiles) {
const option = document.createElement("option");
option.value = profile.name;
option.textContent = profile.name;
select.appendChild(option);
}
if (profiles.some(profile => profile.name === previous)) {
select.value = previous;
} else if (profiles.length > 0) {
select.value = profiles[0].name;
}
}
function renderExecutionWalletAliases(wallets: ReadonlyArray<DemoWalletIdentityPayload>): void {
const select = document.querySelector<HTMLSelectElement>("#walletExecutionAliasSelect");
if (!select) {
return;
}
select.replaceChildren();
const configuredOption = document.createElement("option");
configuredOption.value = "";
configuredOption.textContent = "Utiliser la configuration du profil";
select.appendChild(configuredOption);
for (const wallet of wallets) {
const option = document.createElement("option");
option.value = wallet.alias;
option.textContent = `${wallet.alias}${wallet.public_key}`;
select.appendChild(option);
}
}
function updateExecutionSelectionDisplay(): void {
const profile = selectedExecutionProfilePayload();
const aliasSelect = document.querySelector<HTMLSelectElement>("#walletExecutionAliasSelect");
if (!profile) {
setText("#walletExecutionConfiguredAlias", "—");
setText("#walletExecutionRuntimeAlias", "—");
setText("#walletExecutionEffectiveAlias", "temporaire");
if (aliasSelect) {
aliasSelect.value = "";
}
return;
}
setText("#walletExecutionConfiguredAlias", profile.configured_alias ?? "—");
setText("#walletExecutionRuntimeAlias", profile.runtime_alias ?? "—");
setText("#walletExecutionEffectiveAlias", profile.effective_alias ?? "temporaire");
if (aliasSelect) {
const requested = profile.runtime_alias ?? "";
if (Array.from(aliasSelect.options).some(option => option.value === requested)) {
aliasSelect.value = requested;
} else {
aliasSelect.value = "";
}
}
}
async function applyExecutionWalletSelection(): Promise<void> {
const profile = document.querySelector<HTMLSelectElement>("#walletExecutionProfileSelect")?.value.trim() ?? "";
const aliasText = document.querySelector<HTMLSelectElement>("#walletExecutionAliasSelect")?.value.trim() ?? "";
if (profile.length === 0) {
setText("#walletExecutionSelectionStatus", "Profil Devnet requis");
return;
}
const request: DemoWalletExecutionSelectionRequest = {
profile,
alias: aliasText.length === 0 ? null : aliasText,
};
setText("#walletExecutionSelectionStatus", "Validation...");
try {
const payload = await invoke<DemoWalletInventoryPayload>("demo_wallet_select_execution_wallet", { request });
applyWalletInventoryPayload(payload);
const effective = payload.execution_profiles.find(candidate => candidate.name === profile)?.effective_alias ?? null;
setText("#walletExecutionSelectionStatus", effective ? `Persistant : ${effective}` : "Configuration du profil");
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", `execution wallet selection updated for ${profile}`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletExecutionSelectionStatus", `Erreur : ${message}`);
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `Execution wallet selection failed: ${message}`);
}
}
async function executeWalletRpc(profile: string, method: string, params: unknown[]): Promise<DemoWalletRpcExecutionPayload> {
const request: DemoWalletRpcRequest = {
profile,
method,
params_json: JSON.stringify(params),
};
return await invoke<DemoWalletRpcExecutionPayload>("demo_wallet_rpc_execute", { request });
}
function parseJsonResponse(response: DemoWalletRpcExecutionPayload): unknown {
return JSON.parse(response.response_json) as unknown;
}
function renderTransferFormats(formats: ReadonlyArray<DemoWalletTransferFormatPayload>): void {
for (const selector of ["#walletImportFormatSelect", "#walletExportFormatSelect"]) {
const select = document.querySelector<HTMLSelectElement>(selector);
if (!select) {
continue;
}
const previous = select.value;
select.replaceChildren();
for (const format of formats) {
const option = document.createElement("option");
option.value = format.code;
option.textContent = `${format.label} (.${format.default_extension})`;
option.dataset.defaultExtension = format.default_extension;
select.appendChild(option);
}
if (formats.some(format => format.code === previous)) {
select.value = previous;
}
}
}
function renderExportWalletAliases(wallets: ReadonlyArray<DemoWalletIdentityPayload>): void {
const select = document.querySelector<HTMLSelectElement>("#walletExportAliasSelect");
if (!select) {
return;
}
const previous = select.value;
select.replaceChildren();
if (wallets.length === 0) {
const option = document.createElement("option");
option.value = "";
option.textContent = "Aucun wallet natif";
select.appendChild(option);
return;
}
for (const wallet of wallets) {
const option = document.createElement("option");
option.value = wallet.alias;
option.textContent = `${wallet.alias}${wallet.public_key}`;
select.appendChild(option);
}
if (wallets.some(wallet => wallet.alias === previous)) {
select.value = previous;
}
}
function selectedTransferFormat(selector: string): string {
return document.querySelector<HTMLSelectElement>(selector)?.value.trim() ?? "";
}
function selectedExportFormatPayload(): DemoWalletTransferFormatPayload | null {
const code = selectedTransferFormat("#walletExportFormatSelect");
if (!walletInventory || code.length === 0) {
return null;
}
return walletInventory.transfer_formats.find(format => format.code === code) ?? null;
}
function updateExportFileName(force: boolean): void {
const input = document.querySelector<HTMLInputElement>("#walletExportFileNameInput");
const alias = document.querySelector<HTMLSelectElement>("#walletExportAliasSelect")?.value.trim() ?? "";
const format = selectedExportFormatPayload();
if (!input || alias.length === 0 || !format) {
return;
}
const generated = `${alias}.${format.default_extension}`;
if (force || input.value.trim().length === 0 || input.dataset.autoGenerated === "true") {
input.value = generated;
input.dataset.autoGenerated = "true";
}
}
function setTransferPublicKey(publicKey: string): void {
inspectedTransferPublicKey = publicKey;
setText("#walletTransferPublicKey", publicKey.length > 0 ? publicKey : "—");
const exploreButton = document.querySelector<HTMLButtonElement>("#exploreWalletTransferButton");
if (exploreButton) {
exploreButton.disabled = publicKey.length === 0;
}
}
function renderWalletRows(wallets: ReadonlyArray<DemoWalletIdentityPayload>): void {
const body = document.querySelector<HTMLTableSectionElement>("#walletInventoryTableBody");
if (!body) {
return;
}
body.replaceChildren();
if (wallets.length === 0) {
const row = document.createElement("tr");
const cell = document.createElement("td");
cell.colSpan = 5;
cell.className = "text-body-secondary text-center py-3";
cell.textContent = "Aucun wallet natif trouvé.";
row.appendChild(cell);
body.appendChild(row);
return;
}
for (const wallet of wallets) {
const row = document.createElement("tr");
const aliasCell = document.createElement("td");
const publicKeyCell = document.createElement("td");
const formatCell = document.createElement("td");
const selectedCell = document.createElement("td");
const actionCell = document.createElement("td");
const exploreButton = document.createElement("button");
aliasCell.textContent = wallet.alias;
publicKeyCell.textContent = wallet.public_key;
publicKeyCell.className = "font-monospace text-break";
formatCell.textContent = `v${wallet.format_version}`;
const executionSelected = selectedExecutionProfilePayload()?.effective_alias === wallet.alias;
selectedCell.textContent = executionSelected ? "Oui" : "Non";
exploreButton.type = "button";
exploreButton.className = "btn btn-sm btn-outline-primary";
exploreButton.textContent = "Explorer";
exploreButton.addEventListener("click", () => {
const input = document.querySelector<HTMLInputElement>("#walletAddressInput");
if (input) {
input.value = wallet.public_key;
}
document.querySelector<HTMLElement>("#walletAddressInput")?.scrollIntoView({ behavior: "smooth", block: "center" });
});
actionCell.appendChild(exploreButton);
if (executionSelected) {
row.classList.add("table-primary");
}
row.append(aliasCell, publicKeyCell, formatCell, selectedCell, actionCell);
body.appendChild(row);
}
}
function applyWalletInventoryPayload(payload: DemoWalletInventoryPayload): void {
walletInventory = payload;
setText("#walletProfile", payload.profile);
setText("#walletCluster", payload.cluster);
renderOnchainProfiles(payload.onchain_profiles, payload.profile);
renderExecutionProfiles(payload.execution_profiles);
renderTransferFormats(payload.transfer_formats);
renderExportWalletAliases(payload.wallets);
renderExecutionWalletAliases(payload.wallets);
setText("#walletCreateSecretStatus", payload.create_password_configured ? "configuré" : "absent");
setText("#walletSelectedAlias", payload.selected_alias ?? "—");
setText("#walletInventoryCount", String(payload.wallets.length));
setText("#walletExportDirectory", `${payload.export_directory}/`);
setText("#walletInventoryStatus", "OK");
renderWalletRows(payload.wallets);
updateExecutionSelectionDisplay();
updateExportFileName(false);
}
async function refreshWalletInventory(): Promise<void> {
setText("#walletInventoryStatus", "Chargement...");
try {
const payload = await invoke<DemoWalletInventoryPayload>("demo_wallet_inventory");
applyWalletInventoryPayload(payload);
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", "wallet inventory refreshed");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletInventoryStatus", `Erreur : ${message}`);
setText("#walletInventoryCount", "0");
renderWalletRows([]);
renderExportWalletAliases([]);
renderExecutionWalletAliases([]);
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `Wallet inventory loading failed: ${message}`);
}
}
async function createWallet(): Promise<void> {
const alias = inputValue("#createWalletAliasInput");
if (alias.length === 0) {
setText("#walletCreateStatus", "Alias requis");
return;
}
const request: DemoWalletCreateRequest = { alias };
setText("#walletCreateStatus", "Création...");
try {
const created = await invoke<DemoWalletIdentityPayload>("demo_wallet_create_native", { request });
setText("#walletCreateStatus", `Créé : ${created.alias}`);
const addressInput = document.querySelector<HTMLInputElement>("#walletAddressInput");
if (addressInput) {
addressInput.value = created.public_key;
}
const aliasInput = document.querySelector<HTMLInputElement>("#createWalletAliasInput");
if (aliasInput) {
aliasInput.value = "";
}
await refreshWalletInventory();
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", `native wallet created: ${created.alias}`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletCreateStatus", `Erreur : ${message}`);
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `Native wallet creation failed: ${message}`);
}
}
async function inspectTransferFile(): Promise<void> {
const sourcePath = inputValue("#walletImportSourcePathInput");
const format = selectedTransferFormat("#walletImportFormatSelect");
if (sourcePath.length === 0 || format.length === 0) {
setText("#walletTransferStatus", "Chemin source et format requis");
setTransferPublicKey("");
return;
}
const request: DemoWalletTransferInspectionRequest = { source_path: sourcePath, format };
setText("#walletTransferStatus", "Inspection...");
setTransferPublicKey("");
try {
const payload = await invoke<DemoWalletTransferInspectionPayload>("demo_wallet_inspect_transfer_file", { request });
setTransferPublicKey(payload.public_key);
setText("#walletTransferStatus", `Keypair valide — ${payload.format}`);
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", "external transfer keypair inspected");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletTransferStatus", `Erreur : ${message}`);
setTransferPublicKey("");
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `External transfer inspection failed: ${message}`);
}
}
async function importTransferFile(): Promise<void> {
const sourcePath = inputValue("#walletImportSourcePathInput");
const format = selectedTransferFormat("#walletImportFormatSelect");
const alias = inputValue("#walletImportAliasInput");
if (sourcePath.length === 0 || format.length === 0 || alias.length === 0) {
setText("#walletTransferStatus", "Chemin source, format et alias requis");
return;
}
const request: DemoWalletImportRequest = { alias, source_path: sourcePath, format };
setText("#walletTransferStatus", "Import...");
try {
const payload = await invoke<DemoWalletIdentityPayload>("demo_wallet_import_file", { request });
setTransferPublicKey(payload.public_key);
setText("#walletTransferStatus", `Importé : ${payload.alias}.kswallet`);
const addressInput = document.querySelector<HTMLInputElement>("#walletAddressInput");
if (addressInput) {
addressInput.value = payload.public_key;
}
await refreshWalletInventory();
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", `external keypair imported: ${payload.alias}`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletTransferStatus", `Erreur : ${message}`);
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `External keypair import failed: ${message}`);
}
}
async function exportWallet(): Promise<void> {
const alias = document.querySelector<HTMLSelectElement>("#walletExportAliasSelect")?.value.trim() ?? "";
const format = selectedTransferFormat("#walletExportFormatSelect");
const fileName = inputValue("#walletExportFileNameInput");
if (alias.length === 0 || format.length === 0 || fileName.length === 0) {
setText("#walletExportStatus", "Wallet, format et nom de fichier requis");
return;
}
const request: DemoWalletExportRequest = { alias, file_name: fileName, format };
setText("#walletExportStatus", "Export...");
try {
const exportedPath = await invoke<string>("demo_wallet_export_file", { request });
setText("#walletExportStatus", `Exporté : ${exportedPath}`);
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", `native wallet exported: ${alias}`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletExportStatus", `Erreur : ${message}`);
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `Native wallet export failed: ${message}`);
}
}
function exploreInspectedTransfer(): void {
if (inspectedTransferPublicKey.length === 0) {
return;
}
const addressInput = document.querySelector<HTMLInputElement>("#walletAddressInput");
if (addressInput) {
addressInput.value = inspectedTransferPublicKey;
}
document.querySelector<HTMLElement>("#walletAddressInput")?.scrollIntoView({ behavior: "smooth", block: "center" });
}
function parseTokenAccounts(value: unknown, program: string): ParsedTokenAccount[] {
if (typeof value !== "object" || value === null || !("value" in value)) {
return [];
}
const rows = (value as { value?: unknown }).value;
if (!Array.isArray(rows)) {
return [];
}
const parsed: ParsedTokenAccount[] = [];
for (const row of rows) {
if (typeof row !== "object" || row === null) {
continue;
}
const keyed = row as { pubkey?: unknown; account?: { data?: unknown } };
if (typeof keyed.pubkey !== "string" || typeof keyed.account?.data !== "object" || keyed.account.data === null) {
continue;
}
const data = keyed.account.data as { parsed?: { info?: unknown } };
const info = data.parsed?.info;
if (typeof info !== "object" || info === null) {
continue;
}
const typedInfo = info as { mint?: unknown; tokenAmount?: unknown };
const amount = typedInfo.tokenAmount;
if (typeof typedInfo.mint !== "string" || typeof amount !== "object" || amount === null) {
continue;
}
const typedAmount = amount as { amount?: unknown; decimals?: unknown; uiAmountString?: unknown };
if (typeof typedAmount.amount !== "string" || typeof typedAmount.decimals !== "number" || typeof typedAmount.uiAmountString !== "string") {
continue;
}
parsed.push({
program,
mint: typedInfo.mint,
tokenAccount: keyed.pubkey,
uiAmount: typedAmount.uiAmountString,
rawAmount: typedAmount.amount,
decimals: typedAmount.decimals,
});
}
return parsed;
}
function renderTokenRows(tokens: ReadonlyArray<ParsedTokenAccount>): void {
const body = document.querySelector<HTMLTableSectionElement>("#walletTokensTableBody");
if (!body) {
return;
}
body.replaceChildren();
if (tokens.length === 0) {
const row = document.createElement("tr");
const cell = document.createElement("td");
cell.colSpan = 5;
cell.className = "text-body-secondary text-center py-3";
cell.textContent = "Aucun compte token trouvé.";
row.appendChild(cell);
body.appendChild(row);
return;
}
for (const token of tokens) {
const row = document.createElement("tr");
for (const value of [token.program, token.mint, token.tokenAccount, token.uiAmount, token.rawAmount]) {
const cell = document.createElement("td");
cell.textContent = value;
if (value === token.mint || value === token.tokenAccount) {
cell.className = "font-monospace text-break";
}
row.appendChild(cell);
}
body.appendChild(row);
}
}
function parseSignatureRows(value: unknown): ParsedSignatureInfo[] {
if (!Array.isArray(value)) {
return [];
}
const rows: ParsedSignatureInfo[] = [];
for (const item of value) {
if (typeof item !== "object" || item === null) {
continue;
}
const typed = item as Record<string, unknown>;
if (typeof typed.signature !== "string" || typeof typed.slot !== "number") {
continue;
}
rows.push({
signature: typed.signature,
slot: typed.slot,
err: typed.err ?? null,
memo: typeof typed.memo === "string" ? typed.memo : null,
blockTime: typeof typed.blockTime === "number" ? typed.blockTime : null,
confirmationStatus: typeof typed.confirmationStatus === "string" ? typed.confirmationStatus : null,
});
}
return rows;
}
function formatBlockTime(blockTime: number | null): string {
if (blockTime === null) {
return "—";
}
return new Date(blockTime * 1000).toLocaleString();
}
async function loadTransactionDetails(profile: string, signature: string): Promise<void> {
renderJsonViewer("#walletTransactionJson", { status: "loading", signature });
try {
const response = await executeWalletRpc(profile, "getTransaction", [
signature,
{ commitment: "confirmed", encoding: "jsonParsed", maxSupportedTransactionVersion: 0 },
]);
renderJsonViewer("#walletTransactionJson", parseJsonResponse(response));
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
renderJsonViewer("#walletTransactionJson", { status: "error", message, signature });
}
}
function renderSignatureRows(rows: ReadonlyArray<ParsedSignatureInfo>, profile: string): void {
const body = document.querySelector<HTMLTableSectionElement>("#walletTransactionsTableBody");
if (!body) {
return;
}
body.replaceChildren();
if (rows.length === 0) {
const row = document.createElement("tr");
const cell = document.createElement("td");
cell.colSpan = 6;
cell.className = "text-body-secondary text-center py-3";
cell.textContent = "Aucune transaction trouvée.";
row.appendChild(cell);
body.appendChild(row);
return;
}
for (const item of rows) {
const row = document.createElement("tr");
const signatureCell = document.createElement("td");
const slotCell = document.createElement("td");
const timeCell = document.createElement("td");
const statusCell = document.createElement("td");
const confirmationCell = document.createElement("td");
const actionCell = document.createElement("td");
const detailsButton = document.createElement("button");
signatureCell.textContent = item.signature;
signatureCell.className = "font-monospace text-break";
slotCell.textContent = String(item.slot);
timeCell.textContent = formatBlockTime(item.blockTime);
statusCell.textContent = item.err === null ? "OK" : "Erreur";
confirmationCell.textContent = item.confirmationStatus ?? "—";
detailsButton.type = "button";
detailsButton.className = "btn btn-sm btn-outline-primary";
detailsButton.textContent = "Détails";
detailsButton.addEventListener("click", () => {
void loadTransactionDetails(profile, item.signature);
});
actionCell.appendChild(detailsButton);
row.append(signatureCell, slotCell, timeCell, statusCell, confirmationCell, actionCell);
body.appendChild(row);
}
}
async function loadWalletOnchain(): Promise<void> {
const profile = selectedOnchainProfile();
const address = inputValue("#walletAddressInput");
if (profile.length === 0) {
setText("#walletOnchainStatus", "Profil RPC requis");
return;
}
if (address.length === 0) {
setText("#walletOnchainStatus", "Adresse requise");
return;
}
const requestedLimit = Number.parseInt(inputValue("#walletSignatureLimitInput"), 10);
const signatureLimit = Number.isFinite(requestedLimit) ? Math.max(1, Math.min(100, requestedLimit)) : 20;
setText("#walletOnchainStatus", "Chargement...");
setText("#walletTokenStatus", "Chargement...");
setText("#walletTransactionStatus", "Chargement...");
setText("#walletSolBalance", "—");
setText("#walletLamportBalance", "—");
setText("#walletTokenCount", "—");
setText("#walletTransactionCount", "—");
renderTokenRows([]);
renderSignatureRows([], profile);
renderJsonViewer("#walletTransactionJson", { status: "idle", message: "Sélectionner une transaction." });
const operations: Promise<void>[] = [];
operations.push((async () => {
try {
const balance = await invoke<DemoWalletBalancePayload>("demo_wallet_balance", { profile, address });
setText("#walletSolBalance", balance.sol);
setText("#walletLamportBalance", balance.lamports);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletSolBalance", "Erreur");
setText("#walletLamportBalance", message);
}
})());
operations.push((async () => {
try {
if (!walletInventory) {
throw new Error("Inventaire wallet non chargé.");
}
const [classic, token2022] = await Promise.all([
executeWalletRpc(profile, "getTokenAccountsByOwner", [
address,
{ programId: walletInventory.token_program_id },
{ encoding: "jsonParsed", commitment: "confirmed" },
]),
executeWalletRpc(profile, "getTokenAccountsByOwner", [
address,
{ programId: walletInventory.token_2022_program_id },
{ encoding: "jsonParsed", commitment: "confirmed" },
]),
]);
const tokens = [
...parseTokenAccounts(parseJsonResponse(classic), "SPL Token"),
...parseTokenAccounts(parseJsonResponse(token2022), "Token-2022"),
];
tokens.sort((left, right) => left.mint.localeCompare(right.mint) || left.tokenAccount.localeCompare(right.tokenAccount));
setText("#walletTokenCount", String(tokens.length));
setText("#walletTokenStatus", "OK");
renderTokenRows(tokens);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletTokenCount", "0");
setText("#walletTokenStatus", `Erreur : ${message}`);
renderTokenRows([]);
}
})());
operations.push((async () => {
try {
const history = await executeWalletRpc(profile, "getSignaturesForAddress", [
address,
{ commitment: "confirmed", limit: signatureLimit },
]);
const rows = parseSignatureRows(parseJsonResponse(history));
setText("#walletTransactionCount", String(rows.length));
setText("#walletTransactionStatus", "OK");
renderSignatureRows(rows, profile);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletTransactionCount", "0");
setText("#walletTransactionStatus", `Erreur : ${message}`);
renderSignatureRows([], profile);
}
})());
await Promise.all(operations);
const selected = selectedOnchainProfilePayload();
setText("#walletOnchainStatus", `Terminé — ${selected?.name ?? profile} / ${selected?.cluster ?? "cluster inconnu"}`);
}
async function inspectWalletFile(): Promise<void> {
const path = inputValue("#walletExternalPathInput");
if (path.length === 0) {
setText("#walletInspectionStatus", "Chemin requis");
renderJsonViewer("#walletInspectionJson", { status: "error", message: "Saisir un chemin vers un fichier .kswallet." });
return;
}
setText("#walletInspectionStatus", "Inspection...");
renderJsonViewer("#walletInspectionJson", { status: "loading", message: "Inspection du wallet natif." });
try {
const payload = await invoke<DemoWalletIdentityPayload>("demo_wallet_inspect_file", { path });
setText("#walletInspectionStatus", "OK");
renderJsonViewer("#walletInspectionJson", payload);
const addressInput = document.querySelector<HTMLInputElement>("#walletAddressInput");
if (addressInput) {
addressInput.value = payload.public_key;
}
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", "external wallet inspected");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletInspectionStatus", `Erreur : ${message}`);
renderJsonViewer("#walletInspectionJson", { status: "error", message });
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `External wallet inspection failed: ${message}`);
}
}
function clearWalletInspection(): void {
const pathInput = document.querySelector<HTMLInputElement>("#walletExternalPathInput");
if (pathInput) {
pathInput.value = "";
}
setText("#walletInspectionStatus", "En attente");
renderJsonViewer("#walletInspectionJson", { status: "idle", message: "Aucun fichier inspecté." });
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb-app-demo-desktop.frontend.demo_wallet");
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", "wallet demo window loaded");
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
document.querySelector<HTMLButtonElement>("#refreshWalletInventoryButton")?.addEventListener("click", () => {
void refreshWalletInventory();
});
document.querySelector<HTMLButtonElement>("#applyWalletExecutionSelectionButton")?.addEventListener("click", () => {
void applyExecutionWalletSelection();
});
document.querySelector<HTMLSelectElement>("#walletExecutionProfileSelect")?.addEventListener("change", () => {
updateExecutionSelectionDisplay();
if (walletInventory) {
renderWalletRows(walletInventory.wallets);
}
setText("#walletExecutionSelectionStatus", "En attente");
});
document.querySelector<HTMLButtonElement>("#createWalletButton")?.addEventListener("click", () => {
void createWallet();
});
document.querySelector<HTMLInputElement>("#createWalletAliasInput")?.addEventListener("keydown", event => {
if (event.key === "Enter") {
event.preventDefault();
void createWallet();
}
});
document.querySelector<HTMLButtonElement>("#inspectWalletTransferButton")?.addEventListener("click", () => {
void inspectTransferFile();
});
document.querySelector<HTMLButtonElement>("#importWalletTransferButton")?.addEventListener("click", () => {
void importTransferFile();
});
document.querySelector<HTMLButtonElement>("#exploreWalletTransferButton")?.addEventListener("click", () => {
exploreInspectedTransfer();
});
document.querySelector<HTMLButtonElement>("#exportWalletTransferButton")?.addEventListener("click", () => {
void exportWallet();
});
document.querySelector<HTMLSelectElement>("#walletExportAliasSelect")?.addEventListener("change", () => {
updateExportFileName(false);
});
document.querySelector<HTMLSelectElement>("#walletExportFormatSelect")?.addEventListener("change", () => {
updateExportFileName(false);
});
document.querySelector<HTMLInputElement>("#walletExportFileNameInput")?.addEventListener("input", event => {
const input = event.currentTarget as HTMLInputElement;
input.dataset.autoGenerated = "false";
});
document.querySelector<HTMLInputElement>("#walletImportSourcePathInput")?.addEventListener("input", () => {
setTransferPublicKey("");
setText("#walletTransferStatus", "En attente");
});
document.querySelector<HTMLSelectElement>("#walletImportFormatSelect")?.addEventListener("change", () => {
setTransferPublicKey("");
setText("#walletTransferStatus", "En attente");
});
document.querySelector<HTMLSelectElement>("#walletOnchainProfileSelect")?.addEventListener("change", () => {
updateOnchainClusterLabel();
});
document.querySelector<HTMLButtonElement>("#loadWalletOnchainButton")?.addEventListener("click", () => {
void loadWalletOnchain();
});
document.querySelector<HTMLInputElement>("#walletAddressInput")?.addEventListener("keydown", event => {
if (event.key === "Enter") {
event.preventDefault();
void loadWalletOnchain();
}
});
document.querySelector<HTMLButtonElement>("#inspectWalletFileButton")?.addEventListener("click", () => {
void inspectWalletFile();
});
document.querySelector<HTMLButtonElement>("#clearWalletInspectionButton")?.addEventListener("click", () => {
clearWalletInspection();
});
document.querySelector<HTMLInputElement>("#walletExternalPathInput")?.addEventListener("keydown", event => {
if (event.key === "Enter") {
event.preventDefault();
void inspectWalletFile();
}
});
void refreshWalletInventory();
});

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/main.ts
// version: 6
// version: 8
import * as bootstrap from "bootstrap";
import MarkdownIt from "markdown-it";
@@ -44,6 +44,10 @@ document.addEventListener("DOMContentLoaded", async () => {
event.preventDefault();
void openDemoConfigWindow();
});
document.querySelector<HTMLAnchorElement>("#openDemoWalletLink")?.addEventListener("click", event => {
event.preventDefault();
void invoke("open_demo_wallet_window");
});
frontendDebug("kb-app-demo-desktop.frontend.main", "main window loaded");
const readmeContent = document.querySelector<HTMLElement>("#readmeContent");
if (readmeContent) {
@@ -93,10 +97,10 @@ for (const [elementId, command] of pipelineDemoLinks) {
}
const sqlDemoLinks: ReadonlyArray<readonly [string, string]> = [
["openDemoSqlDiagLink", "open_demo_sql_diag_window"],
["openDemoSqlPgRawLink", "open_demo_sql_pg_raw_window"],
["openDemoSqlPgCoreLink", "open_demo_sql_pg_core_window"],
["openDemoSqlReplayCandidatesLink", "open_demo_sql_replay_candidates_window"],
["openDemoStoreDiagLink", "open_demo_store_diag_window"],
["openDemoStoreRawLink", "open_demo_store_raw_window"],
["openDemoStoreCoreLink", "open_demo_store_core_window"],
["openDemoStoreReplayCandidatesLink", "open_demo_store_replay_candidates_window"],
];
for (const [elementId, command] of sqlDemoLinks) {
const link = document.querySelector<HTMLAnchorElement>(`#${elementId}`);

View File

@@ -1,7 +1,7 @@
{
"name": "kb-app-demo-desktop",
"private": true,
"version": "0.5.1",
"version": "0.5.3",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/app_state.rs
// version: 16
// version: 21
//! Shared Tauri application state and startup initialization.
@@ -17,6 +17,13 @@ pub(crate) struct AppState {
active_profile: ks_config::ProfileConfig,
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:
@@ -30,6 +37,8 @@ pub(crate) struct AppState {
demo_decode_replay_campaign_id: std::sync::Mutex<std::option::Option<std::string::String>>,
demo_execution_solana_core_running: std::sync::atomic::AtomicBool,
demo_execution_solana_core_cancel_requested: std::sync::atomic::AtomicBool,
demo_execution_wallet_alias_overrides:
std::sync::Mutex<std::collections::BTreeMap<std::string::String, std::string::String>>,
startup_sequence_started: std::sync::atomic::AtomicBool,
}
@@ -104,6 +113,8 @@ impl crate::AppState {
active_profile,
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),
@@ -115,6 +126,9 @@ impl crate::AppState {
demo_decode_replay_campaign_id: std::sync::Mutex::new(std::option::Option::None),
demo_execution_solana_core_running: std::sync::atomic::AtomicBool::new(false),
demo_execution_solana_core_cancel_requested: std::sync::atomic::AtomicBool::new(false),
demo_execution_wallet_alias_overrides: std::sync::Mutex::new(
std::collections::BTreeMap::new(),
),
startup_sequence_started: std::sync::atomic::AtomicBool::new(false),
});
}
@@ -169,6 +183,122 @@ impl crate::AppState {
return &self.active_profile;
}
/// Returns the persistent backend-agnostic store opened from the active profile.
pub(crate) async fn store(&self) -> ks_core::Result<&ks_store::Store> {
if let std::option::Option::Some(store) = self.store.get() {
return std::result::Result::Ok(store);
}
let options = match ks_store::StoreOpenOptions::new(
self.active_profile.database.enabled,
self.active_profile.database.backend.clone(),
self.active_profile.database.backend_options.clone(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return self
.store
.get_or_try_init(|| return async move { return ks_store::Store::open(options).await })
.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,
profile_name: &str,
) -> std::result::Result<std::option::Option<std::string::String>, std::string::String> {
let lock_result = self.demo_execution_wallet_alias_overrides.lock();
let guard = match lock_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(
"demo execution wallet selection lock is poisoned".to_string(),
);
},
};
return std::result::Result::Ok(guard.get(profile_name).cloned());
}
/// Sets or clears the session-only execution-wallet alias override for one profile.
pub(crate) fn set_demo_execution_wallet_alias_override(
&self,
profile_name: std::string::String,
alias: std::option::Option<std::string::String>,
) -> std::result::Result<(), std::string::String> {
let lock_result = self.demo_execution_wallet_alias_overrides.lock();
let mut guard = match lock_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(
"demo execution wallet selection lock is poisoned".to_string(),
);
},
};
if let std::option::Option::Some(alias) = alias {
guard.insert(profile_name, alias);
} else {
guard.remove(profile_name.as_str());
}
return std::result::Result::Ok(());
}
/// Returns the configured HTTP endpoint pool.
pub(crate) fn http_pool(&self) -> &ks_onchain_transport::HttpEndpointPool {
return &self.http_pool;

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_backfill.rs
// version: 16
// version: 17
//! Tauri commands and UI payloads for bounded HTTP transaction backfills.
@@ -288,23 +288,10 @@ pub(crate) async fn demo_backfill_execute(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let profile = state.active_profile();
let store_options = match ks_store::PostgresStoreOptions::new(
profile.database.postgres.url.clone(),
profile.database.postgres.max_connections,
profile.database.postgres.connect_timeout_ms,
false,
) {
let store = match crate::shared_store(state.inner()).await {
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 store = match ks_store::PostgresStore::connect(store_options).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
return std::result::Result::Err(error.to_string());
}
let observer = crate::DemoBackfillObserver {
app_handle,
cancel_requested: state.demo_backfill_cancel_requested(),

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_config.rs
// version: 5
// version: 8
//! Configuration demo payloads with explicit public and bounded diagnostic projections.
@@ -164,7 +164,7 @@ pub(crate) struct DemoConfigEndpointDiagnosticPayload {
pub(crate) role_count: u32,
}
/// Store diagnostic projection without DSN or SQLite path content.
/// Backend-agnostic store diagnostic projection without backend option values.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
export,
@@ -173,23 +173,16 @@ pub(crate) struct DemoConfigEndpointDiagnosticPayload {
pub(crate) struct DemoConfigStoreDiagnosticPayload {
/// Whether persistence is enabled.
pub(crate) enabled: bool,
/// Selected store backend.
/// Selected store backend code.
pub(crate) backend: std::string::String,
/// Whether a PostgreSQL URL is configured.
pub(crate) postgres_url_state: std::string::String,
/// PostgreSQL connection pool ceiling.
pub(crate) postgres_max_connections: u32,
/// PostgreSQL connection timeout in milliseconds.
#[ts(type = "number")]
pub(crate) postgres_connect_timeout_ms: u64,
/// Whether PostgreSQL schema initialization is enabled.
pub(crate) postgres_auto_initialize_schema: bool,
/// Whether a SQLite path is configured.
pub(crate) sqlite_path_state: std::string::String,
/// SQLite connection pool ceiling.
pub(crate) sqlite_max_connections: u32,
/// Whether SQLite schema initialization is enabled.
pub(crate) sqlite_auto_initialize_schema: bool,
/// Whether backend-specific configuration validated successfully.
pub(crate) configuration_status: std::string::String,
/// Whether a backend connection or location value is configured.
pub(crate) connection_state: std::string::String,
/// Whether automatic store initialization is enabled.
pub(crate) auto_initialize_schema: bool,
/// Number of opaque backend options supplied after composition.
pub(crate) backend_option_count: u32,
}
/// Wallet diagnostic projection without paths or secret key material.
@@ -203,6 +196,8 @@ pub(crate) struct DemoConfigWalletDiagnosticPayload {
pub(crate) wallet_directory_state: std::string::String,
/// Wallet cluster.
pub(crate) cluster: std::string::String,
/// Persistent native wallet alias selected by configuration, when present.
pub(crate) wallet_alias: std::option::Option<std::string::String>,
/// Whether the managed temporary wallet is enabled.
pub(crate) temporary_wallet_enabled: bool,
/// Temporary wallet alias, which is internal but not secret.
@@ -373,20 +368,11 @@ fn diagnostic_config_payload(state: &crate::AppState) -> DemoConfigDiagnosticPay
source_diagnostic("execution", state.execution_config_path()),
],
endpoints,
store: DemoConfigStoreDiagnosticPayload {
enabled: profile.database.enabled,
backend: profile.database.backend.clone(),
postgres_url_state: configured_state(profile.database.postgres.url.as_str()),
postgres_max_connections: profile.database.postgres.max_connections,
postgres_connect_timeout_ms: profile.database.postgres.connect_timeout_ms,
postgres_auto_initialize_schema: profile.database.postgres.auto_initialize_schema,
sqlite_path_state: configured_state(profile.database.sqlite.path.as_str()),
sqlite_max_connections: profile.database.sqlite.max_connections,
sqlite_auto_initialize_schema: profile.database.sqlite.auto_initialize_schema,
},
store: store_diagnostic(profile),
wallet: DemoConfigWalletDiagnosticPayload {
wallet_directory_state: configured_state(profile.wallet.wallet_dir.as_str()),
cluster: profile.wallet.cluster.clone(),
wallet_alias: profile.wallet.wallet_alias.clone(),
temporary_wallet_enabled: profile.wallet.temporary_wallet_enabled,
temporary_wallet_alias: profile.wallet.temporary_wallet_alias.clone(),
temporary_wallet_persist: profile.wallet.temporary_wallet_persist,
@@ -395,6 +381,55 @@ fn diagnostic_config_payload(state: &crate::AppState) -> DemoConfigDiagnosticPay
};
}
fn store_diagnostic(profile: &ks_config::ProfileConfig) -> DemoConfigStoreDiagnosticPayload {
let backend_option_count = match profile.database.backend_options.as_object() {
std::option::Option::Some(value) => bounded_len(value.len()),
std::option::Option::None => 0,
};
let options = match ks_store::StoreOpenOptions::new(
profile.database.enabled,
profile.database.backend.clone(),
profile.database.backend_options.clone(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return DemoConfigStoreDiagnosticPayload {
enabled: profile.database.enabled,
backend: profile.database.backend.clone(),
configuration_status: "invalid".to_string(),
connection_state: "unknown".to_string(),
auto_initialize_schema: false,
backend_option_count,
};
},
};
let summary = match options.configuration_summary() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return DemoConfigStoreDiagnosticPayload {
enabled: profile.database.enabled,
backend: profile.database.backend.clone(),
configuration_status: "invalid".to_string(),
connection_state: "unknown".to_string(),
auto_initialize_schema: false,
backend_option_count,
};
},
};
return DemoConfigStoreDiagnosticPayload {
enabled: summary.enabled,
backend: summary.backend_code,
configuration_status: "ready".to_string(),
connection_state: if summary.connection_configured {
"configured".to_string()
} else {
"missing".to_string()
},
auto_initialize_schema: summary.auto_initialize_schema,
backend_option_count: summary.backend_option_count,
};
}
fn source_diagnostic(category: &str, path: &str) -> DemoConfigSourceDiagnosticPayload {
return DemoConfigSourceDiagnosticPayload {
category: category.to_string(),
@@ -500,9 +535,19 @@ mod tests {
#[test]
fn public_and_diagnostic_projections_never_serialize_secret_canaries() {
let mut profile = active_fixture_profile();
profile.database.postgres.url =
"postgres://operator:POSTGRES-SECRET-CANARY@localhost/solana".to_string();
profile.database.sqlite.path = "/private/SQLITE-PATH-CANARY.sqlite".to_string();
if let std::option::Option::Some(options) = profile.database.backend_options.as_object_mut()
{
options.insert(
"url".to_string(),
serde_json::Value::String(
"postgres://operator:POSTGRES-SECRET-CANARY@localhost/solana".to_string(),
),
);
options.insert(
"private_path".to_string(),
serde_json::Value::String("/private/STORE-PATH-CANARY".to_string()),
);
}
profile.wallet.wallet_dir = "/private/WALLET-PATH-CANARY".to_string();
if let std::option::Option::Some(endpoint) = profile.solana.http_endpoints.first_mut() {
endpoint.url = "https://provider.invalid/?api-key=HELIUS-SECRET-CANARY".to_string();
@@ -533,20 +578,11 @@ mod tests {
};
})
.collect::<std::vec::Vec<super::DemoConfigEndpointDiagnosticPayload>>(),
store: super::DemoConfigStoreDiagnosticPayload {
enabled: profile.database.enabled,
backend: profile.database.backend.clone(),
postgres_url_state: super::configured_state(profile.database.postgres.url.as_str()),
postgres_max_connections: profile.database.postgres.max_connections,
postgres_connect_timeout_ms: profile.database.postgres.connect_timeout_ms,
postgres_auto_initialize_schema: profile.database.postgres.auto_initialize_schema,
sqlite_path_state: super::configured_state(profile.database.sqlite.path.as_str()),
sqlite_max_connections: profile.database.sqlite.max_connections,
sqlite_auto_initialize_schema: profile.database.sqlite.auto_initialize_schema,
},
store: super::store_diagnostic(&profile),
wallet: super::DemoConfigWalletDiagnosticPayload {
wallet_directory_state: super::configured_state(profile.wallet.wallet_dir.as_str()),
cluster: profile.wallet.cluster.clone(),
wallet_alias: profile.wallet.wallet_alias.clone(),
temporary_wallet_enabled: profile.wallet.temporary_wallet_enabled,
temporary_wallet_alias: profile.wallet.temporary_wallet_alias.clone(),
temporary_wallet_persist: profile.wallet.temporary_wallet_persist,
@@ -559,7 +595,7 @@ mod tests {
};
for secret in [
"POSTGRES-SECRET-CANARY",
"SQLITE-PATH-CANARY",
"STORE-PATH-CANARY",
"WALLET-PATH-CANARY",
"HELIUS-SECRET-CANARY",
"WS-SECRET-CANARY",

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_core_extraction.rs
// version: 14
// version: 17
//! Tauri commands and UI payloads for canonical transaction to core extraction.
@@ -221,7 +221,7 @@ pub(crate) async fn demo_core_extraction_execute(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store_result = crate::connect_postgres_store(state.active_profile()).await;
let store_result = crate::shared_store(state).await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_decode_replay.rs
// version: 40
// version: 46
//! Tauri commands and UI payloads for contextual instruction decode replay.
@@ -255,8 +255,8 @@ pub(crate) struct DemoDecodeCoverageSummaryPayload {
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeDiagnosticsPayload.ts"
)]
pub(crate) struct DemoDecodeDiagnosticsPayload {
/// Decode, materialization and ledger table diagnostics.
pub(crate) tables: std::vec::Vec<crate::DemoSqlTableSnapshot>,
/// Decode, materialization and ledger resource diagnostics.
pub(crate) resources: std::vec::Vec<crate::DemoStoreResourceSnapshot>,
/// Aggregated declared and observed coverage.
pub(crate) coverage: std::vec::Vec<crate::DemoDecodeCoverageSummaryPayload>,
}
@@ -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,
@@ -495,7 +511,7 @@ pub(crate) async fn demo_decode_replay_execute(
materialize_after_decode = pipeline_request.materialize_after_decode,
"start contextual decode replay pipeline"
);
let store_result = crate::connect_postgres_store(state.active_profile()).await;
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) => {
@@ -563,24 +579,24 @@ pub(crate) async fn demo_decode_replay_diagnostics(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::DemoDecodeDiagnosticsPayload, std::string::String> {
tracing::debug!(target: crate::TRACING_TARGET, action = "load_diagnostics", coverage_limit = 500_u32, "load contextual decode replay diagnostics");
let store_result = crate::connect_postgres_store(state.active_profile()).await;
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 table_result = store.known_table_diagnostics().await;
let all_tables = match table_result {
let resource_result = store.known_resource_diagnostics().await;
let all_resources = match resource_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let selected_tables: std::vec::Vec<crate::DemoSqlTableSnapshot> = all_tables
let selected_resources: std::vec::Vec<crate::DemoStoreResourceSnapshot> = all_resources
.iter()
.filter(|table| {
return table.table_name.starts_with("kb_sol_decode_")
|| table.table_name.starts_with("kb_sol_mat_")
|| table.table_name == "kb_sol_ops_processing_ledger";
.filter(|resource| {
return resource.model_code == "processing"
|| resource.model_code == "decode"
|| resource.model_code == "materialization";
})
.map(crate::table_snapshot_from_pg)
.map(crate::store_resource_snapshot)
.collect();
let coverage_result = ks_store::DecodePipelineStore::list_decode_coverage_summary(
&store,
@@ -593,9 +609,9 @@ pub(crate) async fn demo_decode_replay_diagnostics(
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, action = "load_diagnostics", table_count = selected_tables.len(), coverage_count = coverage.len(), "contextual decode replay diagnostics loaded");
tracing::debug!(target: crate::TRACING_TARGET, action = "load_diagnostics", resource_count = selected_resources.len(), coverage_count = coverage.len(), "contextual decode replay diagnostics loaded");
return std::result::Result::Ok(crate::DemoDecodeDiagnosticsPayload {
tables: selected_tables,
resources: selected_resources,
coverage: coverage.into_iter().map(coverage_payload).collect(),
});
}
@@ -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::MaterializedEventFilter::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");
let store_result = crate::connect_postgres_store(state.active_profile()).await;
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_events(&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(
@@ -882,7 +908,7 @@ fn coverage_payload(
}
fn annotation_payload(
row: ks_store::MaterializedEventQueryRow,
row: ks_store::MaterializedOutputQueryRow,
) -> std::result::Result<crate::DemoTransactionAnnotationRow, std::string::String> {
if row.processor_name != "materializer.transaction.annotations"
|| row.materialized_family != "transaction_annotation"
@@ -1274,7 +1300,7 @@ mod tests {
#[test]
fn committed_annotation_row_is_mapped_to_ui_safe_contract() {
let row = ks_store::MaterializedEventQueryRow {
let row = ks_store::MaterializedOutputQueryRow {
processor_name: "materializer.transaction.annotations".to_string(),
processor_version: "0.4.3".to_string(),
input_key: "input".to_string(),
@@ -1310,7 +1336,7 @@ mod tests {
#[test]
fn malformed_annotation_payload_fails_closed() {
let row = ks_store::MaterializedEventQueryRow {
let row = ks_store::MaterializedOutputQueryRow {
processor_name: "materializer.transaction.annotations".to_string(),
processor_version: "0.4.3".to_string(),
input_key: "input".to_string(),

View File

@@ -1,11 +1,13 @@
// file: kb-app-demo-desktop/src/demo_devnet_common.rs
// version: 4
// version: 9
//! Shared Devnet demo UI contracts.
//! Shared Devnet demo UI contracts and profile/store helpers.
use ts_rs::TS; // rust-rules: derive-import
/// Readiness report for one Devnet profile PostgreSQL store.
const DEMO_WALLET_PASSWORD_ENV: &str = "KB_SECRET_DEMO_WALLET_PASSWORD";
/// Backend-agnostic readiness report for one Devnet profile store.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
@@ -15,29 +17,81 @@ use ts_rs::TS; // rust-rules: derive-import
pub(crate) struct DemoExecutionDevnetStoreReadinessPayload {
/// Selected Devnet profile.
pub(crate) profile_name: std::string::String,
/// Selected backend code.
pub(crate) backend: std::string::String,
/// Whether schema creation was allowed by configuration.
pub(crate) auto_initialize_schema: bool,
/// Number of known tables already present before preparation.
pub(crate) existing_tables_before: u32,
/// Number of known tables created during preparation.
pub(crate) created_tables: u32,
/// Number of known tables present after preparation.
pub(crate) existing_tables_after: u32,
/// Total number of known tables expected by the current store.
pub(crate) expected_tables: u32,
/// Number of logical resources already present before preparation.
pub(crate) available_resources_before: u32,
/// Number of logical resources created during preparation.
pub(crate) created_resources: u32,
/// Number of logical resources present after preparation.
pub(crate) available_resources_after: u32,
/// Total number of logical resources expected by the current store contract.
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 {
profile_name: readiness.profile_name,
backend: readiness.backend,
auto_initialize_schema: readiness.auto_initialize_schema,
existing_tables_before: bounded_u32(readiness.existing_tables_before),
created_tables: bounded_u32(readiness.created_tables),
existing_tables_after: bounded_u32(readiness.existing_tables_after),
expected_tables: bounded_u32(readiness.expected_tables),
available_resources_before: bounded_u32(readiness.available_resources_before),
created_resources: bounded_u32(readiness.created_resources),
available_resources_after: bounded_u32(readiness.available_resources_after),
expected_resources: bounded_u32(readiness.expected_resources),
};
}
@@ -73,3 +127,58 @@ fn bounded_u32(value: usize) -> u32 {
std::result::Result::Err(_) => u32::MAX,
};
}
/// Returns whether the backend-only demo wallet password is configured.
pub(crate) fn demo_wallet_password_configured() -> bool {
return std::env::var_os(DEMO_WALLET_PASSWORD_ENV).is_some();
}
/// Loads the backend-only demo wallet password without exposing it to Tauri DTOs.
pub(crate) fn demo_wallet_password()
-> std::result::Result<ks_wallet::WalletPassword, std::string::String> {
let password_text = match std::env::var(DEMO_WALLET_PASSWORD_ENV) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(format!(
"backend secret {DEMO_WALLET_PASSWORD_ENV} is not configured as UTF-8"
));
},
};
return match ks_wallet::WalletPassword::new(password_text) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
}
/// Resolves the temporary or persistent signer selected by one Devnet profile.
pub(crate) async fn resolve_devnet_execution_wallet(
state: &crate::AppState,
profile: &ks_config::ProfileConfig,
) -> std::result::Result<ks_pipeline_demo_scenarios::DevnetExecutionWallet, std::string::String> {
let alias_override = match state.demo_execution_wallet_alias_override(profile.name.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut effective_profile = profile.clone();
if let std::option::Option::Some(alias) = alias_override {
effective_profile.wallet.wallet_alias = std::option::Option::Some(alias);
}
let password = if effective_profile.wallet.wallet_alias.is_some() {
match demo_wallet_password() {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
} else {
std::option::Option::None
};
return match ks_pipeline_demo_scenarios::resolve_devnet_execution_wallet(
&effective_profile,
crate::workspace_root_dir().as_path(),
password,
)
.await
{
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
}

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_execution_metadata_metaplex_token_metadata.rs
// version: 8
// version: 14
//! Desktop adapters for generic and qualified Metaplex Token Metadata execution workflows.
@@ -190,13 +190,10 @@ 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::connect_postgres_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()),
};
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
return std::result::Result::Err(error.to_string());
}
let mut execution_request = match ks_pipeline_demo_scenarios::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json(
request.intent_id,
request.operation_json.as_str(),
@@ -235,22 +232,26 @@ pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_execute(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let workspace_root = crate::workspace_root_dir();
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_token_metadata(
&http_pool,
&store,
&profile,
workspace_root.as_path(),
&execution_request,
decoders.as_slice(),
materializers.as_slice(),
&observer,
)
.await
{
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
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 summary =
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_token_metadata_with_wallet(
&http_pool,
&store,
&profile,
&execution_wallet,
&execution_request,
decoders.as_slice(),
materializers.as_slice(),
&observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let transaction_signature =
summary.send_result.as_ref().map(|value| return value.signature.0.clone());
let confirmation_status =
@@ -417,7 +418,7 @@ async fn prepare_create_fixture_for_family(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let configured = std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str());
let wallet_dir = if configured.is_absolute() {
configured
} else {
@@ -1040,15 +1041,13 @@ 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::connect_postgres_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()),
};
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
return std::result::Result::Err(error.to_string());
}
let workspace_root = crate::workspace_root_dir();
let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let configured_wallet_dir =
std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str());
let wallet_dir = if configured_wallet_dir.is_absolute() {
configured_wallet_dir
} else {
@@ -1070,8 +1069,8 @@ pub(crate) async fn demo_execution_metadata_metaplex_execute_campaign(
wallet_dir,
&http_pool,
&store,
&state,
&profile,
workspace_root.as_path(),
&observer,
)
.await
@@ -1133,8 +1132,8 @@ fn execute_campaign_projection<'a, S, O>(
wallet_dir: std::path::PathBuf,
http_pool: &'a ks_onchain_transport::HttpEndpointPool,
store: &'a S,
state: &'a crate::AppState,
profile: &'a ks_config::ProfileConfig,
workspace_root: &'a std::path::Path,
observer: &'a O,
) -> CampaignProjectionFuture<'a>
where
@@ -1150,8 +1149,8 @@ where
wallet_dir,
http_pool,
store,
state,
profile,
workspace_root,
observer,
));
}
@@ -1161,8 +1160,8 @@ async fn execute_campaign_projection_unboxed<S, O>(
wallet_dir: std::path::PathBuf,
http_pool: &ks_onchain_transport::HttpEndpointPool,
store: &S,
state: &crate::AppState,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
observer: &O,
) -> ks_core::Result<CampaignProjection>
where
@@ -1174,6 +1173,15 @@ where
{
let base_options =
ks_pipeline_demo_scenarios::MetaplexCreateFixturePreparationOptions::new(wallet_dir);
let execution_wallet = match crate::resolve_devnet_execution_wallet(state, profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"demo_wallet_resolution_failed",
error,
));
},
};
return match campaign_id {
"create_mint_nft" => {
execute_create_mint_projection(
@@ -1184,7 +1192,7 @@ where
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
observer,
)
.await
@@ -1198,7 +1206,7 @@ where
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
observer,
)
.await
@@ -1212,7 +1220,7 @@ where
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
observer,
)
.await
@@ -1226,7 +1234,7 @@ where
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
observer,
)
.await
@@ -1240,17 +1248,17 @@ where
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
observer,
)
.await
},
"collection_verify" => {
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_collection_verify_campaign(
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_collection_verify_campaign_with_wallet(
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
&base_options,
observer,
).await {
@@ -1285,11 +1293,11 @@ where
},
"print_burn" => {
let summary =
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_print_burn_campaign(
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_print_burn_campaign_with_wallet(
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
&base_options,
observer,
)
@@ -1327,11 +1335,11 @@ where
ks_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
);
let summary =
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_pnft_lifecycle_campaign(
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_pnft_lifecycle_campaign_with_wallet(
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
&options,
observer,
)
@@ -1361,11 +1369,11 @@ where
})
},
"escrow" => {
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_escrow_campaign(
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_escrow_campaign_with_wallet(
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
&base_options,
observer,
)
@@ -1410,11 +1418,11 @@ where
ks_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft,
);
let summary =
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_maintenance_campaign(
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_maintenance_campaign_with_wallet(
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
&options,
observer,
)
@@ -1465,19 +1473,20 @@ where
let options = base_options.clone().with_asset_family(
ks_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft,
);
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_use_probe(
http_pool,
store,
profile,
workspace_root,
&options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary =
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_use_probe_with_wallet(
http_pool,
store,
profile,
&execution_wallet,
&options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut executions = vec![
metaplex_execution_json("fixture.create", &summary.fixture.create),
metaplex_execution_json("fixture.mint", &summary.fixture.mint),
@@ -1513,7 +1522,7 @@ async fn execute_create_mint_projection<S, O>(
http_pool: &ks_onchain_transport::HttpEndpointPool,
store: &S,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
wallet: &ks_pipeline_demo_scenarios::DevnetExecutionWallet,
observer: &O,
) -> ks_core::Result<CampaignProjection>
where
@@ -1523,19 +1532,15 @@ where
+ Sync,
O: ks_pipeline_demo_scenarios::SolanaExecutionObserver,
{
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_create_mint_campaign(
http_pool,
store,
profile,
workspace_root,
&options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary =
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_create_mint_campaign_with_wallet(
http_pool, store, profile, wallet, &options, observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let executions = vec![
metaplex_execution_json("create", &summary.create),
metaplex_execution_json("mint", &summary.mint),

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_execution_metadata_solana_program.rs
// version: 3
// version: 6
//! Thin desktop adapter for complete Solana Program Metadata Devnet campaigns.
@@ -170,11 +170,14 @@ pub(crate) async fn demo_execution_metadata_solana_program_execute_campaign(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let workspace_root = crate::workspace_root_dir();
let summary = match ks_pipeline_demo_scenarios::execute_devnet_solana_program_metadata_campaign(
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary = match ks_pipeline_demo_scenarios::execute_devnet_solana_program_metadata_campaign_with_wallet(
&http_pool,
&profile,
workspace_root.as_path(),
&execution_wallet,
&options,
&observer,
)

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_execution_metadata_token_2022.rs
// version: 2
// version: 8
//! Thin desktop adapter for the complete Token-2022 Token Metadata Devnet campaign.
@@ -88,15 +88,13 @@ 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::connect_postgres_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()),
};
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
return std::result::Result::Err(error.to_string());
}
let workspace_root = crate::workspace_root_dir();
let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let configured_wallet_dir =
std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str());
let wallet_dir = if configured_wallet_dir.is_absolute() {
configured_wallet_dir
} else {
@@ -109,19 +107,24 @@ pub(crate) async fn demo_execution_metadata_token_2022_execute_campaign(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let summary = match ks_pipeline_demo_scenarios::execute_devnet_token_2022_metadata_campaign(
&http_pool,
&store,
&profile,
workspace_root.as_path(),
&options,
&observer,
)
.await
{
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
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 summary =
match ks_pipeline_demo_scenarios::execute_devnet_token_2022_metadata_campaign_with_wallet(
&http_pool,
&store,
&profile,
&execution_wallet,
&options,
&observer,
)
.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(campaign_summary_payload(request.profile_name, summary));
}

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_execution_solana_core.rs
// version: 17
// 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 PostgreSQL schema 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,14 +443,10 @@ 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::connect_postgres_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 initialize_result = store.initialize_store_schema().await;
if let std::result::Result::Err(error) = initialize_result {
return std::result::Result::Err(error.to_string());
}
let mut pipeline_request = ks_pipeline_demo_scenarios::DevnetSystemTransferRequest::new(
format!("demo-execution-{}", chrono::Utc::now().timestamp_micros()),
ks_lib::MdPubkey(request.recipient.trim().to_string()),
@@ -509,12 +475,15 @@ pub(crate) async fn demo_execution_solana_core_execute(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let workspace_root = crate::workspace_root_dir();
let summary = match ks_pipeline_demo_scenarios::execute_devnet_system_transfer(
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary = match ks_pipeline_demo_scenarios::execute_devnet_system_transfer_with_wallet(
&http_pool,
&store,
&profile,
workspace_root.as_path(),
&execution_wallet,
&pipeline_request,
decoders.as_slice(),
materializers.as_slice(),
@@ -558,13 +527,10 @@ 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::connect_postgres_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()),
};
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
return std::result::Result::Err(error.to_string());
}
let mut pipeline_request = ks_pipeline_demo_scenarios::DevnetMemoExecutionRequest::new(
format!("demo-memo-execution-{}", chrono::Utc::now().timestamp_micros()),
request.message,
@@ -582,12 +548,15 @@ pub(crate) async fn demo_execution_spl_memo_execute(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let workspace_root = crate::workspace_root_dir();
let summary = match ks_pipeline_demo_scenarios::execute_devnet_memo(
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary = match ks_pipeline_demo_scenarios::execute_devnet_memo_with_wallet(
&http_pool,
&store,
&profile,
workspace_root.as_path(),
&execution_wallet,
&pipeline_request,
decoders.as_slice(),
materializers.as_slice(),

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_spl_ata.rs
// version: 13
// 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,13 +276,10 @@ 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::connect_postgres_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()),
};
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
return std::result::Result::Err(error.to_string());
}
let decoders: std::vec::Vec<std::sync::Arc<dyn ks_lib::DcApiInstructionDecoder>> = std::vec![
std::sync::Arc::new(ks_lib::DcSplAssociatedTokenAccountDecoder),
std::sync::Arc::new(ks_lib::DcSplTokenDecoder),
@@ -279,21 +292,26 @@ pub(crate) async fn demo_execution_spl_ata_execute(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_associated_token_account(
&http_pool,
&store,
&profile,
crate::workspace_root_dir().as_path(),
&pipeline_request,
decoders.as_slice(),
materializers.as_slice(),
&observer,
)
.await
{
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
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 summary =
match ks_pipeline_demo_scenarios::execute_devnet_spl_associated_token_account_with_wallet(
&http_pool,
&store,
&profile,
&execution_wallet,
&pipeline_request,
decoders.as_slice(),
materializers.as_slice(),
&observer,
)
.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(summary_payload(summary));
}
@@ -301,52 +319,60 @@ 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::connect_postgres_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::MaterializedEventFilter::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_events(&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(
profile: &ks_config::ProfileConfig,
) -> std::result::Result<ks_wallet::TemporaryWallet, std::string::String> {
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let configured = std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str());
let directory = if configured.is_absolute() {
configured
} else {
@@ -542,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"),
@@ -558,7 +585,7 @@ fn journal_matches(payload: &serde_json::Value, request: &crate::DemoSplAtaJourn
return true;
}
fn journal_row(row: ks_store::MaterializedEventQueryRow) -> crate::DemoSplAtaJournalRow {
fn journal_row(row: ks_store::MaterializedOutputQueryRow) -> crate::DemoSplAtaJournalRow {
let text = |key: &str| {
return row
.payload_json
@@ -603,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: 10
// version: 19
//! Thin Tauri adapter for classic SPL Token execution and materialized journals.
@@ -99,7 +99,7 @@ pub(crate) struct DemoExecutionSplTokenSummaryPayload {
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_token/DemoSplTokenJournalRequest.ts"
)]
pub(crate) struct DemoSplTokenJournalRequest {
/// Devnet profile whose PostgreSQL store is queried.
/// Devnet profile whose configured store is queried.
pub(crate) profile_name: std::string::String,
/// Optional partial signature handled by the bounded store query.
pub(crate) signature_contains: std::option::Option<std::string::String>,
@@ -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,13 +199,10 @@ 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::connect_postgres_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()),
};
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
return std::result::Result::Err(error.to_string());
}
let amount_raw = request.amount_raw.trim().to_string();
let operation = ks_lib::ExSplClassicTokenOperation::Instruction {
value: ks_lib::ExSplClassicTokenSingleOperation::TransferChecked {
@@ -223,12 +236,15 @@ pub(crate) async fn demo_execution_spl_token_execute(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let workspace_root = crate::workspace_root_dir();
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_token(
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_token_with_wallet(
&http_pool,
&store,
&profile,
workspace_root.as_path(),
&execution_wallet,
&pipeline_request,
decoders.as_slice(),
materializers.as_slice(),
@@ -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,46 +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::connect_postgres_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()),
};
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
return std::result::Result::Err(error.to_string());
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})]);
}
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_EVENT_QUERY_ROWS
} else {
validated.limit
};
let filter = match ks_store::MaterializedEventFilter::new(
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_events(&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(
@@ -415,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_EVENT_QUERY_ROWS {
return std::result::Result::Err(format!(
"journal limit must be between 1 and {}",
ks_store::MAX_MATERIALIZED_EVENT_QUERY_ROWS
));
}
if request.profile_name.trim().is_empty() {
return std::result::Result::Err("journal profile name must not be empty".to_string());
}
@@ -459,7 +471,8 @@ fn validate_journal_request(
account,
family,
operation,
limit: request.limit,
cursor: request.cursor,
block_index: request.block_index,
});
}
@@ -476,6 +489,7 @@ fn bounded_optional(
return std::result::Result::Ok(trimmed);
}
#[cfg(test)]
fn payload_matches(
payload: &serde_json::Value,
request: &crate::DemoSplTokenJournalRequest,
@@ -499,7 +513,7 @@ fn payload_matches(
return true;
}
fn journal_row(row: ks_store::MaterializedEventQueryRow) -> DemoSplTokenJournalRow {
fn journal_row(row: ks_store::MaterializedOutputQueryRow) -> DemoSplTokenJournalRow {
let operation = row
.payload_json
.get("operation")
@@ -589,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",
@@ -605,7 +620,7 @@ mod tests {
payload.get("amountRaw").and_then(serde_json::Value::as_str),
std::option::Option::Some("18446744073709551615")
);
let row = super::journal_row(ks_store::MaterializedEventQueryRow {
let row = super::journal_row(ks_store::MaterializedOutputQueryRow {
processor_name: "materializer.token.accounts".to_string(),
processor_version: "0.4.4".to_string(),
input_key: "input".to_string(),
@@ -625,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,
@@ -633,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: 15
// version: 21
//! Thin Tauri adapter for independent public Token-2022 Devnet validation scenarios.
@@ -159,13 +159,10 @@ 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::connect_postgres_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()),
};
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
return std::result::Result::Err(error.to_string());
}
let operation = match operation_from_request(&request) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -192,12 +189,15 @@ pub(crate) async fn demo_execution_spl_token_2022_execute(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let workspace_root = crate::workspace_root_dir();
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_token_2022(
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_token_2022_with_wallet(
&http_pool,
&store,
&profile,
workspace_root.as_path(),
&execution_wallet,
&pipeline_request,
decoders.as_slice(),
materializers.as_slice(),
@@ -220,7 +220,7 @@ pub(crate) fn demo_spl_token_2022_fixture(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let configured = std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str());
let wallet_dir = if configured.is_absolute() {
configured
} else {

View File

@@ -1,312 +0,0 @@
// file: kb-app-demo-desktop/src/demo_sql_common.rs
// version: 13
//! Shared SQL demo helpers and serializable payloads.
use ts_rs::TS; // rust-rules: derive-import
/// UI-safe table diagnostics shown by SQL demo windows.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql/DemoSqlTableSnapshot.ts"
)]
pub(crate) struct DemoSqlTableSnapshot {
/// Table name.
pub(crate) table_name: std::string::String,
/// Logical Solana domain encoded in the table name.
pub(crate) domain: std::string::String,
/// Human-readable table role.
pub(crate) role: std::string::String,
/// Whether the table currently exists.
pub(crate) exists: bool,
/// Row count when the table exists.
#[ts(type = "number | null")]
pub(crate) row_count: std::option::Option<i64>,
/// Minimum slot when available.
#[ts(type = "number | null")]
pub(crate) min_slot: std::option::Option<i64>,
/// Maximum slot when available.
#[ts(type = "number | null")]
pub(crate) max_slot: std::option::Option<i64>,
/// Latest insertion timestamp rendered by PostgreSQL when available.
pub(crate) latest_created_at: std::option::Option<std::string::String>,
}
/// Builds PostgreSQL options from the active profile without coupling ks-store to ks-config.
pub(crate) fn postgres_store_options_from_profile(
profile: &ks_config::ProfileConfig,
auto_initialize_schema: bool,
) -> ks_core::Result<ks_store::PostgresStoreOptions> {
return ks_store::PostgresStoreOptions::new(
profile.database.postgres.url.clone(),
profile.database.postgres.max_connections,
profile.database.postgres.connect_timeout_ms,
auto_initialize_schema,
);
}
/// Connects to PostgreSQL without reapplying schemas initialized at application startup.
pub(crate) async fn connect_postgres_store(
profile: &ks_config::ProfileConfig,
) -> std::result::Result<ks_store::PostgresStore, std::string::String> {
if !profile.database.enabled {
return std::result::Result::Err(std::string::String::from(
"database configuration is disabled in the active profile",
));
}
let options_result = postgres_store_options_from_profile(profile, false);
let mut options = match options_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
options.auto_initialize_schema = false;
let store_result = ks_store::PostgresStore::connect(options).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 store table diagnostics to the UI payload shape.
pub(crate) fn table_snapshot_from_pg(
value: &ks_store::PostgresTableDiagnostics,
) -> crate::DemoSqlTableSnapshot {
let row_count = match &value.statistics {
std::option::Option::Some(statistics) => std::option::Option::Some(statistics.row_count),
std::option::Option::None => std::option::Option::None,
};
let min_slot = match &value.statistics {
std::option::Option::Some(statistics) => statistics.min_slot,
std::option::Option::None => std::option::Option::None,
};
let max_slot = match &value.statistics {
std::option::Option::Some(statistics) => statistics.max_slot,
std::option::Option::None => std::option::Option::None,
};
let latest_created_at = match &value.statistics {
std::option::Option::Some(statistics) => statistics.latest_created_at.clone(),
std::option::Option::None => std::option::Option::None,
};
return crate::DemoSqlTableSnapshot {
table_name: value.table_name.clone(),
domain: value.domain.clone(),
role: value.role.clone(),
exists: value.exists,
row_count,
min_slot,
max_slot,
latest_created_at,
};
}
/// Converts many table diagnostics to UI payloads.
pub(crate) fn table_snapshots_from_pg(
values: &[ks_store::PostgresTableDiagnostics],
) -> std::vec::Vec<crate::DemoSqlTableSnapshot> {
let mut output = std::vec::Vec::new();
for value in values {
output.push(crate::table_snapshot_from_pg(value));
}
return output;
}
/// Initializes PostgreSQL raw, core, decode and materialization schemas during Tauri startup.
pub(crate) async fn initialize_postgres_schema_for_startup(
state: &crate::AppState,
splash_window: &tauri::WebviewWindow,
) {
let profile = state.active_profile();
if !profile.database.enabled {
emit_sql_startup_splash(
splash_window,
"SQL store disabled; schema initialization skipped.",
"info",
false,
);
tracing::debug!(target: crate::TRACING_TARGET, "database is disabled; skip SQL schema initialization");
return;
}
if profile.database.backend != "postgres" {
emit_sql_startup_splash(
splash_window,
"SQL backend is not PostgreSQL; schema initialization skipped.",
"info",
false,
);
tracing::debug!(target: crate::TRACING_TARGET, backend = profile.database.backend.as_str(), "database backend is not postgres; skip SQL schema initialization");
return;
}
let options_result = postgres_store_options_from_profile(
profile,
profile.database.postgres.auto_initialize_schema,
);
let mut options = match options_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let error_message = error.to_string();
emit_sql_startup_error(splash_window, error_message.as_str());
tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "cannot build PostgreSQL store options");
return;
},
};
let initialize_schema = options.auto_initialize_schema;
let masked_dsn = options.masked_dsn();
options.auto_initialize_schema = false;
emit_sql_startup_splash(
splash_window,
"Connexion PostgreSQL et vérification des tables raw/core/decode...",
"info",
false,
);
if !initialize_schema {
emit_sql_startup_splash(
splash_window,
"Auto-initialisation désactivée : vérification sans création de table.",
"info",
true,
);
}
tracing::debug!(target: crate::TRACING_TARGET, dsn = masked_dsn.as_str(), "start PostgreSQL schema initialization");
let store_result = ks_store::PostgresStore::connect(options).await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let error_message = error.to_string();
emit_sql_startup_error(splash_window, error_message.as_str());
tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "PostgreSQL connection failed during startup schema initialization");
return;
},
};
let before_result = store.known_table_diagnostics().await;
let before_tables = match before_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let error_message = error.to_string();
emit_sql_startup_error(splash_window, error_message.as_str());
tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "cannot read table diagnostics before schema initialization");
return;
},
};
if initialize_schema {
let initialize_result = store.initialize_store_schema().await;
if let std::result::Result::Err(error) = initialize_result {
let error_message = error.to_string();
emit_sql_startup_error(splash_window, error_message.as_str());
tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "PostgreSQL store schema initialization failed");
return;
}
}
let after_result = store.known_table_diagnostics().await;
let after_tables = match after_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let error_message = error.to_string();
emit_sql_startup_error(splash_window, error_message.as_str());
tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "cannot read table diagnostics after schema initialization");
return;
},
};
emit_sql_startup_table_report(splash_window, before_tables.as_slice(), after_tables.as_slice());
}
/// Formats debug enums for UI display.
pub(crate) fn debug_status<T: std::fmt::Debug>(value: T) -> std::string::String {
return format!("{value:?}");
}
fn emit_sql_startup_table_report(
splash_window: &tauri::WebviewWindow,
before_tables: &[ks_store::PostgresTableDiagnostics],
after_tables: &[ks_store::PostgresTableDiagnostics],
) {
let mut created_count = 0_u32;
let mut existing_count = 0_u32;
let mut missing_count = 0_u32;
for table in after_tables {
let before_exists = table_exists_in(before_tables, table.table_name.as_str());
if table.exists && before_exists == std::option::Option::Some(false) {
created_count += 1;
emit_sql_startup_splash(
splash_window,
format!("Created table {}", table.table_name).as_str(),
"success",
true,
);
tracing::debug!(target: crate::TRACING_TARGET, table = table.table_name.as_str(), "PostgreSQL table created");
} else if table.exists {
existing_count += 1;
emit_sql_startup_splash(
splash_window,
format!("Table {} already exists", table.table_name).as_str(),
"info",
true,
);
tracing::debug!(target: crate::TRACING_TARGET, table = table.table_name.as_str(), "PostgreSQL table already exists");
} else {
missing_count += 1;
emit_sql_startup_splash(
splash_window,
format!("Table {} is missing after initialization", table.table_name).as_str(),
"warning",
true,
);
tracing::error!(target: crate::TRACING_TARGET, table = table.table_name.as_str(), "PostgreSQL table missing after initialization");
}
}
emit_sql_startup_splash(
splash_window,
format!(
"PostgreSQL schema ready: {created_count} created, {existing_count} already present, {missing_count} missing."
)
.as_str(),
schema_report_status(missing_count),
false,
);
tracing::debug!(target: crate::TRACING_TARGET, created = created_count, existing = existing_count, missing = missing_count, "PostgreSQL schema initialization completed");
}
fn table_exists_in(
tables: &[ks_store::PostgresTableDiagnostics],
table_name: &str,
) -> std::option::Option<bool> {
for table in tables {
if table.table_name.as_str() == table_name {
return std::option::Option::Some(table.exists);
}
}
return std::option::Option::None;
}
fn schema_report_status(missing_count: u32) -> &'static str {
if missing_count == 0 {
return "success";
}
return "warning";
}
fn emit_sql_startup_error(splash_window: &tauri::WebviewWindow, message: &str) {
emit_sql_startup_splash(
splash_window,
format!("PostgreSQL schema initialization error: {message}").as_str(),
"danger",
false,
);
}
fn emit_sql_startup_splash(
splash_window: &tauri::WebviewWindow,
message: &str,
status: &str,
log_only: bool,
) {
let order = if log_only { "add_log" } else { "add_msg" };
crate::emit_splash_order(
splash_window,
order,
std::option::Option::Some(message),
std::option::Option::Some(status),
std::option::Option::None,
);
}

View File

@@ -1,73 +0,0 @@
// file: kb-app-demo-desktop/src/demo_sql_diag.rs
// version: 5
//! SQL diagnostic demo commands.
use ts_rs::TS; // rust-rules: derive-import
/// Complete SQL diagnostic payload shown by `demo_sql_diag`.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_diag/DemoSqlDiagPayload.ts"
)]
pub(crate) struct DemoSqlDiagPayload {
/// Configuration file path.
pub(crate) config_path: std::string::String,
/// Active profile name.
pub(crate) active_profile_name: std::string::String,
/// Configured database backend.
pub(crate) backend: std::string::String,
/// Masked PostgreSQL DSN.
pub(crate) masked_dsn: std::string::String,
/// PostgreSQL current schema.
pub(crate) current_schema: std::option::Option<std::string::String>,
/// PostgreSQL health status.
pub(crate) health_status: std::string::String,
/// PostgreSQL health message.
pub(crate) health_message: std::option::Option<std::string::String>,
/// Migration status.
pub(crate) migration_status: std::string::String,
/// Migration diagnostic message.
pub(crate) migration_message: std::option::Option<std::string::String>,
/// PostgreSQL server version when available.
pub(crate) server_version: std::option::Option<std::string::String>,
/// Known raw/core table diagnostics.
pub(crate) tables: std::vec::Vec<crate::DemoSqlTableSnapshot>,
}
/// Loads read-only SQL diagnostics from the active profile.
pub(crate) async fn load_demo_sql_diag(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::DemoSqlDiagPayload, std::string::String> {
let profile = state.active_profile().clone();
let store_result = crate::connect_postgres_store(&profile).await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let diagnostics_result = store.backend_diagnostics().await;
let diagnostics = match diagnostics_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let tables_result = store.known_table_diagnostics().await;
let tables = match tables_result {
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::DemoSqlDiagPayload {
config_path: state.config_path().to_string(),
active_profile_name: profile.name.clone(),
backend: profile.database.backend.clone(),
masked_dsn: store.options().masked_dsn(),
current_schema: diagnostics.descriptor.current_schema,
health_status: crate::debug_status(diagnostics.health.status),
health_message: diagnostics.health.message,
migration_status: crate::debug_status(diagnostics.migrations.status),
migration_message: diagnostics.migrations.message,
server_version: diagnostics.server_version,
tables: crate::table_snapshots_from_pg(tables.as_slice()),
});
}

View File

@@ -1,44 +0,0 @@
// file: kb-app-demo-desktop/src/demo_sql_pg_core.rs
// version: 6
//! PostgreSQL core store demo commands.
use ts_rs::TS; // rust-rules: derive-import
/// PostgreSQL core store payload shown by `demo_sql_pg_core`.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_pg_core/DemoSqlPgCorePayload.ts"
)]
pub(crate) struct DemoSqlPgCorePayload {
/// Active profile name.
pub(crate) active_profile_name: std::string::String,
/// Masked PostgreSQL DSN.
pub(crate) masked_dsn: std::string::String,
/// Core table diagnostics.
pub(crate) tables: std::vec::Vec<crate::DemoSqlTableSnapshot>,
}
/// Loads read-only core table diagnostics from PostgreSQL.
pub(crate) async fn load_demo_sql_pg_core(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::DemoSqlPgCorePayload, std::string::String> {
let profile = state.active_profile().clone();
let store_result = crate::connect_postgres_store(&profile).await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let tables_result = store.core_table_diagnostics().await;
let tables = match tables_result {
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::DemoSqlPgCorePayload {
active_profile_name: profile.name.clone(),
masked_dsn: store.options().masked_dsn(),
tables: crate::table_snapshots_from_pg(tables.as_slice()),
});
}

View File

@@ -1,44 +0,0 @@
// file: kb-app-demo-desktop/src/demo_sql_pg_raw.rs
// version: 7
//! PostgreSQL canonical acquisition store demo commands.
use ts_rs::TS; // rust-rules: derive-import
/// PostgreSQL canonical acquisition payload shown by `demo_sql_pg_raw`.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_pg_raw/DemoSqlPgRawPayload.ts"
)]
pub(crate) struct DemoSqlPgRawPayload {
/// Active profile name.
pub(crate) active_profile_name: std::string::String,
/// Masked PostgreSQL DSN.
pub(crate) masked_dsn: std::string::String,
/// Raw table diagnostics.
pub(crate) tables: std::vec::Vec<crate::DemoSqlTableSnapshot>,
}
/// Loads read-only raw table diagnostics from PostgreSQL.
pub(crate) async fn load_demo_sql_pg_raw(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::DemoSqlPgRawPayload, std::string::String> {
let profile = state.active_profile().clone();
let store_result = crate::connect_postgres_store(&profile).await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let tables_result = store.raw_table_diagnostics().await;
let tables = match tables_result {
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::DemoSqlPgRawPayload {
active_profile_name: profile.name.clone(),
masked_dsn: store.options().masked_dsn(),
tables: crate::table_snapshots_from_pg(tables.as_slice()),
});
}

View File

@@ -0,0 +1,336 @@
// file: kb-app-demo-desktop/src/demo_store_common.rs
// 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")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store/DemoStoreResourceSnapshot.ts"
)]
pub(crate) struct DemoStoreResourceSnapshot {
/// Stable logical resource code independent of the active backend.
pub(crate) resource_code: std::string::String,
/// Logical store model containing the resource.
pub(crate) model_code: std::string::String,
/// Human-readable resource role.
pub(crate) role: std::string::String,
/// Whether the logical resource is currently available.
pub(crate) available: bool,
/// Record count when the resource is available.
#[ts(type = "number | null")]
pub(crate) record_count: std::option::Option<i64>,
/// Minimum slot when available.
#[ts(type = "number | null")]
pub(crate) min_slot: std::option::Option<i64>,
/// Maximum slot when available.
#[ts(type = "number | null")]
pub(crate) max_slot: std::option::Option<i64>,
/// Latest persistence timestamp when available.
pub(crate) latest_created_at: std::option::Option<std::string::String>,
}
/// Clones the persistent store held by application state.
pub(crate) async fn shared_store(
state: &crate::AppState,
) -> std::result::Result<ks_store::Store, std::string::String> {
let store_result = state.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.to_string()),
};
}
/// 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,
) -> crate::DemoStoreResourceSnapshot {
let record_count = match &value.statistics {
std::option::Option::Some(statistics) => std::option::Option::Some(statistics.record_count),
std::option::Option::None => std::option::Option::None,
};
let min_slot = match &value.statistics {
std::option::Option::Some(statistics) => statistics.min_slot,
std::option::Option::None => std::option::Option::None,
};
let max_slot = match &value.statistics {
std::option::Option::Some(statistics) => statistics.max_slot,
std::option::Option::None => std::option::Option::None,
};
let latest_created_at = match &value.statistics {
std::option::Option::Some(statistics) => statistics.latest_created_at.clone(),
std::option::Option::None => std::option::Option::None,
};
return crate::DemoStoreResourceSnapshot {
resource_code: value.resource_code.clone(),
model_code: value.model_code.clone(),
role: value.role.clone(),
available: value.available,
record_count,
min_slot,
max_slot,
latest_created_at,
};
}
/// Converts many logical store resource diagnostics to UI payloads.
pub(crate) fn store_resource_snapshots(
values: &[ks_store::StoreResourceDiagnostics],
) -> std::vec::Vec<crate::DemoStoreResourceSnapshot> {
let mut output = std::vec::Vec::with_capacity(values.len());
for value in values {
output.push(crate::store_resource_snapshot(value));
}
return output;
}
/// Opens, auto-initializes when configured, and verifies the persistent store during startup.
pub(crate) async fn initialize_store_for_startup(
state: &crate::AppState,
splash_window: &tauri::WebviewWindow,
) {
let store_result = state.store().await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let error_message = error.to_string();
emit_store_startup_error(splash_window, error_message.as_str());
tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "store startup failed");
return;
},
};
let configuration = store.configuration_summary();
if !configuration.enabled {
emit_store_startup_splash(
splash_window,
"Store disabled; initialization skipped.",
"info",
false,
);
tracing::debug!(target: crate::TRACING_TARGET, backend = configuration.backend_code.as_str(), "store disabled; startup initialization skipped");
return;
}
emit_store_startup_splash(
splash_window,
format!(
"Store backend {}: vérification des modèles de persistance...",
configuration.backend_code
)
.as_str(),
"info",
false,
);
let initialization = store.initialization_summary();
for model in &initialization.models {
emit_store_startup_splash(
splash_window,
format!(
"Modèle {}: {}/{} ressources disponibles, {} créées.",
model.model_code,
model.available_resource_count,
model.expected_resource_count,
model.created_resource_count
)
.as_str(),
model_status(&model.status),
true,
);
}
for object in &initialization.objects {
emit_store_startup_splash(
splash_window,
format!(
"Objets {}: {}/{} disponibles, {} créés.",
object.object_kind,
object.available_count,
object.expected_count,
object.created_count
)
.as_str(),
"info",
true,
);
}
emit_store_startup_splash(
splash_window,
format!(
"Store {:?}: ressources {}/{}, {} créées.",
initialization.status,
initialization.available_resource_count,
initialization.expected_resource_count,
initialization.created_resource_count
)
.as_str(),
initialization_status(&initialization.status),
false,
);
tracing::debug!(target: crate::TRACING_TARGET, backend = configuration.backend_code.as_str(), available_resources = initialization.available_resource_count, expected_resources = initialization.expected_resource_count, created_resources = initialization.created_resource_count, "store startup verification completed");
}
/// Returns the safe connection descriptor exposed by the active backend.
pub(crate) async fn store_connection_descriptor(
store: &ks_store::Store,
) -> std::result::Result<std::string::String, std::string::String> {
let runtime = match store.runtime_summary().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let backend = match runtime.backend {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok("disabled".to_string()),
};
let descriptor = match backend.descriptor.masked_connection_descriptor {
std::option::Option::Some(value) => value,
std::option::Option::None => "configured".to_string(),
};
return std::result::Result::Ok(descriptor);
}
/// Formats debug enums for UI display.
pub(crate) fn debug_status<T: std::fmt::Debug>(value: T) -> std::string::String {
return format!("{value:?}");
}
fn model_status(status: &ks_store::StoreModelVerificationStatus) -> &'static str {
return match status {
ks_store::StoreModelVerificationStatus::Ready => "success",
ks_store::StoreModelVerificationStatus::Incomplete => "warning",
};
}
fn initialization_status(status: &ks_store::StoreInitializationStatus) -> &'static str {
return match status {
ks_store::StoreInitializationStatus::Ready => "success",
ks_store::StoreInitializationStatus::Disabled => "info",
ks_store::StoreInitializationStatus::Partial => "warning",
ks_store::StoreInitializationStatus::Failed => "danger",
};
}
fn emit_store_startup_error(splash_window: &tauri::WebviewWindow, message: &str) {
emit_store_startup_splash(
splash_window,
format!("Store initialization error: {message}").as_str(),
"danger",
false,
);
}
fn emit_store_startup_splash(
splash_window: &tauri::WebviewWindow,
message: &str,
status: &str,
log_only: bool,
) {
let order = if log_only { "add_log" } else { "add_msg" };
crate::emit_splash_order(
splash_window,
order,
std::option::Option::Some(message),
std::option::Option::Some(status),
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

@@ -0,0 +1,45 @@
// file: kb-app-demo-desktop/src/demo_store_core.rs
// version: 9
//! Backend-agnostic Core store demo commands retained by the store window.
use ts_rs::TS; // rust-rules: derive-import
/// Backend-agnostic Core store payload shown by the existing `demo_store_core` window.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_core/DemoStoreCorePayload.ts"
)]
pub(crate) struct DemoStoreCorePayload {
/// Active profile name.
pub(crate) active_profile_name: std::string::String,
/// Safe backend connection descriptor.
pub(crate) connection_descriptor: std::string::String,
/// Logical Core-resource diagnostics.
pub(crate) resources: std::vec::Vec<crate::DemoStoreResourceSnapshot>,
}
/// Loads read-only Core resource diagnostics from the active store.
pub(crate) async fn load_demo_store_core(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::DemoStoreCorePayload, std::string::String> {
let store = match crate::shared_store(state.inner()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let resources = match store.core_resource_diagnostics().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let connection_descriptor = match crate::store_connection_descriptor(&store).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::DemoStoreCorePayload {
active_profile_name: state.active_profile().name.clone(),
connection_descriptor,
resources: crate::store_resource_snapshots(resources.as_slice()),
});
}

View File

@@ -0,0 +1,81 @@
// file: kb-app-demo-desktop/src/demo_store_diag.rs
// version: 9
//! Backend-agnostic store diagnostic demo commands retained by the Store diagnostics window.
use ts_rs::TS; // rust-rules: derive-import
/// Complete backend-neutral store diagnostic payload shown by `demo_store_diag`.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_diag/DemoStoreDiagPayload.ts"
)]
pub(crate) struct DemoStoreDiagPayload {
/// Configuration file path.
pub(crate) config_path: std::string::String,
/// Active profile name.
pub(crate) active_profile_name: std::string::String,
/// Active backend code.
pub(crate) backend: std::string::String,
/// Safe backend connection descriptor.
pub(crate) connection_descriptor: std::string::String,
/// Backend namespace, schema, or equivalent logical location.
pub(crate) namespace: std::option::Option<std::string::String>,
/// Store health status.
pub(crate) health_status: std::string::String,
/// Store health message.
pub(crate) health_message: std::option::Option<std::string::String>,
/// Migration status.
pub(crate) migration_status: std::string::String,
/// Migration diagnostic message.
pub(crate) migration_message: std::option::Option<std::string::String>,
/// Backend version when available.
pub(crate) backend_version: std::option::Option<std::string::String>,
/// Known logical store resource diagnostics.
pub(crate) resources: std::vec::Vec<crate::DemoStoreResourceSnapshot>,
}
/// Loads read-only diagnostics from the active store.
pub(crate) async fn load_demo_store_diag(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::DemoStoreDiagPayload, std::string::String> {
let store = match crate::shared_store(state.inner()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let runtime = match store.runtime_summary().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let resources = match store.known_resource_diagnostics().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let backend = match runtime.backend {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
"store backend diagnostics are unavailable".to_string(),
);
},
};
let connection_descriptor = match backend.descriptor.masked_connection_descriptor.clone() {
std::option::Option::Some(value) => value,
std::option::Option::None => "configured".to_string(),
};
return std::result::Result::Ok(crate::DemoStoreDiagPayload {
config_path: state.config_path().to_string(),
active_profile_name: state.active_profile().name.clone(),
backend: backend.descriptor.backend_code,
connection_descriptor,
namespace: backend.descriptor.namespace,
health_status: crate::debug_status(backend.health.status),
health_message: backend.health.message,
migration_status: crate::debug_status(backend.migrations.status),
migration_message: backend.migrations.message,
backend_version: backend.backend_version,
resources: crate::store_resource_snapshots(resources.as_slice()),
});
}

View File

@@ -0,0 +1,45 @@
// file: kb-app-demo-desktop/src/demo_store_raw.rs
// version: 10
//! Backend-agnostic level-1 raw store demo commands retained by the store window.
use ts_rs::TS; // rust-rules: derive-import
/// Backend-agnostic raw store payload shown by the existing `demo_store_raw` window.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_raw/DemoStoreRawPayload.ts"
)]
pub(crate) struct DemoStoreRawPayload {
/// Active profile name.
pub(crate) active_profile_name: std::string::String,
/// Safe backend connection descriptor.
pub(crate) connection_descriptor: std::string::String,
/// Logical raw-resource diagnostics.
pub(crate) resources: std::vec::Vec<crate::DemoStoreResourceSnapshot>,
}
/// Loads read-only level-1 raw resource diagnostics from the active store.
pub(crate) async fn load_demo_store_raw(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::DemoStoreRawPayload, std::string::String> {
let store = match crate::shared_store(state.inner()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let resources = match store.raw_resource_diagnostics().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let connection_descriptor = match crate::store_connection_descriptor(&store).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::DemoStoreRawPayload {
active_profile_name: state.active_profile().name.clone(),
connection_descriptor,
resources: crate::store_resource_snapshots(resources.as_slice()),
});
}

View File

@@ -1,7 +1,7 @@
// file: kb-app-demo-desktop/src/demo_sql_replay_candidates.rs
// version: 13
// file: kb-app-demo-desktop/src/demo_store_replay_candidates.rs
// version: 17
//! Read-only SQL replay candidate browser commands.
//! Backend-agnostic read-only replay candidate browser commands.
use ts_rs::TS; // rust-rules: derive-import
@@ -10,9 +10,9 @@ use ts_rs::TS; // rust-rules: derive-import
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayKnownProgramOption.ts"
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayKnownProgramOption.ts"
)]
pub(crate) struct DemoSqlReplayKnownProgramOption {
pub(crate) struct DemoStoreReplayKnownProgramOption {
/// Stable lower snake case program code.
pub(crate) code: std::string::String,
/// Base58 Solana program identifier.
@@ -24,17 +24,17 @@ pub(crate) struct DemoSqlReplayKnownProgramOption {
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayOptionsPayload.ts"
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayOptionsPayload.ts"
)]
pub(crate) struct DemoSqlReplayOptionsPayload {
pub(crate) struct DemoStoreReplayOptionsPayload {
/// Active profile name.
pub(crate) active_profile_name: std::string::String,
/// Masked PostgreSQL DSN.
pub(crate) masked_dsn: std::string::String,
/// Maximum rows accepted by one query.
pub(crate) maximum_limit: u32,
/// Safe backend connection descriptor.
pub(crate) connection_descriptor: std::string::String,
/// 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::DemoSqlReplayKnownProgramOption>,
pub(crate) known_programs: std::vec::Vec<crate::DemoStoreReplayKnownProgramOption>,
}
/// UI request for bounded transaction replay candidates.
@@ -42,9 +42,9 @@ pub(crate) struct DemoSqlReplayOptionsPayload {
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRequest.ts"
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayTransactionRequest.ts"
)]
pub(crate) struct DemoSqlReplayTransactionRequest {
pub(crate) struct DemoStoreReplayTransactionRequest {
/// Optional partial signature search.
pub(crate) signature_contains: std::option::Option<std::string::String>,
/// Optional inclusive minimum slot.
@@ -59,14 +59,16 @@ pub(crate) struct DemoSqlReplayTransactionRequest {
pub(crate) ledger_status: std::option::Option<std::string::String>,
/// Optional exact program id.
pub(crate) program_id: std::option::Option<std::string::String>,
/// Program scope code: any, outer, inner or logs.
/// Program scope code: any, top_level, inner or logs.
pub(crate) program_scope: std::string::String,
/// Optional entity kind: mint, owner or account_key.
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,
}
@@ -76,9 +78,9 @@ pub(crate) struct DemoSqlReplayTransactionRequest {
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRow.ts"
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayTransactionRow.ts"
)]
pub(crate) struct DemoSqlReplayTransactionRow {
pub(crate) struct DemoStoreReplayTransactionRow {
/// Canonical transaction signature.
pub(crate) signature: std::string::String,
/// Transaction slot.
@@ -100,13 +102,13 @@ pub(crate) struct DemoSqlReplayTransactionRow {
pub(crate) attempt_count: i32,
/// Number of top-level instructions.
#[ts(type = "number")]
pub(crate) outer_instruction_count: i64,
pub(crate) top_level_instruction_count: i64,
/// Number of inner instructions.
#[ts(type = "number")]
pub(crate) inner_instruction_count: i64,
/// Number of distinct top-level programs.
#[ts(type = "number")]
pub(crate) outer_program_count: i64,
pub(crate) top_level_program_count: i64,
/// Number of distinct inner programs.
#[ts(type = "number")]
pub(crate) inner_program_count: i64,
@@ -114,18 +116,34 @@ pub(crate) struct DemoSqlReplayTransactionRow {
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")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRequest.ts"
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayProgramRequest.ts"
)]
pub(crate) struct DemoSqlReplayProgramRequest {
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.
@@ -133,9 +151,9 @@ pub(crate) struct DemoSqlReplayProgramRequest {
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRow.ts"
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayProgramRow.ts"
)]
pub(crate) struct DemoSqlReplayProgramRow {
pub(crate) struct DemoStoreReplayProgramRow {
/// Optional stable code from `ks_program_ids`.
pub(crate) program_code: std::option::Option<std::string::String>,
/// Program id.
@@ -145,7 +163,7 @@ pub(crate) struct DemoSqlReplayProgramRow {
pub(crate) transaction_count: i64,
/// Number of top-level instruction occurrences.
#[ts(type = "number")]
pub(crate) outer_instruction_count: i64,
pub(crate) top_level_instruction_count: i64,
/// Number of inner instruction occurrences.
#[ts(type = "number")]
pub(crate) inner_instruction_count: i64,
@@ -160,20 +178,36 @@ pub(crate) struct DemoSqlReplayProgramRow {
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")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRequest.ts"
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayEntityRequest.ts"
)]
pub(crate) struct DemoSqlReplayEntityRequest {
pub(crate) struct DemoStoreReplayEntityRequest {
/// Entity kind: mint, owner or account_key.
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.
@@ -181,9 +215,9 @@ pub(crate) struct DemoSqlReplayEntityRequest {
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRow.ts"
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayEntityRow.ts"
)]
pub(crate) struct DemoSqlReplayEntityRow {
pub(crate) struct DemoStoreReplayEntityRow {
/// Stable entity kind code.
pub(crate) entity_kind: std::string::String,
/// Mint, owner or account-key address.
@@ -202,8 +236,22 @@ pub(crate) struct DemoSqlReplayEntityRow {
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_sql_replay_csv(
pub(crate) async fn export_demo_store_replay_csv(
file_name: std::string::String,
content: std::string::String,
) -> std::result::Result<std::string::String, std::string::String> {
@@ -240,37 +288,40 @@ pub(crate) async fn export_demo_sql_replay_csv(
return std::result::Result::Ok(display_path);
}
/// Loads static browser options from the active PostgreSQL profile.
pub(crate) async fn demo_sql_replay_options(
/// Loads static browser options from the active store.
pub(crate) async fn demo_store_replay_options(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::DemoSqlReplayOptionsPayload, std::string::String> {
) -> std::result::Result<crate::DemoStoreReplayOptionsPayload, std::string::String> {
let profile = state.active_profile().clone();
let options_result = crate::postgres_store_options_from_profile(&profile, false);
let options = match options_result {
let store = match crate::shared_store(state.inner()).await {
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 connection_descriptor = match crate::store_connection_descriptor(&store).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut known_programs =
std::vec::Vec::with_capacity(ks_program_ids::registered_program_ids().len());
for entry in ks_program_ids::registered_program_ids() {
known_programs.push(crate::DemoSqlReplayKnownProgramOption {
known_programs.push(crate::DemoStoreReplayKnownProgramOption {
code: entry.code().to_owned(),
program_id: entry.program_id().to_owned(),
});
}
return std::result::Result::Ok(crate::DemoSqlReplayOptionsPayload {
return std::result::Result::Ok(crate::DemoStoreReplayOptionsPayload {
active_profile_name: profile.name,
masked_dsn: options.masked_dsn(),
maximum_limit: ks_store::MAX_REPLAY_CANDIDATE_ROWS,
connection_descriptor,
block_size: crate::DEMO_STORE_BLOCK_SIZE,
known_programs,
});
}
/// Loads bounded transaction replay candidates from PostgreSQL.
pub(crate) async fn load_demo_sql_replay_transactions(
/// Loads bounded transaction replay candidates from the active store.
pub(crate) async fn load_demo_store_replay_transactions(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoSqlReplayTransactionRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoSqlReplayTransactionRow>, std::string::String> {
request: crate::DemoStoreReplayTransactionRequest,
) -> 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,
@@ -281,7 +332,7 @@ pub(crate) async fn load_demo_sql_replay_transactions(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let filter_result = ks_store::PostgresReplayTransactionFilter::new(
let filter_result = ks_store::ReplayTransactionFilter::new(
request.signature_contains,
request.min_slot,
request.max_slot,
@@ -291,101 +342,138 @@ pub(crate) async fn load_demo_sql_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 store_result = crate::connect_postgres_store(state.active_profile()).await;
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_pg(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 PostgreSQL.
pub(crate) async fn load_demo_sql_replay_programs(
/// Loads one counted Store block of program summaries.
pub(crate) async fn load_demo_store_replay_programs(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoSqlReplayProgramRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoSqlReplayProgramRow>, std::string::String> {
let filter_result =
ks_store::PostgresReplayProgramFilter::new(request.program_id_contains, request.limit);
request: crate::DemoStoreReplayProgramRequest,
) -> 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 store_result = crate::connect_postgres_store(state.active_profile()).await;
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_pg(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 PostgreSQL core tables.
pub(crate) async fn load_demo_sql_replay_entities(
/// 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::DemoSqlReplayEntityRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoSqlReplayEntityRow>, std::string::String> {
request: crate::DemoStoreReplayEntityRequest,
) -> 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::PostgresReplayEntityFilter::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 store_result = crate::connect_postgres_store(state.active_profile()).await;
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_pg(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_pg(
row: ks_store::PostgresReplayTransactionCandidate,
) -> crate::DemoSqlReplayTransactionRow {
return crate::DemoSqlReplayTransactionRow {
fn transaction_row_from_store(
row: ks_store::ReplayTransactionCandidate,
) -> crate::DemoStoreReplayTransactionRow {
return crate::DemoStoreReplayTransactionRow {
signature: row.signature,
slot: row.slot,
raw_processing_state: row.raw_processing_state,
@@ -395,24 +483,22 @@ fn transaction_row_from_pg(
ledger_status: row.ledger_status,
processor_version: row.processor_version,
attempt_count: row.attempt_count,
outer_instruction_count: row.outer_instruction_count,
top_level_instruction_count: row.top_level_instruction_count,
inner_instruction_count: row.inner_instruction_count,
outer_program_count: row.outer_program_count,
top_level_program_count: row.top_level_program_count,
inner_program_count: row.inner_program_count,
updated_at: row.updated_at,
};
}
fn program_row_from_pg(
row: ks_store::PostgresReplayProgramSummary,
) -> crate::DemoSqlReplayProgramRow {
fn program_row_from_store(row: ks_store::ReplayProgramSummary) -> crate::DemoStoreReplayProgramRow {
let program_code = ks_program_ids::find_registered_program_id(&row.program_id)
.map(|entry| return entry.code().to_owned());
return crate::DemoSqlReplayProgramRow {
return crate::DemoStoreReplayProgramRow {
program_code,
program_id: row.program_id,
transaction_count: row.transaction_count,
outer_instruction_count: row.outer_instruction_count,
top_level_instruction_count: row.top_level_instruction_count,
inner_instruction_count: row.inner_instruction_count,
log_count: row.log_count,
min_slot: row.min_slot,
@@ -420,8 +506,8 @@ fn program_row_from_pg(
};
}
fn entity_row_from_pg(row: ks_store::PostgresReplayEntitySummary) -> crate::DemoSqlReplayEntityRow {
return crate::DemoSqlReplayEntityRow {
fn entity_row_from_store(row: ks_store::ReplayEntitySummary) -> crate::DemoStoreReplayEntityRow {
return crate::DemoStoreReplayEntityRow {
entity_kind: row.entity_kind,
entity_value: row.entity_value,
transaction_count: row.transaction_count,
@@ -480,20 +566,19 @@ async fn available_csv_export_path(
fn program_scope_from_code(
code: &str,
) -> std::result::Result<ks_store::PostgresReplayProgramScope, std::string::String> {
) -> std::result::Result<ks_store::ReplayProgramScope, std::string::String> {
return match code {
"any" => std::result::Result::Ok(ks_store::PostgresReplayProgramScope::Any),
"outer" => std::result::Result::Ok(ks_store::PostgresReplayProgramScope::Outer),
"inner" => std::result::Result::Ok(ks_store::PostgresReplayProgramScope::Inner),
"logs" => std::result::Result::Ok(ks_store::PostgresReplayProgramScope::Logs),
"any" => std::result::Result::Ok(ks_store::ReplayProgramScope::Any),
"top_level" => std::result::Result::Ok(ks_store::ReplayProgramScope::TopLevel),
"inner" => std::result::Result::Ok(ks_store::ReplayProgramScope::Inner),
"logs" => std::result::Result::Ok(ks_store::ReplayProgramScope::Logs),
_ => std::result::Result::Err(format!("unsupported replay program scope: {code}")),
};
}
fn optional_entity_kind_from_code(
code: std::option::Option<&str>,
) -> std::result::Result<std::option::Option<ks_store::PostgresReplayEntityKind>, std::string::String>
{
) -> std::result::Result<std::option::Option<ks_store::ReplayEntityKind>, std::string::String> {
return match code {
std::option::Option::Some(value) => {
let result = entity_kind_from_code(value);
@@ -510,11 +595,11 @@ fn optional_entity_kind_from_code(
fn entity_kind_from_code(
code: &str,
) -> std::result::Result<ks_store::PostgresReplayEntityKind, std::string::String> {
) -> std::result::Result<ks_store::ReplayEntityKind, std::string::String> {
return match code {
"mint" => std::result::Result::Ok(ks_store::PostgresReplayEntityKind::Mint),
"owner" => std::result::Result::Ok(ks_store::PostgresReplayEntityKind::Owner),
"account_key" => std::result::Result::Ok(ks_store::PostgresReplayEntityKind::AccountKey),
"mint" => std::result::Result::Ok(ks_store::ReplayEntityKind::Mint),
"owner" => std::result::Result::Ok(ks_store::ReplayEntityKind::Owner),
"account_key" => std::result::Result::Ok(ks_store::ReplayEntityKind::AccountKey),
_ => std::result::Result::Err(format!("unsupported replay entity kind: {code}")),
};
}
@@ -524,7 +609,7 @@ mod tests {
#[test]
fn program_scope_parser_accepts_logs() {
let result = super::program_scope_from_code("logs");
assert_eq!(result, std::result::Result::Ok(ks_store::PostgresReplayProgramScope::Logs));
assert_eq!(result, std::result::Result::Ok(ks_store::ReplayProgramScope::Logs));
}
#[test]
@@ -551,16 +636,16 @@ mod tests {
#[test]
fn program_row_exposes_registered_code() {
let row = ks_store::PostgresReplayProgramSummary {
let row = ks_store::ReplayProgramSummary {
program_id: ks_program_ids::SYSTEM_PROGRAM_ID.to_owned(),
transaction_count: 1,
outer_instruction_count: 1,
top_level_instruction_count: 1,
inner_instruction_count: 0,
log_count: 1,
min_slot: 1,
max_slot: 1,
};
let converted = super::program_row_from_pg(row);
let converted = super::program_row_from_store(row);
assert_eq!(converted.program_code.as_deref(), std::option::Option::Some("system"));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_ws.rs
// version: 23
// version: 24
//! Standard Solana WebSocket demo commands backed by `ks_onchain_transport::WsSession`.
@@ -1031,7 +1031,7 @@ mod tests {
<crate::DemoWsUnsubscribeRequest as TS>::decl(&config),
<crate::DemoBackfillProgressPayload as TS>::decl(&config),
<crate::DemoBackfillSummaryPayload as TS>::decl(&config),
<crate::DemoSqlTableSnapshot as TS>::decl(&config),
<crate::DemoStoreResourceSnapshot as TS>::decl(&config),
];
for declaration in declarations {
assert!(!declaration.contains("bigint"));

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/lib.rs
// version: 37
// version: 50
//! Tauri desktop demo application for `khadhroony-bot3`.
@@ -24,12 +24,13 @@ mod demo_http;
mod demo_spl_ata;
mod demo_spl_token;
mod demo_spl_token_2022;
mod demo_sql_common;
mod demo_sql_diag;
mod demo_sql_pg_core;
mod demo_sql_pg_raw;
mod demo_sql_replay_candidates;
mod demo_store_common;
mod demo_store_core;
mod demo_store_diag;
mod demo_store_raw;
mod demo_store_replay_candidates;
mod demo_transport;
mod demo_wallet;
mod demo_ws;
mod desktop_config;
mod frontend_log;
@@ -132,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.
@@ -146,14 +149,22 @@ pub(crate) use self::demo_decode_replay::demo_decode_replay_diagnostics;
pub(crate) use self::demo_decode_replay::demo_decode_replay_execute;
/// Returns available decoders and default replay bounds.
pub(crate) use self::demo_decode_replay::demo_decode_replay_options;
/// Readiness report for one Devnet profile PostgreSQL store.
/// Backend-agnostic readiness report for one Devnet profile store.
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;
/// Converts the reusable scenario readiness report to the desktop TS-RS payload.
pub(crate) use self::demo_devnet_common::devnet_store_readiness_payload;
/// 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;
/// 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.
@@ -230,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.
@@ -244,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.
@@ -276,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.
@@ -290,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.
@@ -308,60 +319,122 @@ 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;
/// UI-safe SQL table diagnostic snapshot.
pub(crate) use self::demo_sql_common::DemoSqlTableSnapshot;
/// Connects to the configured PostgreSQL store.
pub(crate) use self::demo_sql_common::connect_postgres_store;
/// 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_sql_common::debug_status;
/// Initializes the PostgreSQL schema during startup.
pub(crate) use self::demo_sql_common::initialize_postgres_schema_for_startup;
/// Builds PostgreSQL options from the active profile.
pub(crate) use self::demo_sql_common::postgres_store_options_from_profile;
/// Converts one PostgreSQL table diagnostic.
pub(crate) use self::demo_sql_common::table_snapshot_from_pg;
/// Converts PostgreSQL table diagnostics.
pub(crate) use self::demo_sql_common::table_snapshots_from_pg;
/// SQL diagnostic payload.
pub(crate) use self::demo_sql_diag::DemoSqlDiagPayload;
/// Loads read-only SQL diagnostics from the active profile.
pub(crate) use self::demo_sql_diag::load_demo_sql_diag;
/// PostgreSQL core-table payload.
pub(crate) use self::demo_sql_pg_core::DemoSqlPgCorePayload;
/// Loads read-only core table diagnostics from PostgreSQL.
pub(crate) use self::demo_sql_pg_core::load_demo_sql_pg_core;
/// PostgreSQL raw-table payload.
pub(crate) use self::demo_sql_pg_raw::DemoSqlPgRawPayload;
/// Loads read-only raw table diagnostics from PostgreSQL.
pub(crate) use self::demo_sql_pg_raw::load_demo_sql_pg_raw;
/// SQL replay entity request.
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayEntityRequest;
/// SQL replay entity row.
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayEntityRow;
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;
/// 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.
pub(crate) use self::demo_store_common::store_resource_snapshot;
/// Converts logical store resource diagnostics.
pub(crate) use self::demo_store_common::store_resource_snapshots;
/// Backend-agnostic Core store diagnostic payload.
pub(crate) use self::demo_store_core::DemoStoreCorePayload;
/// Loads read-only Core resource diagnostics from the active store.
pub(crate) use self::demo_store_core::load_demo_store_core;
/// Backend-agnostic store diagnostic payload.
pub(crate) use self::demo_store_diag::DemoStoreDiagPayload;
/// Loads read-only Store diagnostics from the active profile.
pub(crate) use self::demo_store_diag::load_demo_store_diag;
/// Backend-agnostic raw store diagnostic payload.
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.
pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayEntityRow;
/// Known program option.
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayKnownProgramOption;
/// SQL replay options payload.
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayOptionsPayload;
/// SQL replay program request.
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayProgramRequest;
/// SQL replay program row.
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayProgramRow;
/// SQL replay transaction request.
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayTransactionRequest;
/// SQL replay transaction row.
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayTransactionRow;
/// Loads static browser options from the active PostgreSQL profile.
pub(crate) use self::demo_sql_replay_candidates::demo_sql_replay_options;
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.
pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayTransactionRow;
/// Loads static replay browser options from the active store profile.
pub(crate) use self::demo_store_replay_candidates::demo_store_replay_options;
/// Writes a bounded replay-candidate CSV file into `./data/exports_csv/`.
pub(crate) use self::demo_sql_replay_candidates::export_demo_sql_replay_csv;
/// Loads bounded mint, owner or account-key summaries from PostgreSQL core tables.
pub(crate) use self::demo_sql_replay_candidates::load_demo_sql_replay_entities;
/// Loads bounded program summaries from PostgreSQL.
pub(crate) use self::demo_sql_replay_candidates::load_demo_sql_replay_programs;
/// Loads bounded transaction replay candidates from PostgreSQL.
pub(crate) use self::demo_sql_replay_candidates::load_demo_sql_replay_transactions;
pub(crate) use self::demo_store_replay_candidates::export_demo_store_replay_csv;
/// Loads bounded mint, owner or account-key summaries from Core store resources.
pub(crate) use self::demo_store_replay_candidates::load_demo_store_replay_entities;
/// Loads bounded program summaries from the active store.
pub(crate) use self::demo_store_replay_candidates::load_demo_store_replay_programs;
/// Loads bounded transaction replay candidates from the active store.
pub(crate) use self::demo_store_replay_candidates::load_demo_store_replay_transactions;
/// UI-safe endpoint role shared by HTTP and WebSocket diagnostics.
pub(crate) use self::demo_transport::DemoEndpointRolePayload;
/// Exact public SOL balance projected by the wallet demo.
pub(crate) use self::demo_wallet::DemoWalletBalancePayload;
/// Request to create one native wallet from the backend-only demo secret.
pub(crate) use self::demo_wallet::DemoWalletCreateRequest;
/// One Devnet profile and its effective execution-wallet selection.
pub(crate) use self::demo_wallet::DemoWalletExecutionProfilePayload;
/// Request to set or clear one session-only execution-wallet selection.
pub(crate) use self::demo_wallet::DemoWalletExecutionSelectionRequest;
/// Request to export one native wallet secret through the backend-only demo secret.
pub(crate) use self::demo_wallet::DemoWalletExportRequest;
/// UI-safe native wallet identity exposed by the desktop adapter.
pub(crate) use self::demo_wallet::DemoWalletIdentityPayload;
/// Request to import one external keypair into the native wallet store.
pub(crate) use self::demo_wallet::DemoWalletImportRequest;
/// UI-safe native wallet inventory exposed by the desktop adapter.
pub(crate) use self::demo_wallet::DemoWalletInventoryPayload;
/// One selectable profile for read-only wallet on-chain exploration.
pub(crate) use self::demo_wallet::DemoWalletOnchainProfilePayload;
/// Result of one wallet-scoped public JSON-RPC read.
pub(crate) use self::demo_wallet::DemoWalletRpcExecutionPayload;
/// Request for one wallet-scoped public JSON-RPC read.
pub(crate) use self::demo_wallet::DemoWalletRpcRequest;
/// One secret transfer format selectable by the wallet demo.
pub(crate) use self::demo_wallet::DemoWalletTransferFormatPayload;
/// Safe public identity extracted from one external keypair transfer file.
pub(crate) use self::demo_wallet::DemoWalletTransferInspectionPayload;
/// Request to inspect one external keypair transfer file without importing it.
pub(crate) use self::demo_wallet::DemoWalletTransferInspectionRequest;
/// Loads the exact SOL balance of one public wallet address.
pub(crate) use self::demo_wallet::demo_wallet_balance;
/// Creates one password-protected native wallet without exposing the password to Tauri IPC.
pub(crate) use self::demo_wallet::demo_wallet_create_native;
/// Exports one native wallet through an explicitly selected transfer format.
pub(crate) use self::demo_wallet::demo_wallet_export_file;
/// Imports one external keypair into a new native wallet.
pub(crate) use self::demo_wallet::demo_wallet_import_file;
/// Inspects an explicitly selected native wallet file without exposing its path in the result.
pub(crate) use self::demo_wallet::demo_wallet_inspect_file;
/// Inspects one external transfer file and returns only its derived public identity.
pub(crate) use self::demo_wallet::demo_wallet_inspect_transfer_file;
/// Returns native wallet identities without paths, passwords or protected payloads.
pub(crate) use self::demo_wallet::demo_wallet_inventory;
/// Executes one bounded read-only wallet explorer RPC through an explicitly selected profile.
pub(crate) use self::demo_wallet::demo_wallet_rpc_execute;
/// Sets or clears the session-only native wallet used by Devnet execution demos.
pub(crate) use self::demo_wallet::demo_wallet_select_execution_wallet;
/// UI-safe WebSocket endpoint snapshot.
pub(crate) use self::demo_ws::DemoWsEndpointPayload;
/// WebSocket execution response payload.

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/splash.rs
// version: 5
// version: 6
//! Splash-window payloads and startup sequencing helpers.
@@ -112,7 +112,7 @@ pub(crate) async fn splash_frontend_ready(
std::option::Option::Some("success"),
std::option::Option::None,
);
crate::initialize_postgres_schema_for_startup(state, &window).await;
crate::initialize_store_for_startup(state, &window).await;
crate::emit_splash_order(
&window,
"add_msg",

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/tauri.rs
// version: 38
// version: 47
//! Tauri runtime assembly and private command wrappers.
@@ -36,6 +36,16 @@ pub fn run() -> ks_core::Result<()> {
load_project_readme,
open_demo_config_window,
load_demo_config,
open_demo_wallet_window,
demo_wallet_inventory,
demo_wallet_select_execution_wallet,
demo_wallet_create_native,
demo_wallet_inspect_transfer_file,
demo_wallet_import_file,
demo_wallet_export_file,
demo_wallet_balance,
demo_wallet_inspect_file,
demo_wallet_rpc_execute,
open_demo_http_window,
demo_http_list_pool_clients,
demo_http_options,
@@ -61,18 +71,18 @@ pub fn run() -> ks_core::Result<()> {
demo_decode_replay_cancel,
demo_decode_replay_diagnostics,
demo_decode_replay_annotations,
open_demo_sql_diag_window,
load_demo_sql_diag,
open_demo_sql_pg_raw_window,
load_demo_sql_pg_raw,
open_demo_sql_pg_core_window,
load_demo_sql_pg_core,
open_demo_sql_replay_candidates_window,
export_demo_sql_replay_csv,
demo_sql_replay_options,
load_demo_sql_replay_transactions,
load_demo_sql_replay_programs,
load_demo_sql_replay_entities,
open_demo_store_diag_window,
load_demo_store_diag,
open_demo_store_raw_window,
load_demo_store_raw,
open_demo_store_core_window,
load_demo_store_core,
open_demo_store_replay_candidates_window,
export_demo_store_replay_csv,
demo_store_replay_options,
load_demo_store_replay_transactions,
load_demo_store_replay_programs,
load_demo_store_replay_entities,
open_demo_execution_solana_core_window,
demo_execution_devnet_prepare_profile,
demo_execution_solana_core_options,
@@ -235,6 +245,84 @@ fn load_demo_config(state: tauri::State<'_, crate::AppState>) -> crate::DemoConf
return crate::demo_config_payload(state.inner());
}
#[tauri::command]
fn open_demo_wallet_window(
app_handle: tauri::AppHandle,
) -> std::result::Result<(), std::string::String> {
return open_or_focus_demo_window(app_handle, "demo_wallet", "demo_wallet.html", "Wallets");
}
#[tauri::command]
async fn demo_wallet_inventory(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::DemoWalletInventoryPayload, std::string::String> {
return crate::demo_wallet_inventory(state.inner()).await;
}
#[tauri::command]
async fn demo_wallet_select_execution_wallet(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoWalletExecutionSelectionRequest,
) -> std::result::Result<crate::DemoWalletInventoryPayload, std::string::String> {
return crate::demo_wallet_select_execution_wallet(state.inner(), request).await;
}
#[tauri::command]
async fn demo_wallet_create_native(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoWalletCreateRequest,
) -> std::result::Result<crate::DemoWalletIdentityPayload, std::string::String> {
return crate::demo_wallet_create_native(state.inner(), request).await;
}
#[tauri::command]
async fn demo_wallet_inspect_transfer_file(
request: crate::DemoWalletTransferInspectionRequest,
) -> std::result::Result<crate::DemoWalletTransferInspectionPayload, std::string::String> {
return crate::demo_wallet_inspect_transfer_file(request).await;
}
#[tauri::command]
async fn demo_wallet_import_file(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoWalletImportRequest,
) -> std::result::Result<crate::DemoWalletIdentityPayload, std::string::String> {
return crate::demo_wallet_import_file(state.inner(), request).await;
}
#[tauri::command]
async fn demo_wallet_export_file(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoWalletExportRequest,
) -> std::result::Result<std::string::String, std::string::String> {
return crate::demo_wallet_export_file(state.inner(), request).await;
}
#[tauri::command]
async fn demo_wallet_balance(
state: tauri::State<'_, crate::AppState>,
profile: std::string::String,
address: std::string::String,
) -> std::result::Result<crate::DemoWalletBalancePayload, std::string::String> {
return crate::demo_wallet_balance(state.inner(), profile, address).await;
}
#[tauri::command]
async fn demo_wallet_inspect_file(
state: tauri::State<'_, crate::AppState>,
path: std::string::String,
) -> std::result::Result<crate::DemoWalletIdentityPayload, std::string::String> {
return crate::demo_wallet_inspect_file(state.inner(), path).await;
}
#[tauri::command]
async fn demo_wallet_rpc_execute(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoWalletRpcRequest,
) -> std::result::Result<crate::DemoWalletRpcExecutionPayload, std::string::String> {
return crate::demo_wallet_rpc_execute(state.inner(), request).await;
}
#[tauri::command]
fn open_demo_backfill_window(
app_handle: tauri::AppHandle,
@@ -425,116 +513,116 @@ 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;
}
#[tauri::command]
fn open_demo_sql_diag_window(
fn open_demo_store_diag_window(
app_handle: tauri::AppHandle,
) -> std::result::Result<(), std::string::String> {
return open_or_focus_demo_window(
app_handle,
"demo_sql_diag",
"demo_sql_diag.html",
"SQL diagnostics",
"demo_store_diag",
"demo_store_diag.html",
"Store diagnostics",
);
}
#[tauri::command]
async fn load_demo_sql_diag(
async fn load_demo_store_diag(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::DemoSqlDiagPayload, std::string::String> {
return crate::load_demo_sql_diag(state).await;
) -> std::result::Result<crate::DemoStoreDiagPayload, std::string::String> {
return crate::load_demo_store_diag(state).await;
}
#[tauri::command]
fn open_demo_sql_pg_raw_window(
fn open_demo_store_raw_window(
app_handle: tauri::AppHandle,
) -> std::result::Result<(), std::string::String> {
return open_or_focus_demo_window(
app_handle,
"demo_sql_pg_raw",
"demo_sql_pg_raw.html",
"PostgreSQL canonical acquisition",
"demo_store_raw",
"demo_store_raw.html",
"Raw store",
);
}
#[tauri::command]
async fn load_demo_sql_pg_raw(
async fn load_demo_store_raw(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::DemoSqlPgRawPayload, std::string::String> {
return crate::load_demo_sql_pg_raw(state).await;
) -> std::result::Result<crate::DemoStoreRawPayload, std::string::String> {
return crate::load_demo_store_raw(state).await;
}
#[tauri::command]
fn open_demo_sql_pg_core_window(
fn open_demo_store_core_window(
app_handle: tauri::AppHandle,
) -> std::result::Result<(), std::string::String> {
return open_or_focus_demo_window(
app_handle,
"demo_sql_pg_core",
"demo_sql_pg_core.html",
"PostgreSQL core store",
"demo_store_core",
"demo_store_core.html",
"Core store",
);
}
#[tauri::command]
async fn load_demo_sql_pg_core(
async fn load_demo_store_core(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::DemoSqlPgCorePayload, std::string::String> {
return crate::load_demo_sql_pg_core(state).await;
) -> std::result::Result<crate::DemoStoreCorePayload, std::string::String> {
return crate::load_demo_store_core(state).await;
}
#[tauri::command]
fn open_demo_sql_replay_candidates_window(
fn open_demo_store_replay_candidates_window(
app_handle: tauri::AppHandle,
) -> std::result::Result<(), std::string::String> {
return open_or_focus_demo_window(
app_handle,
"demo_sql_replay_candidates",
"demo_sql_replay_candidates.html",
"SQL replay candidates",
"demo_store_replay_candidates",
"demo_store_replay_candidates.html",
"Store replay candidates",
);
}
#[tauri::command]
async fn export_demo_sql_replay_csv(
async fn export_demo_store_replay_csv(
file_name: std::string::String,
content: std::string::String,
) -> std::result::Result<std::string::String, std::string::String> {
return crate::export_demo_sql_replay_csv(file_name, content).await;
return crate::export_demo_store_replay_csv(file_name, content).await;
}
#[tauri::command]
async fn demo_sql_replay_options(
async fn demo_store_replay_options(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::DemoSqlReplayOptionsPayload, std::string::String> {
return crate::demo_sql_replay_options(state).await;
) -> std::result::Result<crate::DemoStoreReplayOptionsPayload, std::string::String> {
return crate::demo_store_replay_options(state).await;
}
#[tauri::command]
async fn load_demo_sql_replay_transactions(
async fn load_demo_store_replay_transactions(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoSqlReplayTransactionRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoSqlReplayTransactionRow>, std::string::String> {
return crate::load_demo_sql_replay_transactions(state, request).await;
request: crate::DemoStoreReplayTransactionRequest,
) -> std::result::Result<crate::DemoStoreReplayTransactionPagePayload, std::string::String> {
return crate::load_demo_store_replay_transactions(state, request).await;
}
#[tauri::command]
async fn load_demo_sql_replay_programs(
async fn load_demo_store_replay_programs(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoSqlReplayProgramRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoSqlReplayProgramRow>, std::string::String> {
return crate::load_demo_sql_replay_programs(state, request).await;
request: crate::DemoStoreReplayProgramRequest,
) -> std::result::Result<crate::DemoStoreReplayProgramPagePayload, std::string::String> {
return crate::load_demo_store_replay_programs(state, request).await;
}
#[tauri::command]
async fn load_demo_sql_replay_entities(
async fn load_demo_store_replay_entities(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoSqlReplayEntityRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoSqlReplayEntityRow>, std::string::String> {
return crate::load_demo_sql_replay_entities(state, request).await;
request: crate::DemoStoreReplayEntityRequest,
) -> std::result::Result<crate::DemoStoreReplayEntityPagePayload, std::string::String> {
return crate::load_demo_store_replay_entities(state, request).await;
}
#[tauri::command]
@@ -769,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;
}
@@ -794,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;
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Khadhroony Bot3 Demo Desktop",
"version": "0.5.1",
"version": "0.5.3",
"identifier": "com.sasedev.kb-app-demo-desktop",
"build": {
"beforeDevCommand": "npm run dev",

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/vite.config.ts
// version: 9
// version: 11
import { defineConfig, normalizePath } from "vite";
import { NodePackageImporter } from "sass-embedded";
@@ -24,15 +24,16 @@ export default defineConfig(() => ({
"splash": normalizePath(resolve(__dirname, 'frontend/splash.html')),
"main": normalizePath(resolve(__dirname, 'frontend/main.html')),
"demo_config": normalizePath(resolve(__dirname, 'frontend/demo_config.html')),
"demo_wallet": normalizePath(resolve(__dirname, 'frontend/demo_wallet.html')),
"demo_http": normalizePath(resolve(__dirname, 'frontend/demo_http.html')),
"demo_ws": normalizePath(resolve(__dirname, 'frontend/demo_ws.html')),
"demo_backfill": normalizePath(resolve(__dirname, 'frontend/demo_backfill.html')),
"demo_core_extraction": normalizePath(resolve(__dirname, 'frontend/demo_core_extraction.html')),
"demo_decode_replay": normalizePath(resolve(__dirname, 'frontend/demo_decode_replay.html')),
"demo_sql_diag": normalizePath(resolve(__dirname, 'frontend/demo_sql_diag.html')),
"demo_sql_pg_raw": normalizePath(resolve(__dirname, 'frontend/demo_sql_pg_raw.html')),
"demo_sql_pg_core": normalizePath(resolve(__dirname, 'frontend/demo_sql_pg_core.html')),
"demo_sql_replay_candidates": normalizePath(resolve(__dirname, 'frontend/demo_sql_replay_candidates.html')),
"demo_store_diag": normalizePath(resolve(__dirname, 'frontend/demo_store_diag.html')),
"demo_store_raw": normalizePath(resolve(__dirname, 'frontend/demo_store_raw.html')),
"demo_store_core": normalizePath(resolve(__dirname, 'frontend/demo_store_core.html')),
"demo_store_replay_candidates": normalizePath(resolve(__dirname, 'frontend/demo_store_replay_candidates.html')),
"demo_execution_solana_core": normalizePath(resolve(__dirname, 'frontend/demo_execution_solana_core.html')),
"demo_execution_spl": normalizePath(resolve(__dirname, 'frontend/demo_execution_spl.html')),
"demo_execution_metatdata": normalizePath(resolve(__dirname, 'frontend/demo_execution_metadata.html')),

View File

@@ -1,8 +1,40 @@
<!-- file: ks-config/CHANGELOG.md -->
<!-- version: 20 -->
<!-- version: 26 -->
# CHANGELOG — ks-config
## `0.5.3-pre.002-delta-fix-001`
- aligne tous les contrats/types publics de `ks-config` sur la règle workspace imposant `crate::Item` depuis leur crate propriétaire, y compris dans leur module de déclaration et dans les tests ;
- aligne les appels aux fonctions publiques réexportées sur le crate-root et ajoute le canari `KH_CFG015` pour empêcher le retour d'une référence locale non qualifiée à un type public de configuration.
## `0.5.3-pre.002`
- remplace les contrats runtime `PostgresConfig`/`SqliteConfig` par `DatabaseConfig { enabled, backend, backend_options }` afin que `ks-config` ne connaisse plus les paramètres d'un moteur non sélectionné ;
- limite la validation store à la structure backend-agnostique et laisse `ks-store` interpréter/valider les options concrètes, sans modifier les conventions `KS_SECRET_*` ni la résolution des placeholders ;
- aligne `store.config.json`, son exemple, le schéma spécialisé, le schéma runtime et les fixtures de configuration sur ce contrat opaque.
## `0.5.2-pre.006-delta-fix-007`
- corrige le test de séparation des répertoires wallet afin quil résolve dabord `${KS_WALLETS_DIRECTORY:-wallets}` comme le chemin de chargement runtime ;
- rend le test indépendant dun éventuel override `KS_WALLETS_DIRECTORY` en vérifiant la relation `temporary_wallet_dir = wallet_dir/temporary/local_devnet` plutôt quune racine littérale.
## `0.5.2-pre.006-delta-fix-006`
- réinclut dans le delta `config/wallet.config.json` et `config/exemples/example.wallet.config.json`, omis par erreur dans `fix-005`, afin que leurs sous-répertoires temporaires `temporary/...` correspondent au contrat runtime résolu ;
- précise dans le schéma wallet que `wallets_directory` est la racine persistante `.kswallet` et que `profile.directory` désigne les wallets/fixtures temporaires du profil.
## `0.5.2-pre.006-delta-fix-005`
- sépare le store natif persistant `wallet_dir` de `temporary_wallet_dir` : les `.kswallet` utilisent désormais directement la racine `wallets_directory`, tandis que les wallets et keypairs de fixtures restent sous le sous-répertoire temporaire du profil ;
- propage les deux chemins dans le contrat runtime résolu et son schéma sans déplacer automatiquement les fichiers existants.
## `0.5.2-pre.006`
- ajoute `wallet_alias` aux profils du document wallet comme sélection optionnelle/nullable d'un wallet natif persistant, sans password ni matériau secret dans `ks-config` ;
- propage cette sélection non sensible vers le contrat runtime `WalletConfig` et valide l'alias avec les mêmes bornes que `ks-wallet` ;
- garde les documents `0.5.1` compatibles lorsque `wallet_alias` est omis, conserve `null` comme absence explicite de sélection et documente un exemple de sélection persistante.
## `0.5.1-pre.009`
- réconcilie schémas, exemples sous `config/exemples/`, guide de configuration et documentation de crate avant archivage du plan `0.5.1` ;

View File

@@ -1,5 +1,5 @@
<!-- file: ks-config/README.md -->
<!-- version: 12 -->
<!-- version: 14 -->
# ks-config
@@ -10,6 +10,7 @@
- charger l'environnement du workspace et résoudre les placeholders `${KS_*}` / `${KB_*}` ;
- valider les documents transport, listeners, store, wallet, execution et composition contre leurs schémas sous `config/schemas/` ;
- fournir les types partagés d'endpoints, listeners, stockage, wallet, exécution et profils runtime ;
- permettre à un profil wallet de sélectionner un alias persistant non sensible, sans devenir gestionnaire de password ou de matériau privé ;
- résoudre les defaults de chaque document ou les overrides d'une composition ;
- refuser une référence de fichier ou de profil inexistante ;
- reconstruire temporairement `AppConfig/ProfileConfig` pour les consommateurs existants ;
@@ -58,6 +59,8 @@ Les valeurs globales telles que `logs_directory` et `wallets_directory` ne sont
Depuis `0.5.1`, `ks-config` ne dépend plus de TS-RS et ne produit plus de bindings TypeScript. Ses contrats source/runtime peuvent contenir des secrets résolus : ils ne dérivent donc ni `serde::Serialize` ni `Debug`. Les applications propriétaires construisent leurs DTO publics et diagnostics bornés champ par champ.
Depuis `0.5.3-pre.002`, `DatabaseConfig` ne modélise plus PostgreSQL/SQLite : il conserve uniquement `enabled`, le code `backend` et un objet `backend_options` opaque. `ks-config` charge, compose et résout les placeholders ; la validation des paramètres du moteur et la création de la connexion appartiennent exclusivement à `ks-store`.
La section `application` d'une composition est volontairement opaque pour `ks-config`. Chaque binaire propriétaire définit son schéma et le valide avec `validate_json_value_against_schema`; le desktop utilise `config/schemas/kb-app-demo-desktop.application.config.schema.json`.
## Documents

View File

@@ -1,5 +1,5 @@
<!-- file: ks-config/USAGE.md -->
<!-- version: 10 -->
<!-- version: 13 -->
# Utilisation de ks-config
@@ -9,6 +9,22 @@
`ks-config` ne connaît pas la liste des futurs workers ou applications.
## Sélectionner un wallet persistant par alias
Le document `wallet.config.json` accepte un champ optionnel `wallet_alias` par profil. Son omission ou `null` signifie qu'aucun wallet natif persistant n'est sélectionné ; une chaîne valide sélectionne uniquement l'identité logique. Le mot de passe reste fourni au runtime directement à `ks-wallet` et n'appartient jamais à `ks-config`.
```json
{
"name": "worker_devnet",
"directory": "workers/devnet",
"cluster": "devnet",
"wallet_alias": "worker-operator",
"temporary_wallet_enabled": false,
"temporary_wallet_alias": "worker-temporary-disabled",
"temporary_wallet_persist": false
}
```
## Charger les defaults partagés
```rust
@@ -72,13 +88,13 @@ Les endpoints WebSocket source sélectionnent une classe `ws_endpoint_defaults`;
## Store
`store.config.json` possède les paramètres de backend et les profils PostgreSQL/SQLite. Les helpers principaux sont `read_store_json_file_with_environment`, `default_store_profile` et `store_profile`.
`store.config.json` sélectionne un backend par profil et transporte ses paramètres sous `backend_options`. Ce bloc reste opaque pour `ks-config` : seuls le code backend non vide et la forme objet sont validés ici. Les helpers principaux sont `read_store_json_file_with_environment`, `default_store_profile` et `store_profile`; `ks-store` interprète ensuite les options du backend sélectionné et crée la connexion.
## Wallet
`wallet.config.json` porte `wallets_directory` hors profils. Chaque profil définit un chemin relatif, le cluster, l'alias et la politique temporaire/persistante. `resolved_wallet_profile` construit le chemin runtime final.
`wallet.config.json` porte `wallets_directory` hors profils. Cette valeur résout directement `wallet.wallet_dir`, racine commune des `.kswallet` persistants. Chaque profil conserve `directory` uniquement pour ses wallets/keypairs temporaires ; `resolved_wallet_profile` le résout séparément dans `wallet.temporary_wallet_dir` (par défaut `wallets/temporary/<profil>`).
Les autorisations de soumission ne font plus partie du wallet.
Un changement de profil ne déplace donc pas les `.kswallet` persistants. Les fixtures temporaires restent isolées par profil et les autorisations de soumission ne font plus partie du wallet.
## Exécution

View File

@@ -1,5 +1,5 @@
// file: ks-config/src/composition.rs
// version: 3
// version: 5
//! Binary composition configuration and resolution into the shared runtime profile contract.
@@ -18,9 +18,9 @@ pub struct CompositionConfigDocument {
/// Active binary composition profile name.
pub active_profile: std::string::String,
/// Shared configuration documents referenced by this composition.
pub sources: CompositionConfigSources,
pub sources: crate::CompositionConfigSources,
/// Named binary composition profiles.
pub profiles: std::vec::Vec<CompositionProfileConfig>,
pub profiles: std::vec::Vec<crate::CompositionProfileConfig>,
}
/// Paths of shared configuration documents consumed by one binary composition.
@@ -65,7 +65,7 @@ pub struct CompositionProfileConfig {
#[derive(Clone, Eq, PartialEq)]
pub struct ComposedAppConfig {
/// Parsed binary composition document.
pub composition: CompositionConfigDocument,
pub composition: crate::CompositionConfigDocument,
/// Resolved runtime application configuration.
pub app_config: crate::AppConfig,
/// Resolved logging source path.
@@ -101,7 +101,7 @@ pub fn composition_json_schema_value() -> ks_core::Result<serde_json::Value> {
/// Validates raw binary composition JSON against its embedded schema.
pub fn validate_composition_json_schema(raw_json: &str) -> ks_core::Result<()> {
let schema = match composition_json_schema_value() {
let schema = match crate::composition_json_schema_value() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -133,12 +133,12 @@ pub fn validate_composition_json_schema(raw_json: &str) -> ks_core::Result<()> {
}
/// Parses and validates one binary composition document.
pub fn parse_composition_json(raw_json: &str) -> ks_core::Result<CompositionConfigDocument> {
match validate_composition_json_schema(raw_json) {
pub fn parse_composition_json(raw_json: &str) -> ks_core::Result<crate::CompositionConfigDocument> {
match crate::validate_composition_json_schema(raw_json) {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let document = match serde_json::from_str::<CompositionConfigDocument>(raw_json) {
let document = match serde_json::from_str::<crate::CompositionConfigDocument>(raw_json) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ks_core::Error::new(
@@ -147,7 +147,7 @@ pub fn parse_composition_json(raw_json: &str) -> ks_core::Result<CompositionConf
));
},
};
return match validate_composition_document(&document) {
return match crate::validate_composition_document(&document) {
std::result::Result::Ok(()) => std::result::Result::Ok(document),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
@@ -156,7 +156,7 @@ pub fn parse_composition_json(raw_json: &str) -> ks_core::Result<CompositionConf
/// Reads and parses one binary composition document from a filesystem path.
pub fn read_composition_json_file(
path: &std::path::Path,
) -> ks_core::Result<CompositionConfigDocument> {
) -> ks_core::Result<crate::CompositionConfigDocument> {
let raw_json = match std::fs::read_to_string(path) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
@@ -166,14 +166,14 @@ pub fn read_composition_json_file(
));
},
};
return parse_composition_json(&raw_json);
return crate::parse_composition_json(&raw_json);
}
/// Loads workspace environment values, resolves placeholders and parses one binary composition document.
pub fn read_composition_json_file_with_environment(
path: &std::path::Path,
workspace_root: &std::path::Path,
) -> ks_core::Result<CompositionConfigDocument> {
) -> ks_core::Result<crate::CompositionConfigDocument> {
match crate::load_workspace_environment(workspace_root) {
std::result::Result::Ok(_) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -187,14 +187,14 @@ pub fn read_composition_json_file_with_environment(
));
},
};
return parse_composition_json(&crate::resolve_environment_placeholders(&raw_json));
return crate::parse_composition_json(&crate::resolve_environment_placeholders(&raw_json));
}
/// Returns one named binary composition profile.
pub fn composition_profile<'a>(
document: &'a CompositionConfigDocument,
document: &'a crate::CompositionConfigDocument,
profile_name: &str,
) -> ks_core::Result<&'a CompositionProfileConfig> {
) -> ks_core::Result<&'a crate::CompositionProfileConfig> {
for profile in &document.profiles {
if profile.name == profile_name {
return std::result::Result::Ok(profile);
@@ -208,9 +208,9 @@ pub fn composition_profile<'a>(
/// Returns the active binary composition profile.
pub fn active_composition_profile(
document: &CompositionConfigDocument,
) -> ks_core::Result<&CompositionProfileConfig> {
return composition_profile(document, document.active_profile.as_str());
document: &crate::CompositionConfigDocument,
) -> ks_core::Result<&crate::CompositionProfileConfig> {
return crate::composition_profile(document, document.active_profile.as_str());
}
/// Resolves a source path relative to the workspace root unless already absolute.
@@ -238,7 +238,7 @@ pub fn resolve_profile_selection<'a>(
/// Resolves all shared source selections into the runtime application contract.
pub fn compose_app_config(
document: &CompositionConfigDocument,
document: &crate::CompositionConfigDocument,
transport: &crate::TransportConfigDocument,
listeners: &crate::ListenersConfigDocument,
store: &crate::StoreConfigDocument,
@@ -247,23 +247,23 @@ pub fn compose_app_config(
) -> ks_core::Result<crate::AppConfig> {
let mut profiles = std::vec::Vec::<crate::ProfileConfig>::new();
for profile in &document.profiles {
let transport_name = resolve_profile_selection(
let transport_name = crate::resolve_profile_selection(
profile.transport_profile.as_deref(),
transport.default_profile.as_str(),
);
let listeners_name = resolve_profile_selection(
let listeners_name = crate::resolve_profile_selection(
profile.listeners_profile.as_deref(),
listeners.default_profile.as_str(),
);
let store_name = resolve_profile_selection(
let store_name = crate::resolve_profile_selection(
profile.store_profile.as_deref(),
store.default_profile.as_str(),
);
let wallet_name = resolve_profile_selection(
let wallet_name = crate::resolve_profile_selection(
profile.wallet_profile.as_deref(),
wallet.default_profile.as_str(),
);
let execution_name = resolve_profile_selection(
let execution_name = crate::resolve_profile_selection(
profile.execution_profile.as_deref(),
execution.default_profile.as_str(),
);
@@ -313,24 +313,34 @@ pub fn compose_app_config(
pub fn read_composed_app_config_with_environment(
composition_path: &std::path::Path,
workspace_root: &std::path::Path,
) -> ks_core::Result<ComposedAppConfig> {
let composition =
match read_composition_json_file_with_environment(composition_path, workspace_root) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let logging_path =
resolve_composition_source_path(workspace_root, composition.sources.logging.as_str());
let transport_path =
resolve_composition_source_path(workspace_root, composition.sources.transport.as_str());
let listeners_path =
resolve_composition_source_path(workspace_root, composition.sources.listeners.as_str());
) -> ks_core::Result<crate::ComposedAppConfig> {
let composition = match crate::read_composition_json_file_with_environment(
composition_path,
workspace_root,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let logging_path = crate::resolve_composition_source_path(
workspace_root,
composition.sources.logging.as_str(),
);
let transport_path = crate::resolve_composition_source_path(
workspace_root,
composition.sources.transport.as_str(),
);
let listeners_path = crate::resolve_composition_source_path(
workspace_root,
composition.sources.listeners.as_str(),
);
let store_path =
resolve_composition_source_path(workspace_root, composition.sources.store.as_str());
crate::resolve_composition_source_path(workspace_root, composition.sources.store.as_str());
let wallet_path =
resolve_composition_source_path(workspace_root, composition.sources.wallet.as_str());
let execution_path =
resolve_composition_source_path(workspace_root, composition.sources.execution.as_str());
crate::resolve_composition_source_path(workspace_root, composition.sources.wallet.as_str());
let execution_path = crate::resolve_composition_source_path(
workspace_root,
composition.sources.execution.as_str(),
);
let transport =
match crate::read_transport_json_file_with_environment(&transport_path, workspace_root) {
std::result::Result::Ok(value) => value,
@@ -359,13 +369,18 @@ pub fn read_composed_app_config_with_environment(
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let app_config =
match compose_app_config(&composition, &transport, &listeners, &store, &wallet, &execution)
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ComposedAppConfig {
let app_config = match crate::compose_app_config(
&composition,
&transport,
&listeners,
&store,
&wallet,
&execution,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::ComposedAppConfig {
composition,
app_config,
logging_path,
@@ -421,9 +436,9 @@ pub fn read_default_shared_app_config_with_environment(
} else {
"shared_default".to_string()
};
let composition = CompositionConfigDocument {
let composition = crate::CompositionConfigDocument {
active_profile: profile_name.clone(),
sources: CompositionConfigSources {
sources: crate::CompositionConfigSources {
logging: DEFAULT_LOGGING_CONFIG_PATH.to_string(),
transport: DEFAULT_TRANSPORT_CONFIG_PATH.to_string(),
listeners: DEFAULT_LISTENERS_CONFIG_PATH.to_string(),
@@ -431,7 +446,7 @@ pub fn read_default_shared_app_config_with_environment(
wallet: DEFAULT_WALLET_CONFIG_PATH.to_string(),
execution: DEFAULT_EXECUTION_CONFIG_PATH.to_string(),
},
profiles: vec![CompositionProfileConfig {
profiles: vec![crate::CompositionProfileConfig {
name: profile_name.clone(),
application: std::option::Option::None,
logging_profile: std::option::Option::None,
@@ -442,11 +457,20 @@ pub fn read_default_shared_app_config_with_environment(
execution_profile: std::option::Option::None,
}],
};
return compose_app_config(&composition, &transport, &listeners, &store, &wallet, &execution);
return crate::compose_app_config(
&composition,
&transport,
&listeners,
&store,
&wallet,
&execution,
);
}
/// Validates a typed binary composition document before referenced documents are loaded.
pub fn validate_composition_document(document: &CompositionConfigDocument) -> ks_core::Result<()> {
pub fn validate_composition_document(
document: &crate::CompositionConfigDocument,
) -> ks_core::Result<()> {
match crate::require_non_empty(&document.active_profile, "composition.active_profile") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -498,7 +522,7 @@ pub fn validate_composition_document(document: &CompositionConfigDocument) -> ks
));
}
}
return match active_composition_profile(document) {
return match crate::active_composition_profile(document) {
std::result::Result::Ok(_) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
@@ -507,7 +531,7 @@ pub fn validate_composition_document(document: &CompositionConfigDocument) -> ks
fn validate_logging_references_with_environment(
path: &std::path::Path,
workspace_root: &std::path::Path,
composition: &CompositionConfigDocument,
composition: &crate::CompositionConfigDocument,
) -> ks_core::Result<()> {
match crate::load_workspace_environment(workspace_root) {
std::result::Result::Ok(_) => (),
@@ -566,7 +590,7 @@ fn validate_logging_references_with_environment(
}
for profile in &composition.profiles {
let selected =
resolve_profile_selection(profile.logging_profile.as_deref(), default_profile);
crate::resolve_profile_selection(profile.logging_profile.as_deref(), default_profile);
if !names.contains(selected) {
return std::result::Result::Err(ks_core::Error::new(
"composition_logging_profile_not_found",
@@ -586,17 +610,17 @@ mod tests {
#[test]
fn default_and_example_compositions_validate() {
assert!(super::parse_composition_json(DEFAULT_COMPOSITION).is_ok());
assert!(super::parse_composition_json(EXAMPLE_COMPOSITION).is_ok());
assert!(crate::parse_composition_json(DEFAULT_COMPOSITION).is_ok());
assert!(crate::parse_composition_json(EXAMPLE_COMPOSITION).is_ok());
}
#[test]
fn local_devnet_composition_uses_shared_defaults() {
let composition = match super::parse_composition_json(DEFAULT_COMPOSITION) {
let composition = match crate::parse_composition_json(DEFAULT_COMPOSITION) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("default composition must parse: {error}"),
};
let profile = match super::composition_profile(&composition, "local_devnet") {
let profile = match crate::composition_profile(&composition, "local_devnet") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("local Devnet composition must resolve: {error}")

View File

@@ -1,5 +1,5 @@
// file: ks-config/src/environment.rs
// version: 6
// version: 8
//! Environment-file loading and configuration placeholder resolution.
@@ -16,7 +16,7 @@ pub struct EnvironmentLoadReport {
/// fallback text declared with `${NAME:-fallback}` placeholders.
pub fn load_workspace_environment(
workspace_root: &std::path::Path,
) -> ks_core::Result<EnvironmentLoadReport> {
) -> ks_core::Result<crate::EnvironmentLoadReport> {
let explicit_path = std::env::var("KS_ENV_FILE").ok();
let selected_path = match explicit_path {
std::option::Option::Some(path) if !path.trim().is_empty() => {
@@ -26,10 +26,10 @@ pub fn load_workspace_environment(
_ => workspace_root.join(".env"),
};
if !selected_path.exists() {
return std::result::Result::Ok(EnvironmentLoadReport { loaded_path: None });
return std::result::Result::Ok(crate::EnvironmentLoadReport { loaded_path: None });
}
return match dotenvy::from_path(&selected_path) {
std::result::Result::Ok(()) => std::result::Result::Ok(EnvironmentLoadReport {
std::result::Result::Ok(()) => std::result::Result::Ok(crate::EnvironmentLoadReport {
loaded_path: std::option::Option::Some(selected_path),
}),
std::result::Result::Err(_) => std::result::Result::Err(ks_core::Error::new(
@@ -82,14 +82,14 @@ mod tests {
#[test]
fn fallback_is_used_when_variable_is_absent() {
let resolved =
super::resolve_environment_placeholders("${KS_CONFIG_TEST_MISSING:-fallback}");
crate::resolve_environment_placeholders("${KS_CONFIG_TEST_MISSING:-fallback}");
assert_eq!(resolved, "fallback");
}
#[test]
fn unresolved_required_placeholder_is_preserved() {
let resolved =
super::resolve_environment_placeholders("prefix-${KS_CONFIG_TEST_MISSING}-suffix");
crate::resolve_environment_placeholders("prefix-${KS_CONFIG_TEST_MISSING}-suffix");
assert_eq!(resolved, "prefix-${KS_CONFIG_TEST_MISSING}-suffix");
}
}

View File

@@ -1,5 +1,5 @@
// file: ks-config/src/execution.rs
// version: 3
// version: 5
//! Independent execution policy configuration document loading and validation.
@@ -12,7 +12,7 @@ pub struct ExecutionConfigDocument {
/// Default execution profile used when a composition does not override it.
pub default_profile: std::string::String,
/// Named execution policy profiles.
pub profiles: std::vec::Vec<ExecutionProfileConfig>,
pub profiles: std::vec::Vec<crate::ExecutionProfileConfig>,
}
/// Named execution policy profile.
@@ -31,7 +31,7 @@ pub fn execution_json_schema_text() -> &'static str {
}
/// Parses and validates one execution configuration document.
pub fn parse_execution_json(raw_json: &str) -> ks_core::Result<ExecutionConfigDocument> {
pub fn parse_execution_json(raw_json: &str) -> ks_core::Result<crate::ExecutionConfigDocument> {
let schema = match serde_json::from_str::<serde_json::Value>(EXECUTION_JSON_SCHEMA) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
@@ -65,7 +65,7 @@ pub fn parse_execution_json(raw_json: &str) -> ks_core::Result<ExecutionConfigDo
"execution configuration does not satisfy its schema",
));
}
let document = match serde_json::from_str::<ExecutionConfigDocument>(raw_json) {
let document = match serde_json::from_str::<crate::ExecutionConfigDocument>(raw_json) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ks_core::Error::new(
@@ -74,7 +74,7 @@ pub fn parse_execution_json(raw_json: &str) -> ks_core::Result<ExecutionConfigDo
));
},
};
return match validate_execution_document(&document) {
return match crate::validate_execution_document(&document) {
std::result::Result::Ok(()) => std::result::Result::Ok(document),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
@@ -84,7 +84,7 @@ pub fn parse_execution_json(raw_json: &str) -> ks_core::Result<ExecutionConfigDo
pub fn read_execution_json_file_with_environment(
path: &std::path::Path,
workspace_root: &std::path::Path,
) -> ks_core::Result<ExecutionConfigDocument> {
) -> ks_core::Result<crate::ExecutionConfigDocument> {
match crate::load_workspace_environment(workspace_root) {
std::result::Result::Ok(_) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -98,14 +98,14 @@ pub fn read_execution_json_file_with_environment(
));
},
};
return parse_execution_json(&crate::resolve_environment_placeholders(&raw_json));
return crate::parse_execution_json(&crate::resolve_environment_placeholders(&raw_json));
}
/// Returns one named execution profile.
pub fn execution_profile<'a>(
document: &'a ExecutionConfigDocument,
document: &'a crate::ExecutionConfigDocument,
profile_name: &str,
) -> ks_core::Result<&'a ExecutionProfileConfig> {
) -> ks_core::Result<&'a crate::ExecutionProfileConfig> {
for profile in &document.profiles {
if profile.name == profile_name {
return std::result::Result::Ok(profile);
@@ -119,13 +119,15 @@ pub fn execution_profile<'a>(
/// Returns the default execution profile.
pub fn default_execution_profile(
document: &ExecutionConfigDocument,
) -> ks_core::Result<&ExecutionProfileConfig> {
return execution_profile(document, document.default_profile.as_str());
document: &crate::ExecutionConfigDocument,
) -> ks_core::Result<&crate::ExecutionProfileConfig> {
return crate::execution_profile(document, document.default_profile.as_str());
}
/// Validates one typed execution configuration document.
pub fn validate_execution_document(document: &ExecutionConfigDocument) -> ks_core::Result<()> {
pub fn validate_execution_document(
document: &crate::ExecutionConfigDocument,
) -> ks_core::Result<()> {
match crate::require_non_empty(&document.default_profile, "execution.default_profile") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -153,7 +155,7 @@ pub fn validate_execution_document(document: &ExecutionConfigDocument) -> ks_cor
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
return match default_execution_profile(document) {
return match crate::default_execution_profile(document) {
std::result::Result::Ok(_) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
@@ -167,7 +169,7 @@ mod tests {
#[test]
fn default_and_example_execution_configs_validate() {
assert!(super::parse_execution_json(DEFAULT_EXECUTION).is_ok());
assert!(super::parse_execution_json(EXAMPLE_EXECUTION).is_ok());
assert!(crate::parse_execution_json(DEFAULT_EXECUTION).is_ok());
assert!(crate::parse_execution_json(EXAMPLE_EXECUTION).is_ok());
}
}

View File

@@ -1,5 +1,5 @@
// file: ks-config/src/lib.rs
// version: 9
// version: 11
//! Khadhroony Solana shared configuration contracts and composition helpers.
#![warn(missing_docs)]
@@ -125,16 +125,12 @@ pub use self::settings::HttpEndpointConfig;
pub use self::settings::ListenerConfig;
/// Exposes the log listener configuration type.
pub use self::settings::LogListenerConfig;
/// Exposes the PostgreSQL configuration type.
pub use self::settings::PostgresConfig;
/// Exposes the profile configuration type.
pub use self::settings::ProfileConfig;
/// Exposes the program listener configuration type.
pub use self::settings::ProgramListenerConfig;
/// Exposes the Solana configuration type.
pub use self::settings::SolanaConfig;
/// Exposes the SQLite configuration type.
pub use self::settings::SqliteConfig;
/// Exposes the wallet configuration type.
pub use self::settings::WalletConfig;
/// Exposes the WebSocket endpoint configuration type.
@@ -234,5 +230,7 @@ pub(crate) use self::settings::validate_http_endpoint;
pub(crate) use self::settings::validate_listeners;
/// Keeps `validate_wallet` available to internal configuration modules.
pub(crate) use self::settings::validate_wallet;
/// Keeps wallet-alias validation shared by source and resolved configuration modules.
pub(crate) use self::settings::validate_wallet_alias;
/// Keeps `validate_ws_endpoint` available to internal configuration modules.
pub(crate) use self::settings::validate_ws_endpoint;

View File

@@ -1,5 +1,5 @@
// file: ks-config/src/listeners.rs
// version: 3
// version: 5
//! Independent Solana listener configuration document loading and validation.
@@ -12,7 +12,7 @@ pub struct ListenersConfigDocument {
/// Default listener profile used by consumers that do not provide an explicit selection.
pub default_profile: std::string::String,
/// Named listener profiles available in this document.
pub profiles: std::vec::Vec<ListenersProfileConfig>,
pub profiles: std::vec::Vec<crate::ListenersProfileConfig>,
}
/// Named listener profile.
@@ -51,7 +51,7 @@ pub fn listeners_json_schema_value() -> ks_core::Result<serde_json::Value> {
/// Validates raw listener JSON against the embedded listener schema.
pub fn validate_listeners_json_schema(raw_json: &str) -> ks_core::Result<()> {
let schema = match listeners_json_schema_value() {
let schema = match crate::listeners_json_schema_value() {
std::result::Result::Ok(schema) => schema,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -83,12 +83,12 @@ pub fn validate_listeners_json_schema(raw_json: &str) -> ks_core::Result<()> {
}
/// Parses and validates one independent listener configuration document.
pub fn parse_listeners_json(raw_json: &str) -> ks_core::Result<ListenersConfigDocument> {
match validate_listeners_json_schema(raw_json) {
pub fn parse_listeners_json(raw_json: &str) -> ks_core::Result<crate::ListenersConfigDocument> {
match crate::validate_listeners_json_schema(raw_json) {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let document = match serde_json::from_str::<ListenersConfigDocument>(raw_json) {
let document = match serde_json::from_str::<crate::ListenersConfigDocument>(raw_json) {
std::result::Result::Ok(document) => document,
std::result::Result::Err(_) => {
return std::result::Result::Err(ks_core::Error::new(
@@ -97,7 +97,7 @@ pub fn parse_listeners_json(raw_json: &str) -> ks_core::Result<ListenersConfigDo
));
},
};
return match validate_listeners_document(&document) {
return match crate::validate_listeners_document(&document) {
std::result::Result::Ok(()) => std::result::Result::Ok(document),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
@@ -106,7 +106,7 @@ pub fn parse_listeners_json(raw_json: &str) -> ks_core::Result<ListenersConfigDo
/// Reads and parses one listener configuration document from a filesystem path.
pub fn read_listeners_json_file(
path: &std::path::Path,
) -> ks_core::Result<ListenersConfigDocument> {
) -> ks_core::Result<crate::ListenersConfigDocument> {
let raw_json = match std::fs::read_to_string(path) {
std::result::Result::Ok(content) => content,
std::result::Result::Err(error) => {
@@ -116,14 +116,14 @@ pub fn read_listeners_json_file(
));
},
};
return parse_listeners_json(&raw_json);
return crate::parse_listeners_json(&raw_json);
}
/// Loads workspace environment values, resolves placeholders and parses listener configuration.
pub fn read_listeners_json_file_with_environment(
path: &std::path::Path,
workspace_root: &std::path::Path,
) -> ks_core::Result<ListenersConfigDocument> {
) -> ks_core::Result<crate::ListenersConfigDocument> {
match crate::load_workspace_environment(workspace_root) {
std::result::Result::Ok(_) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -138,14 +138,14 @@ pub fn read_listeners_json_file_with_environment(
},
};
let resolved = crate::resolve_environment_placeholders(&raw_json);
return parse_listeners_json(&resolved);
return crate::parse_listeners_json(&resolved);
}
/// Returns one named listener profile.
pub fn listeners_profile<'a>(
document: &'a ListenersConfigDocument,
document: &'a crate::ListenersConfigDocument,
profile_name: &str,
) -> ks_core::Result<&'a ListenersProfileConfig> {
) -> ks_core::Result<&'a crate::ListenersProfileConfig> {
for profile in &document.profiles {
if profile.name == profile_name {
return std::result::Result::Ok(profile);
@@ -159,13 +159,13 @@ pub fn listeners_profile<'a>(
/// Returns the default listener profile declared by the document.
pub fn default_listeners_profile(
document: &ListenersConfigDocument,
) -> ks_core::Result<&ListenersProfileConfig> {
return listeners_profile(document, document.default_profile.as_str());
document: &crate::ListenersConfigDocument,
) -> ks_core::Result<&crate::ListenersProfileConfig> {
return crate::listeners_profile(document, document.default_profile.as_str());
}
/// Converts one named listener profile into the resolved runtime listener contract.
pub fn resolved_listener_config(profile: &ListenersProfileConfig) -> crate::ListenerConfig {
pub fn resolved_listener_config(profile: &crate::ListenersProfileConfig) -> crate::ListenerConfig {
return crate::ListenerConfig {
enabled: profile.enabled,
default_commitment: profile.default_commitment.clone(),
@@ -176,7 +176,9 @@ pub fn resolved_listener_config(profile: &ListenersProfileConfig) -> crate::List
}
/// Validates a typed listener configuration document.
pub fn validate_listeners_document(document: &ListenersConfigDocument) -> ks_core::Result<()> {
pub fn validate_listeners_document(
document: &crate::ListenersConfigDocument,
) -> ks_core::Result<()> {
match crate::require_non_empty(&document.default_profile, "default_profile") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -199,13 +201,13 @@ pub fn validate_listeners_document(document: &ListenersConfigDocument) -> ks_cor
profile.name.clone(),
));
}
let resolved = resolved_listener_config(profile);
let resolved = crate::resolved_listener_config(profile);
match crate::validate_listeners(&resolved) {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
return match default_listeners_profile(document) {
return match crate::default_listeners_profile(document) {
std::result::Result::Ok(_) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
@@ -219,7 +221,7 @@ mod tests {
#[test]
fn default_and_example_listener_configs_validate() {
assert!(super::parse_listeners_json(DEFAULT_LISTENERS).is_ok());
assert!(super::parse_listeners_json(EXAMPLE_LISTENERS).is_ok());
assert!(crate::parse_listeners_json(DEFAULT_LISTENERS).is_ok());
assert!(crate::parse_listeners_json(EXAMPLE_LISTENERS).is_ok());
}
}

View File

@@ -1,5 +1,5 @@
// file: ks-config/src/schema.rs
// version: 1
// version: 2
//! Generic JSON Schema validation helpers for binary-owned configuration fragments.
@@ -42,7 +42,7 @@ mod tests {
fn validation_error_never_echoes_instance_secret() {
let schema = r#"{"type":"object","additionalProperties":false,"required":["value"],"properties":{"value":{"type":"integer"}}}"#;
let instance = serde_json::json!({"value": "SECRET-CANARY"});
let result = super::validate_json_value_against_schema(
let result = crate::validate_json_value_against_schema(
schema,
&instance,
"test_fragment_validation_failed",

View File

@@ -1,5 +1,5 @@
// file: ks-config/src/sensitivity.rs
// version: 1
// version: 3
//! Application environment-variable sensitivity classification.
@@ -20,7 +20,7 @@ pub enum EnvironmentValueSensitivity {
/// one `KS_SECRET_*` placeholder is classified as secret even when the surrounding URL is public.
pub fn classify_environment_template(
raw: &str,
) -> std::option::Option<EnvironmentValueSensitivity> {
) -> std::option::Option<crate::EnvironmentValueSensitivity> {
let bytes = raw.as_bytes();
let mut sensitivity = std::option::Option::None;
let mut index = 0_usize;
@@ -36,7 +36,8 @@ pub fn classify_environment_template(
std::option::Option::Some((name, _)) => name,
std::option::Option::None => expression,
};
sensitivity = merge_sensitivity(sensitivity, classify_environment_variable(name));
sensitivity =
merge_sensitivity(sensitivity, crate::classify_environment_variable(name));
index = end + 1;
continue;
}
@@ -49,15 +50,15 @@ pub fn classify_environment_template(
/// Classifies a Khadhroony-owned environment variable by its namespace.
pub fn classify_environment_variable(
name: &str,
) -> std::option::Option<EnvironmentValueSensitivity> {
) -> std::option::Option<crate::EnvironmentValueSensitivity> {
if name.starts_with("KS_SECRET_") || name.starts_with("KB_SECRET_") {
return std::option::Option::Some(EnvironmentValueSensitivity::Secret);
return std::option::Option::Some(crate::EnvironmentValueSensitivity::Secret);
}
if name.starts_with("KS_PUBLIC_") || name.starts_with("KB_PUBLIC_") {
return std::option::Option::Some(EnvironmentValueSensitivity::Public);
return std::option::Option::Some(crate::EnvironmentValueSensitivity::Public);
}
if name.starts_with("KS_") || name.starts_with("KB_") {
return std::option::Option::Some(EnvironmentValueSensitivity::Internal);
return std::option::Option::Some(crate::EnvironmentValueSensitivity::Internal);
}
return std::option::Option::None;
}
@@ -67,14 +68,14 @@ pub fn diagnostic_environment_value(
name: &str,
value: &str,
) -> std::option::Option<std::string::String> {
return match classify_environment_variable(name) {
std::option::Option::Some(EnvironmentValueSensitivity::Secret) => {
return match crate::classify_environment_variable(name) {
std::option::Option::Some(crate::EnvironmentValueSensitivity::Secret) => {
std::option::Option::Some("<redacted>".to_string())
},
std::option::Option::Some(EnvironmentValueSensitivity::Internal) => {
std::option::Option::Some(crate::EnvironmentValueSensitivity::Internal) => {
std::option::Option::Some("<configured>".to_string())
},
std::option::Option::Some(EnvironmentValueSensitivity::Public) => {
std::option::Option::Some(crate::EnvironmentValueSensitivity::Public) => {
std::option::Option::Some(value.to_string())
},
std::option::Option::None => std::option::Option::None,
@@ -82,23 +83,23 @@ pub fn diagnostic_environment_value(
}
fn merge_sensitivity(
left: std::option::Option<EnvironmentValueSensitivity>,
right: std::option::Option<EnvironmentValueSensitivity>,
) -> std::option::Option<EnvironmentValueSensitivity> {
if left == std::option::Option::Some(EnvironmentValueSensitivity::Secret)
|| right == std::option::Option::Some(EnvironmentValueSensitivity::Secret)
left: std::option::Option<crate::EnvironmentValueSensitivity>,
right: std::option::Option<crate::EnvironmentValueSensitivity>,
) -> std::option::Option<crate::EnvironmentValueSensitivity> {
if left == std::option::Option::Some(crate::EnvironmentValueSensitivity::Secret)
|| right == std::option::Option::Some(crate::EnvironmentValueSensitivity::Secret)
{
return std::option::Option::Some(EnvironmentValueSensitivity::Secret);
return std::option::Option::Some(crate::EnvironmentValueSensitivity::Secret);
}
if left == std::option::Option::Some(EnvironmentValueSensitivity::Internal)
|| right == std::option::Option::Some(EnvironmentValueSensitivity::Internal)
if left == std::option::Option::Some(crate::EnvironmentValueSensitivity::Internal)
|| right == std::option::Option::Some(crate::EnvironmentValueSensitivity::Internal)
{
return std::option::Option::Some(EnvironmentValueSensitivity::Internal);
return std::option::Option::Some(crate::EnvironmentValueSensitivity::Internal);
}
if left == std::option::Option::Some(EnvironmentValueSensitivity::Public)
|| right == std::option::Option::Some(EnvironmentValueSensitivity::Public)
if left == std::option::Option::Some(crate::EnvironmentValueSensitivity::Public)
|| right == std::option::Option::Some(crate::EnvironmentValueSensitivity::Public)
{
return std::option::Option::Some(EnvironmentValueSensitivity::Public);
return std::option::Option::Some(crate::EnvironmentValueSensitivity::Public);
}
return std::option::Option::None;
}
@@ -108,19 +109,19 @@ mod tests {
#[test]
fn composed_values_inherit_the_strongest_placeholder_sensitivity() {
assert_eq!(
super::classify_environment_template(
crate::classify_environment_template(
"https://rpc.invalid/?api-key=${KS_SECRET_HELIUS_API_KEY}&mint=${KS_PUBLIC_DEVNET_TOKEN_2022_MINT}"
),
std::option::Option::Some(super::EnvironmentValueSensitivity::Secret)
);
assert_eq!(
super::classify_environment_template(
crate::classify_environment_template(
"${KS_LOGS_DIRECTORY:-logs}/${KS_PUBLIC_SPL_TOKEN_PROGRAM_ID}"
),
std::option::Option::Some(super::EnvironmentValueSensitivity::Internal)
);
assert_eq!(
super::classify_environment_template("${KS_PUBLIC_SPL_TOKEN_PROGRAM_ID}"),
crate::classify_environment_template("${KS_PUBLIC_SPL_TOKEN_PROGRAM_ID}"),
std::option::Option::Some(super::EnvironmentValueSensitivity::Public)
);
}
@@ -128,15 +129,15 @@ mod tests {
#[test]
fn generic_diagnostic_never_reveals_secret_or_internal_values() {
assert_eq!(
super::diagnostic_environment_value("KS_SECRET_HELIUS_API_KEY", "SECRET-CANARY"),
crate::diagnostic_environment_value("KS_SECRET_HELIUS_API_KEY", "SECRET-CANARY"),
std::option::Option::Some("<redacted>".to_string())
);
assert_eq!(
super::diagnostic_environment_value("KS_LOGS_DIRECTORY", "/private/logs"),
crate::diagnostic_environment_value("KS_LOGS_DIRECTORY", "/private/logs"),
std::option::Option::Some("<configured>".to_string())
);
assert_eq!(
super::diagnostic_environment_value("KS_PUBLIC_SPL_TOKEN_PROGRAM_ID", "Token111"),
crate::diagnostic_environment_value("KS_PUBLIC_SPL_TOKEN_PROGRAM_ID", "Token111"),
std::option::Option::Some("Token111".to_string())
);
}
@@ -144,17 +145,17 @@ mod tests {
#[test]
fn namespaces_map_to_secret_internal_and_public() {
assert_eq!(
super::classify_environment_variable("KS_SECRET_HELIUS_API_KEY"),
crate::classify_environment_variable("KS_SECRET_HELIUS_API_KEY"),
std::option::Option::Some(super::EnvironmentValueSensitivity::Secret)
);
assert_eq!(
super::classify_environment_variable("KS_LOGS_DIRECTORY"),
crate::classify_environment_variable("KS_LOGS_DIRECTORY"),
std::option::Option::Some(super::EnvironmentValueSensitivity::Internal)
);
assert_eq!(
super::classify_environment_variable("KB_PUBLIC_DEMO_VALUE"),
crate::classify_environment_variable("KB_PUBLIC_DEMO_VALUE"),
std::option::Option::Some(super::EnvironmentValueSensitivity::Public)
);
assert_eq!(super::classify_environment_variable("HOME"), std::option::Option::None);
assert_eq!(crate::classify_environment_variable("HOME"), std::option::Option::None);
}
}

Some files were not shown because too many files have changed in this diff Show More