Compare commits
15 Commits
v0.5.1
...
v0.5.3-pre
| Author | SHA1 | Date | |
|---|---|---|---|
| 734a27f2ca | |||
| b6583bd9fb | |||
| 400ced4832 | |||
| 8448ad1079 | |||
| 01d78b5845 | |||
| 34674e8187 | |||
| 279fd67cc0 | |||
| 56572cec40 | |||
| 1ee1d0297d | |||
| 066d969f5a | |||
| e3acb8ceb1 | |||
| b8e6751747 | |||
| ef630e3123 | |||
| eaa24e6f11 | |||
| 0bd5bbf1c4 |
@@ -17,6 +17,10 @@ KS_SECRET_POSTGRES_TEST_URL=postgres://solana:solana@localhost:5432/khadhroony_t
|
|||||||
# Optional binary composition override for the desktop demo.
|
# Optional binary composition override for the desktop demo.
|
||||||
# KB_APP_DEMO_DESKTOP_CONFIG_PATH=config/kb-app-demo-desktop.default.config.json
|
# 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.
|
# 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_LOGGING_CONFIG_PATH=config/logging.config.json
|
||||||
# KS_LOGS_DIRECTORY=logs
|
# KS_LOGS_DIRECTORY=logs
|
||||||
|
|||||||
37
CHANGELOG.md
37
CHANGELOG.md
@@ -1,10 +1,45 @@
|
|||||||
<!-- file: CHANGELOG.md -->
|
<!-- file: CHANGELOG.md -->
|
||||||
<!-- version: 17 -->
|
<!-- version: 19 -->
|
||||||
|
|
||||||
# CHANGELOG — khadhroony-bot3
|
# 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.
|
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
|
## 0.5.1 — namespaces Khadhroony Solana et configuration sûre
|
||||||
|
|
||||||
### Namespace et ownership
|
### Namespace et ownership
|
||||||
|
|||||||
12
Cargo.toml
12
Cargo.toml
@@ -1,5 +1,5 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 54
|
# version: 74
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
@@ -14,11 +14,12 @@ members = [
|
|||||||
"ks-onchain-transport",
|
"ks-onchain-transport",
|
||||||
"ks-store",
|
"ks-store",
|
||||||
"ks-wallet",
|
"ks-wallet",
|
||||||
|
"ks-wallet-demo-scenarios",
|
||||||
"kb-app-demo-desktop",
|
"kb-app-demo-desktop",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.5.1"
|
version = "0.5.3-pre.5"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3"
|
||||||
@@ -26,7 +27,7 @@ authors = ["SinuS von SifriduS <sinus@sasedev.net>"]
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
dotenvy = {version = "^0.15", features = ["clap", "cli"]}
|
dotenvy = { version = "^0.15", features = ["clap", "cli"] }
|
||||||
argon2 = { version = "^0.5", features = ["std", "zeroize"] }
|
argon2 = { version = "^0.5", features = ["std", "zeroize"] }
|
||||||
async-trait = { version = "^0.1", features = [] }
|
async-trait = { version = "^0.1", features = [] }
|
||||||
base64 = { version = "^0.23", 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_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"] }
|
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"] }
|
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"] }
|
chrono = { version = "^0.4", features = ["serde"] }
|
||||||
fs2 = { version = "^0.4", features = [] }
|
fs2 = { version = "^0.4", features = [] }
|
||||||
futures-util = { version = "^0.3", features = ["default", "futures-sink"] }
|
futures-util = { version = "^0.3", features = ["default", "futures-sink"] }
|
||||||
|
getrandom = { version = "^0.4", features = ["std", "sys_rng"] }
|
||||||
jsonschema = { version = "^0.49", features = [] }
|
jsonschema = { version = "^0.49", features = [] }
|
||||||
mpl-token-metadata = { version = "^5.1", features = ["serde"] }
|
mpl-token-metadata = { version = "^5.1", features = ["serde"] }
|
||||||
rand = { version = "^0.10", features = ["chacha", "serde", "std", "sys_rng"] }
|
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-group-interface = { version = "^0.7", features = [] }
|
||||||
spl-token-interface = { version = "^3.0", features = [] }
|
spl-token-interface = { version = "^3.0", features = [] }
|
||||||
spl-token-2022-interface = { version = "^3.1", features = ["serde"] }
|
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 = { version = "^2.11", features = ["default", "tray-icon"] }
|
||||||
tauri-build = { version = "^2.6", features = [] }
|
tauri-build = { version = "^2.6", features = [] }
|
||||||
tauri-plugin-tracing = { version = "^0.3", default-features = false, features = [] }
|
tauri-plugin-tracing = { version = "^0.3", default-features = false, features = [] }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: README.md -->
|
<!-- file: README.md -->
|
||||||
<!-- version: 24 -->
|
<!-- version: 26 -->
|
||||||
|
|
||||||
# Khadhroony Bot3
|
# Khadhroony Bot3
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Le workspace contient onze crates :
|
Le workspace contient douze crates :
|
||||||
|
|
||||||
- `ks-core` : erreurs et primitives transversales ;
|
- `ks-core` : erreurs et primitives transversales ;
|
||||||
- `ks-config` : chargement et validation de la configuration ;
|
- `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-onchain-transport` : transports RPC HTTP et WebSocket ;
|
||||||
- `ks-store` : contrats de stockage et adaptateur PostgreSQL ;
|
- `ks-store` : contrats de stockage et adaptateur PostgreSQL ;
|
||||||
- `ks-wallet` : frontière de wallet et signataires ;
|
- `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.
|
- `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.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 :
|
Références :
|
||||||
|
|
||||||
- [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) ;
|
- [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) ;
|
||||||
|
|||||||
57
ROADMAP.md
57
ROADMAP.md
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ROADMAP.md -->
|
<!-- file: ROADMAP.md -->
|
||||||
<!-- version: 38 -->
|
<!-- version: 44 -->
|
||||||
|
|
||||||
# ROADMAP — khadhroony-bot3
|
# ROADMAP — khadhroony-bot3
|
||||||
|
|
||||||
@@ -96,30 +96,49 @@ Les préfixes SQL historiques `kb_sol_*` restent volontairement inchangés jusqu
|
|||||||
|
|
||||||
### 0.5.2 — `ks-wallet`
|
### 0.5.2 — `ks-wallet`
|
||||||
|
|
||||||
- auditer et normaliser les frontières identité, secret, déverrouillage et signature ;
|
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.
|
||||||
- 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.
|
|
||||||
|
|
||||||
### 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 ;
|
### 0.5.3 — audit, gel de schéma et abstraction de `ks-store`
|
||||||
- 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.
|
|
||||||
|
|
||||||
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
|
### 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 d’accounts 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` ;
|
- 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 ;
|
- 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 ;
|
- comparer les décodeurs existants aux exécuteurs disponibles et identifier les exécuteurs réellement manquants ;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: config/README.md -->
|
<!-- file: config/README.md -->
|
||||||
<!-- version: 25 -->
|
<!-- version: 27 -->
|
||||||
|
|
||||||
# Configuration locale
|
# Configuration locale
|
||||||
|
|
||||||
@@ -90,6 +90,7 @@ Actuellement :
|
|||||||
|
|
||||||
- `logging.config.json.logs_directory` vaut `${KS_LOGS_DIRECTORY:-logs}` ;
|
- `logging.config.json.logs_directory` vaut `${KS_LOGS_DIRECTORY:-logs}` ;
|
||||||
- `wallet.config.json.wallets_directory` vaut `${KS_WALLETS_DIRECTORY:-wallets}`.
|
- `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.
|
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
|
||||||
|
|
||||||
`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
|
## Frontière source/runtime/public/diagnostic
|
||||||
|
|
||||||
|
|||||||
@@ -34,14 +34,34 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"target_filters": [
|
"target_filters": [
|
||||||
|
{
|
||||||
|
"target": "h2",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"target": "hyper",
|
"target": "hyper",
|
||||||
"level": "warn"
|
"level": "warn"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"target": "hyper_util",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"target": "reqwest",
|
"target": "reqwest",
|
||||||
"level": "warn"
|
"level": "warn"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"target": "rustls",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "rustls_platform_verifier",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "sqlx",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"target": "tao",
|
"target": "tao",
|
||||||
"level": "warn"
|
"level": "warn"
|
||||||
|
|||||||
@@ -6,19 +6,11 @@
|
|||||||
"database": {
|
"database": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"backend": "postgres",
|
"backend": "postgres",
|
||||||
"postgres": {
|
"backend_options": {
|
||||||
"url": "${KS_SECRET_POSTGRES_DEVNET_URL}",
|
"url": "${KS_SECRET_POSTGRES_DEVNET_URL}",
|
||||||
"max_connections": 8,
|
"max_connections": 8,
|
||||||
"connect_timeout_ms": 5000,
|
"connect_timeout_ms": 5000,
|
||||||
"auto_initialize_schema": true
|
"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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
"get_recent_performance_samples",
|
"get_recent_performance_samples",
|
||||||
"get_recent_prioritization_fees",
|
"get_recent_prioritization_fees",
|
||||||
"get_signature_statuses",
|
"get_signature_statuses",
|
||||||
|
"get_signatures_for_address",
|
||||||
"get_slot",
|
"get_slot",
|
||||||
"get_slot_leader",
|
"get_slot_leader",
|
||||||
"get_slot_leaders",
|
"get_slot_leaders",
|
||||||
|
|||||||
@@ -6,9 +6,10 @@
|
|||||||
"name": "local_devnet",
|
"name": "local_devnet",
|
||||||
"directory": "temporary/local_devnet",
|
"directory": "temporary/local_devnet",
|
||||||
"cluster": "devnet",
|
"cluster": "devnet",
|
||||||
"temporary_wallet_enabled": true,
|
"wallet_alias": "example-operator",
|
||||||
"temporary_wallet_alias": "local-devnet-operator",
|
"temporary_wallet_enabled": false,
|
||||||
"temporary_wallet_persist": true
|
"temporary_wallet_alias": "example-temporary-disabled",
|
||||||
|
"temporary_wallet_persist": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1168,14 +1168,34 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"target_filters": [
|
"target_filters": [
|
||||||
|
{
|
||||||
|
"target": "h2",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"target": "hyper",
|
"target": "hyper",
|
||||||
"level": "warn"
|
"level": "warn"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"target": "hyper_util",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"target": "reqwest",
|
"target": "reqwest",
|
||||||
"level": "warn"
|
"level": "warn"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"target": "rustls",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "rustls_platform_verifier",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "sqlx",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"target": "tao",
|
"target": "tao",
|
||||||
"level": "warn"
|
"level": "warn"
|
||||||
@@ -2352,14 +2372,34 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"target_filters": [
|
"target_filters": [
|
||||||
|
{
|
||||||
|
"target": "h2",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"target": "hyper",
|
"target": "hyper",
|
||||||
"level": "warn"
|
"level": "warn"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"target": "hyper_util",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"target": "reqwest",
|
"target": "reqwest",
|
||||||
"level": "warn"
|
"level": "warn"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"target": "rustls",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "rustls_platform_verifier",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "sqlx",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"target": "tao",
|
"target": "tao",
|
||||||
"level": "warn"
|
"level": "warn"
|
||||||
@@ -3536,14 +3576,34 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"target_filters": [
|
"target_filters": [
|
||||||
|
{
|
||||||
|
"target": "h2",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"target": "hyper",
|
"target": "hyper",
|
||||||
"level": "warn"
|
"level": "warn"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"target": "hyper_util",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"target": "reqwest",
|
"target": "reqwest",
|
||||||
"level": "warn"
|
"level": "warn"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"target": "rustls",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "rustls_platform_verifier",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "sqlx",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"target": "tao",
|
"target": "tao",
|
||||||
"level": "warn"
|
"level": "warn"
|
||||||
|
|||||||
@@ -59,85 +59,17 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"enabled",
|
"enabled",
|
||||||
"backend",
|
"backend",
|
||||||
"postgres",
|
"backend_options"
|
||||||
"sqlite"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"backend": {
|
"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"
|
"$ref": "#/$defs/non_empty_string"
|
||||||
},
|
},
|
||||||
"max_connections": {
|
"backend_options": {
|
||||||
"type": "integer",
|
"type": "object"
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -465,6 +397,7 @@
|
|||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
"required": [
|
||||||
"wallet_dir",
|
"wallet_dir",
|
||||||
|
"temporary_wallet_dir",
|
||||||
"cluster",
|
"cluster",
|
||||||
"temporary_wallet_enabled",
|
"temporary_wallet_enabled",
|
||||||
"temporary_wallet_alias",
|
"temporary_wallet_alias",
|
||||||
@@ -474,9 +407,21 @@
|
|||||||
"wallet_dir": {
|
"wallet_dir": {
|
||||||
"$ref": "#/$defs/non_empty_string"
|
"$ref": "#/$defs/non_empty_string"
|
||||||
},
|
},
|
||||||
|
"temporary_wallet_dir": {
|
||||||
|
"$ref": "#/$defs/non_empty_string"
|
||||||
|
},
|
||||||
"cluster": {
|
"cluster": {
|
||||||
"$ref": "#/$defs/non_empty_string"
|
"$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": {
|
"temporary_wallet_enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,85 +31,17 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"enabled",
|
"enabled",
|
||||||
"backend",
|
"backend",
|
||||||
"postgres",
|
"backend_options"
|
||||||
"sqlite"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"backend": {
|
"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"
|
"$ref": "#/$defs/non_empty_string"
|
||||||
},
|
},
|
||||||
"max_connections": {
|
"backend_options": {
|
||||||
"type": "integer",
|
"type": "object"
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"wallets_directory": {
|
"wallets_directory": {
|
||||||
"$ref": "#/$defs/non_empty_string"
|
"$ref": "#/$defs/non_empty_string",
|
||||||
|
"description": "Root directory dedicated to persistent native .kswallet containers."
|
||||||
},
|
},
|
||||||
"default_profile": {
|
"default_profile": {
|
||||||
"$ref": "#/$defs/non_empty_string"
|
"$ref": "#/$defs/non_empty_string"
|
||||||
@@ -45,11 +46,21 @@
|
|||||||
"$ref": "#/$defs/non_empty_string"
|
"$ref": "#/$defs/non_empty_string"
|
||||||
},
|
},
|
||||||
"directory": {
|
"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": {
|
"cluster": {
|
||||||
"$ref": "#/$defs/non_empty_string"
|
"$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": {
|
"temporary_wallet_enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,19 +6,11 @@
|
|||||||
"database": {
|
"database": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"backend": "postgres",
|
"backend": "postgres",
|
||||||
"postgres": {
|
"backend_options": {
|
||||||
"url": "${KS_SECRET_POSTGRES_DEVNET_URL}",
|
"url": "${KS_SECRET_POSTGRES_DEVNET_URL}",
|
||||||
"max_connections": 8,
|
"max_connections": 8,
|
||||||
"connect_timeout_ms": 5000,
|
"connect_timeout_ms": 5000,
|
||||||
"auto_initialize_schema": true
|
"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": {
|
"database": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"backend": "postgres",
|
"backend": "postgres",
|
||||||
"postgres": {
|
"backend_options": {
|
||||||
"url": "${KS_SECRET_POSTGRES_MAINNET_URL}",
|
"url": "${KS_SECRET_POSTGRES_MAINNET_URL}",
|
||||||
"max_connections": 16,
|
"max_connections": 16,
|
||||||
"connect_timeout_ms": 5000,
|
"connect_timeout_ms": 5000,
|
||||||
"auto_initialize_schema": true
|
"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": {
|
"database": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"backend": "postgres",
|
"backend": "postgres",
|
||||||
"postgres": {
|
"backend_options": {
|
||||||
"url": "${KS_SECRET_POSTGRES_MAINNET_URL}",
|
"url": "${KS_SECRET_POSTGRES_MAINNET_URL}",
|
||||||
"max_connections": 16,
|
"max_connections": 16,
|
||||||
"connect_timeout_ms": 5000,
|
"connect_timeout_ms": 5000,
|
||||||
"auto_initialize_schema": true
|
"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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
"get_recent_performance_samples",
|
"get_recent_performance_samples",
|
||||||
"get_recent_prioritization_fees",
|
"get_recent_prioritization_fees",
|
||||||
"get_signature_statuses",
|
"get_signature_statuses",
|
||||||
|
"get_signatures_for_address",
|
||||||
"get_slot",
|
"get_slot",
|
||||||
"get_slot_leader",
|
"get_slot_leader",
|
||||||
"get_slot_leaders",
|
"get_slot_leaders",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"name": "local_devnet",
|
"name": "local_devnet",
|
||||||
"directory": "temporary/local_devnet",
|
"directory": "temporary/local_devnet",
|
||||||
"cluster": "devnet",
|
"cluster": "devnet",
|
||||||
|
"wallet_alias": null,
|
||||||
"temporary_wallet_enabled": true,
|
"temporary_wallet_enabled": true,
|
||||||
"temporary_wallet_alias": "local-devnet-operator",
|
"temporary_wallet_alias": "local-devnet-operator",
|
||||||
"temporary_wallet_persist": true
|
"temporary_wallet_persist": true
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
"name": "mainnet_research",
|
"name": "mainnet_research",
|
||||||
"directory": "temporary/mainnet_research",
|
"directory": "temporary/mainnet_research",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet-beta",
|
||||||
|
"wallet_alias": null,
|
||||||
"temporary_wallet_enabled": false,
|
"temporary_wallet_enabled": false,
|
||||||
"temporary_wallet_alias": "mainnet_research-disabled",
|
"temporary_wallet_alias": "mainnet_research-disabled",
|
||||||
"temporary_wallet_persist": false
|
"temporary_wallet_persist": false
|
||||||
@@ -22,6 +24,7 @@
|
|||||||
"name": "mainnet",
|
"name": "mainnet",
|
||||||
"directory": "temporary/mainnet",
|
"directory": "temporary/mainnet",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet-beta",
|
||||||
|
"wallet_alias": null,
|
||||||
"temporary_wallet_enabled": false,
|
"temporary_wallet_enabled": false,
|
||||||
"temporary_wallet_alias": "mainnet-disabled",
|
"temporary_wallet_alias": "mainnet-disabled",
|
||||||
"temporary_wallet_persist": false
|
"temporary_wallet_persist": false
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/DEVNET_EXECUTION_GUIDE.md -->
|
<!-- file: docs/DEVNET_EXECUTION_GUIDE.md -->
|
||||||
<!-- version: 26 -->
|
<!-- version: 28 -->
|
||||||
|
|
||||||
# Guide d’exécution Devnet
|
# Guide d’exécution Devnet
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ Les commandes réutilisables sont centralisées en section 4. Chaque scénario r
|
|||||||
- le wallet temporaire persistant n’est jamais copié dans les logs ou les preuves ;
|
- le wallet temporaire persistant n’est jamais copié dans les logs ou les preuves ;
|
||||||
- le financement est réalisé par un faucet Web Devnet, pas par `solana airdrop`.
|
- le financement est réalisé par un faucet Web Devnet, pas par `solana airdrop`.
|
||||||
|
|
||||||
Lorsque `database.postgres.auto_initialize_schema` vaut `true`, l’ouverture d’une fenêtre Devnet crée les tables manquantes. Lorsqu’il vaut `false`, le schéma doit être préparé avant l’ouverture.
|
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 l’option backend `auto_initialize_schema` vaut `true`, l’ouverture persistante du `Store` peut initialiser le schéma du backend actif. Lorsqu’elle vaut `false`, le modèle de stockage doit déjà être disponible.
|
||||||
|
|
||||||
## 3. Organisation des terminaux
|
## 3. Organisation des terminaux
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ Attendre avant tout scénario :
|
|||||||
- le démarrage de Vite ;
|
- le démarrage de Vite ;
|
||||||
- le lancement du binaire `kb-app-demo-desktop` ;
|
- le lancement du binaire `kb-app-demo-desktop` ;
|
||||||
- l’ouverture de la fenêtre ;
|
- l’ouverture 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`.
|
- la résolution correcte du profil `local_devnet`.
|
||||||
|
|
||||||
Ne pas exécuter les commandes `C06` ou `C07` dans ce terminal.
|
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";
|
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
|
### C02 — Capturer les versions CLI
|
||||||
|
|
||||||
```bash
|
```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.
|
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 :
|
La commande :
|
||||||
|
|
||||||
- crée silencieusement les keypairs manquants avec des permissions `0600` ;
|
- crée silencieusement les keypairs manquants avec des permissions `0600` ;
|
||||||
@@ -326,7 +330,7 @@ Un scénario n’est validé que si le replay ne produit ni échec fonctionnel n
|
|||||||
1. Dans le terminal B, exécuter `C01`, `C02`, `C03` et `C04`.
|
1. Dans le terminal B, exécuter `C01`, `C02`, `C03` et `C04`.
|
||||||
2. Dans le terminal B, exécuter `C05` pour repartir d’une base propre.
|
2. Dans le terminal B, exécuter `C05` pour repartir d’une base propre.
|
||||||
3. Dans le terminal A, exécuter `T01`.
|
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.
|
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`.
|
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.
|
7. Financer la pubkey par un faucet Web Devnet si le solde est insuffisant.
|
||||||
|
|||||||
215
docs/NATIVE_FORMAT.md
Normal file
215
docs/NATIVE_FORMAT.md
Normal 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.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/README.md -->
|
<!-- file: docs/README.md -->
|
||||||
<!-- version: 33 -->
|
<!-- version: 37 -->
|
||||||
|
|
||||||
# Documentation active de Khadhroony Bot3
|
# 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) ;
|
- [`IDL_TO_KB_LIB_NOMENCLATURE.md`](IDL_TO_KB_LIB_NOMENCLATURE.md) ;
|
||||||
- [`MISSING_PROGRAM_IDLS.md`](MISSING_PROGRAM_IDLS.md) ;
|
- [`MISSING_PROGRAM_IDLS.md`](MISSING_PROGRAM_IDLS.md) ;
|
||||||
- [`OPERATION_NAMING_CONVENTION.md`](OPERATION_NAMING_CONVENTION.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 d’import/export wallet Solana et décisions d’adaptation ;
|
||||||
- [`IDEA_REMINDERS.md`](IDEA_REMINDERS.md).
|
- [`IDEA_REMINDERS.md`](IDEA_REMINDERS.md).
|
||||||
|
|
||||||
Les idées ne rejoignent un `TODO.md` qu’après confirmation, attribution et reformulation en tâche vérifiable.
|
Les idées ne rejoignent un `TODO.md` qu’aprè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 d’intégr
|
|||||||
|
|
||||||
## 7. Documentation par crate
|
## 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-core`](../ks-core/README.md) ;
|
||||||
- [`ks-config`](../ks-config/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-onchain-transport`](../ks-onchain-transport/README.md) ;
|
||||||
- [`ks-store`](../ks-store/README.md) ;
|
- [`ks-store`](../ks-store/README.md) ;
|
||||||
- [`ks-wallet`](../ks-wallet/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).
|
- [`kb-app-demo-desktop`](../kb-app-demo-desktop/README.md).
|
||||||
|
|
||||||
## 8. Guides
|
## 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) ;
|
- [RPC, backfill et WebSocket](guides/RPC_BACKFILL_AND_WEBSOCKET.md) ;
|
||||||
- [Extraction Core, replay et matérialisation](guides/REPLAY_CORE_EXTRACTION_AND_MATERIALIZATION.md) ;
|
- [Extraction Core, replay et matérialisation](guides/REPLAY_CORE_EXTRACTION_AND_MATERIALIZATION.md) ;
|
||||||
- [PostgreSQL et stockage](guides/POSTGRES_STORAGE.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
|
## 9. Rapports de validation actifs
|
||||||
|
|
||||||
- [Validation Metaplex Token Metadata 0.4.7](validation/V0_4_7_METAPLEX_TOKEN_METADATA_VALIDATION_REPORT.md) ;
|
- [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 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 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/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).
|
- [`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
|
## 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` n’est 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
|
## 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).
|
||||||
|
|||||||
116
docs/WALLET_FORMAT_COMPATIBILITY.md
Normal file
116
docs/WALLET_FORMAT_COMPATIBILITY.md
Normal 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`.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/CRATE_MAP.md -->
|
<!-- file: docs/architecture/CRATE_MAP.md -->
|
||||||
<!-- version: 7 -->
|
<!-- version: 8 -->
|
||||||
|
|
||||||
# Carte des crates
|
# Carte des crates
|
||||||
|
|
||||||
@@ -17,9 +17,10 @@
|
|||||||
| `ks-onchain-transport` | bibliothèque | transports RPC HTTP/WebSocket et pools d’endpoints | README/TODO/USAGE/CHANGELOG présents |
|
| `ks-onchain-transport` | bibliothèque | transports RPC HTTP/WebSocket et pools d’endpoints | README/TODO/USAGE/CHANGELOG présents |
|
||||||
| `ks-store` | bibliothèque | contrats de stockage et adaptateur PostgreSQL | nom `ks-*` actif ; normalisation `0.5.3` |
|
| `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` | 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` |
|
| `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 qu’il 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 qu’il appartient au domaine applicatif Bot. Voir [`../decisions/KHADHROONY_SOLANA_NAMESPACE_POLICY.md`](../decisions/KHADHROONY_SOLANA_NAMESPACE_POLICY.md).
|
||||||
|
|
||||||
## 2. Consolidations principales depuis bot2
|
## 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 responsabilités de pipeline ont été réunies dans `ks-pipeline` ;
|
||||||
- les transports Solana sont réunis dans `ks-onchain-transport` ;
|
- les transports Solana sont réunis dans `ks-onchain-transport` ;
|
||||||
- l’application et sa bibliothèque sont réunies dans `kb-app-demo-desktop` ;
|
- l’application 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 n’est 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 l’audit d’alignement.
|
Cette carte n’est 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 l’audit d’alignement.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/STORAGE_ARCHITECTURE.md -->
|
<!-- file: docs/architecture/STORAGE_ARCHITECTURE.md -->
|
||||||
<!-- version: 5 -->
|
<!-- version: 9 -->
|
||||||
|
|
||||||
# Architecture du stockage
|
# Architecture du stockage
|
||||||
|
|
||||||
@@ -7,49 +7,172 @@
|
|||||||
|
|
||||||
`ks-store` réunit :
|
`ks-store` réunit :
|
||||||
|
|
||||||
|
- la façade publique backend-agnostique `Store` et ses options d'ouverture ;
|
||||||
- les contrats store-neutral ;
|
- les contrats store-neutral ;
|
||||||
- les DTO et entités persistées ;
|
- les DTO et entités persistés ;
|
||||||
- la pagination et les rapports de santé ;
|
- la pagination, le replay et les rapports de santé ;
|
||||||
- les traits de repositories ;
|
- les traits de repositories ;
|
||||||
- l’implémentation PostgreSQL ;
|
- les résumés runtime/init sanitisés ;
|
||||||
- les migrations, requêtes et mécanismes de replay associés.
|
- l'implémentation PostgreSQL privée ;
|
||||||
|
- les ressources de schéma, maintenance et requêtes appartenant au backend actif.
|
||||||
|
|
||||||
La consolidation remplace l’ancien 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. Frontières
|
||||||
|
|
||||||
### 2.1 Contrats store-neutral
|
### 2.1 Contrats store-neutral
|
||||||
|
|
||||||
Les contrats ne doivent pas dépendre des détails SQL lorsqu’une 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
|
### 2.2 Adaptateur PostgreSQL
|
||||||
|
|
||||||
Le module PostgreSQL possède :
|
Le module PostgreSQL possède :
|
||||||
|
|
||||||
- la connexion et l’initialisation ;
|
- la validation de ses options ;
|
||||||
- l’application idempotente des migrations ;
|
- la connexion et l'initialisation ;
|
||||||
|
- l'orchestrateur transactionnel global du baseline ;
|
||||||
|
- les ressources SQL `include_str!` ;
|
||||||
- les requêtes typées ;
|
- les requêtes typées ;
|
||||||
- les diagnostics ;
|
- les diagnostics physiques ;
|
||||||
- les opérations de replay et de sélection de candidats.
|
- 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
|
### 2.3 Modèles partagés
|
||||||
|
|
||||||
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é.
|
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 ;
|
- **N1** — acquisition et raw canonique ;
|
||||||
- transactions et instructions canoniques ;
|
- **N2** — Core Solana canonique ;
|
||||||
- événements de décodage et diagnostics ;
|
- **N3** — decode/materialization générique et journaux versionnés ;
|
||||||
- matérialisations ;
|
- **N4** — projections spécialisées/queryables additives.
|
||||||
- états de campagne et candidats de replay ;
|
|
||||||
- informations opérationnelles et de santé.
|
|
||||||
|
|
||||||
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 d’elles.
|
||||||
|
|
||||||
|
### 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` n’appartient au contrat durable.
|
||||||
|
|
||||||
|
### 7.2 Invalidation descendante
|
||||||
|
|
||||||
|
Un remplacement N2 Core d’une 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 l’identité 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 d’un 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 ;
|
- initialisation idempotente ;
|
||||||
- pagination bornée ;
|
- 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 ;
|
- absence de double effet lors des replays ;
|
||||||
- validation stricte des entrées ;
|
- validation stricte des entrées ;
|
||||||
- erreurs explicites ;
|
- 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 l’arrivée des protocoles trading. Elle doit notamment distinguer `slot`, `block_time` et les timestamps d’acquisition/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 d’idempotence.
|
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 n’est donc pas nécessaire de conserver indéfiniment l’ancien 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/` lorsqu'elles 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.
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/decisions/KHADHROONY_SOLANA_NAMESPACE_POLICY.md -->
|
<!-- file: docs/decisions/KHADHROONY_SOLANA_NAMESPACE_POLICY.md -->
|
||||||
<!-- version: 8 -->
|
<!-- version: 9 -->
|
||||||
|
|
||||||
# Politique de namespace Khadhroony Solana
|
# 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`.
|
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.
|
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`.
|
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
|
```text
|
||||||
k_sol_*
|
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.
|
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
|
## 8. Ordre des migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/guides/CONFIGURATION.md -->
|
<!-- file: docs/guides/CONFIGURATION.md -->
|
||||||
<!-- version: 10 -->
|
<!-- version: 11 -->
|
||||||
|
|
||||||
# Guide de configuration
|
# 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
|
## 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 qu’aucun wallet persistant n’est 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 :
|
Les autorisations :
|
||||||
|
|
||||||
|
|||||||
@@ -1,69 +1,163 @@
|
|||||||
<!-- file: docs/guides/POSTGRES_STORAGE.md -->
|
<!-- file: docs/guides/POSTGRES_STORAGE.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 7 -->
|
||||||
|
|
||||||
# Guide PostgreSQL et contrats de stockage
|
# Guide PostgreSQL et contrats de stockage
|
||||||
|
|
||||||
## Objectif
|
## 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
|
## 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
|
```rust
|
||||||
let options = match ks_store::PostgresStoreOptions::new(
|
let options = match ks_store::StoreOpenOptions::new(
|
||||||
database_url,
|
|
||||||
10,
|
|
||||||
10_000,
|
|
||||||
true,
|
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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
|
let store = match ks_store::Store::open(options).await {
|
||||||
let store = match ks_store::PostgresStore::connect(options).await {
|
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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
|
## Domaines
|
||||||
|
|
||||||
- raw : acquisitions et observations ;
|
- N1 raw/acquisition : transactions canoniques, observations de transactions et observations génériques de comptes ;
|
||||||
- Core : transactions, instructions, comptes et contexte normalisés ;
|
- N2 Core : transactions, clés, top-level, CPI, logs, variations de balances, return data et états de comptes ;
|
||||||
- decode : ledger, observations décodées et matérialisations ;
|
- N3/transverse : ledger, observations décodées, couverture et `k_sol_mat_outputs` ;
|
||||||
- replay : candidats et résumés bornés.
|
- 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 d’un remplacement de signature ;
|
||||||
|
- invalidation decode -> matérialisation pour l’identité 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 l’ordre de replay CPI et les recherches d’invalidation/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
|
## 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
|
## Repositories
|
||||||
|
|
||||||
Les traits publics séparent le contrat de l’implé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
|
## Diagnostics
|
||||||
|
|
||||||
|
La surface publique expose :
|
||||||
|
|
||||||
- health snapshot ;
|
- health snapshot ;
|
||||||
- migration snapshot ;
|
- schema/migration snapshot générique ;
|
||||||
- backend diagnostics ;
|
- backend descriptor masqué ;
|
||||||
- diagnostics des tables raw, Core et decode ;
|
- résumé de configuration sanitisé ;
|
||||||
- validation des noms de tables.
|
- 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
|
## Invariants
|
||||||
|
|
||||||
- aucune donnée canonique ne doit être dupliquée sans justification ;
|
- aucune donnée canonique ne doit être dupliquée sans justification ;
|
||||||
- les écritures rejouables doivent être idempotentes ;
|
- les écritures rejouables doivent être idempotentes ;
|
||||||
- la progression de campagne doit rester cohérente avec les lignes effectivement traitées ;
|
- la progression de campagne doit rester cohérente avec les lignes effectivement traitées ;
|
||||||
- les requêtes dynamiques n’acceptent que des identifiants validés ;
|
- les requêtes dynamiques n'acceptent que des identifiants validés ;
|
||||||
- les erreurs PostgreSQL restent distinctes des erreurs de contrat.
|
- 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
|
## Références
|
||||||
|
|
||||||
|
|||||||
324
docs/guides/WALLETS.md
Normal file
324
docs/guides/WALLETS.md
Normal 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)
|
||||||
1686
docs/plans/V0_5_3_KS_STORE_NORMALIZATION_PLAN.md
Normal file
1686
docs/plans/V0_5_3_KS_STORE_NORMALIZATION_PLAN.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/rules/RULES_RUST.md -->
|
<!-- file: docs/rules/RULES_RUST.md -->
|
||||||
<!-- version: 8 -->
|
<!-- version: 9 -->
|
||||||
|
|
||||||
# Règles Rust générales
|
# Règles Rust générales
|
||||||
|
|
||||||
@@ -101,6 +101,7 @@ implicit_saturating_sub = "allow"
|
|||||||
## Visibilité et API de crate
|
## 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 d’entrée de la 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 d’entré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` 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.
|
- 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.
|
- Les chemins internes de modules ne font pas partie de l'API stable.
|
||||||
|
|||||||
185
docs/validation/V0_5_2_WALLET_VALIDATION_REPORT.md
Normal file
185
docs/validation/V0_5_2_WALLET_VALIDATION_REPORT.md
Normal 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.**
|
||||||
@@ -1,8 +1,114 @@
|
|||||||
<!-- file: kb-app-demo-desktop/CHANGELOG.md -->
|
<!-- file: kb-app-demo-desktop/CHANGELOG.md -->
|
||||||
<!-- version: 52 -->
|
<!-- version: 71 -->
|
||||||
|
|
||||||
# CHANGELOG — kb-app-demo-desktop
|
# 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é d’une absence de requête ;
|
||||||
|
- corrige la préparation automatique des profils Execution Devnet afin qu’elle utilise elle aussi le `ks_store::Store` partagé par profil : l’ouverture d’une 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 qu’un 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 d’objets 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 l’UI 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 d’un 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 d’export `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 l’explorateur on-chain ; le profil actif reste la valeur initiale mais Devnet/Mainnet peuvent être interrogés sans redémarrer l’application 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 à l’historique, tandis que le solde garde son adaptateur typé ;
|
||||||
|
- conserve la gestion locale des `.kswallet` sur le profil applicatif actif : la sélection RPC de l’explorateur 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 l’adresse et détail `getTransaction` via le JsonViewer commun ;
|
||||||
|
- étend le rôle `http_queries` Devnet avec `get_signatures_for_address` afin que l’explorateur fonctionne sur `local_devnet` comme sur les profils Mainnet déjà dotés des rôles d’historique ;
|
||||||
|
- 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`
|
## `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 l’URL des snapshots backend et supprime `endpointUrl` des payloads d’exé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.
|
- Fix: remplace les snapshots `ks-onchain-transport` retournés directement par les commandes Tauri HTTP/WS par des DTO desktop sanitisés, fait supprimer l’URL des snapshots backend et supprime `endpointUrl` des payloads d’exé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.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: kb-app-demo-desktop/README.md -->
|
<!-- file: kb-app-demo-desktop/README.md -->
|
||||||
<!-- version: 15 -->
|
<!-- version: 24 -->
|
||||||
|
|
||||||
# kb-app-demo-desktop
|
# kb-app-demo-desktop
|
||||||
|
|
||||||
@@ -8,8 +8,9 @@
|
|||||||
## Fonctionnalités
|
## Fonctionnalités
|
||||||
|
|
||||||
- configuration et diagnostics de logging ;
|
- configuration et diagnostics de logging ;
|
||||||
|
- fenêtre Wallets dédiée : inventaire sûr des identités `.kswallet`, création d’un 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 d’un `.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 ;
|
- 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 ;
|
- 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 ;
|
- 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 ;
|
- 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`.
|
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. Lorsqu’une 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.
|
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é.
|
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é.
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<!-- file: kb-app-demo-desktop/TODO.md -->
|
<!-- file: kb-app-demo-desktop/TODO.md -->
|
||||||
<!-- version: 16 -->
|
<!-- version: 23 -->
|
||||||
|
|
||||||
# TODO — kb-app-demo-desktop
|
# TODO — kb-app-demo-desktop
|
||||||
|
|
||||||
## Évolutions générales
|
## É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 lorsqu’elle 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`.
|
- [ ] `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.
|
- [ ] 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.
|
- [ ] Observabilité — afficher explicitement les comptes relus et les projections matérialisées.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: kb-app-demo-desktop/USAGE.md -->
|
<!-- file: kb-app-demo-desktop/USAGE.md -->
|
||||||
<!-- version: 15 -->
|
<!-- version: 25 -->
|
||||||
|
|
||||||
# Utilisation de kb-app-demo-desktop
|
# Utilisation de kb-app-demo-desktop
|
||||||
|
|
||||||
@@ -7,6 +7,71 @@
|
|||||||
|
|
||||||
L’application expose une interface opérateur pour diagnostiquer, simuler et valider les couches de bot3.
|
L’application 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 d’une adresse publique. Les DTO frontend ne contiennent jamais de matériau secret. Aucun password, chemin interne du store, sel, nonce, ciphertext ou byte privé n’est 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 d’un 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 d’un `.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é. L’UI ne reçoit qu’un booléen indiquant si ce secret backend est configuré.
|
||||||
|
|
||||||
|
La création demande uniquement l’alias. 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 d’utiliser un autre mot de passe pour de nouveaux wallets ; ce secret de démonstration n’est 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é
|
||||||
|
|
||||||
|
L’explorateur on-chain ne déverrouille aucun wallet : seule la pubkey publique est nécessaire. Le bouton `Explorer` d’un wallet inventorié remplit l’adresse 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 l’application :
|
||||||
|
|
||||||
|
- `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 l’adresse ;
|
||||||
|
- `getTransaction` à la demande pour afficher le détail JSON d’une signature.
|
||||||
|
|
||||||
|
La liste est donc intitulée **transactions impliquant l’adresse** : 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 l’explorateur 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
|
## Lancer le desktop
|
||||||
|
|
||||||
Depuis la racine du workspace :
|
Depuis la racine du workspace :
|
||||||
@@ -51,14 +116,23 @@ Les autres fenêtres sont ouvertes par les commandes Tauri dédiées :
|
|||||||
- backfill ;
|
- backfill ;
|
||||||
- HTTP ;
|
- HTTP ;
|
||||||
- WebSocket ;
|
- WebSocket ;
|
||||||
- diagnostics SQL ;
|
- diagnostics store ;
|
||||||
- candidats replay ;
|
- candidats replay ;
|
||||||
- configuration ;
|
- configuration ;
|
||||||
|
- wallets ;
|
||||||
- extraction Core ;
|
- extraction Core ;
|
||||||
- decode replay ;
|
- decode replay ;
|
||||||
- exécution Solana Core ;
|
- exécution Solana Core ;
|
||||||
- exécution SPL.
|
- exécution SPL.
|
||||||
|
|
||||||
|
## Pagination des listings Store
|
||||||
|
|
||||||
|
Les listings de consultation Store n’exposent plus de champ `limit` éditable lorsqu’ils 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 n’est jamais saisi par l’opérateur. Modifier un filtre serveur invalide les curseurs mémorisés et impose de recharger le premier bloc. Lorsqu’une 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 s’applique 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
|
## Interface Tauri publique
|
||||||
|
|
||||||
Les commandes Tauri constituent l’interface applicative frontend/backend. Elles ne sont pas des APIs Rust publiques pour les autres crates.
|
Les commandes Tauri constituent l’interface applicative frontend/backend. Elles ne sont pas des APIs Rust publiques pour les autres crates.
|
||||||
@@ -68,6 +142,7 @@ Les commandes Tauri constituent l’interface applicative frontend/backend. Elle
|
|||||||
```typescript
|
```typescript
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
await invoke("open_demo_wallet_window");
|
||||||
await invoke("open_demo_http_window");
|
await invoke("open_demo_http_window");
|
||||||
await invoke("open_demo_ws_window");
|
await invoke("open_demo_ws_window");
|
||||||
await invoke("open_demo_decode_replay_window");
|
await invoke("open_demo_decode_replay_window");
|
||||||
@@ -244,22 +319,24 @@ L’inventaire 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 d’exécution reste sous la grille, sur toute la largeur.
|
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 d’exécution reste sous la grille, sur toute la largeur.
|
||||||
|
|
||||||
### Diagnostics PostgreSQL
|
### Diagnostics du store
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const diagnostics = await invoke(
|
const diagnostics = await invoke(
|
||||||
"load_demo_sql_diag"
|
"load_demo_store_diag"
|
||||||
);
|
);
|
||||||
const rawTables = await invoke(
|
const rawTables = await invoke(
|
||||||
"load_demo_sql_pg_raw"
|
"load_demo_store_raw"
|
||||||
);
|
);
|
||||||
const coreTables = await invoke(
|
const coreTables = await invoke(
|
||||||
"load_demo_sql_pg_core"
|
"load_demo_store_core"
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(diagnostics, rawTables, coreTables);
|
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
|
## 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.
|
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 de System Transfer et Memo ;
|
||||||
- tests ATA, SPL Token et Token-2022 ;
|
- tests ATA, SPL Token et Token-2022 ;
|
||||||
- tests de backfill, extraction Core et decode replay ;
|
- 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 ;
|
- tests de journalisation frontend ;
|
||||||
- test du verrou d’état applicatif et de la séquence splash.
|
- 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 choisir un scénario synthétique ou un contrat de campagne Devnet ;
|
||||||
- de sélectionner le profil 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 ;
|
||||||
- d’afficher les contrats, contrôles de préflight et preuves attendues dans le JSON viewer commun ;
|
- d’afficher 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.
|
- 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
|
### 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 ;
|
- la fixture et son setup ;
|
||||||
- les exécutions confirmées et les probes indisponibles dans leur ordre réel ;
|
- 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
|
### Campagne Token-2022 Token Metadata
|
||||||
|
|
||||||
Ouvrir l’accordé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 l’accordé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
|
```text
|
||||||
Initialize -> UpdateField -> Emit -> RemoveKey -> UpdateAuthority
|
Initialize -> UpdateField -> Emit -> RemoveKey -> UpdateAuthority
|
||||||
|
|||||||
@@ -6,15 +6,16 @@
|
|||||||
"splash",
|
"splash",
|
||||||
"main",
|
"main",
|
||||||
"demo_config",
|
"demo_config",
|
||||||
|
"demo_wallet",
|
||||||
"demo_http",
|
"demo_http",
|
||||||
"demo_ws",
|
"demo_ws",
|
||||||
"demo_backfill",
|
"demo_backfill",
|
||||||
"demo_core_extraction",
|
"demo_core_extraction",
|
||||||
"demo_decode_replay",
|
"demo_decode_replay",
|
||||||
"demo_sql_diag",
|
"demo_store_diag",
|
||||||
"demo_sql_pg_raw",
|
"demo_store_raw",
|
||||||
"demo_sql_pg_core",
|
"demo_store_core",
|
||||||
"demo_sql_replay_candidates",
|
"demo_store_replay_candidates",
|
||||||
"demo_execution_solana_core",
|
"demo_execution_solana_core",
|
||||||
"demo_execution_spl",
|
"demo_execution_spl",
|
||||||
"demo_execution_metadata"
|
"demo_execution_metadata"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: kb-app-demo-desktop/frontend/demo_core_extraction.html -->
|
<!-- file: kb-app-demo-desktop/frontend/demo_core_extraction.html -->
|
||||||
<!-- version: 10 -->
|
<!-- version: 11 -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<div class="card shadow-sm border-0">
|
<div class="card shadow-sm border-0">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h1 class="h3 card-title">Extraction transactionnelle canonical vers core</h1>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,4 +163,4 @@
|
|||||||
<script defer="" src="ts/demo_core_extraction.ts" type="module"></script>
|
<script defer="" src="ts/demo_core_extraction.ts" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: kb-app-demo-desktop/frontend/demo_decode_replay.html -->
|
<!-- file: kb-app-demo-desktop/frontend/demo_decode_replay.html -->
|
||||||
<!-- version: 8 -->
|
<!-- version: 10 -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-6">
|
<div class="col-12 col-lg-6">
|
||||||
<label class="form-label" for="decodeReplaySignaturesTextarea">Signatures explicites, une par ligne</label>
|
<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>
|
||||||
<div class="col-12 col-lg-6">
|
<div class="col-12 col-lg-6">
|
||||||
<label class="form-label" for="decodeReplayPathsTextarea">Instruction paths exacts, un par ligne</label>
|
<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>
|
<label class="form-label" for="annotationSignatureInput">Journal des annotations — signature contient</label>
|
||||||
<input id="annotationSignatureInput" class="form-control font-monospace" placeholder="Filtre facultatif" />
|
<input id="annotationSignatureInput" class="form-control font-monospace" placeholder="Filtre facultatif" />
|
||||||
</div>
|
</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>
|
<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 d’annotations">
|
||||||
|
<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>
|
</div>
|
||||||
<p id="annotationJournalStatus" class="text-body-secondary mb-3">Annotations non chargées.</p>
|
<p id="annotationJournalStatus" class="text-body-secondary mb-3">Annotations non chargées.</p>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: kb-app-demo-desktop/frontend/demo_execution_spl.html -->
|
<!-- file: kb-app-demo-desktop/frontend/demo_execution_spl.html -->
|
||||||
<!-- version: 15 -->
|
<!-- version: 16 -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
|
|
||||||
@@ -306,7 +306,12 @@
|
|||||||
<h2 class="h5 mb-0">Journal métier SPL Token</h2>
|
<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 class="form-text">Événements commités uniquement, sans solde final reconstruit ni OHLC.</div>
|
||||||
</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>
|
||||||
<div class="row g-2 mb-3">
|
<div class="row g-2 mb-3">
|
||||||
<div class="col-12 col-lg-6">
|
<div class="col-12 col-lg-6">
|
||||||
@@ -330,15 +335,15 @@
|
|||||||
<option value="risk">risk</option>
|
<option value="risk">risk</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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">
|
<div class="col-12">
|
||||||
<label class="form-label" for="tokenJournalOperationInput">Opération exacte</label>
|
<label class="form-label" for="tokenJournalOperationInput">Opération exacte</label>
|
||||||
<input class="form-control font-monospace" id="tokenJournalOperationInput" placeholder="transfer_checked" type="text" />
|
<input class="form-control font-monospace" id="tokenJournalOperationInput" placeholder="transfer_checked" type="text" />
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm table-hover align-middle">
|
<table class="table table-sm table-hover align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -375,7 +380,12 @@
|
|||||||
<h2 class="h5 mb-0">Journal lifecycle ATA</h2>
|
<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 class="form-text">Création, réutilisation idempotente et récupération nested, sans duplication des CPI Token.</div>
|
||||||
</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>
|
||||||
<div class="row g-2 mb-3">
|
<div class="row g-2 mb-3">
|
||||||
<div class="col-12 col-lg-6">
|
<div class="col-12 col-lg-6">
|
||||||
@@ -390,14 +400,14 @@
|
|||||||
<label class="form-label" for="ataJournalAddressInput">ATA exact</label>
|
<label class="form-label" for="ataJournalAddressInput">ATA exact</label>
|
||||||
<input class="form-control font-monospace" id="ataJournalAddressInput" type="text" />
|
<input class="form-control font-monospace" id="ataJournalAddressInput" type="text" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-8 col-lg-4">
|
<div class="col-12 col-lg-6">
|
||||||
<label class="form-label" for="ataJournalOperationInput">Opération</label>
|
<label class="form-label" for="ataJournalOperationInput">Opération</label>
|
||||||
<input class="form-control font-monospace" id="ataJournalOperationInput" type="text" />
|
<input class="form-control font-monospace" id="ataJournalOperationInput" type="text" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4 col-lg-2">
|
</div>
|
||||||
<label class="form-label" for="ataJournalLimitInput">Limite</label>
|
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
|
||||||
<input class="form-control" id="ataJournalLimitInput" max="500" min="1" type="number" value="100" />
|
<span class="badge text-bg-light border" id="ataJournalBlockSummary">Bloc 0 / 0 · 0 résultat · 500/bloc</span>
|
||||||
</div>
|
<span class="form-text">Le total de blocs est recalculé à chaque chargement ou navigation.</span>
|
||||||
</div>
|
</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>
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: kb-app-demo-desktop/frontend/demo_sql_pg_core.html -->
|
<!-- file: kb-app-demo-desktop/frontend/demo_store_core.html -->
|
||||||
<!-- version: 6 -->
|
<!-- version: 9 -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
<link rel="icon" href="imgs/logo.png">
|
<link rel="icon" href="imgs/logo.png">
|
||||||
<link rel="stylesheet" href="sass/main.scss">
|
<link rel="stylesheet" href="sass/main.scss">
|
||||||
<title>Khadhroony Bot3 — PostgreSQL core store</title>
|
<title>Khadhroony Bot3 — Core store diagnostics</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<div class="container-fluid px-4 my-0">
|
<div class="container-fluid px-4 my-0">
|
||||||
<div class="navbar-brand d-flex align-items-center">
|
<div class="navbar-brand d-flex align-items-center">
|
||||||
<img alt="Logo" src="imgs/logo.png" class="app-logo">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -32,15 +32,15 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
|
<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>
|
<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>
|
</div>
|
||||||
<dl class="row mb-0">
|
<dl class="row mb-0">
|
||||||
<dt class="col-sm-3">Statut</dt>
|
<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>
|
<dt class="col-sm-3">Profil</dt>
|
||||||
<dd class="col-sm-9"><code id="sqlCoreProfile">—</code></dd>
|
<dd class="col-sm-9"><code id="storeCoreProfile">—</code></dd>
|
||||||
<dt class="col-sm-3">DSN</dt>
|
<dt class="col-sm-3">Connexion</dt>
|
||||||
<dd class="col-sm-9"><code id="sqlCoreDsn">—</code></dd>
|
<dd class="col-sm-9"><code id="storeCoreConnection">—</code></dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,13 +49,13 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card shadow-sm border-0">
|
<div class="card shadow-sm border-0">
|
||||||
<div class="card-body">
|
<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">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Table</th>
|
<th>Ressource</th>
|
||||||
<th>Domaine</th>
|
<th>Modèle</th>
|
||||||
<th>Rôle</th>
|
<th>Rôle</th>
|
||||||
<th>Existe</th>
|
<th>Existe</th>
|
||||||
<th>Lignes</th>
|
<th>Lignes</th>
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
<th>Dernière insertion</th>
|
<th>Dernière insertion</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="sqlCoreTableBody"></tbody>
|
<tbody id="storeCoreTableBody"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
<div class="card shadow-sm border-0">
|
<div class="card shadow-sm border-0">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="h5 card-title mb-3">Payload JSON</h2>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</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>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: kb-app-demo-desktop/frontend/demo_sql_diag.html -->
|
<!-- file: kb-app-demo-desktop/frontend/demo_store_diag.html -->
|
||||||
<!-- version: 5 -->
|
<!-- version: 8 -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
<link rel="icon" href="imgs/logo.png">
|
<link rel="icon" href="imgs/logo.png">
|
||||||
<link rel="stylesheet" href="sass/main.scss">
|
<link rel="stylesheet" href="sass/main.scss">
|
||||||
<title>Khadhroony Bot3 — SQL diagnostics</title>
|
<title>Khadhroony Bot3 — Store diagnostics</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<div class="container-fluid px-4 my-0">
|
<div class="container-fluid px-4 my-0">
|
||||||
<div class="navbar-brand d-flex align-items-center">
|
<div class="navbar-brand d-flex align-items-center">
|
||||||
<img alt="Logo" src="imgs/logo.png" class="app-logo">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -31,29 +31,29 @@
|
|||||||
<div class="card shadow-sm border-0">
|
<div class="card shadow-sm border-0">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
|
<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">
|
<div class="d-flex gap-2">
|
||||||
<button id="refreshSqlDiagButton" class="btn btn-primary" type="button">Rafraîchir</button>
|
<button id="refreshStoreDiagButton" class="btn btn-primary" type="button">Rafraîchir</button>
|
||||||
<button id="copySqlDiagButton" class="btn btn-outline-secondary" type="button">Copier JSON</button>
|
<button id="copyStoreDiagButton" class="btn btn-outline-secondary" type="button">Copier JSON</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<dl class="row mb-0">
|
<dl class="row mb-0">
|
||||||
<dt class="col-sm-3">Statut</dt>
|
<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>
|
<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>
|
<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>
|
<dt class="col-sm-3">Backend</dt>
|
||||||
<dd class="col-sm-9"><code id="sqlDiagBackend">—</code></dd>
|
<dd class="col-sm-9"><code id="storeDiagBackend">—</code></dd>
|
||||||
<dt class="col-sm-3">DSN</dt>
|
<dt class="col-sm-3">Connexion</dt>
|
||||||
<dd class="col-sm-9"><code id="sqlDiagDsn">—</code></dd>
|
<dd class="col-sm-9"><code id="storeDiagConnection">—</code></dd>
|
||||||
<dt class="col-sm-3">Schema courant</dt>
|
<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>
|
<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>
|
<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>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,13 +62,13 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card shadow-sm border-0">
|
<div class="card shadow-sm border-0">
|
||||||
<div class="card-body">
|
<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">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Table</th>
|
<th>Ressource</th>
|
||||||
<th>Domaine</th>
|
<th>Modèle</th>
|
||||||
<th>Rôle</th>
|
<th>Rôle</th>
|
||||||
<th>Existe</th>
|
<th>Existe</th>
|
||||||
<th>Lignes</th>
|
<th>Lignes</th>
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
<th>Dernière insertion</th>
|
<th>Dernière insertion</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="sqlDiagTableBody"></tbody>
|
<tbody id="storeDiagTableBody"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
<div class="card shadow-sm border-0">
|
<div class="card shadow-sm border-0">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="h5 card-title mb-3">Payload JSON</h2>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script type="module" src="ts/demo_sql_diag.ts" defer></script>
|
<script type="module" src="ts/demo_store_diag.ts" defer></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: kb-app-demo-desktop/frontend/demo_sql_pg_raw.html -->
|
<!-- file: kb-app-demo-desktop/frontend/demo_store_raw.html -->
|
||||||
<!-- version: 7 -->
|
<!-- version: 10 -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
<link rel="icon" href="imgs/logo.png">
|
<link rel="icon" href="imgs/logo.png">
|
||||||
<link rel="stylesheet" href="sass/main.scss">
|
<link rel="stylesheet" href="sass/main.scss">
|
||||||
<title>Khadhroony Bot3 — PostgreSQL canonical acquisition</title>
|
<title>Khadhroony Bot3 — Raw store diagnostics</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<div class="container-fluid px-4 my-0">
|
<div class="container-fluid px-4 my-0">
|
||||||
<div class="navbar-brand d-flex align-items-center">
|
<div class="navbar-brand d-flex align-items-center">
|
||||||
<img alt="Logo" src="imgs/logo.png" class="app-logo">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -32,15 +32,15 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
|
<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>
|
<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>
|
</div>
|
||||||
<dl class="row mb-0">
|
<dl class="row mb-0">
|
||||||
<dt class="col-sm-3">Statut</dt>
|
<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>
|
<dt class="col-sm-3">Profil</dt>
|
||||||
<dd class="col-sm-9"><code id="sqlRawProfile">—</code></dd>
|
<dd class="col-sm-9"><code id="storeRawProfile">—</code></dd>
|
||||||
<dt class="col-sm-3">DSN</dt>
|
<dt class="col-sm-3">Connexion</dt>
|
||||||
<dd class="col-sm-9"><code id="sqlRawDsn">—</code></dd>
|
<dd class="col-sm-9"><code id="storeRawConnection">—</code></dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,11 +51,11 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="h5 card-title mb-3">Transactions canoniques et observations</h2>
|
<h2 class="h5 card-title mb-3">Transactions canoniques et observations</h2>
|
||||||
<div class="table-responsive app-table-full-width">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Table</th>
|
<th>Ressource</th>
|
||||||
<th>Domaine</th>
|
<th>Modèle</th>
|
||||||
<th>Rôle</th>
|
<th>Rôle</th>
|
||||||
<th>Existe</th>
|
<th>Existe</th>
|
||||||
<th>Lignes</th>
|
<th>Lignes</th>
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
<th>Dernière insertion</th>
|
<th>Dernière insertion</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="sqlRawTableBody"></tbody>
|
<tbody id="storeRawTableBody"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
<div class="card shadow-sm border-0">
|
<div class="card shadow-sm border-0">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="h5 card-title mb-3">Payload JSON</h2>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</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>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: kb-app-demo-desktop/frontend/demo_sql_replay_candidates.html -->
|
<!-- file: kb-app-demo-desktop/frontend/demo_store_replay_candidates.html -->
|
||||||
<!-- version: 11 -->
|
<!-- version: 18 -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
<link rel="icon" href="imgs/logo.png">
|
<link rel="icon" href="imgs/logo.png">
|
||||||
<link rel="stylesheet" href="sass/main.scss">
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<div class="container-fluid px-4 my-0">
|
<div class="container-fluid px-4 my-0">
|
||||||
<div class="navbar-brand d-flex align-items-center">
|
<div class="navbar-brand d-flex align-items-center">
|
||||||
<img alt="Logo" src="imgs/logo.png" class="app-logo">
|
<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>
|
||||||
<div class="ms-auto d-flex align-items-center gap-2">
|
<div class="ms-auto d-flex align-items-center gap-2">
|
||||||
<span id="replayCandidateProfileBadge" class="badge text-bg-info">Profil —</span>
|
<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 shadow-sm border-0 mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h1 class="h3 card-title">Exploration read-only des candidats</h1>
|
<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 l’export 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 d’import, 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 l’export 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 d’import, car le format CSV ne décrit pas formellement son encodage ni son séparateur.</p>
|
||||||
<div class="small text-body-secondary">
|
<div class="small text-body-secondary">
|
||||||
<span class="fw-semibold">DSN :</span>
|
<span class="fw-semibold">Connexion :</span>
|
||||||
<code id="replayCandidateDsn">—</code>
|
<code id="replayCandidateDsn">—</code>
|
||||||
<span class="ms-3 fw-semibold">Limite serveur maximale :</span>
|
<span class="ms-3 fw-semibold">Taille fixe d’un bloc Store :</span>
|
||||||
<code id="replayCandidateMaximumLimit">—</code>
|
<code id="replayCandidateBlockSize">—</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
<section class="tab-pane fade show active" id="transactionsTabPane" role="tabpanel" aria-labelledby="transactionsTabButton" tabindex="0">
|
<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 shadow-sm border-0 mb-4">
|
||||||
<div class="card-body">
|
<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="row g-3 align-items-end">
|
||||||
<div class="col-12 col-xl-4">
|
<div class="col-12 col-xl-4">
|
||||||
<label for="transactionSignatureContainsInput" class="form-label">Signature contient</label>
|
<label for="transactionSignatureContainsInput" class="form-label">Signature contient</label>
|
||||||
@@ -110,8 +110,8 @@
|
|||||||
<div class="col-6 col-md-3 col-xl-2">
|
<div class="col-6 col-md-3 col-xl-2">
|
||||||
<label for="transactionProgramScopeSelect" class="form-label">Portée programme</label>
|
<label for="transactionProgramScopeSelect" class="form-label">Portée programme</label>
|
||||||
<select id="transactionProgramScopeSelect" class="form-select">
|
<select id="transactionProgramScopeSelect" class="form-select">
|
||||||
<option value="any">outer + inner + logs</option>
|
<option value="any">top-level + inner + logs</option>
|
||||||
<option value="outer">outer uniquement</option>
|
<option value="top_level">top-level uniquement</option>
|
||||||
<option value="inner">inner uniquement</option>
|
<option value="inner">inner uniquement</option>
|
||||||
<option value="logs">logs liés uniquement</option>
|
<option value="logs">logs liés uniquement</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -129,10 +129,6 @@
|
|||||||
<label for="transactionEntityValueInput" class="form-label">Entité exacte</label>
|
<label for="transactionEntityValueInput" class="form-label">Entité exacte</label>
|
||||||
<input id="transactionEntityValueInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
<input id="transactionEntityValueInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
||||||
</div>
|
</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="col-6 col-md-3 col-xl-2">
|
||||||
<div class="form-check form-switch mb-2">
|
<div class="form-check form-switch mb-2">
|
||||||
<input id="transactionNewestFirstInput" class="form-check-input" type="checkbox" role="switch" checked />
|
<input id="transactionNewestFirstInput" class="form-check-input" type="checkbox" role="switch" checked />
|
||||||
@@ -155,6 +151,14 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="h5 card-title mb-1">Transactions candidates</h2>
|
<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 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>
|
<div>
|
||||||
<div class="btn-group flex-wrap" role="group" aria-label="Actions signatures">
|
<div class="btn-group flex-wrap" role="group" aria-label="Actions signatures">
|
||||||
@@ -199,15 +203,11 @@
|
|||||||
<option value="">Tous les programmes connus</option>
|
<option value="">Tous les programmes connus</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
<label for="programContainsInput" class="form-label">Program ID contient</label>
|
||||||
<input id="programContainsInput" class="form-control font-monospace" type="text" list="knownProgramDatalist" autocomplete="off" />
|
<input id="programContainsInput" class="form-control font-monospace" type="text" list="knownProgramDatalist" autocomplete="off" />
|
||||||
<datalist id="knownProgramDatalist"></datalist>
|
<datalist id="knownProgramDatalist"></datalist>
|
||||||
</div>
|
</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="col-12 col-lg-2">
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<button id="resetProgramsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
<button id="resetProgramsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
||||||
@@ -223,6 +223,14 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="h5 card-title mb-1">Programmes indexés</h2>
|
<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 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>
|
<div>
|
||||||
<div class="btn-group flex-wrap" role="group" aria-label="Actions programmes">
|
<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 shadow-sm border-0 mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-3 align-items-end">
|
<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>
|
<label for="mintContainsInput" class="form-label">Mint contient</label>
|
||||||
<input id="mintContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
<input id="mintContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
||||||
</div>
|
</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="col-12 col-lg-3">
|
||||||
<div class="d-grid d-sm-flex gap-2">
|
<div class="d-grid d-sm-flex gap-2">
|
||||||
<button id="resetMintsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
<button id="resetMintsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
||||||
@@ -281,7 +285,15 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="h5 card-title mb-1">Mints</h2>
|
<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 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>
|
<div>
|
||||||
<div class="btn-group flex-wrap" role="group" aria-label="Actions mints">
|
<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 shadow-sm border-0 mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-3 align-items-end">
|
<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>
|
<label for="ownerContainsInput" class="form-label">Owner contient</label>
|
||||||
<input id="ownerContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
<input id="ownerContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
||||||
</div>
|
</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="col-12 col-lg-3">
|
||||||
<div class="d-grid d-sm-flex gap-2">
|
<div class="d-grid d-sm-flex gap-2">
|
||||||
<button id="resetOwnersButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
<button id="resetOwnersButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
||||||
@@ -339,7 +347,15 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="h5 card-title mb-1">Owners</h2>
|
<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 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>
|
<div>
|
||||||
<div class="btn-group flex-wrap" role="group" aria-label="Actions owners">
|
<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 shadow-sm border-0 mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-3 align-items-end">
|
<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>
|
<label for="accountContainsInput" class="form-label">Account key contient</label>
|
||||||
<input id="accountContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
<input id="accountContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
||||||
</div>
|
</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="col-12 col-lg-3">
|
||||||
<div class="d-grid d-sm-flex gap-2">
|
<div class="d-grid d-sm-flex gap-2">
|
||||||
<button id="resetAccountsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
<button id="resetAccountsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
||||||
@@ -397,7 +409,15 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="h5 card-title mb-1">Comptes</h2>
|
<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 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>
|
<div>
|
||||||
<div class="btn-group flex-wrap" role="group" aria-label="Actions comptes">
|
<div class="btn-group flex-wrap" role="group" aria-label="Actions comptes">
|
||||||
@@ -441,7 +461,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</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>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
331
kb-app-demo-desktop/frontend/demo_wallet.html
Normal file
331
kb-app-demo-desktop/frontend/demo_wallet.html
Normal 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">© 2026 SASEDEV</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script type="module" src="ts/demo_wallet.ts" defer></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: kb-app-demo-desktop/frontend/main.html -->
|
<!-- file: kb-app-demo-desktop/frontend/main.html -->
|
||||||
<!-- version: 11 -->
|
<!-- version: 14 -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li><a id="openDemoConfigLink" class="dropdown-item" href="#">Configuration</a></li>
|
<li><a id="openDemoConfigLink" class="dropdown-item" href="#">Configuration</a></li>
|
||||||
|
<li><a id="openDemoWalletLink" class="dropdown-item" href="#">Wallets</a></li>
|
||||||
<li>
|
<li>
|
||||||
<hr class="dropdown-divider">
|
<hr class="dropdown-divider">
|
||||||
</li>
|
</li>
|
||||||
@@ -47,12 +48,12 @@
|
|||||||
<hr class="dropdown-divider">
|
<hr class="dropdown-divider">
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<h6 class="dropdown-header">SQL</h6>
|
<h6 class="dropdown-header">Store</h6>
|
||||||
</li>
|
</li>
|
||||||
<li><a id="openDemoSqlDiagLink" class="dropdown-item" href="#">SQL diagnostics</a></li>
|
<li><a id="openDemoStoreDiagLink" class="dropdown-item" href="#">Store diagnostics</a></li>
|
||||||
<li><a id="openDemoSqlPgRawLink" class="dropdown-item" href="#">SQL PostgreSQL raw</a></li>
|
<li><a id="openDemoStoreRawLink" class="dropdown-item" href="#">Raw store</a></li>
|
||||||
<li><a id="openDemoSqlPgCoreLink" class="dropdown-item" href="#">SQL PostgreSQL core</a></li>
|
<li><a id="openDemoStoreCoreLink" class="dropdown-item" href="#">Core store</a></li>
|
||||||
<li><a id="openDemoSqlReplayCandidatesLink" class="dropdown-item" href="#">SQL replay candidates</a></li>
|
<li><a id="openDemoStoreReplayCandidatesLink" class="dropdown-item" href="#">Store replay candidates</a></li>
|
||||||
<li>
|
<li>
|
||||||
<hr class="dropdown-divider">
|
<hr class="dropdown-divider">
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/frontend/ts/demo_core_extraction.ts
|
// file: kb-app-demo-desktop/frontend/ts/demo_core_extraction.ts
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
import * as bootstrap from "bootstrap";
|
import * as bootstrap from "bootstrap";
|
||||||
import "simplebar";
|
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 {
|
try {
|
||||||
await invoke(command);
|
await invoke(command);
|
||||||
appendLog({ timestamp: new Date().toISOString(), level: "info", message: `${label} ouvert.` });
|
appendLog({ timestamp: new Date().toISOString(), level: "info", message: `${label} ouvert.` });
|
||||||
@@ -166,13 +166,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
element<HTMLButtonElement>("#openRawDiagnosticsButton").addEventListener("click", () => {
|
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", () => {
|
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", () => {
|
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", () => {
|
element<HTMLButtonElement>("#cancelCoreExtractionButton").addEventListener("click", () => {
|
||||||
void cancelCoreExtraction();
|
void cancelCoreExtraction();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/frontend/ts/demo_decode_replay.ts
|
// file: kb-app-demo-desktop/frontend/ts/demo_decode_replay.ts
|
||||||
// version: 7
|
// version: 9
|
||||||
|
|
||||||
import * as bootstrap from "bootstrap";
|
import * as bootstrap from "bootstrap";
|
||||||
import "simplebar";
|
import "simplebar";
|
||||||
@@ -9,11 +9,21 @@ import DataTable, { type Api } from "datatables.net-bs5";
|
|||||||
import "datatables.net-bs5/css/dataTables.bootstrap5.css";
|
import "datatables.net-bs5/css/dataTables.bootstrap5.css";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log.ts";
|
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 { 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 { 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 { 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 { 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 { 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 { 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";
|
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[] = [];
|
const logLines: string[] = [];
|
||||||
let running = false;
|
let running = false;
|
||||||
let materializationAvailable = 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 {
|
function element<T extends HTMLElement>(selector: string): T {
|
||||||
const value = document.querySelector<T>(selector);
|
const value = document.querySelector<T>(selector);
|
||||||
@@ -204,7 +223,7 @@ async function loadDiagnostics(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", "Invoke demo_decode_replay_diagnostics");
|
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", "Invoke demo_decode_replay_diagnostics");
|
||||||
const diagnostics = await invoke<DemoDecodeDiagnosticsPayload>("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);
|
element<HTMLTextAreaElement>("#decodeReplayDiagnosticsOutput").value = JSON.stringify(diagnostics, null, 2);
|
||||||
} catch (caughtError) {
|
} catch (caughtError) {
|
||||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||||
@@ -250,7 +269,7 @@ function renderAnnotations(rows: DemoTransactionAnnotationRow[]): void {
|
|||||||
annotationJournalTable = new DataTable<DemoTransactionAnnotationRow>("#annotationJournalTable", {
|
annotationJournalTable = new DataTable<DemoTransactionAnnotationRow>("#annotationJournalTable", {
|
||||||
autoWidth: false,
|
autoWidth: false,
|
||||||
pageLength: 25,
|
pageLength: 25,
|
||||||
lengthMenu: [10, 25, 50, 100],
|
lengthMenu: [10, 25, 50, 100, 250, 500],
|
||||||
order: [[0, "desc"]],
|
order: [[0, "desc"]],
|
||||||
scrollX: true,
|
scrollX: true,
|
||||||
language: {
|
language: {
|
||||||
@@ -265,24 +284,54 @@ function renderAnnotations(rows: DemoTransactionAnnotationRow[]): void {
|
|||||||
element<HTMLElement>("#annotationJournalStatus").textContent = `${rows.length} annotation(s) committed chargée(s).`;
|
element<HTMLElement>("#annotationJournalStatus").textContent = `${rows.length} annotation(s) committed chargée(s).`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadAnnotations(): Promise<void> {
|
async function startAnnotationLoad(): Promise<void> {
|
||||||
const request: DemoTransactionAnnotationRequest = {
|
annotationBaseRequest = {
|
||||||
signatureContains: inputValue("#annotationSignatureInput") || null,
|
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 {
|
try {
|
||||||
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_annotations limit=${request.limit}`);
|
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_annotations block=${blockIndex}`);
|
||||||
const rows = await invoke<DemoTransactionAnnotationRow[]>("demo_decode_replay_annotations", { request });
|
const payload = await invoke<DemoTransactionAnnotationPagePayload>("demo_decode_replay_annotations", { request });
|
||||||
renderAnnotations(rows);
|
renderAnnotations(payload.rows);
|
||||||
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_annotations completed rows=${rows.length}`);
|
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) {
|
} catch (caughtError) {
|
||||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||||
element<HTMLElement>("#annotationJournalStatus").textContent = `Erreur : ${message}`;
|
element<HTMLElement>("#annotationJournalStatus").textContent = `Erreur : ${message}`;
|
||||||
|
renderStoreBlockControls(annotationBlockState, annotationBlockControls);
|
||||||
frontendError("kb_app_demo_desktop.frontend.demo_decode_replay", `Annotation journal loading failed: ${message}`);
|
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 {
|
try {
|
||||||
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke ${command}`);
|
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke ${command}`);
|
||||||
await invoke(command);
|
await invoke(command);
|
||||||
@@ -357,9 +406,16 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
element<HTMLButtonElement>("#startDecodeReplayButton").addEventListener("click", () => void executeDecodeReplay());
|
element<HTMLButtonElement>("#startDecodeReplayButton").addEventListener("click", () => void executeDecodeReplay());
|
||||||
element<HTMLButtonElement>("#cancelDecodeReplayButton").addEventListener("click", () => void cancelDecodeReplay());
|
element<HTMLButtonElement>("#cancelDecodeReplayButton").addEventListener("click", () => void cancelDecodeReplay());
|
||||||
element<HTMLButtonElement>("#loadDecodeDiagnosticsButton").addEventListener("click", () => void loadDiagnostics());
|
element<HTMLButtonElement>("#loadDecodeDiagnosticsButton").addEventListener("click", () => void loadDiagnostics());
|
||||||
element<HTMLButtonElement>("#loadAnnotationsButton").addEventListener("click", () => void loadAnnotations());
|
element<HTMLButtonElement>("#loadAnnotationsButton").addEventListener("click", () => void startAnnotationLoad());
|
||||||
element<HTMLButtonElement>("#openReplayCandidatesFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_sql_replay_candidates_window"));
|
element<HTMLButtonElement>(annotationBlockControls.firstSelector).addEventListener("click", () => void loadAnnotationBlock(1, null));
|
||||||
element<HTMLButtonElement>("#openCoreDiagnosticsFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_sql_pg_core_window"));
|
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 listen<DemoDecodeReplayProgressPayload>("demo-decode-replay-progress", event => appendLog(event.payload));
|
||||||
void loadOptions().catch(caughtError => {
|
void loadOptions().catch(caughtError => {
|
||||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/frontend/ts/demo_execution_metadata.ts
|
// file: kb-app-demo-desktop/frontend/ts/demo_execution_metadata.ts
|
||||||
// version: 14
|
// version: 15
|
||||||
|
|
||||||
import * as bootstrap from "bootstrap";
|
import * as bootstrap from "bootstrap";
|
||||||
import "simplebar";
|
import "simplebar";
|
||||||
@@ -479,7 +479,9 @@ async function prepareProfile(): Promise<void> {
|
|||||||
setExecutionRunning(executionRunning);
|
setExecutionRunning(executionRunning);
|
||||||
updateProfileStatus("Préparation", "badge text-bg-warning");
|
updateProfileStatus("Préparation", "badge text-bg-warning");
|
||||||
try {
|
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 });
|
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);
|
synchronizeProfileSelections(profileName);
|
||||||
window.localStorage.setItem(profileStorageKey, profileName);
|
window.localStorage.setItem(profileStorageKey, profileName);
|
||||||
for (const selector of profileOutputSelectors) {
|
for (const selector of profileOutputSelectors) {
|
||||||
@@ -580,9 +582,18 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
void prepareProfile();
|
void prepareProfile();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
element<HTMLButtonElement>("#prepareMetadataProfileButton").addEventListener("click", () => void prepareProfile());
|
element<HTMLButtonElement>("#prepareMetadataProfileButton").addEventListener("click", () => {
|
||||||
element<HTMLButtonElement>("#prepareToken2022MetadataProfileButton").addEventListener("click", () => void prepareProfile());
|
frontendDebug("kb-app-demo-desktop.frontend.execution_metadata", "Metaplex profile load button clicked");
|
||||||
element<HTMLButtonElement>("#prepareSolanaProgramMetadataProfileButton").addEventListener("click", () => void prepareProfile());
|
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<HTMLButtonElement>("#refreshMetadataScenariosButton").addEventListener("click", () => void loadScenarios());
|
||||||
element<HTMLTextAreaElement>("#metadataExecutionLogOutput").addEventListener("app-log-clear", () => { metadataLogLines.length = 0; });
|
element<HTMLTextAreaElement>("#metadataExecutionLogOutput").addEventListener("app-log-clear", () => { metadataLogLines.length = 0; });
|
||||||
void listen<DemoExecutionMetadataProgressPayload>("demo-execution-metadata-progress", event => appendMetadataLog(event.payload));
|
void listen<DemoExecutionMetadataProgressPayload>("demo-execution-metadata-progress", event => appendMetadataLog(event.payload));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/frontend/ts/demo_execution_solana_core.ts
|
// file: kb-app-demo-desktop/frontend/ts/demo_execution_solana_core.ts
|
||||||
// version: 10
|
// version: 11
|
||||||
|
|
||||||
import * as bootstrap from "bootstrap";
|
import * as bootstrap from "bootstrap";
|
||||||
import "simplebar";
|
import "simplebar";
|
||||||
@@ -20,18 +20,18 @@ const logLines: string[] = [];
|
|||||||
let running = false;
|
let running = false;
|
||||||
let profileStoreReady = false;
|
let profileStoreReady = false;
|
||||||
let profileOptions: DemoExecutionSolanaCoreProfileOption[] = [];
|
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 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 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 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 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 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 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 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 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 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 n’a été produit.",error:message},null,2); element<HTMLTextAreaElement>("#executionSimulationOutput").value=JSON.stringify({title:"Simulation exacte",status:"not_started",reason:"preflight_failed",message:"La simulation n’a 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, l’envoi, la confirmation et le replay n’ont pas été exécutés.":"La confirmation et le replay ne s’appliquent pas à cette simulation refusée.",error:message},null,2); appendLog({timestamp:new Date().toISOString(),level:"error",stage:"failed",message}); }
|
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 n’a été produit.", error: message }, null, 2); element<HTMLTextAreaElement>("#executionSimulationOutput").value = JSON.stringify({ title: "Simulation exacte", status: "not_started", reason: "preflight_failed", message: "La simulation n’a 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, l’envoi, la confirmation et le replay n’ont pas été exécutés." : "La confirmation et le replay ne s’appliquent 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 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 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();}
|
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}`);}});
|
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}`); } });
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/frontend/ts/demo_execution_spl.ts
|
// file: kb-app-demo-desktop/frontend/ts/demo_execution_spl.ts
|
||||||
// version: 10
|
// version: 13
|
||||||
|
|
||||||
import * as bootstrap from "bootstrap";
|
import * as bootstrap from "bootstrap";
|
||||||
import "simplebar";
|
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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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";
|
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 profileStoreReady = false;
|
||||||
let profileOptions: DemoExecutionSolanaCoreProfileOption[] = [];
|
let profileOptions: DemoExecutionSolanaCoreProfileOption[] = [];
|
||||||
let token_2022Fixture: DemoSplToken2022FixturePayload | null = null;
|
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 {
|
function element<T extends HTMLElement>(selector: string): T {
|
||||||
const value = document.querySelector<T>(selector);
|
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>("#ataJournalSignatureInput").value = summary.transactionSignature;
|
||||||
element<HTMLInputElement>("#ataJournalMintInput").value = request.mint;
|
element<HTMLInputElement>("#ataJournalMintInput").value = request.mint;
|
||||||
element<HTMLInputElement>("#ataJournalAddressInput").value = summary.associatedTokenAccount;
|
element<HTMLInputElement>("#ataJournalAddressInput").value = summary.associatedTokenAccount;
|
||||||
await loadAtaJournal();
|
await startAtaJournalLoad();
|
||||||
}
|
}
|
||||||
} catch (caughtError) {
|
} catch (caughtError) {
|
||||||
const message = caughtError instanceof Error ? caughtError.message : String(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 {
|
return {
|
||||||
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
|
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
|
||||||
signatureContains: optionalText("#ataJournalSignatureInput"),
|
signatureContains: optionalText("#ataJournalSignatureInput"),
|
||||||
mint: optionalText("#ataJournalMintInput"),
|
mint: optionalText("#ataJournalMintInput"),
|
||||||
associatedTokenAccount: optionalText("#ataJournalAddressInput"),
|
associatedTokenAccount: optionalText("#ataJournalAddressInput"),
|
||||||
operation: optionalText("#ataJournalOperationInput"),
|
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");
|
const button = element<HTMLButtonElement>("#refreshAtaJournalButton");
|
||||||
button.disabled = true;
|
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 {
|
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({
|
element<HTMLTextAreaElement>("#ataJournalOutput").value = JSON.stringify({
|
||||||
title: "Journal lifecycle ATA",
|
title: "Journal lifecycle ATA",
|
||||||
status: "success",
|
status: "success",
|
||||||
count: rows.length,
|
block: payload.page,
|
||||||
rows: rows.map(row => ({
|
count: payload.rows.length,
|
||||||
|
rows: payload.rows.map(row => ({
|
||||||
...row,
|
...row,
|
||||||
payloadJson: parsedJson(row.payloadJson),
|
payloadJson: parsedJson(row.payloadJson),
|
||||||
})),
|
})),
|
||||||
@@ -445,11 +498,17 @@ async function loadAtaJournal(): Promise<void> {
|
|||||||
message,
|
message,
|
||||||
error: message,
|
error: message,
|
||||||
}, null, 2);
|
}, null, 2);
|
||||||
|
renderStoreBlockControls(ataJournalBlockState, ataJournalBlockControls);
|
||||||
} finally {
|
} finally {
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markAtaJournalFiltersDirty(): void {
|
||||||
|
ataJournalBaseRequest = null;
|
||||||
|
markStoreBlockFiltersDirty(ataJournalBlockState, ataJournalBlockControls);
|
||||||
|
}
|
||||||
|
|
||||||
async function execute(submit: boolean): Promise<void> {
|
async function execute(submit: boolean): Promise<void> {
|
||||||
if (running) {
|
if (running) {
|
||||||
return;
|
return;
|
||||||
@@ -567,7 +626,7 @@ async function executeToken(submit: boolean): Promise<void> {
|
|||||||
});
|
});
|
||||||
if (summary.transactionSignature) {
|
if (summary.transactionSignature) {
|
||||||
element<HTMLInputElement>("#tokenJournalSignatureInput").value = summary.transactionSignature;
|
element<HTMLInputElement>("#tokenJournalSignatureInput").value = summary.transactionSignature;
|
||||||
await loadTokenJournal();
|
await startTokenJournalLoad();
|
||||||
}
|
}
|
||||||
} catch (caughtError) {
|
} catch (caughtError) {
|
||||||
const message = caughtError instanceof Error ? caughtError.message : String(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 {
|
return {
|
||||||
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
|
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
|
||||||
signatureContains: optionalText("#tokenJournalSignatureInput"),
|
signatureContains: optionalText("#tokenJournalSignatureInput"),
|
||||||
@@ -587,7 +646,6 @@ function tokenJournalRequest(): DemoSplTokenJournalRequest {
|
|||||||
account: optionalText("#tokenJournalAccountInput"),
|
account: optionalText("#tokenJournalAccountInput"),
|
||||||
family: element<HTMLSelectElement>("#tokenJournalFamilySelect").value || null,
|
family: element<HTMLSelectElement>("#tokenJournalFamilySelect").value || null,
|
||||||
operation: optionalText("#tokenJournalOperationInput"),
|
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");
|
const button = element<HTMLButtonElement>("#refreshTokenJournalButton");
|
||||||
button.disabled = true;
|
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 {
|
try {
|
||||||
const rows = await invoke<DemoSplTokenJournalRow[]>("demo_spl_token_journal", { request: tokenJournalRequest() });
|
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", `Invoke demo_spl_token_journal block=${blockIndex}`);
|
||||||
renderTokenJournal(rows);
|
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({
|
appendLog({
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
level: "info",
|
level: "info",
|
||||||
stage: "spl_token_journal",
|
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) {
|
} catch (caughtError) {
|
||||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||||
@@ -851,12 +934,18 @@ async function loadTokenJournal(): Promise<void> {
|
|||||||
message,
|
message,
|
||||||
error: message,
|
error: message,
|
||||||
}, null, 2);
|
}, null, 2);
|
||||||
|
renderStoreBlockControls(tokenJournalBlockState, tokenJournalBlockControls);
|
||||||
frontendError("kb-app-demo-desktop.frontend.demo_execution_spl", `Token journal loading failed: ${message}`);
|
frontendError("kb-app-demo-desktop.frontend.demo_execution_spl", `Token journal loading failed: ${message}`);
|
||||||
} finally {
|
} finally {
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markTokenJournalFiltersDirty(): void {
|
||||||
|
tokenJournalBaseRequest = null;
|
||||||
|
markStoreBlockFiltersDirty(tokenJournalBlockState, tokenJournalBlockControls);
|
||||||
|
}
|
||||||
|
|
||||||
async function generateRecipient(): Promise<void> {
|
async function generateRecipient(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const payload = await invoke<DemoExecutionSolanaCoreGeneratedRecipientPayload>("demo_execution_solana_core_generate_recipient");
|
const payload = await invoke<DemoExecutionSolanaCoreGeneratedRecipientPayload>("demo_execution_solana_core_generate_recipient");
|
||||||
@@ -896,11 +985,13 @@ async function prepareSelectedProfile(): Promise<void> {
|
|||||||
if (!profile) {
|
if (!profile) {
|
||||||
return;
|
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 {
|
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 });
|
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;
|
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) {
|
} catch (caughtError) {
|
||||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||||
frontendError("kb-app-demo-desktop.frontend.demo_execution_spl", `Devnet store preparation failed: ${message}`);
|
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));
|
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(item => new bootstrap.Tooltip(item));
|
||||||
element<HTMLSelectElement>("#executionProfileSelect").addEventListener("change", () => {
|
element<HTMLSelectElement>("#executionProfileSelect").addEventListener("change", () => {
|
||||||
updateProfileHelp();
|
updateProfileHelp();
|
||||||
|
markTokenJournalFiltersDirty();
|
||||||
|
markAtaJournalFiltersDirty();
|
||||||
void prepareSelectedProfile();
|
void prepareSelectedProfile();
|
||||||
});
|
});
|
||||||
element<HTMLButtonElement>("#generateRecipientButton").addEventListener("click", () => {
|
element<HTMLButtonElement>("#generateRecipientButton").addEventListener("click", () => {
|
||||||
@@ -982,8 +1075,27 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
void executeToken2022(true);
|
void executeToken2022(true);
|
||||||
});
|
});
|
||||||
element<HTMLButtonElement>("#refreshTokenJournalButton").addEventListener("click", () => {
|
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", () => {
|
element<HTMLButtonElement>("#deriveAtaButton").addEventListener("click", () => {
|
||||||
void deriveAta();
|
void deriveAta();
|
||||||
});
|
});
|
||||||
@@ -994,8 +1106,26 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
void executeAta(true);
|
void executeAta(true);
|
||||||
});
|
});
|
||||||
element<HTMLButtonElement>("#refreshAtaJournalButton").addEventListener("click", () => {
|
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", () => {
|
element<HTMLButtonElement>("#cancelExecutionButton").addEventListener("click", () => {
|
||||||
void cancelExecution();
|
void cancelExecution();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
|
||||||
});
|
|
||||||
@@ -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();
|
|
||||||
});
|
|
||||||
@@ -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();
|
|
||||||
});
|
|
||||||
114
kb-app-demo-desktop/frontend/ts/demo_store_block_pagination.ts
Normal file
114
kb-app-demo-desktop/frontend/ts/demo_store_block_pagination.ts
Normal 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);
|
||||||
|
}
|
||||||
42
kb-app-demo-desktop/frontend/ts/demo_store_core.ts
Normal file
42
kb-app-demo-desktop/frontend/ts/demo_store_core.ts
Normal 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();
|
||||||
|
});
|
||||||
60
kb-app-demo-desktop/frontend/ts/demo_store_diag.ts
Normal file
60
kb-app-demo-desktop/frontend/ts/demo_store_diag.ts
Normal 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();
|
||||||
|
});
|
||||||
42
kb-app-demo-desktop/frontend/ts/demo_store_raw.ts
Normal file
42
kb-app-demo-desktop/frontend/ts/demo_store_raw.ts
Normal 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();
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/frontend/ts/demo_sql_replay_candidates.ts
|
// file: kb-app-demo-desktop/frontend/ts/demo_store_replay_candidates.ts
|
||||||
// version: 9
|
// version: 14
|
||||||
|
|
||||||
import * as bootstrap from "bootstrap";
|
import * as bootstrap from "bootstrap";
|
||||||
import "simplebar";
|
import "simplebar";
|
||||||
@@ -10,14 +10,27 @@ import "datatables.net-select-bs5";
|
|||||||
import "datatables.net-select-bs5/css/select.bootstrap5.css";
|
import "datatables.net-select-bs5/css/select.bootstrap5.css";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
|
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
|
||||||
import type { DemoSqlReplayEntityRequest } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRequest";
|
import {
|
||||||
import type { DemoSqlReplayEntityRow } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRow";
|
applyStoreBlockMetadata,
|
||||||
import type { DemoSqlReplayKnownProgramOption } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayKnownProgramOption";
|
createStoreBlockState,
|
||||||
import type { DemoSqlReplayOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayOptionsPayload";
|
cursorForBlock,
|
||||||
import type { DemoSqlReplayProgramRequest } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRequest";
|
markStoreBlockFiltersDirty,
|
||||||
import type { DemoSqlReplayProgramRow } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRow";
|
resetStoreBlockState,
|
||||||
import type { DemoSqlReplayTransactionRequest } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRequest";
|
renderStoreBlockControls,
|
||||||
import type { DemoSqlReplayTransactionRow } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRow";
|
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 & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||||
@@ -30,7 +43,7 @@ type EntityViewConfig = {
|
|||||||
pluralLabel: string;
|
pluralLabel: string;
|
||||||
tableSelector: string;
|
tableSelector: string;
|
||||||
containsSelector: string;
|
containsSelector: string;
|
||||||
limitSelector: string;
|
blockControls: StoreBlockControls;
|
||||||
summarySelector: string;
|
summarySelector: string;
|
||||||
loadButtonSelector: string;
|
loadButtonSelector: string;
|
||||||
resetButtonSelector: string;
|
resetButtonSelector: string;
|
||||||
@@ -40,7 +53,7 @@ type EntityViewConfig = {
|
|||||||
fileName: string;
|
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[] = [
|
const entityViewConfigs: EntityViewConfig[] = [
|
||||||
{
|
{
|
||||||
kind: "mint",
|
kind: "mint",
|
||||||
@@ -48,7 +61,12 @@ const entityViewConfigs: EntityViewConfig[] = [
|
|||||||
pluralLabel: "mints",
|
pluralLabel: "mints",
|
||||||
tableSelector: "#mintCandidateTable",
|
tableSelector: "#mintCandidateTable",
|
||||||
containsSelector: "#mintContainsInput",
|
containsSelector: "#mintContainsInput",
|
||||||
limitSelector: "#mintLimitInput",
|
blockControls: {
|
||||||
|
firstSelector: "#mintFirstBlockButton",
|
||||||
|
previousSelector: "#mintPreviousBlockButton",
|
||||||
|
nextSelector: "#mintNextBlockButton",
|
||||||
|
summarySelector: "#mintBlockSummary",
|
||||||
|
},
|
||||||
summarySelector: "#mintResultSummary",
|
summarySelector: "#mintResultSummary",
|
||||||
loadButtonSelector: "#loadMintsButton",
|
loadButtonSelector: "#loadMintsButton",
|
||||||
resetButtonSelector: "#resetMintsButton",
|
resetButtonSelector: "#resetMintsButton",
|
||||||
@@ -63,7 +81,12 @@ const entityViewConfigs: EntityViewConfig[] = [
|
|||||||
pluralLabel: "owners",
|
pluralLabel: "owners",
|
||||||
tableSelector: "#ownerCandidateTable",
|
tableSelector: "#ownerCandidateTable",
|
||||||
containsSelector: "#ownerContainsInput",
|
containsSelector: "#ownerContainsInput",
|
||||||
limitSelector: "#ownerLimitInput",
|
blockControls: {
|
||||||
|
firstSelector: "#ownerFirstBlockButton",
|
||||||
|
previousSelector: "#ownerPreviousBlockButton",
|
||||||
|
nextSelector: "#ownerNextBlockButton",
|
||||||
|
summarySelector: "#ownerBlockSummary",
|
||||||
|
},
|
||||||
summarySelector: "#ownerResultSummary",
|
summarySelector: "#ownerResultSummary",
|
||||||
loadButtonSelector: "#loadOwnersButton",
|
loadButtonSelector: "#loadOwnersButton",
|
||||||
resetButtonSelector: "#resetOwnersButton",
|
resetButtonSelector: "#resetOwnersButton",
|
||||||
@@ -78,7 +101,12 @@ const entityViewConfigs: EntityViewConfig[] = [
|
|||||||
pluralLabel: "comptes",
|
pluralLabel: "comptes",
|
||||||
tableSelector: "#accountCandidateTable",
|
tableSelector: "#accountCandidateTable",
|
||||||
containsSelector: "#accountContainsInput",
|
containsSelector: "#accountContainsInput",
|
||||||
limitSelector: "#accountLimitInput",
|
blockControls: {
|
||||||
|
firstSelector: "#accountFirstBlockButton",
|
||||||
|
previousSelector: "#accountPreviousBlockButton",
|
||||||
|
nextSelector: "#accountNextBlockButton",
|
||||||
|
summarySelector: "#accountBlockSummary",
|
||||||
|
},
|
||||||
summarySelector: "#accountResultSummary",
|
summarySelector: "#accountResultSummary",
|
||||||
loadButtonSelector: "#loadAccountsButton",
|
loadButtonSelector: "#loadAccountsButton",
|
||||||
resetButtonSelector: "#resetAccountsButton",
|
resetButtonSelector: "#resetAccountsButton",
|
||||||
@@ -88,13 +116,37 @@ const entityViewConfigs: EntityViewConfig[] = [
|
|||||||
fileName: "replay_account_keys.csv",
|
fileName: "replay_account_keys.csv",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
let transactionTable: Api<DemoSqlReplayTransactionRow> | null = null;
|
let transactionTable: Api<DemoStoreReplayTransactionRow> | null = null;
|
||||||
let programTable: Api<DemoSqlReplayProgramRow> | null = null;
|
let programTable: Api<DemoStoreReplayProgramRow> | null = null;
|
||||||
let mintTable: Api<DemoSqlReplayEntityRow> | null = null;
|
let mintTable: Api<DemoStoreReplayEntityRow> | null = null;
|
||||||
let ownerTable: Api<DemoSqlReplayEntityRow> | null = null;
|
let ownerTable: Api<DemoStoreReplayEntityRow> | null = null;
|
||||||
let accountTable: Api<DemoSqlReplayEntityRow> | null = null;
|
let accountTable: Api<DemoStoreReplayEntityRow> | null = null;
|
||||||
let busyOperationCount = 0;
|
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 {
|
function element<T extends HTMLElement>(selector: string): T {
|
||||||
const value = document.querySelector<T>(selector);
|
const value = document.querySelector<T>(selector);
|
||||||
@@ -117,24 +169,6 @@ function requiredInputValue(selector: string): string {
|
|||||||
return value;
|
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 {
|
function optionalNonNegativeIntegerValue(selector: string): number | null {
|
||||||
const raw = optionalInputValue(selector);
|
const raw = optionalInputValue(selector);
|
||||||
if (raw === null) {
|
if (raw === null) {
|
||||||
@@ -199,7 +233,7 @@ function badgeDisplay(value: unknown, type: string): string {
|
|||||||
return `<span class="badge ${className}">${escapeHtml(text)}</span>`;
|
return `<span class="badge ${className}">${escapeHtml(text)}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function coreStatusText(row: DemoSqlReplayTransactionRow): string {
|
function coreStatusText(row: DemoStoreReplayTransactionRow): string {
|
||||||
if (!row.hasCoreTransaction) {
|
if (!row.hasCoreTransaction) {
|
||||||
return "absent";
|
return "absent";
|
||||||
}
|
}
|
||||||
@@ -209,7 +243,7 @@ function coreStatusText(row: DemoSqlReplayTransactionRow): string {
|
|||||||
return "présent";
|
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 (type === "display") {
|
||||||
if (!row.hasCoreTransaction) {
|
if (!row.hasCoreTransaction) {
|
||||||
return '<span class="badge text-bg-secondary">absent</span>';
|
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;
|
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;
|
const ratio = row.transactionCount > 0 ? row.occurrenceCount / row.transactionCount : 0;
|
||||||
if (type === "display" || type === "filter") {
|
if (type === "display" || type === "filter") {
|
||||||
return ratio.toFixed(2);
|
return ratio.toFixed(2);
|
||||||
@@ -236,7 +270,7 @@ function occurrenceRatioDisplay(_value: unknown, type: string, row: DemoSqlRepla
|
|||||||
return ratio;
|
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);
|
const span = Math.max(0, row.maxSlot - row.minSlot);
|
||||||
if (type === "display" || type === "filter") {
|
if (type === "display" || type === "filter") {
|
||||||
return String(span);
|
return String(span);
|
||||||
@@ -258,8 +292,8 @@ function commonLanguage(emptyTable: string, infoLabel: string, zeroRecords: stri
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTransactionTable(): Api<DemoSqlReplayTransactionRow> {
|
function createTransactionTable(): Api<DemoStoreReplayTransactionRow> {
|
||||||
return new DataTable<DemoSqlReplayTransactionRow>("#transactionCandidateTable", {
|
return new DataTable<DemoStoreReplayTransactionRow>("#transactionCandidateTable", {
|
||||||
data: [],
|
data: [],
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@@ -277,15 +311,15 @@ function createTransactionTable(): Api<DemoSqlReplayTransactionRow> {
|
|||||||
{ data: "ledgerStatus", render: badgeDisplay },
|
{ data: "ledgerStatus", render: badgeDisplay },
|
||||||
{ data: "processorVersion", defaultContent: "—", render: codeDisplay },
|
{ data: "processorVersion", defaultContent: "—", render: codeDisplay },
|
||||||
{ data: "attemptCount" },
|
{ data: "attemptCount" },
|
||||||
{ data: "outerInstructionCount" },
|
{ data: "topLevelInstructionCount" },
|
||||||
{ data: "innerInstructionCount" },
|
{ data: "innerInstructionCount" },
|
||||||
{ data: "outerProgramCount" },
|
{ data: "topLevelProgramCount" },
|
||||||
{ data: "innerProgramCount" },
|
{ data: "innerProgramCount" },
|
||||||
{ data: "updatedAt" },
|
{ data: "updatedAt" },
|
||||||
],
|
],
|
||||||
order: [[2, "desc"]],
|
order: [[2, "desc"]],
|
||||||
pageLength: 10,
|
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" },
|
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
|
||||||
language: commonLanguage(
|
language: commonLanguage(
|
||||||
"Aucune transaction chargée",
|
"Aucune transaction chargée",
|
||||||
@@ -296,8 +330,8 @@ function createTransactionTable(): Api<DemoSqlReplayTransactionRow> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createProgramTable(): Api<DemoSqlReplayProgramRow> {
|
function createProgramTable(): Api<DemoStoreReplayProgramRow> {
|
||||||
return new DataTable<DemoSqlReplayProgramRow>("#programCandidateTable", {
|
return new DataTable<DemoStoreReplayProgramRow>("#programCandidateTable", {
|
||||||
data: [],
|
data: [],
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@@ -311,7 +345,7 @@ function createProgramTable(): Api<DemoSqlReplayProgramRow> {
|
|||||||
{ data: "programCode", defaultContent: "—", render: codeDisplay },
|
{ data: "programCode", defaultContent: "—", render: codeDisplay },
|
||||||
{ data: "programId", render: copyableCodeDisplay("le program ID") },
|
{ data: "programId", render: copyableCodeDisplay("le program ID") },
|
||||||
{ data: "transactionCount" },
|
{ data: "transactionCount" },
|
||||||
{ data: "outerInstructionCount" },
|
{ data: "topLevelInstructionCount" },
|
||||||
{ data: "innerInstructionCount" },
|
{ data: "innerInstructionCount" },
|
||||||
{ data: "logCount" },
|
{ data: "logCount" },
|
||||||
{ data: "minSlot" },
|
{ data: "minSlot" },
|
||||||
@@ -319,7 +353,7 @@ function createProgramTable(): Api<DemoSqlReplayProgramRow> {
|
|||||||
],
|
],
|
||||||
order: [[3, "desc"]],
|
order: [[3, "desc"]],
|
||||||
pageLength: 10,
|
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" },
|
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
|
||||||
language: commonLanguage(
|
language: commonLanguage(
|
||||||
"Aucun programme chargé",
|
"Aucun programme chargé",
|
||||||
@@ -330,8 +364,8 @@ function createProgramTable(): Api<DemoSqlReplayProgramRow> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEntityTable(config: EntityViewConfig): Api<DemoSqlReplayEntityRow> {
|
function createEntityTable(config: EntityViewConfig): Api<DemoStoreReplayEntityRow> {
|
||||||
return new DataTable<DemoSqlReplayEntityRow>(config.tableSelector, {
|
return new DataTable<DemoStoreReplayEntityRow>(config.tableSelector, {
|
||||||
data: [],
|
data: [],
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@@ -352,7 +386,7 @@ function createEntityTable(config: EntityViewConfig): Api<DemoSqlReplayEntityRow
|
|||||||
],
|
],
|
||||||
order: [[2, "desc"]],
|
order: [[2, "desc"]],
|
||||||
pageLength: 10,
|
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" },
|
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
|
||||||
language: commonLanguage(
|
language: commonLanguage(
|
||||||
`Aucun ${config.label} chargé`,
|
`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") {
|
if (kind === "mint") {
|
||||||
return mintTable;
|
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.`);
|
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 {
|
function setBusy(busy: boolean, message: string): void {
|
||||||
busyOperationCount = busy ? busyOperationCount + 1 : Math.max(0, busyOperationCount - 1);
|
busyOperationCount = busy ? busyOperationCount + 1 : Math.max(0, busyOperationCount - 1);
|
||||||
const operationInProgress = busyOperationCount > 0;
|
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.textContent = operationInProgress ? (busy ? message : "Chargement en cours") : "Prêt";
|
||||||
badge.className = operationInProgress ? "badge text-bg-warning" : "badge text-bg-success";
|
badge.className = operationInProgress ? "badge text-bg-warning" : "badge text-bg-success";
|
||||||
document.querySelectorAll<HTMLButtonElement>("button").forEach(button => {
|
document.querySelectorAll<HTMLButtonElement>("button").forEach(button => {
|
||||||
if (button.id !== "openCoreExtractionButton") {
|
if (button.id === "openCoreExtractionButton") {
|
||||||
button.disabled = operationInProgress;
|
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 entityKind = optionalInputValue("#transactionEntityKindSelect");
|
||||||
const entityValue = optionalInputValue("#transactionEntityValueInput");
|
const entityValue = optionalInputValue("#transactionEntityValueInput");
|
||||||
if ((entityKind === null) !== (entityValue === null)) {
|
if ((entityKind === null) !== (entityValue === null)) {
|
||||||
@@ -441,11 +497,31 @@ function buildTransactionRequest(): DemoSqlReplayTransactionRequest {
|
|||||||
programScope: requiredInputValue("#transactionProgramScopeSelect"),
|
programScope: requiredInputValue("#transactionProgramScopeSelect"),
|
||||||
entityKind,
|
entityKind,
|
||||||
entityValue,
|
entityValue,
|
||||||
limit: boundedPositiveIntegerValue("#transactionLimitInput"),
|
|
||||||
newestFirst: element<HTMLInputElement>("#transactionNewestFirstInput").checked,
|
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> {
|
async function resetTransactionFilters(): Promise<void> {
|
||||||
element<HTMLInputElement>("#transactionSignatureContainsInput").value = "";
|
element<HTMLInputElement>("#transactionSignatureContainsInput").value = "";
|
||||||
element<HTMLInputElement>("#transactionMinSlotInput").value = "";
|
element<HTMLInputElement>("#transactionMinSlotInput").value = "";
|
||||||
@@ -456,95 +532,190 @@ async function resetTransactionFilters(): Promise<void> {
|
|||||||
element<HTMLSelectElement>("#transactionProgramScopeSelect").value = "any";
|
element<HTMLSelectElement>("#transactionProgramScopeSelect").value = "any";
|
||||||
element<HTMLSelectElement>("#transactionEntityKindSelect").value = "";
|
element<HTMLSelectElement>("#transactionEntityKindSelect").value = "";
|
||||||
element<HTMLInputElement>("#transactionEntityValueInput").value = "";
|
element<HTMLInputElement>("#transactionEntityValueInput").value = "";
|
||||||
element<HTMLInputElement>("#transactionLimitInput").value = "500";
|
|
||||||
element<HTMLInputElement>("#transactionNewestFirstInput").checked = true;
|
element<HTMLInputElement>("#transactionNewestFirstInput").checked = true;
|
||||||
resetTableState(transactionTable);
|
resetTableState(transactionTable);
|
||||||
await loadTransactions();
|
await startTransactionLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetProgramFilters(): Promise<void> {
|
async function resetProgramFilters(): Promise<void> {
|
||||||
element<HTMLSelectElement>("#knownProgramSelect").value = "";
|
element<HTMLSelectElement>("#knownProgramSelect").value = "";
|
||||||
element<HTMLInputElement>("#programContainsInput").value = "";
|
element<HTMLInputElement>("#programContainsInput").value = "";
|
||||||
element<HTMLInputElement>("#programLimitInput").value = "500";
|
|
||||||
resetTableState(programTable);
|
resetTableState(programTable);
|
||||||
await loadPrograms();
|
await startProgramLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetEntityFilters(config: EntityViewConfig): Promise<void> {
|
async function resetEntityFilters(config: EntityViewConfig): Promise<void> {
|
||||||
element<HTMLInputElement>(config.containsSelector).value = "";
|
element<HTMLInputElement>(config.containsSelector).value = "";
|
||||||
element<HTMLInputElement>(config.limitSelector).value = "500";
|
|
||||||
resetTableState(entityTable(config.kind));
|
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) {
|
if (transactionTable === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (transactionBaseRequest === null) {
|
||||||
|
throw new Error("Rechargez les transactions pour initialiser les filtres Store.");
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
setBusy(true, "Chargement transactions");
|
setBusy(true, "Chargement transactions");
|
||||||
const request = buildTransactionRequest();
|
const request: DemoStoreReplayTransactionRequest = {
|
||||||
const rows = await invoke<DemoSqlReplayTransactionRow[]>("load_demo_sql_replay_transactions", { request });
|
...transactionBaseRequest,
|
||||||
replaceRows(transactionTable, rows);
|
cursor,
|
||||||
element<HTMLElement>("#transactionResultSummary").textContent = `${rows.length} transaction(s) chargée(s) depuis PostgreSQL.`;
|
blockIndex,
|
||||||
frontendDebug(tracingTarget, `loaded ${rows.length} replay transaction candidates`);
|
};
|
||||||
|
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) {
|
} catch (caughtError) {
|
||||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||||
element<HTMLElement>("#transactionResultSummary").textContent = `Erreur : ${message}`;
|
element<HTMLElement>("#transactionResultSummary").textContent = `Erreur : ${message}`;
|
||||||
|
renderStoreBlockControls(transactionBlockState, transactionBlockControls);
|
||||||
frontendError(tracingTarget, `transaction candidate loading failed: ${message}`);
|
frontendError(tracingTarget, `transaction candidate loading failed: ${message}`);
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false, "Prêt");
|
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) {
|
if (programTable === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (programBaseRequest === null) {
|
||||||
|
throw new Error("Rechargez les programmes pour initialiser les filtres Store.");
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
setBusy(true, "Chargement programmes");
|
setBusy(true, "Chargement programmes");
|
||||||
const request: DemoSqlReplayProgramRequest = {
|
const request: DemoStoreReplayProgramRequest = {
|
||||||
programIdContains: optionalInputValue("#programContainsInput"),
|
...programBaseRequest,
|
||||||
limit: boundedPositiveIntegerValue("#programLimitInput"),
|
cursor,
|
||||||
|
blockIndex,
|
||||||
};
|
};
|
||||||
const rows = await invoke<DemoSqlReplayProgramRow[]>("load_demo_sql_replay_programs", { request });
|
const payload = await invoke<DemoStoreReplayProgramPagePayload>(
|
||||||
replaceRows(programTable, rows);
|
"load_demo_store_replay_programs",
|
||||||
element<HTMLElement>("#programResultSummary").textContent = `${rows.length} programme(s) chargé(s), avec occurrences outer, inner et logs liés.`;
|
{ request },
|
||||||
frontendDebug(tracingTarget, `loaded ${rows.length} replay program summaries`);
|
);
|
||||||
|
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) {
|
} catch (caughtError) {
|
||||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||||
element<HTMLElement>("#programResultSummary").textContent = `Erreur : ${message}`;
|
element<HTMLElement>("#programResultSummary").textContent = `Erreur : ${message}`;
|
||||||
|
renderStoreBlockControls(programBlockState, programBlockControls);
|
||||||
frontendError(tracingTarget, `program summary loading failed: ${message}`);
|
frontendError(tracingTarget, `program summary loading failed: ${message}`);
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false, "Prêt");
|
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);
|
const table = entityTable(config.kind);
|
||||||
if (table === null) {
|
if (table === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const baseRequest = entityBaseRequests.get(config.kind);
|
||||||
|
if (baseRequest === undefined) {
|
||||||
|
throw new Error(`Rechargez les ${config.pluralLabel} pour initialiser les filtres Store.`);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
setBusy(true, `Chargement ${config.pluralLabel}`);
|
setBusy(true, `Chargement ${config.pluralLabel}`);
|
||||||
const request: DemoSqlReplayEntityRequest = {
|
const request: DemoStoreReplayEntityRequest = {
|
||||||
entityKind: config.kind,
|
...baseRequest,
|
||||||
entityValueContains: optionalInputValue(config.containsSelector),
|
cursor,
|
||||||
limit: boundedPositiveIntegerValue(config.limitSelector),
|
blockIndex,
|
||||||
};
|
};
|
||||||
const rows = await invoke<DemoSqlReplayEntityRow[]>("load_demo_sql_replay_entities", { request });
|
const payload = await invoke<DemoStoreReplayEntityPagePayload>(
|
||||||
replaceRows(table, rows);
|
"load_demo_store_replay_entities",
|
||||||
element<HTMLElement>(config.summarySelector).textContent = `${rows.length} ${config.pluralLabel} chargé(s) depuis PostgreSQL.`;
|
{ request },
|
||||||
frontendDebug(tracingTarget, `loaded ${rows.length} replay ${config.kind} summaries`);
|
);
|
||||||
|
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) {
|
} catch (caughtError) {
|
||||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||||
element<HTMLElement>(config.summarySelector).textContent = `Erreur : ${message}`;
|
element<HTMLElement>(config.summarySelector).textContent = `Erreur : ${message}`;
|
||||||
|
renderStoreBlockControls(entityBlockState(config.kind), config.blockControls);
|
||||||
frontendError(tracingTarget, `${config.kind} summary loading failed: ${message}`);
|
frontendError(tracingTarget, `${config.kind} summary loading failed: ${message}`);
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false, "Prêt");
|
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> {
|
async function copyLines(lines: string[], label: string): Promise<void> {
|
||||||
if (lines.length === 0) {
|
if (lines.length === 0) {
|
||||||
throw new Error(`Aucune ${label} à copier.`);
|
throw new Error(`Aucune ${label} à copier.`);
|
||||||
@@ -567,7 +738,7 @@ async function saveCsv(fileName: string, content: string, rowCount: number): Pro
|
|||||||
if (rowCount === 0) {
|
if (rowCount === 0) {
|
||||||
throw new Error("Aucune ligne à exporter.");
|
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");
|
const badge = element<HTMLElement>("#replayCandidateStatusBadge");
|
||||||
badge.textContent = `${rowCount} ligne(s) exportée(s) : ${path}`;
|
badge.textContent = `${rowCount} ligne(s) exportée(s) : ${path}`;
|
||||||
badge.className = "badge text-bg-success";
|
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> {
|
async function exportTransactionsCsv(): Promise<void> {
|
||||||
const rows = transactionRowsForExport();
|
const rows = transactionRowsForExport();
|
||||||
const content = csvContent(
|
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"],
|
["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.outerInstructionCount, row.innerInstructionCount, row.outerProgramCount, row.innerProgramCount, row.updatedAt]),
|
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);
|
await saveCsv("replay_transactions.csv", content, rows.length);
|
||||||
}
|
}
|
||||||
@@ -586,8 +757,8 @@ async function exportTransactionsCsv(): Promise<void> {
|
|||||||
async function exportProgramsCsv(): Promise<void> {
|
async function exportProgramsCsv(): Promise<void> {
|
||||||
const rows = programRowsForExport();
|
const rows = programRowsForExport();
|
||||||
const content = csvContent(
|
const content = csvContent(
|
||||||
["program_code", "program_id", "transaction_count", "outer_instruction_count", "inner_instruction_count", "log_count", "min_slot", "max_slot"],
|
["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.outerInstructionCount, row.innerInstructionCount, row.logCount, row.minSlot, row.maxSlot]),
|
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);
|
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);
|
await saveCsv(config.fileName, content, rows.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
function transactionRowsForExport(): DemoSqlReplayTransactionRow[] {
|
function transactionRowsForExport(): DemoStoreReplayTransactionRow[] {
|
||||||
if (transactionTable === null) {
|
if (transactionTable === null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return selectedOrFilteredRows(transactionTable);
|
return selectedOrFilteredRows(transactionTable);
|
||||||
}
|
}
|
||||||
|
|
||||||
function programRowsForExport(): DemoSqlReplayProgramRow[] {
|
function programRowsForExport(): DemoStoreReplayProgramRow[] {
|
||||||
if (programTable === null) {
|
if (programTable === null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return selectedOrFilteredRows(programTable);
|
return selectedOrFilteredRows(programTable);
|
||||||
}
|
}
|
||||||
|
|
||||||
function entityRowsForExport(kind: EntityKindCode): DemoSqlReplayEntityRow[] {
|
function entityRowsForExport(kind: EntityKindCode): DemoStoreReplayEntityRow[] {
|
||||||
const table = entityTable(kind);
|
const table = entityTable(kind);
|
||||||
if (table === null) {
|
if (table === null) {
|
||||||
return [];
|
return [];
|
||||||
@@ -635,7 +806,7 @@ async function useSelectedProgram(): Promise<void> {
|
|||||||
element<HTMLInputElement>("#transactionProgramIdInput").value = row.programId;
|
element<HTMLInputElement>("#transactionProgramIdInput").value = row.programId;
|
||||||
element<HTMLSelectElement>("#transactionProgramScopeSelect").value = "any";
|
element<HTMLSelectElement>("#transactionProgramScopeSelect").value = "any";
|
||||||
showTransactionsTab();
|
showTransactionsTab();
|
||||||
await loadTransactions();
|
await startTransactionLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function useSelectedEntity(config: EntityViewConfig): Promise<void> {
|
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<HTMLSelectElement>("#transactionEntityKindSelect").value = config.kind;
|
||||||
element<HTMLInputElement>("#transactionEntityValueInput").value = row.entityValue;
|
element<HTMLInputElement>("#transactionEntityValueInput").value = row.entityValue;
|
||||||
showTransactionsTab();
|
showTransactionsTab();
|
||||||
await loadTransactions();
|
await startTransactionLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openCoreExtraction(): Promise<void> {
|
async function openCoreExtraction(): Promise<void> {
|
||||||
@@ -660,21 +831,17 @@ async function openCoreExtraction(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadOptions(): Promise<void> {
|
async function loadOptions(): Promise<void> {
|
||||||
const options = await invoke<DemoSqlReplayOptionsPayload>("demo_sql_replay_options");
|
const options = await invoke<DemoStoreReplayOptionsPayload>("demo_store_replay_options");
|
||||||
if (!Number.isSafeInteger(options.maximumLimit) || options.maximumLimit <= 0) {
|
if (!Number.isSafeInteger(options.blockSize) || options.blockSize !== 500) {
|
||||||
throw new Error("La limite maximale des candidats replay retournée par le backend est invalide.");
|
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>("#replayCandidateProfileBadge").textContent = `Profil ${options.activeProfileName}`;
|
||||||
element<HTMLElement>("#replayCandidateDsn").textContent = options.maskedDsn;
|
element<HTMLElement>("#replayCandidateDsn").textContent = options.connectionDescriptor;
|
||||||
element<HTMLElement>("#replayCandidateMaximumLimit").textContent = String(options.maximumLimit);
|
element<HTMLElement>("#replayCandidateBlockSize").textContent = `${options.blockSize} lignes`;
|
||||||
document.querySelectorAll<HTMLInputElement>("input[type=number][max]").forEach(input => {
|
|
||||||
input.max = String(options.maximumLimit);
|
|
||||||
});
|
|
||||||
populateKnownPrograms(options.knownPrograms);
|
populateKnownPrograms(options.knownPrograms);
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateKnownPrograms(programs: DemoSqlReplayKnownProgramOption[]): void {
|
function populateKnownPrograms(programs: DemoStoreReplayKnownProgramOption[]): void {
|
||||||
const select = element<HTMLSelectElement>("#knownProgramSelect");
|
const select = element<HTMLSelectElement>("#knownProgramSelect");
|
||||||
const datalist = element<HTMLDataListElement>("#knownProgramDatalist");
|
const datalist = element<HTMLDataListElement>("#knownProgramDatalist");
|
||||||
select.replaceChildren(new Option("Tous les programmes connus", ""));
|
select.replaceChildren(new Option("Tous les programmes connus", ""));
|
||||||
@@ -729,14 +896,20 @@ function reportUiError(action: string, caughtError: unknown): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function installHandlers(): void {
|
function installHandlers(): void {
|
||||||
element<HTMLButtonElement>("#loadTransactionsButton").addEventListener("click", () => void loadTransactions());
|
element<HTMLButtonElement>("#loadTransactionsButton").addEventListener("click", () => void startTransactionLoad());
|
||||||
element<HTMLButtonElement>("#resetTransactionsButton").addEventListener("click", () => {
|
element<HTMLButtonElement>("#resetTransactionsButton").addEventListener("click", () => {
|
||||||
void resetTransactionFilters().catch(error => reportUiError("reset transaction filters", error));
|
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", () => {
|
element<HTMLButtonElement>("#resetProgramsButton").addEventListener("click", () => {
|
||||||
void resetProgramFilters().catch(error => reportUiError("reset program filters", error));
|
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>("#openCoreExtractionButton").addEventListener("click", () => void openCoreExtraction());
|
||||||
element<HTMLButtonElement>("#copySignaturesButton").addEventListener("click", () => {
|
element<HTMLButtonElement>("#copySignaturesButton").addEventListener("click", () => {
|
||||||
void copyLines(transactionRowsForExport().map(row => row.signature), "signature").catch(error => reportUiError("copy signatures", error));
|
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));
|
void useSelectedProgram().catch(error => reportUiError("use selected program", error));
|
||||||
});
|
});
|
||||||
for (const config of entityViewConfigs) {
|
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", () => {
|
element<HTMLButtonElement>(config.resetButtonSelector).addEventListener("click", () => {
|
||||||
void resetEntityFilters(config).catch(error => reportUiError(`reset ${config.pluralLabel} filters`, error));
|
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", () => {
|
element<HTMLButtonElement>(config.copyButtonSelector).addEventListener("click", () => {
|
||||||
void copyLines(entityRowsForExport(config.kind).map(row => row.entityValue), config.label).catch(error => reportUiError(`copy ${config.pluralLabel}`, error));
|
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", () => {
|
element<HTMLButtonElement>(config.useButtonSelector).addEventListener("click", () => {
|
||||||
void useSelectedEntity(config).catch(error => reportUiError(`use selected ${config.label}`, error));
|
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 => {
|
element<HTMLSelectElement>("#knownProgramSelect").addEventListener("change", event => {
|
||||||
const target = event.currentTarget as HTMLSelectElement;
|
const target = event.currentTarget as HTMLSelectElement;
|
||||||
element<HTMLInputElement>("#programContainsInput").value = target.value;
|
element<HTMLInputElement>("#programContainsInput").value = target.value;
|
||||||
|
programBaseRequest = null;
|
||||||
|
markStoreBlockFiltersDirty(programBlockState, programBlockControls);
|
||||||
});
|
});
|
||||||
document.querySelectorAll<HTMLButtonElement>('[data-bs-toggle="tab"]').forEach(button => {
|
document.querySelectorAll<HTMLButtonElement>('[data-bs-toggle="tab"]').forEach(button => {
|
||||||
button.addEventListener("shown.bs.tab", adjustVisibleTables);
|
button.addEventListener("shown.bs.tab", adjustVisibleTables);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
installFrontendConsoleBridge(tracingTarget);
|
installFrontendConsoleBridge(tracingTarget);
|
||||||
frontendDebug(tracingTarget, "SQL replay candidate browser loaded");
|
frontendDebug(tracingTarget, "Store replay candidate browser loaded");
|
||||||
transactionTable = createTransactionTable();
|
transactionTable = createTransactionTable();
|
||||||
programTable = createProgramTable();
|
programTable = createProgramTable();
|
||||||
mintTable = createEntityTable(entityViewConfigs[0]);
|
mintTable = createEntityTable(entityViewConfigs[0]);
|
||||||
@@ -790,11 +999,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
installInlineCopyHandler();
|
installInlineCopyHandler();
|
||||||
void loadOptions()
|
void loadOptions()
|
||||||
.then(() => Promise.all([
|
.then(() => Promise.all([
|
||||||
loadTransactions(),
|
startTransactionLoad(),
|
||||||
loadPrograms(),
|
startProgramLoad(),
|
||||||
loadEntity(entityViewConfigs[0]),
|
startEntityLoad(entityViewConfigs[0]),
|
||||||
loadEntity(entityViewConfigs[1]),
|
startEntityLoad(entityViewConfigs[1]),
|
||||||
loadEntity(entityViewConfigs[2]),
|
startEntityLoad(entityViewConfigs[2]),
|
||||||
]))
|
]))
|
||||||
.catch(caughtError => reportUiError("initial loading", caughtError));
|
.catch(caughtError => reportUiError("initial loading", caughtError));
|
||||||
});
|
});
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
// file: kb-app-demo-desktop/frontend/ts/demo_sql_tables.ts
|
// file: kb-app-demo-desktop/frontend/ts/demo_store_tables.ts
|
||||||
// version: 4
|
// version: 6
|
||||||
|
|
||||||
import DataTable, { type Api } from "datatables.net-bs5";
|
import DataTable, { type Api } from "datatables.net-bs5";
|
||||||
import "datatables.net-bs5/css/dataTables.bootstrap5.css";
|
import "datatables.net-bs5/css/dataTables.bootstrap5.css";
|
||||||
import { jsonText, renderJsonViewer } from "./json_viewer.ts";
|
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>();
|
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);
|
const existing = tableInstances.get(tableSelector);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.destroy();
|
existing.destroy();
|
||||||
@@ -26,16 +26,16 @@ export function renderTables(tableSelector: string, bodySelector: string, tables
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tbody.textContent = "";
|
tbody.textContent = "";
|
||||||
for (const table of tables) {
|
for (const resource of resources) {
|
||||||
const row = document.createElement("tr");
|
const row = document.createElement("tr");
|
||||||
row.appendChild(cell(table.tableName, true));
|
row.appendChild(cell(resource.resourceCode, true));
|
||||||
row.appendChild(cell(table.domain, false));
|
row.appendChild(cell(resource.modelCode, false));
|
||||||
row.appendChild(cell(table.role, false));
|
row.appendChild(cell(resource.role, false));
|
||||||
row.appendChild(cell(table.exists ? "oui" : "non", false));
|
row.appendChild(cell(resource.available ? "oui" : "non", false));
|
||||||
row.appendChild(cell(table.rowCount === null ? "—" : String(table.rowCount), false));
|
row.appendChild(cell(resource.recordCount === null ? "—" : String(resource.recordCount), false));
|
||||||
row.appendChild(cell(table.minSlot === null ? "—" : String(table.minSlot), false));
|
row.appendChild(cell(resource.minSlot === null ? "—" : String(resource.minSlot), false));
|
||||||
row.appendChild(cell(table.maxSlot === null ? "—" : String(table.maxSlot), false));
|
row.appendChild(cell(resource.maxSlot === null ? "—" : String(resource.maxSlot), false));
|
||||||
row.appendChild(cell(table.latestCreatedAt ?? "—", false));
|
row.appendChild(cell(resource.latestCreatedAt ?? "—", false));
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
}
|
}
|
||||||
const tableElement = document.querySelector<HTMLTableElement>(tableSelector);
|
const tableElement = document.querySelector<HTMLTableElement>(tableSelector);
|
||||||
@@ -48,12 +48,12 @@ export function renderTables(tableSelector: string, bodySelector: string, tables
|
|||||||
lengthMenu: [10, 25, 50, 100],
|
lengthMenu: [10, 25, 50, 100],
|
||||||
order: [[0, "asc"]],
|
order: [[0, "asc"]],
|
||||||
language: {
|
language: {
|
||||||
emptyTable: "Aucune table disponible",
|
emptyTable: "Aucune ressource disponible",
|
||||||
info: "Tables _START_ à _END_ sur _TOTAL_",
|
info: "Ressources _START_ à _END_ sur _TOTAL_",
|
||||||
infoEmpty: "Aucune table",
|
infoEmpty: "Aucune ressource",
|
||||||
lengthMenu: "Afficher _MENU_ tables",
|
lengthMenu: "Afficher _MENU_ ressources",
|
||||||
search: "Rechercher :",
|
search: "Rechercher :",
|
||||||
zeroRecords: "Aucune table correspondante",
|
zeroRecords: "Aucune ressource correspondante",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
855
kb-app-demo-desktop/frontend/ts/demo_wallet.ts
Normal file
855
kb-app-demo-desktop/frontend/ts/demo_wallet.ts
Normal 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();
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/frontend/ts/main.ts
|
// file: kb-app-demo-desktop/frontend/ts/main.ts
|
||||||
// version: 6
|
// version: 8
|
||||||
|
|
||||||
import * as bootstrap from "bootstrap";
|
import * as bootstrap from "bootstrap";
|
||||||
import MarkdownIt from "markdown-it";
|
import MarkdownIt from "markdown-it";
|
||||||
@@ -44,6 +44,10 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
void openDemoConfigWindow();
|
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");
|
frontendDebug("kb-app-demo-desktop.frontend.main", "main window loaded");
|
||||||
const readmeContent = document.querySelector<HTMLElement>("#readmeContent");
|
const readmeContent = document.querySelector<HTMLElement>("#readmeContent");
|
||||||
if (readmeContent) {
|
if (readmeContent) {
|
||||||
@@ -93,10 +97,10 @@ for (const [elementId, command] of pipelineDemoLinks) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sqlDemoLinks: ReadonlyArray<readonly [string, string]> = [
|
const sqlDemoLinks: ReadonlyArray<readonly [string, string]> = [
|
||||||
["openDemoSqlDiagLink", "open_demo_sql_diag_window"],
|
["openDemoStoreDiagLink", "open_demo_store_diag_window"],
|
||||||
["openDemoSqlPgRawLink", "open_demo_sql_pg_raw_window"],
|
["openDemoStoreRawLink", "open_demo_store_raw_window"],
|
||||||
["openDemoSqlPgCoreLink", "open_demo_sql_pg_core_window"],
|
["openDemoStoreCoreLink", "open_demo_store_core_window"],
|
||||||
["openDemoSqlReplayCandidatesLink", "open_demo_sql_replay_candidates_window"],
|
["openDemoStoreReplayCandidatesLink", "open_demo_store_replay_candidates_window"],
|
||||||
];
|
];
|
||||||
for (const [elementId, command] of sqlDemoLinks) {
|
for (const [elementId, command] of sqlDemoLinks) {
|
||||||
const link = document.querySelector<HTMLAnchorElement>(`#${elementId}`);
|
const link = document.querySelector<HTMLAnchorElement>(`#${elementId}`);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "kb-app-demo-desktop",
|
"name": "kb-app-demo-desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.5.1",
|
"version": "0.5.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/app_state.rs
|
// file: kb-app-demo-desktop/src/app_state.rs
|
||||||
// version: 16
|
// version: 21
|
||||||
|
|
||||||
//! Shared Tauri application state and startup initialization.
|
//! Shared Tauri application state and startup initialization.
|
||||||
|
|
||||||
@@ -17,6 +17,13 @@ pub(crate) struct AppState {
|
|||||||
active_profile: ks_config::ProfileConfig,
|
active_profile: ks_config::ProfileConfig,
|
||||||
logging_guard: std::sync::Mutex<ks_logging::LoggingGuard>,
|
logging_guard: std::sync::Mutex<ks_logging::LoggingGuard>,
|
||||||
http_pool: ks_onchain_transport::HttpEndpointPool,
|
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:
|
ws_pool:
|
||||||
std::sync::Mutex<std::option::Option<std::sync::Arc<ks_onchain_transport::WsEndpointPool>>>,
|
std::sync::Mutex<std::option::Option<std::sync::Arc<ks_onchain_transport::WsEndpointPool>>>,
|
||||||
demo_ws_session:
|
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_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_running: std::sync::atomic::AtomicBool,
|
||||||
demo_execution_solana_core_cancel_requested: 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,
|
startup_sequence_started: std::sync::atomic::AtomicBool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +113,8 @@ impl crate::AppState {
|
|||||||
active_profile,
|
active_profile,
|
||||||
logging_guard: std::sync::Mutex::new(logging_guard),
|
logging_guard: std::sync::Mutex::new(logging_guard),
|
||||||
http_pool,
|
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),
|
ws_pool: std::sync::Mutex::new(std::option::Option::None),
|
||||||
demo_ws_session: tokio::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),
|
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_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_running: std::sync::atomic::AtomicBool::new(false),
|
||||||
demo_execution_solana_core_cancel_requested: 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),
|
startup_sequence_started: std::sync::atomic::AtomicBool::new(false),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -169,6 +183,122 @@ impl crate::AppState {
|
|||||||
return &self.active_profile;
|
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.
|
/// Returns the configured HTTP endpoint pool.
|
||||||
pub(crate) fn http_pool(&self) -> &ks_onchain_transport::HttpEndpointPool {
|
pub(crate) fn http_pool(&self) -> &ks_onchain_transport::HttpEndpointPool {
|
||||||
return &self.http_pool;
|
return &self.http_pool;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/demo_backfill.rs
|
// file: kb-app-demo-desktop/src/demo_backfill.rs
|
||||||
// version: 16
|
// version: 17
|
||||||
|
|
||||||
//! Tauri commands and UI payloads for bounded HTTP transaction backfills.
|
//! 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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
let profile = state.active_profile();
|
let store = match crate::shared_store(state.inner()).await {
|
||||||
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,
|
|
||||||
) {
|
|
||||||
std::result::Result::Ok(value) => value,
|
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 {
|
let observer = crate::DemoBackfillObserver {
|
||||||
app_handle,
|
app_handle,
|
||||||
cancel_requested: state.demo_backfill_cancel_requested(),
|
cancel_requested: state.demo_backfill_cancel_requested(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/demo_config.rs
|
// file: kb-app-demo-desktop/src/demo_config.rs
|
||||||
// version: 5
|
// version: 8
|
||||||
|
|
||||||
//! Configuration demo payloads with explicit public and bounded diagnostic projections.
|
//! Configuration demo payloads with explicit public and bounded diagnostic projections.
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ pub(crate) struct DemoConfigEndpointDiagnosticPayload {
|
|||||||
pub(crate) role_count: u32,
|
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)]
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
#[ts(
|
#[ts(
|
||||||
export,
|
export,
|
||||||
@@ -173,23 +173,16 @@ pub(crate) struct DemoConfigEndpointDiagnosticPayload {
|
|||||||
pub(crate) struct DemoConfigStoreDiagnosticPayload {
|
pub(crate) struct DemoConfigStoreDiagnosticPayload {
|
||||||
/// Whether persistence is enabled.
|
/// Whether persistence is enabled.
|
||||||
pub(crate) enabled: bool,
|
pub(crate) enabled: bool,
|
||||||
/// Selected store backend.
|
/// Selected store backend code.
|
||||||
pub(crate) backend: std::string::String,
|
pub(crate) backend: std::string::String,
|
||||||
/// Whether a PostgreSQL URL is configured.
|
/// Whether backend-specific configuration validated successfully.
|
||||||
pub(crate) postgres_url_state: std::string::String,
|
pub(crate) configuration_status: std::string::String,
|
||||||
/// PostgreSQL connection pool ceiling.
|
/// Whether a backend connection or location value is configured.
|
||||||
pub(crate) postgres_max_connections: u32,
|
pub(crate) connection_state: std::string::String,
|
||||||
/// PostgreSQL connection timeout in milliseconds.
|
/// Whether automatic store initialization is enabled.
|
||||||
#[ts(type = "number")]
|
pub(crate) auto_initialize_schema: bool,
|
||||||
pub(crate) postgres_connect_timeout_ms: u64,
|
/// Number of opaque backend options supplied after composition.
|
||||||
/// Whether PostgreSQL schema initialization is enabled.
|
pub(crate) backend_option_count: u32,
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wallet diagnostic projection without paths or secret key material.
|
/// 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,
|
pub(crate) wallet_directory_state: std::string::String,
|
||||||
/// Wallet cluster.
|
/// Wallet cluster.
|
||||||
pub(crate) cluster: std::string::String,
|
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.
|
/// Whether the managed temporary wallet is enabled.
|
||||||
pub(crate) temporary_wallet_enabled: bool,
|
pub(crate) temporary_wallet_enabled: bool,
|
||||||
/// Temporary wallet alias, which is internal but not secret.
|
/// 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()),
|
source_diagnostic("execution", state.execution_config_path()),
|
||||||
],
|
],
|
||||||
endpoints,
|
endpoints,
|
||||||
store: DemoConfigStoreDiagnosticPayload {
|
store: store_diagnostic(profile),
|
||||||
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,
|
|
||||||
},
|
|
||||||
wallet: DemoConfigWalletDiagnosticPayload {
|
wallet: DemoConfigWalletDiagnosticPayload {
|
||||||
wallet_directory_state: configured_state(profile.wallet.wallet_dir.as_str()),
|
wallet_directory_state: configured_state(profile.wallet.wallet_dir.as_str()),
|
||||||
cluster: profile.wallet.cluster.clone(),
|
cluster: profile.wallet.cluster.clone(),
|
||||||
|
wallet_alias: profile.wallet.wallet_alias.clone(),
|
||||||
temporary_wallet_enabled: profile.wallet.temporary_wallet_enabled,
|
temporary_wallet_enabled: profile.wallet.temporary_wallet_enabled,
|
||||||
temporary_wallet_alias: profile.wallet.temporary_wallet_alias.clone(),
|
temporary_wallet_alias: profile.wallet.temporary_wallet_alias.clone(),
|
||||||
temporary_wallet_persist: profile.wallet.temporary_wallet_persist,
|
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 {
|
fn source_diagnostic(category: &str, path: &str) -> DemoConfigSourceDiagnosticPayload {
|
||||||
return DemoConfigSourceDiagnosticPayload {
|
return DemoConfigSourceDiagnosticPayload {
|
||||||
category: category.to_string(),
|
category: category.to_string(),
|
||||||
@@ -500,9 +535,19 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn public_and_diagnostic_projections_never_serialize_secret_canaries() {
|
fn public_and_diagnostic_projections_never_serialize_secret_canaries() {
|
||||||
let mut profile = active_fixture_profile();
|
let mut profile = active_fixture_profile();
|
||||||
profile.database.postgres.url =
|
if let std::option::Option::Some(options) = profile.database.backend_options.as_object_mut()
|
||||||
"postgres://operator:POSTGRES-SECRET-CANARY@localhost/solana".to_string();
|
{
|
||||||
profile.database.sqlite.path = "/private/SQLITE-PATH-CANARY.sqlite".to_string();
|
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();
|
profile.wallet.wallet_dir = "/private/WALLET-PATH-CANARY".to_string();
|
||||||
if let std::option::Option::Some(endpoint) = profile.solana.http_endpoints.first_mut() {
|
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();
|
endpoint.url = "https://provider.invalid/?api-key=HELIUS-SECRET-CANARY".to_string();
|
||||||
@@ -533,20 +578,11 @@ mod tests {
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
.collect::<std::vec::Vec<super::DemoConfigEndpointDiagnosticPayload>>(),
|
.collect::<std::vec::Vec<super::DemoConfigEndpointDiagnosticPayload>>(),
|
||||||
store: super::DemoConfigStoreDiagnosticPayload {
|
store: super::store_diagnostic(&profile),
|
||||||
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,
|
|
||||||
},
|
|
||||||
wallet: super::DemoConfigWalletDiagnosticPayload {
|
wallet: super::DemoConfigWalletDiagnosticPayload {
|
||||||
wallet_directory_state: super::configured_state(profile.wallet.wallet_dir.as_str()),
|
wallet_directory_state: super::configured_state(profile.wallet.wallet_dir.as_str()),
|
||||||
cluster: profile.wallet.cluster.clone(),
|
cluster: profile.wallet.cluster.clone(),
|
||||||
|
wallet_alias: profile.wallet.wallet_alias.clone(),
|
||||||
temporary_wallet_enabled: profile.wallet.temporary_wallet_enabled,
|
temporary_wallet_enabled: profile.wallet.temporary_wallet_enabled,
|
||||||
temporary_wallet_alias: profile.wallet.temporary_wallet_alias.clone(),
|
temporary_wallet_alias: profile.wallet.temporary_wallet_alias.clone(),
|
||||||
temporary_wallet_persist: profile.wallet.temporary_wallet_persist,
|
temporary_wallet_persist: profile.wallet.temporary_wallet_persist,
|
||||||
@@ -559,7 +595,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
for secret in [
|
for secret in [
|
||||||
"POSTGRES-SECRET-CANARY",
|
"POSTGRES-SECRET-CANARY",
|
||||||
"SQLITE-PATH-CANARY",
|
"STORE-PATH-CANARY",
|
||||||
"WALLET-PATH-CANARY",
|
"WALLET-PATH-CANARY",
|
||||||
"HELIUS-SECRET-CANARY",
|
"HELIUS-SECRET-CANARY",
|
||||||
"WS-SECRET-CANARY",
|
"WS-SECRET-CANARY",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/demo_core_extraction.rs
|
// 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.
|
//! 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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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 {
|
let store = match store_result {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/demo_decode_replay.rs
|
// file: kb-app-demo-desktop/src/demo_decode_replay.rs
|
||||||
// version: 40
|
// version: 46
|
||||||
|
|
||||||
//! Tauri commands and UI payloads for contextual instruction decode replay.
|
//! 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"
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeDiagnosticsPayload.ts"
|
||||||
)]
|
)]
|
||||||
pub(crate) struct DemoDecodeDiagnosticsPayload {
|
pub(crate) struct DemoDecodeDiagnosticsPayload {
|
||||||
/// Decode, materialization and ledger table diagnostics.
|
/// Decode, materialization and ledger resource diagnostics.
|
||||||
pub(crate) tables: std::vec::Vec<crate::DemoSqlTableSnapshot>,
|
pub(crate) resources: std::vec::Vec<crate::DemoStoreResourceSnapshot>,
|
||||||
/// Aggregated declared and observed coverage.
|
/// Aggregated declared and observed coverage.
|
||||||
pub(crate) coverage: std::vec::Vec<crate::DemoDecodeCoverageSummaryPayload>,
|
pub(crate) coverage: std::vec::Vec<crate::DemoDecodeCoverageSummaryPayload>,
|
||||||
}
|
}
|
||||||
@@ -271,8 +271,10 @@ pub(crate) struct DemoDecodeDiagnosticsPayload {
|
|||||||
pub(crate) struct DemoTransactionAnnotationRequest {
|
pub(crate) struct DemoTransactionAnnotationRequest {
|
||||||
/// Optional partial transaction signature.
|
/// Optional partial transaction signature.
|
||||||
pub(crate) signature_contains: std::option::Option<std::string::String>,
|
pub(crate) signature_contains: std::option::Option<std::string::String>,
|
||||||
/// Maximum returned rows.
|
/// Opaque cursor identifying the requested Store block.
|
||||||
pub(crate) limit: u32,
|
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.
|
/// UI-safe committed transaction annotation row.
|
||||||
@@ -313,6 +315,20 @@ pub(crate) struct DemoTransactionAnnotationRow {
|
|||||||
pub(crate) updated_at: std::string::String,
|
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) struct DemoDecodeReplayObserver<'a> {
|
||||||
pub(crate) app_handle: tauri::AppHandle,
|
pub(crate) app_handle: tauri::AppHandle,
|
||||||
pub(crate) campaign_id: std::string::String,
|
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,
|
materialize_after_decode = pipeline_request.materialize_after_decode,
|
||||||
"start contextual decode replay pipeline"
|
"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 {
|
let store = match store_result {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => {
|
std::result::Result::Err(error) => {
|
||||||
@@ -563,24 +579,24 @@ pub(crate) async fn demo_decode_replay_diagnostics(
|
|||||||
state: tauri::State<'_, crate::AppState>,
|
state: tauri::State<'_, crate::AppState>,
|
||||||
) -> std::result::Result<crate::DemoDecodeDiagnosticsPayload, std::string::String> {
|
) -> 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");
|
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 {
|
let store = match store_result {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
let table_result = store.known_table_diagnostics().await;
|
let resource_result = store.known_resource_diagnostics().await;
|
||||||
let all_tables = match table_result {
|
let all_resources = match resource_result {
|
||||||
std::result::Result::Ok(value) => value,
|
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.to_string()),
|
||||||
};
|
};
|
||||||
let selected_tables: std::vec::Vec<crate::DemoSqlTableSnapshot> = all_tables
|
let selected_resources: std::vec::Vec<crate::DemoStoreResourceSnapshot> = all_resources
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|table| {
|
.filter(|resource| {
|
||||||
return table.table_name.starts_with("kb_sol_decode_")
|
return resource.model_code == "processing"
|
||||||
|| table.table_name.starts_with("kb_sol_mat_")
|
|| resource.model_code == "decode"
|
||||||
|| table.table_name == "kb_sol_ops_processing_ledger";
|
|| resource.model_code == "materialization";
|
||||||
})
|
})
|
||||||
.map(crate::table_snapshot_from_pg)
|
.map(crate::store_resource_snapshot)
|
||||||
.collect();
|
.collect();
|
||||||
let coverage_result = ks_store::DecodePipelineStore::list_decode_coverage_summary(
|
let coverage_result = ks_store::DecodePipelineStore::list_decode_coverage_summary(
|
||||||
&store,
|
&store,
|
||||||
@@ -593,9 +609,9 @@ pub(crate) async fn demo_decode_replay_diagnostics(
|
|||||||
std::result::Result::Ok(value) => value,
|
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.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 {
|
return std::result::Result::Ok(crate::DemoDecodeDiagnosticsPayload {
|
||||||
tables: selected_tables,
|
resources: selected_resources,
|
||||||
coverage: coverage.into_iter().map(coverage_payload).collect(),
|
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(
|
pub(crate) async fn demo_decode_replay_annotations(
|
||||||
state: tauri::State<'_, crate::AppState>,
|
state: tauri::State<'_, crate::AppState>,
|
||||||
request: crate::DemoTransactionAnnotationRequest,
|
request: crate::DemoTransactionAnnotationRequest,
|
||||||
) -> std::result::Result<std::vec::Vec<crate::DemoTransactionAnnotationRow>, std::string::String> {
|
) -> std::result::Result<crate::DemoTransactionAnnotationPagePayload, std::string::String> {
|
||||||
let filter_result = ks_store::MaterializedEventFilter::new(
|
let filter = ks_store::MaterializedOutputPageFilter::new(
|
||||||
std::option::Option::Some("materializer.transaction.annotations".to_string()),
|
std::option::Option::Some("materializer.transaction.annotations".to_string()),
|
||||||
std::option::Option::Some("transaction_annotation".to_string()),
|
std::option::Option::Some("transaction_annotation".to_string()),
|
||||||
|
std::option::Option::Some("spl.memo".to_string()),
|
||||||
request.signature_contains,
|
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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
tracing::debug!(target: crate::TRACING_TARGET, action = "load_transaction_annotations", signature_contains = ?filter.signature_contains, limit = filter.limit, "load bounded committed transaction annotation journal");
|
tracing::debug!(target: crate::TRACING_TARGET, action = "load_transaction_annotations", block_index = request.block_index, "load committed transaction annotation Store block");
|
||||||
let store_result = crate::connect_postgres_store(state.active_profile()).await;
|
let store_result = crate::shared_store(state.inner()).await;
|
||||||
let store = match store_result {
|
let store = match store_result {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
let rows_result =
|
let page_result = store.materialized_output_page(&filter, &page_request).await;
|
||||||
ks_store::DecodePipelineStore::list_materialized_events(&store, &filter).await;
|
let page = match page_result {
|
||||||
let rows = match rows_result {
|
|
||||||
std::result::Result::Ok(value) => value,
|
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.to_string()),
|
||||||
};
|
};
|
||||||
let mut output = std::vec::Vec::with_capacity(rows.len());
|
let metadata_result =
|
||||||
for row in rows {
|
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);
|
let mapped = annotation_payload(row);
|
||||||
match mapped {
|
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),
|
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");
|
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(output);
|
return std::result::Result::Ok(crate::DemoTransactionAnnotationPagePayload {
|
||||||
|
page: metadata,
|
||||||
|
rows,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn register_active_campaign(
|
fn register_active_campaign(
|
||||||
@@ -882,7 +908,7 @@ fn coverage_payload(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn annotation_payload(
|
fn annotation_payload(
|
||||||
row: ks_store::MaterializedEventQueryRow,
|
row: ks_store::MaterializedOutputQueryRow,
|
||||||
) -> std::result::Result<crate::DemoTransactionAnnotationRow, std::string::String> {
|
) -> std::result::Result<crate::DemoTransactionAnnotationRow, std::string::String> {
|
||||||
if row.processor_name != "materializer.transaction.annotations"
|
if row.processor_name != "materializer.transaction.annotations"
|
||||||
|| row.materialized_family != "transaction_annotation"
|
|| row.materialized_family != "transaction_annotation"
|
||||||
@@ -1274,7 +1300,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn committed_annotation_row_is_mapped_to_ui_safe_contract() {
|
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_name: "materializer.transaction.annotations".to_string(),
|
||||||
processor_version: "0.4.3".to_string(),
|
processor_version: "0.4.3".to_string(),
|
||||||
input_key: "input".to_string(),
|
input_key: "input".to_string(),
|
||||||
@@ -1310,7 +1336,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn malformed_annotation_payload_fails_closed() {
|
fn malformed_annotation_payload_fails_closed() {
|
||||||
let row = ks_store::MaterializedEventQueryRow {
|
let row = ks_store::MaterializedOutputQueryRow {
|
||||||
processor_name: "materializer.transaction.annotations".to_string(),
|
processor_name: "materializer.transaction.annotations".to_string(),
|
||||||
processor_version: "0.4.3".to_string(),
|
processor_version: "0.4.3".to_string(),
|
||||||
input_key: "input".to_string(),
|
input_key: "input".to_string(),
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
// file: kb-app-demo-desktop/src/demo_devnet_common.rs
|
// 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
|
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)]
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(
|
#[ts(
|
||||||
@@ -15,29 +17,81 @@ use ts_rs::TS; // rust-rules: derive-import
|
|||||||
pub(crate) struct DemoExecutionDevnetStoreReadinessPayload {
|
pub(crate) struct DemoExecutionDevnetStoreReadinessPayload {
|
||||||
/// Selected Devnet profile.
|
/// Selected Devnet profile.
|
||||||
pub(crate) profile_name: std::string::String,
|
pub(crate) profile_name: std::string::String,
|
||||||
|
/// Selected backend code.
|
||||||
|
pub(crate) backend: std::string::String,
|
||||||
/// Whether schema creation was allowed by configuration.
|
/// Whether schema creation was allowed by configuration.
|
||||||
pub(crate) auto_initialize_schema: bool,
|
pub(crate) auto_initialize_schema: bool,
|
||||||
/// Number of known tables already present before preparation.
|
/// Number of logical resources already present before preparation.
|
||||||
pub(crate) existing_tables_before: u32,
|
pub(crate) available_resources_before: u32,
|
||||||
/// Number of known tables created during preparation.
|
/// Number of logical resources created during preparation.
|
||||||
pub(crate) created_tables: u32,
|
pub(crate) created_resources: u32,
|
||||||
/// Number of known tables present after preparation.
|
/// Number of logical resources present after preparation.
|
||||||
pub(crate) existing_tables_after: u32,
|
pub(crate) available_resources_after: u32,
|
||||||
/// Total number of known tables expected by the current store.
|
/// Total number of logical resources expected by the current store contract.
|
||||||
pub(crate) expected_tables: u32,
|
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.
|
/// 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,
|
readiness: ks_pipeline_demo_scenarios::DevnetProfileStoreReadiness,
|
||||||
) -> DemoExecutionDevnetStoreReadinessPayload {
|
) -> DemoExecutionDevnetStoreReadinessPayload {
|
||||||
return DemoExecutionDevnetStoreReadinessPayload {
|
return DemoExecutionDevnetStoreReadinessPayload {
|
||||||
profile_name: readiness.profile_name,
|
profile_name: readiness.profile_name,
|
||||||
|
backend: readiness.backend,
|
||||||
auto_initialize_schema: readiness.auto_initialize_schema,
|
auto_initialize_schema: readiness.auto_initialize_schema,
|
||||||
existing_tables_before: bounded_u32(readiness.existing_tables_before),
|
available_resources_before: bounded_u32(readiness.available_resources_before),
|
||||||
created_tables: bounded_u32(readiness.created_tables),
|
created_resources: bounded_u32(readiness.created_resources),
|
||||||
existing_tables_after: bounded_u32(readiness.existing_tables_after),
|
available_resources_after: bounded_u32(readiness.available_resources_after),
|
||||||
expected_tables: bounded_u32(readiness.expected_tables),
|
expected_resources: bounded_u32(readiness.expected_resources),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,3 +127,58 @@ fn bounded_u32(value: usize) -> u32 {
|
|||||||
std::result::Result::Err(_) => u32::MAX,
|
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()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/demo_execution_metadata_metaplex_token_metadata.rs
|
// 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.
|
//! 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::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.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::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.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(
|
let mut execution_request = match ks_pipeline_demo_scenarios::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json(
|
||||||
request.intent_id,
|
request.intent_id,
|
||||||
request.operation_json.as_str(),
|
request.operation_json.as_str(),
|
||||||
@@ -235,22 +232,26 @@ pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_execute(
|
|||||||
app_handle,
|
app_handle,
|
||||||
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
||||||
};
|
};
|
||||||
let workspace_root = crate::workspace_root_dir();
|
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
|
||||||
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
|
|
||||||
{
|
|
||||||
std::result::Result::Ok(value) => value,
|
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 =
|
let transaction_signature =
|
||||||
summary.send_result.as_ref().map(|value| return value.signature.0.clone());
|
summary.send_result.as_ref().map(|value| return value.signature.0.clone());
|
||||||
let confirmation_status =
|
let confirmation_status =
|
||||||
@@ -417,7 +418,7 @@ async fn prepare_create_fixture_for_family(
|
|||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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() {
|
let wallet_dir = if configured.is_absolute() {
|
||||||
configured
|
configured
|
||||||
} else {
|
} else {
|
||||||
@@ -1040,15 +1041,13 @@ pub(crate) async fn demo_execution_metadata_metaplex_execute_campaign(
|
|||||||
std::result::Result::Ok(value) => value,
|
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.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::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.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 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() {
|
let wallet_dir = if configured_wallet_dir.is_absolute() {
|
||||||
configured_wallet_dir
|
configured_wallet_dir
|
||||||
} else {
|
} else {
|
||||||
@@ -1070,8 +1069,8 @@ pub(crate) async fn demo_execution_metadata_metaplex_execute_campaign(
|
|||||||
wallet_dir,
|
wallet_dir,
|
||||||
&http_pool,
|
&http_pool,
|
||||||
&store,
|
&store,
|
||||||
|
&state,
|
||||||
&profile,
|
&profile,
|
||||||
workspace_root.as_path(),
|
|
||||||
&observer,
|
&observer,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -1133,8 +1132,8 @@ fn execute_campaign_projection<'a, S, O>(
|
|||||||
wallet_dir: std::path::PathBuf,
|
wallet_dir: std::path::PathBuf,
|
||||||
http_pool: &'a ks_onchain_transport::HttpEndpointPool,
|
http_pool: &'a ks_onchain_transport::HttpEndpointPool,
|
||||||
store: &'a S,
|
store: &'a S,
|
||||||
|
state: &'a crate::AppState,
|
||||||
profile: &'a ks_config::ProfileConfig,
|
profile: &'a ks_config::ProfileConfig,
|
||||||
workspace_root: &'a std::path::Path,
|
|
||||||
observer: &'a O,
|
observer: &'a O,
|
||||||
) -> CampaignProjectionFuture<'a>
|
) -> CampaignProjectionFuture<'a>
|
||||||
where
|
where
|
||||||
@@ -1150,8 +1149,8 @@ where
|
|||||||
wallet_dir,
|
wallet_dir,
|
||||||
http_pool,
|
http_pool,
|
||||||
store,
|
store,
|
||||||
|
state,
|
||||||
profile,
|
profile,
|
||||||
workspace_root,
|
|
||||||
observer,
|
observer,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -1161,8 +1160,8 @@ async fn execute_campaign_projection_unboxed<S, O>(
|
|||||||
wallet_dir: std::path::PathBuf,
|
wallet_dir: std::path::PathBuf,
|
||||||
http_pool: &ks_onchain_transport::HttpEndpointPool,
|
http_pool: &ks_onchain_transport::HttpEndpointPool,
|
||||||
store: &S,
|
store: &S,
|
||||||
|
state: &crate::AppState,
|
||||||
profile: &ks_config::ProfileConfig,
|
profile: &ks_config::ProfileConfig,
|
||||||
workspace_root: &std::path::Path,
|
|
||||||
observer: &O,
|
observer: &O,
|
||||||
) -> ks_core::Result<CampaignProjection>
|
) -> ks_core::Result<CampaignProjection>
|
||||||
where
|
where
|
||||||
@@ -1174,6 +1173,15 @@ where
|
|||||||
{
|
{
|
||||||
let base_options =
|
let base_options =
|
||||||
ks_pipeline_demo_scenarios::MetaplexCreateFixturePreparationOptions::new(wallet_dir);
|
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 {
|
return match campaign_id {
|
||||||
"create_mint_nft" => {
|
"create_mint_nft" => {
|
||||||
execute_create_mint_projection(
|
execute_create_mint_projection(
|
||||||
@@ -1184,7 +1192,7 @@ where
|
|||||||
http_pool,
|
http_pool,
|
||||||
store,
|
store,
|
||||||
profile,
|
profile,
|
||||||
workspace_root,
|
&execution_wallet,
|
||||||
observer,
|
observer,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -1198,7 +1206,7 @@ where
|
|||||||
http_pool,
|
http_pool,
|
||||||
store,
|
store,
|
||||||
profile,
|
profile,
|
||||||
workspace_root,
|
&execution_wallet,
|
||||||
observer,
|
observer,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -1212,7 +1220,7 @@ where
|
|||||||
http_pool,
|
http_pool,
|
||||||
store,
|
store,
|
||||||
profile,
|
profile,
|
||||||
workspace_root,
|
&execution_wallet,
|
||||||
observer,
|
observer,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -1226,7 +1234,7 @@ where
|
|||||||
http_pool,
|
http_pool,
|
||||||
store,
|
store,
|
||||||
profile,
|
profile,
|
||||||
workspace_root,
|
&execution_wallet,
|
||||||
observer,
|
observer,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -1240,17 +1248,17 @@ where
|
|||||||
http_pool,
|
http_pool,
|
||||||
store,
|
store,
|
||||||
profile,
|
profile,
|
||||||
workspace_root,
|
&execution_wallet,
|
||||||
observer,
|
observer,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
},
|
},
|
||||||
"collection_verify" => {
|
"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,
|
http_pool,
|
||||||
store,
|
store,
|
||||||
profile,
|
profile,
|
||||||
workspace_root,
|
&execution_wallet,
|
||||||
&base_options,
|
&base_options,
|
||||||
observer,
|
observer,
|
||||||
).await {
|
).await {
|
||||||
@@ -1285,11 +1293,11 @@ where
|
|||||||
},
|
},
|
||||||
"print_burn" => {
|
"print_burn" => {
|
||||||
let summary =
|
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,
|
http_pool,
|
||||||
store,
|
store,
|
||||||
profile,
|
profile,
|
||||||
workspace_root,
|
&execution_wallet,
|
||||||
&base_options,
|
&base_options,
|
||||||
observer,
|
observer,
|
||||||
)
|
)
|
||||||
@@ -1327,11 +1335,11 @@ where
|
|||||||
ks_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
|
ks_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
|
||||||
);
|
);
|
||||||
let summary =
|
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,
|
http_pool,
|
||||||
store,
|
store,
|
||||||
profile,
|
profile,
|
||||||
workspace_root,
|
&execution_wallet,
|
||||||
&options,
|
&options,
|
||||||
observer,
|
observer,
|
||||||
)
|
)
|
||||||
@@ -1361,11 +1369,11 @@ where
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
"escrow" => {
|
"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,
|
http_pool,
|
||||||
store,
|
store,
|
||||||
profile,
|
profile,
|
||||||
workspace_root,
|
&execution_wallet,
|
||||||
&base_options,
|
&base_options,
|
||||||
observer,
|
observer,
|
||||||
)
|
)
|
||||||
@@ -1410,11 +1418,11 @@ where
|
|||||||
ks_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft,
|
ks_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft,
|
||||||
);
|
);
|
||||||
let summary =
|
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,
|
http_pool,
|
||||||
store,
|
store,
|
||||||
profile,
|
profile,
|
||||||
workspace_root,
|
&execution_wallet,
|
||||||
&options,
|
&options,
|
||||||
observer,
|
observer,
|
||||||
)
|
)
|
||||||
@@ -1465,19 +1473,20 @@ where
|
|||||||
let options = base_options.clone().with_asset_family(
|
let options = base_options.clone().with_asset_family(
|
||||||
ks_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft,
|
ks_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft,
|
||||||
);
|
);
|
||||||
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_use_probe(
|
let summary =
|
||||||
http_pool,
|
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_use_probe_with_wallet(
|
||||||
store,
|
http_pool,
|
||||||
profile,
|
store,
|
||||||
workspace_root,
|
profile,
|
||||||
&options,
|
&execution_wallet,
|
||||||
observer,
|
&options,
|
||||||
)
|
observer,
|
||||||
.await
|
)
|
||||||
{
|
.await
|
||||||
std::result::Result::Ok(value) => value,
|
{
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Ok(value) => value,
|
||||||
};
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
let mut executions = vec![
|
let mut executions = vec![
|
||||||
metaplex_execution_json("fixture.create", &summary.fixture.create),
|
metaplex_execution_json("fixture.create", &summary.fixture.create),
|
||||||
metaplex_execution_json("fixture.mint", &summary.fixture.mint),
|
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,
|
http_pool: &ks_onchain_transport::HttpEndpointPool,
|
||||||
store: &S,
|
store: &S,
|
||||||
profile: &ks_config::ProfileConfig,
|
profile: &ks_config::ProfileConfig,
|
||||||
workspace_root: &std::path::Path,
|
wallet: &ks_pipeline_demo_scenarios::DevnetExecutionWallet,
|
||||||
observer: &O,
|
observer: &O,
|
||||||
) -> ks_core::Result<CampaignProjection>
|
) -> ks_core::Result<CampaignProjection>
|
||||||
where
|
where
|
||||||
@@ -1523,19 +1532,15 @@ where
|
|||||||
+ Sync,
|
+ Sync,
|
||||||
O: ks_pipeline_demo_scenarios::SolanaExecutionObserver,
|
O: ks_pipeline_demo_scenarios::SolanaExecutionObserver,
|
||||||
{
|
{
|
||||||
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_create_mint_campaign(
|
let summary =
|
||||||
http_pool,
|
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_create_mint_campaign_with_wallet(
|
||||||
store,
|
http_pool, store, profile, wallet, &options, observer,
|
||||||
profile,
|
)
|
||||||
workspace_root,
|
.await
|
||||||
&options,
|
{
|
||||||
observer,
|
std::result::Result::Ok(value) => value,
|
||||||
)
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
.await
|
};
|
||||||
{
|
|
||||||
std::result::Result::Ok(value) => value,
|
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
||||||
};
|
|
||||||
let executions = vec![
|
let executions = vec![
|
||||||
metaplex_execution_json("create", &summary.create),
|
metaplex_execution_json("create", &summary.create),
|
||||||
metaplex_execution_json("mint", &summary.mint),
|
metaplex_execution_json("mint", &summary.mint),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/demo_execution_metadata_solana_program.rs
|
// 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.
|
//! 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,
|
app_handle,
|
||||||
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
||||||
};
|
};
|
||||||
let workspace_root = crate::workspace_root_dir();
|
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
|
||||||
let summary = match ks_pipeline_demo_scenarios::execute_devnet_solana_program_metadata_campaign(
|
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,
|
&http_pool,
|
||||||
&profile,
|
&profile,
|
||||||
workspace_root.as_path(),
|
&execution_wallet,
|
||||||
&options,
|
&options,
|
||||||
&observer,
|
&observer,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/demo_execution_metadata_token_2022.rs
|
// 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.
|
//! 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::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.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::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.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 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() {
|
let wallet_dir = if configured_wallet_dir.is_absolute() {
|
||||||
configured_wallet_dir
|
configured_wallet_dir
|
||||||
} else {
|
} else {
|
||||||
@@ -109,19 +107,24 @@ pub(crate) async fn demo_execution_metadata_token_2022_execute_campaign(
|
|||||||
app_handle,
|
app_handle,
|
||||||
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
||||||
};
|
};
|
||||||
let summary = match ks_pipeline_demo_scenarios::execute_devnet_token_2022_metadata_campaign(
|
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
|
||||||
&http_pool,
|
|
||||||
&store,
|
|
||||||
&profile,
|
|
||||||
workspace_root.as_path(),
|
|
||||||
&options,
|
|
||||||
&observer,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
std::result::Result::Ok(value) => value,
|
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));
|
return std::result::Result::Ok(campaign_summary_payload(request.profile_name, summary));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/demo_execution_solana_core.rs
|
// file: kb-app-demo-desktop/src/demo_execution_solana_core.rs
|
||||||
// version: 17
|
// version: 26
|
||||||
|
|
||||||
//! Tauri adapter for bounded Solana Core execution on Devnet.
|
//! 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(
|
pub(crate) fn execution_cluster_code(
|
||||||
cluster: ks_lib::ExApiExecutionCluster,
|
cluster: ks_lib::ExApiExecutionCluster,
|
||||||
) -> std::string::String {
|
) -> 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.
|
/// Returns compatible Devnet profiles and conservative defaults.
|
||||||
pub(crate) fn demo_execution_solana_core_options(
|
pub(crate) fn demo_execution_solana_core_options(
|
||||||
state: tauri::State<'_, crate::AppState>,
|
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::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.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::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.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(
|
let mut pipeline_request = ks_pipeline_demo_scenarios::DevnetSystemTransferRequest::new(
|
||||||
format!("demo-execution-{}", chrono::Utc::now().timestamp_micros()),
|
format!("demo-execution-{}", chrono::Utc::now().timestamp_micros()),
|
||||||
ks_lib::MdPubkey(request.recipient.trim().to_string()),
|
ks_lib::MdPubkey(request.recipient.trim().to_string()),
|
||||||
@@ -509,12 +475,15 @@ pub(crate) async fn demo_execution_solana_core_execute(
|
|||||||
app_handle,
|
app_handle,
|
||||||
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
||||||
};
|
};
|
||||||
let workspace_root = crate::workspace_root_dir();
|
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
|
||||||
let summary = match ks_pipeline_demo_scenarios::execute_devnet_system_transfer(
|
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,
|
&http_pool,
|
||||||
&store,
|
&store,
|
||||||
&profile,
|
&profile,
|
||||||
workspace_root.as_path(),
|
&execution_wallet,
|
||||||
&pipeline_request,
|
&pipeline_request,
|
||||||
decoders.as_slice(),
|
decoders.as_slice(),
|
||||||
materializers.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::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.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::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.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(
|
let mut pipeline_request = ks_pipeline_demo_scenarios::DevnetMemoExecutionRequest::new(
|
||||||
format!("demo-memo-execution-{}", chrono::Utc::now().timestamp_micros()),
|
format!("demo-memo-execution-{}", chrono::Utc::now().timestamp_micros()),
|
||||||
request.message,
|
request.message,
|
||||||
@@ -582,12 +548,15 @@ pub(crate) async fn demo_execution_spl_memo_execute(
|
|||||||
app_handle,
|
app_handle,
|
||||||
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
||||||
};
|
};
|
||||||
let workspace_root = crate::workspace_root_dir();
|
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
|
||||||
let summary = match ks_pipeline_demo_scenarios::execute_devnet_memo(
|
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,
|
&http_pool,
|
||||||
&store,
|
&store,
|
||||||
&profile,
|
&profile,
|
||||||
workspace_root.as_path(),
|
&execution_wallet,
|
||||||
&pipeline_request,
|
&pipeline_request,
|
||||||
decoders.as_slice(),
|
decoders.as_slice(),
|
||||||
materializers.as_slice(),
|
materializers.as_slice(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/demo_spl_ata.rs
|
// 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.
|
//! 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>,
|
pub(crate) associated_token_account: std::option::Option<std::string::String>,
|
||||||
/// Optional exact materialized operation.
|
/// Optional exact materialized operation.
|
||||||
pub(crate) operation: std::option::Option<std::string::String>,
|
pub(crate) operation: std::option::Option<std::string::String>,
|
||||||
/// Maximum returned rows.
|
/// Opaque cursor identifying the requested Store block.
|
||||||
pub(crate) limit: u32,
|
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.
|
/// 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::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.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::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.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![
|
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::DcSplAssociatedTokenAccountDecoder),
|
||||||
std::sync::Arc::new(ks_lib::DcSplTokenDecoder),
|
std::sync::Arc::new(ks_lib::DcSplTokenDecoder),
|
||||||
@@ -279,21 +292,26 @@ pub(crate) async fn demo_execution_spl_ata_execute(
|
|||||||
app_handle,
|
app_handle,
|
||||||
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
||||||
};
|
};
|
||||||
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_associated_token_account(
|
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
|
||||||
&http_pool,
|
|
||||||
&store,
|
|
||||||
&profile,
|
|
||||||
crate::workspace_root_dir().as_path(),
|
|
||||||
&pipeline_request,
|
|
||||||
decoders.as_slice(),
|
|
||||||
materializers.as_slice(),
|
|
||||||
&observer,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
std::result::Result::Ok(value) => value,
|
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));
|
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(
|
pub(crate) async fn demo_spl_ata_journal(
|
||||||
state: tauri::State<'_, crate::AppState>,
|
state: tauri::State<'_, crate::AppState>,
|
||||||
request: crate::DemoSplAtaJournalRequest,
|
request: crate::DemoSplAtaJournalRequest,
|
||||||
) -> std::result::Result<std::vec::Vec<crate::DemoSplAtaJournalRow>, std::string::String> {
|
) -> std::result::Result<crate::DemoSplAtaJournalPagePayload, 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());
|
|
||||||
}
|
|
||||||
let profile =
|
let profile =
|
||||||
match crate::select_devnet_profile(state.app_config(), request.profile_name.as_str()) {
|
match crate::select_devnet_profile(state.app_config(), request.profile_name.as_str()) {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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::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.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::Some("materializer.token.accounts".to_string()),
|
||||||
std::option::Option::None,
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some("spl.associated_token_account".to_string()),
|
||||||
request.signature_contains.clone(),
|
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::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.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::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();
|
let rows = page.rows.into_iter().map(journal_row).collect();
|
||||||
for row in rows {
|
return std::result::Result::Ok(crate::DemoSplAtaJournalPagePayload { page: metadata, 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_profile_wallet(
|
async fn load_profile_wallet(
|
||||||
profile: &ks_config::ProfileConfig,
|
profile: &ks_config::ProfileConfig,
|
||||||
) -> std::result::Result<ks_wallet::TemporaryWallet, std::string::String> {
|
) -> 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() {
|
let directory = if configured.is_absolute() {
|
||||||
configured
|
configured
|
||||||
} else {
|
} else {
|
||||||
@@ -542,6 +568,7 @@ fn summary_payload(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
fn journal_matches(payload: &serde_json::Value, request: &crate::DemoSplAtaJournalRequest) -> bool {
|
fn journal_matches(payload: &serde_json::Value, request: &crate::DemoSplAtaJournalRequest) -> bool {
|
||||||
for (expected, key) in [
|
for (expected, key) in [
|
||||||
(&request.operation, "operation"),
|
(&request.operation, "operation"),
|
||||||
@@ -558,7 +585,7 @@ fn journal_matches(payload: &serde_json::Value, request: &crate::DemoSplAtaJourn
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn journal_row(row: ks_store::MaterializedEventQueryRow) -> crate::DemoSplAtaJournalRow {
|
fn journal_row(row: ks_store::MaterializedOutputQueryRow) -> crate::DemoSplAtaJournalRow {
|
||||||
let text = |key: &str| {
|
let text = |key: &str| {
|
||||||
return row
|
return row
|
||||||
.payload_json
|
.payload_json
|
||||||
@@ -603,7 +630,8 @@ mod tests {
|
|||||||
mint: std::option::Option::Some("mint111".to_string()),
|
mint: std::option::Option::Some("mint111".to_string()),
|
||||||
associated_token_account: std::option::Option::Some("ata111".to_string()),
|
associated_token_account: std::option::Option::Some("ata111".to_string()),
|
||||||
operation: std::option::Option::Some("create_idempotent".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(
|
assert!(super::journal_matches(
|
||||||
&serde_json::json!({
|
&serde_json::json!({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/demo_spl_token.rs
|
// 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.
|
//! 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"
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_token/DemoSplTokenJournalRequest.ts"
|
||||||
)]
|
)]
|
||||||
pub(crate) struct DemoSplTokenJournalRequest {
|
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,
|
pub(crate) profile_name: std::string::String,
|
||||||
/// Optional partial signature handled by the bounded store query.
|
/// Optional partial signature handled by the bounded store query.
|
||||||
pub(crate) signature_contains: std::option::Option<std::string::String>,
|
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>,
|
pub(crate) family: std::option::Option<std::string::String>,
|
||||||
/// Optional exact operation code without the `spl.token.` prefix.
|
/// Optional exact operation code without the `spl.token.` prefix.
|
||||||
pub(crate) operation: std::option::Option<std::string::String>,
|
pub(crate) operation: std::option::Option<std::string::String>,
|
||||||
/// Maximum number of rows returned after typed filtering.
|
/// Opaque cursor identifying the requested Store block.
|
||||||
pub(crate) limit: u32,
|
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.
|
/// 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::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.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::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.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 amount_raw = request.amount_raw.trim().to_string();
|
||||||
let operation = ks_lib::ExSplClassicTokenOperation::Instruction {
|
let operation = ks_lib::ExSplClassicTokenOperation::Instruction {
|
||||||
value: ks_lib::ExSplClassicTokenSingleOperation::TransferChecked {
|
value: ks_lib::ExSplClassicTokenSingleOperation::TransferChecked {
|
||||||
@@ -223,12 +236,15 @@ pub(crate) async fn demo_execution_spl_token_execute(
|
|||||||
app_handle,
|
app_handle,
|
||||||
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
||||||
};
|
};
|
||||||
let workspace_root = crate::workspace_root_dir();
|
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
|
||||||
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_token(
|
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,
|
&http_pool,
|
||||||
&store,
|
&store,
|
||||||
&profile,
|
&profile,
|
||||||
workspace_root.as_path(),
|
&execution_wallet,
|
||||||
&pipeline_request,
|
&pipeline_request,
|
||||||
decoders.as_slice(),
|
decoders.as_slice(),
|
||||||
materializers.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(
|
pub(crate) async fn demo_spl_token_journal(
|
||||||
state: tauri::State<'_, crate::AppState>,
|
state: tauri::State<'_, crate::AppState>,
|
||||||
request: crate::DemoSplTokenJournalRequest,
|
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) {
|
let validated = match validate_journal_request(request) {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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::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.to_string()),
|
||||||
};
|
};
|
||||||
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
|
let mut payload_match_groups = std::vec::Vec::new();
|
||||||
return std::result::Result::Err(error.to_string());
|
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 =
|
if let std::option::Option::Some(mint) = validated.mint.as_ref() {
|
||||||
validated.mint.is_some() || validated.account.is_some() || validated.operation.is_some();
|
payload_match_groups.push(vec![
|
||||||
let query_limit = if needs_payload_filter {
|
serde_json::json!({"mint": mint}),
|
||||||
ks_store::MAX_MATERIALIZED_EVENT_QUERY_ROWS
|
serde_json::json!({"accounts": [{"role": "mint", "accountKey": mint}]}),
|
||||||
} else {
|
]);
|
||||||
validated.limit
|
}
|
||||||
};
|
if let std::option::Option::Some(account) = validated.account.as_ref() {
|
||||||
let filter = match ks_store::MaterializedEventFilter::new(
|
payload_match_groups.push(vec![serde_json::json!({"accounts": [{"accountKey": account}]})]);
|
||||||
|
}
|
||||||
|
let filter = ks_store::MaterializedOutputPageFilter::new(
|
||||||
std::option::Option::None,
|
std::option::Option::None,
|
||||||
validated.family.clone(),
|
validated.family.clone(),
|
||||||
|
std::option::Option::Some("spl.token".to_string()),
|
||||||
validated.signature_contains.clone(),
|
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::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.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::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();
|
let rows = page.rows.into_iter().map(journal_row).collect();
|
||||||
for row in rows {
|
return std::result::Result::Ok(crate::DemoSplTokenJournalPagePayload { page: metadata, 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn summary_payload(
|
fn summary_payload(
|
||||||
@@ -415,12 +433,6 @@ fn replay_diagnostics(summary: &ks_pipeline::DecodeReplaySummary) -> serde_json:
|
|||||||
fn validate_journal_request(
|
fn validate_journal_request(
|
||||||
request: crate::DemoSplTokenJournalRequest,
|
request: crate::DemoSplTokenJournalRequest,
|
||||||
) -> std::result::Result<crate::DemoSplTokenJournalRequest, std::string::String> {
|
) -> 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() {
|
if request.profile_name.trim().is_empty() {
|
||||||
return std::result::Result::Err("journal profile name must not be empty".to_string());
|
return std::result::Result::Err("journal profile name must not be empty".to_string());
|
||||||
}
|
}
|
||||||
@@ -459,7 +471,8 @@ fn validate_journal_request(
|
|||||||
account,
|
account,
|
||||||
family,
|
family,
|
||||||
operation,
|
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);
|
return std::result::Result::Ok(trimmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
fn payload_matches(
|
fn payload_matches(
|
||||||
payload: &serde_json::Value,
|
payload: &serde_json::Value,
|
||||||
request: &crate::DemoSplTokenJournalRequest,
|
request: &crate::DemoSplTokenJournalRequest,
|
||||||
@@ -499,7 +513,7 @@ fn payload_matches(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn journal_row(row: ks_store::MaterializedEventQueryRow) -> DemoSplTokenJournalRow {
|
fn journal_row(row: ks_store::MaterializedOutputQueryRow) -> DemoSplTokenJournalRow {
|
||||||
let operation = row
|
let operation = row
|
||||||
.payload_json
|
.payload_json
|
||||||
.get("operation")
|
.get("operation")
|
||||||
@@ -589,7 +603,8 @@ mod tests {
|
|||||||
account: std::option::Option::Some("source111".to_string()),
|
account: std::option::Option::Some("source111".to_string()),
|
||||||
family: std::option::Option::Some("token_account".to_string()),
|
family: std::option::Option::Some("token_account".to_string()),
|
||||||
operation: std::option::Option::Some("transfer_checked".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!({
|
let payload = serde_json::json!({
|
||||||
"operation": "transfer_checked",
|
"operation": "transfer_checked",
|
||||||
@@ -605,7 +620,7 @@ mod tests {
|
|||||||
payload.get("amountRaw").and_then(serde_json::Value::as_str),
|
payload.get("amountRaw").and_then(serde_json::Value::as_str),
|
||||||
std::option::Option::Some("18446744073709551615")
|
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_name: "materializer.token.accounts".to_string(),
|
||||||
processor_version: "0.4.4".to_string(),
|
processor_version: "0.4.4".to_string(),
|
||||||
input_key: "input".to_string(),
|
input_key: "input".to_string(),
|
||||||
@@ -625,7 +640,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn journal_rejects_unbounded_or_unknown_family_requests() {
|
fn journal_rejects_unknown_family_requests() {
|
||||||
let request = crate::DemoSplTokenJournalRequest {
|
let request = crate::DemoSplTokenJournalRequest {
|
||||||
profile_name: "local_devnet".to_string(),
|
profile_name: "local_devnet".to_string(),
|
||||||
signature_contains: std::option::Option::None,
|
signature_contains: std::option::Option::None,
|
||||||
@@ -633,7 +648,8 @@ mod tests {
|
|||||||
account: std::option::Option::None,
|
account: std::option::Option::None,
|
||||||
family: std::option::Option::Some("fee".to_string()),
|
family: std::option::Option::Some("fee".to_string()),
|
||||||
operation: std::option::Option::None,
|
operation: std::option::Option::None,
|
||||||
limit: 501,
|
cursor: std::option::Option::None,
|
||||||
|
block_index: 1,
|
||||||
};
|
};
|
||||||
assert!(super::validate_journal_request(request).is_err());
|
assert!(super::validate_journal_request(request).is_err());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/demo_spl_token_2022.rs
|
// 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.
|
//! 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::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.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::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.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) {
|
let operation = match operation_from_request(&request) {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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,
|
app_handle,
|
||||||
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
||||||
};
|
};
|
||||||
let workspace_root = crate::workspace_root_dir();
|
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
|
||||||
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_token_2022(
|
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,
|
&http_pool,
|
||||||
&store,
|
&store,
|
||||||
&profile,
|
&profile,
|
||||||
workspace_root.as_path(),
|
&execution_wallet,
|
||||||
&pipeline_request,
|
&pipeline_request,
|
||||||
decoders.as_slice(),
|
decoders.as_slice(),
|
||||||
materializers.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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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() {
|
let wallet_dir = if configured.is_absolute() {
|
||||||
configured
|
configured
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
336
kb-app-demo-desktop/src/demo_store_common.rs
Normal file
336
kb-app-demo-desktop/src/demo_store_common.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
kb-app-demo-desktop/src/demo_store_core.rs
Normal file
45
kb-app-demo-desktop/src/demo_store_core.rs
Normal 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()),
|
||||||
|
});
|
||||||
|
}
|
||||||
81
kb-app-demo-desktop/src/demo_store_diag.rs
Normal file
81
kb-app-demo-desktop/src/demo_store_diag.rs
Normal 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()),
|
||||||
|
});
|
||||||
|
}
|
||||||
45
kb-app-demo-desktop/src/demo_store_raw.rs
Normal file
45
kb-app-demo-desktop/src/demo_store_raw.rs
Normal 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()),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// file: kb-app-demo-desktop/src/demo_sql_replay_candidates.rs
|
// file: kb-app-demo-desktop/src/demo_store_replay_candidates.rs
|
||||||
// version: 13
|
// 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
|
use ts_rs::TS; // rust-rules: derive-import
|
||||||
|
|
||||||
@@ -10,9 +10,9 @@ use ts_rs::TS; // rust-rules: derive-import
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(
|
#[ts(
|
||||||
export,
|
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.
|
/// Stable lower snake case program code.
|
||||||
pub(crate) code: std::string::String,
|
pub(crate) code: std::string::String,
|
||||||
/// Base58 Solana program identifier.
|
/// Base58 Solana program identifier.
|
||||||
@@ -24,17 +24,17 @@ pub(crate) struct DemoSqlReplayKnownProgramOption {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(
|
#[ts(
|
||||||
export,
|
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.
|
/// Active profile name.
|
||||||
pub(crate) active_profile_name: std::string::String,
|
pub(crate) active_profile_name: std::string::String,
|
||||||
/// Masked PostgreSQL DSN.
|
/// Safe backend connection descriptor.
|
||||||
pub(crate) masked_dsn: std::string::String,
|
pub(crate) connection_descriptor: std::string::String,
|
||||||
/// Maximum rows accepted by one query.
|
/// Fixed Store block size used by every replay-candidate listing.
|
||||||
pub(crate) maximum_limit: u32,
|
pub(crate) block_size: u16,
|
||||||
/// Program identifiers enumerable from `ks_program_ids`.
|
/// 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.
|
/// UI request for bounded transaction replay candidates.
|
||||||
@@ -42,9 +42,9 @@ pub(crate) struct DemoSqlReplayOptionsPayload {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(
|
#[ts(
|
||||||
export,
|
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.
|
/// Optional partial signature search.
|
||||||
pub(crate) signature_contains: std::option::Option<std::string::String>,
|
pub(crate) signature_contains: std::option::Option<std::string::String>,
|
||||||
/// Optional inclusive minimum slot.
|
/// Optional inclusive minimum slot.
|
||||||
@@ -59,14 +59,16 @@ pub(crate) struct DemoSqlReplayTransactionRequest {
|
|||||||
pub(crate) ledger_status: std::option::Option<std::string::String>,
|
pub(crate) ledger_status: std::option::Option<std::string::String>,
|
||||||
/// Optional exact program id.
|
/// Optional exact program id.
|
||||||
pub(crate) program_id: std::option::Option<std::string::String>,
|
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,
|
pub(crate) program_scope: std::string::String,
|
||||||
/// Optional entity kind: mint, owner or account_key.
|
/// Optional entity kind: mint, owner or account_key.
|
||||||
pub(crate) entity_kind: std::option::Option<std::string::String>,
|
pub(crate) entity_kind: std::option::Option<std::string::String>,
|
||||||
/// Optional exact entity value.
|
/// Optional exact entity value.
|
||||||
pub(crate) entity_value: std::option::Option<std::string::String>,
|
pub(crate) entity_value: std::option::Option<std::string::String>,
|
||||||
/// Maximum returned rows.
|
/// Opaque cursor identifying the requested Store block.
|
||||||
pub(crate) limit: u32,
|
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.
|
/// Orders newest slots first when true.
|
||||||
pub(crate) newest_first: bool,
|
pub(crate) newest_first: bool,
|
||||||
}
|
}
|
||||||
@@ -76,9 +78,9 @@ pub(crate) struct DemoSqlReplayTransactionRequest {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(
|
#[ts(
|
||||||
export,
|
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.
|
/// Canonical transaction signature.
|
||||||
pub(crate) signature: std::string::String,
|
pub(crate) signature: std::string::String,
|
||||||
/// Transaction slot.
|
/// Transaction slot.
|
||||||
@@ -100,13 +102,13 @@ pub(crate) struct DemoSqlReplayTransactionRow {
|
|||||||
pub(crate) attempt_count: i32,
|
pub(crate) attempt_count: i32,
|
||||||
/// Number of top-level instructions.
|
/// Number of top-level instructions.
|
||||||
#[ts(type = "number")]
|
#[ts(type = "number")]
|
||||||
pub(crate) outer_instruction_count: i64,
|
pub(crate) top_level_instruction_count: i64,
|
||||||
/// Number of inner instructions.
|
/// Number of inner instructions.
|
||||||
#[ts(type = "number")]
|
#[ts(type = "number")]
|
||||||
pub(crate) inner_instruction_count: i64,
|
pub(crate) inner_instruction_count: i64,
|
||||||
/// Number of distinct top-level programs.
|
/// Number of distinct top-level programs.
|
||||||
#[ts(type = "number")]
|
#[ts(type = "number")]
|
||||||
pub(crate) outer_program_count: i64,
|
pub(crate) top_level_program_count: i64,
|
||||||
/// Number of distinct inner programs.
|
/// Number of distinct inner programs.
|
||||||
#[ts(type = "number")]
|
#[ts(type = "number")]
|
||||||
pub(crate) inner_program_count: i64,
|
pub(crate) inner_program_count: i64,
|
||||||
@@ -114,18 +116,34 @@ pub(crate) struct DemoSqlReplayTransactionRow {
|
|||||||
pub(crate) updated_at: std::string::String,
|
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.
|
/// UI request for bounded program summaries.
|
||||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(
|
#[ts(
|
||||||
export,
|
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.
|
/// Optional partial program id search.
|
||||||
pub(crate) program_id_contains: std::option::Option<std::string::String>,
|
pub(crate) program_id_contains: std::option::Option<std::string::String>,
|
||||||
/// Maximum returned rows.
|
/// Opaque cursor identifying the requested Store block.
|
||||||
pub(crate) limit: u32,
|
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.
|
/// Aggregated program row shown by the replay candidate browser.
|
||||||
@@ -133,9 +151,9 @@ pub(crate) struct DemoSqlReplayProgramRequest {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(
|
#[ts(
|
||||||
export,
|
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`.
|
/// Optional stable code from `ks_program_ids`.
|
||||||
pub(crate) program_code: std::option::Option<std::string::String>,
|
pub(crate) program_code: std::option::Option<std::string::String>,
|
||||||
/// Program id.
|
/// Program id.
|
||||||
@@ -145,7 +163,7 @@ pub(crate) struct DemoSqlReplayProgramRow {
|
|||||||
pub(crate) transaction_count: i64,
|
pub(crate) transaction_count: i64,
|
||||||
/// Number of top-level instruction occurrences.
|
/// Number of top-level instruction occurrences.
|
||||||
#[ts(type = "number")]
|
#[ts(type = "number")]
|
||||||
pub(crate) outer_instruction_count: i64,
|
pub(crate) top_level_instruction_count: i64,
|
||||||
/// Number of inner instruction occurrences.
|
/// Number of inner instruction occurrences.
|
||||||
#[ts(type = "number")]
|
#[ts(type = "number")]
|
||||||
pub(crate) inner_instruction_count: i64,
|
pub(crate) inner_instruction_count: i64,
|
||||||
@@ -160,20 +178,36 @@ pub(crate) struct DemoSqlReplayProgramRow {
|
|||||||
pub(crate) max_slot: i64,
|
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.
|
/// UI request for bounded core entity summaries.
|
||||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(
|
#[ts(
|
||||||
export,
|
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.
|
/// Entity kind: mint, owner or account_key.
|
||||||
pub(crate) entity_kind: std::string::String,
|
pub(crate) entity_kind: std::string::String,
|
||||||
/// Optional partial entity value search.
|
/// Optional partial entity value search.
|
||||||
pub(crate) entity_value_contains: std::option::Option<std::string::String>,
|
pub(crate) entity_value_contains: std::option::Option<std::string::String>,
|
||||||
/// Maximum returned rows.
|
/// Opaque cursor identifying the requested Store block.
|
||||||
pub(crate) limit: u32,
|
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.
|
/// Aggregated core entity row shown by the replay candidate browser.
|
||||||
@@ -181,9 +215,9 @@ pub(crate) struct DemoSqlReplayEntityRequest {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(
|
#[ts(
|
||||||
export,
|
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.
|
/// Stable entity kind code.
|
||||||
pub(crate) entity_kind: std::string::String,
|
pub(crate) entity_kind: std::string::String,
|
||||||
/// Mint, owner or account-key address.
|
/// Mint, owner or account-key address.
|
||||||
@@ -202,8 +236,22 @@ pub(crate) struct DemoSqlReplayEntityRow {
|
|||||||
pub(crate) max_slot: i64,
|
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/`.
|
/// 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,
|
file_name: std::string::String,
|
||||||
content: std::string::String,
|
content: std::string::String,
|
||||||
) -> std::result::Result<std::string::String, 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);
|
return std::result::Result::Ok(display_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads static browser options from the active PostgreSQL profile.
|
/// Loads static browser options from the active store.
|
||||||
pub(crate) async fn demo_sql_replay_options(
|
pub(crate) async fn demo_store_replay_options(
|
||||||
state: tauri::State<'_, crate::AppState>,
|
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 profile = state.active_profile().clone();
|
||||||
let options_result = crate::postgres_store_options_from_profile(&profile, false);
|
let store = match crate::shared_store(state.inner()).await {
|
||||||
let options = match options_result {
|
|
||||||
std::result::Result::Ok(value) => value,
|
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 =
|
let mut known_programs =
|
||||||
std::vec::Vec::with_capacity(ks_program_ids::registered_program_ids().len());
|
std::vec::Vec::with_capacity(ks_program_ids::registered_program_ids().len());
|
||||||
for entry in ks_program_ids::registered_program_ids() {
|
for entry in ks_program_ids::registered_program_ids() {
|
||||||
known_programs.push(crate::DemoSqlReplayKnownProgramOption {
|
known_programs.push(crate::DemoStoreReplayKnownProgramOption {
|
||||||
code: entry.code().to_owned(),
|
code: entry.code().to_owned(),
|
||||||
program_id: entry.program_id().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,
|
active_profile_name: profile.name,
|
||||||
masked_dsn: options.masked_dsn(),
|
connection_descriptor,
|
||||||
maximum_limit: ks_store::MAX_REPLAY_CANDIDATE_ROWS,
|
block_size: crate::DEMO_STORE_BLOCK_SIZE,
|
||||||
known_programs,
|
known_programs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads bounded transaction replay candidates from PostgreSQL.
|
/// Loads bounded transaction replay candidates from the active store.
|
||||||
pub(crate) async fn load_demo_sql_replay_transactions(
|
pub(crate) async fn load_demo_store_replay_transactions(
|
||||||
state: tauri::State<'_, crate::AppState>,
|
state: tauri::State<'_, crate::AppState>,
|
||||||
request: crate::DemoSqlReplayTransactionRequest,
|
request: crate::DemoStoreReplayTransactionRequest,
|
||||||
) -> std::result::Result<std::vec::Vec<crate::DemoSqlReplayTransactionRow>, std::string::String> {
|
) -> std::result::Result<crate::DemoStoreReplayTransactionPagePayload, std::string::String> {
|
||||||
let program_scope_result = program_scope_from_code(request.program_scope.as_str());
|
let program_scope_result = program_scope_from_code(request.program_scope.as_str());
|
||||||
let program_scope = match program_scope_result {
|
let program_scope = match program_scope_result {
|
||||||
std::result::Result::Ok(value) => value,
|
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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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.signature_contains,
|
||||||
request.min_slot,
|
request.min_slot,
|
||||||
request.max_slot,
|
request.max_slot,
|
||||||
@@ -291,101 +342,138 @@ pub(crate) async fn load_demo_sql_replay_transactions(
|
|||||||
program_scope,
|
program_scope,
|
||||||
entity_kind,
|
entity_kind,
|
||||||
request.entity_value,
|
request.entity_value,
|
||||||
request.limit,
|
|
||||||
request.newest_first,
|
request.newest_first,
|
||||||
);
|
);
|
||||||
let filter = match filter_result {
|
let filter = match filter_result {
|
||||||
std::result::Result::Ok(value) => value,
|
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.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 {
|
let store = match store_result {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
let rows_result = store.replay_transaction_candidates(&filter).await;
|
let page_result = store.replay_transaction_candidates(&filter, &page_request).await;
|
||||||
let rows = match rows_result {
|
let page = match page_result {
|
||||||
std::result::Result::Ok(value) => value,
|
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.to_string()),
|
||||||
};
|
};
|
||||||
tracing::debug!(target: crate::TRACING_TARGET, rows = rows.len(), "loaded replay transaction candidates");
|
let metadata_result =
|
||||||
let mut output = std::vec::Vec::with_capacity(rows.len());
|
crate::demo_store_block_metadata(page.total_rows, page.next_cursor, request.block_index);
|
||||||
for row in rows {
|
let metadata = match metadata_result {
|
||||||
output.push(transaction_row_from_pg(row));
|
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.
|
/// Loads one counted Store block of program summaries.
|
||||||
pub(crate) async fn load_demo_sql_replay_programs(
|
pub(crate) async fn load_demo_store_replay_programs(
|
||||||
state: tauri::State<'_, crate::AppState>,
|
state: tauri::State<'_, crate::AppState>,
|
||||||
request: crate::DemoSqlReplayProgramRequest,
|
request: crate::DemoStoreReplayProgramRequest,
|
||||||
) -> std::result::Result<std::vec::Vec<crate::DemoSqlReplayProgramRow>, std::string::String> {
|
) -> std::result::Result<crate::DemoStoreReplayProgramPagePayload, std::string::String> {
|
||||||
let filter_result =
|
let filter_result = ks_store::ReplayProgramFilter::new(request.program_id_contains);
|
||||||
ks_store::PostgresReplayProgramFilter::new(request.program_id_contains, request.limit);
|
|
||||||
let filter = match filter_result {
|
let filter = match filter_result {
|
||||||
std::result::Result::Ok(value) => value,
|
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.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 {
|
let store = match store_result {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
let rows_result = store.replay_program_summaries(&filter).await;
|
let page_result = store.replay_program_summaries(&filter, &page_request).await;
|
||||||
let rows = match rows_result {
|
let page = match page_result {
|
||||||
std::result::Result::Ok(value) => value,
|
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.to_string()),
|
||||||
};
|
};
|
||||||
tracing::debug!(target: crate::TRACING_TARGET, rows = rows.len(), "loaded replay program summaries");
|
let metadata_result =
|
||||||
let mut output = std::vec::Vec::with_capacity(rows.len());
|
crate::demo_store_block_metadata(page.total_rows, page.next_cursor, request.block_index);
|
||||||
for row in rows {
|
let metadata = match metadata_result {
|
||||||
output.push(program_row_from_pg(row));
|
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.
|
/// Loads one counted Store block of mint, owner or account-key summaries.
|
||||||
pub(crate) async fn load_demo_sql_replay_entities(
|
pub(crate) async fn load_demo_store_replay_entities(
|
||||||
state: tauri::State<'_, crate::AppState>,
|
state: tauri::State<'_, crate::AppState>,
|
||||||
request: crate::DemoSqlReplayEntityRequest,
|
request: crate::DemoStoreReplayEntityRequest,
|
||||||
) -> std::result::Result<std::vec::Vec<crate::DemoSqlReplayEntityRow>, std::string::String> {
|
) -> std::result::Result<crate::DemoStoreReplayEntityPagePayload, std::string::String> {
|
||||||
let entity_kind_result = entity_kind_from_code(request.entity_kind.as_str());
|
let entity_kind_result = entity_kind_from_code(request.entity_kind.as_str());
|
||||||
let entity_kind = match entity_kind_result {
|
let entity_kind = match entity_kind_result {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
let filter_result = ks_store::PostgresReplayEntityFilter::new(
|
let filter_result =
|
||||||
entity_kind,
|
ks_store::ReplayEntityFilter::new(entity_kind, request.entity_value_contains);
|
||||||
request.entity_value_contains,
|
|
||||||
request.limit,
|
|
||||||
);
|
|
||||||
let filter = match filter_result {
|
let filter = match filter_result {
|
||||||
std::result::Result::Ok(value) => value,
|
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.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 {
|
let store = match store_result {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
let rows_result = store.replay_entity_summaries(&filter).await;
|
let page_result = store.replay_entity_summaries(&filter, &page_request).await;
|
||||||
let rows = match rows_result {
|
let page = match page_result {
|
||||||
std::result::Result::Ok(value) => value,
|
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.to_string()),
|
||||||
};
|
};
|
||||||
tracing::debug!(target: crate::TRACING_TARGET, rows = rows.len(), "loaded replay entity summaries");
|
let metadata_result =
|
||||||
let mut output = std::vec::Vec::with_capacity(rows.len());
|
crate::demo_store_block_metadata(page.total_rows, page.next_cursor, request.block_index);
|
||||||
for row in rows {
|
let metadata = match metadata_result {
|
||||||
output.push(entity_row_from_pg(row));
|
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(
|
fn transaction_row_from_store(
|
||||||
row: ks_store::PostgresReplayTransactionCandidate,
|
row: ks_store::ReplayTransactionCandidate,
|
||||||
) -> crate::DemoSqlReplayTransactionRow {
|
) -> crate::DemoStoreReplayTransactionRow {
|
||||||
return crate::DemoSqlReplayTransactionRow {
|
return crate::DemoStoreReplayTransactionRow {
|
||||||
signature: row.signature,
|
signature: row.signature,
|
||||||
slot: row.slot,
|
slot: row.slot,
|
||||||
raw_processing_state: row.raw_processing_state,
|
raw_processing_state: row.raw_processing_state,
|
||||||
@@ -395,24 +483,22 @@ fn transaction_row_from_pg(
|
|||||||
ledger_status: row.ledger_status,
|
ledger_status: row.ledger_status,
|
||||||
processor_version: row.processor_version,
|
processor_version: row.processor_version,
|
||||||
attempt_count: row.attempt_count,
|
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,
|
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,
|
inner_program_count: row.inner_program_count,
|
||||||
updated_at: row.updated_at,
|
updated_at: row.updated_at,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn program_row_from_pg(
|
fn program_row_from_store(row: ks_store::ReplayProgramSummary) -> crate::DemoStoreReplayProgramRow {
|
||||||
row: ks_store::PostgresReplayProgramSummary,
|
|
||||||
) -> crate::DemoSqlReplayProgramRow {
|
|
||||||
let program_code = ks_program_ids::find_registered_program_id(&row.program_id)
|
let program_code = ks_program_ids::find_registered_program_id(&row.program_id)
|
||||||
.map(|entry| return entry.code().to_owned());
|
.map(|entry| return entry.code().to_owned());
|
||||||
return crate::DemoSqlReplayProgramRow {
|
return crate::DemoStoreReplayProgramRow {
|
||||||
program_code,
|
program_code,
|
||||||
program_id: row.program_id,
|
program_id: row.program_id,
|
||||||
transaction_count: row.transaction_count,
|
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,
|
inner_instruction_count: row.inner_instruction_count,
|
||||||
log_count: row.log_count,
|
log_count: row.log_count,
|
||||||
min_slot: row.min_slot,
|
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 {
|
fn entity_row_from_store(row: ks_store::ReplayEntitySummary) -> crate::DemoStoreReplayEntityRow {
|
||||||
return crate::DemoSqlReplayEntityRow {
|
return crate::DemoStoreReplayEntityRow {
|
||||||
entity_kind: row.entity_kind,
|
entity_kind: row.entity_kind,
|
||||||
entity_value: row.entity_value,
|
entity_value: row.entity_value,
|
||||||
transaction_count: row.transaction_count,
|
transaction_count: row.transaction_count,
|
||||||
@@ -480,20 +566,19 @@ async fn available_csv_export_path(
|
|||||||
|
|
||||||
fn program_scope_from_code(
|
fn program_scope_from_code(
|
||||||
code: &str,
|
code: &str,
|
||||||
) -> std::result::Result<ks_store::PostgresReplayProgramScope, std::string::String> {
|
) -> std::result::Result<ks_store::ReplayProgramScope, std::string::String> {
|
||||||
return match code {
|
return match code {
|
||||||
"any" => std::result::Result::Ok(ks_store::PostgresReplayProgramScope::Any),
|
"any" => std::result::Result::Ok(ks_store::ReplayProgramScope::Any),
|
||||||
"outer" => std::result::Result::Ok(ks_store::PostgresReplayProgramScope::Outer),
|
"top_level" => std::result::Result::Ok(ks_store::ReplayProgramScope::TopLevel),
|
||||||
"inner" => std::result::Result::Ok(ks_store::PostgresReplayProgramScope::Inner),
|
"inner" => std::result::Result::Ok(ks_store::ReplayProgramScope::Inner),
|
||||||
"logs" => std::result::Result::Ok(ks_store::PostgresReplayProgramScope::Logs),
|
"logs" => std::result::Result::Ok(ks_store::ReplayProgramScope::Logs),
|
||||||
_ => std::result::Result::Err(format!("unsupported replay program scope: {code}")),
|
_ => std::result::Result::Err(format!("unsupported replay program scope: {code}")),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn optional_entity_kind_from_code(
|
fn optional_entity_kind_from_code(
|
||||||
code: std::option::Option<&str>,
|
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 {
|
return match code {
|
||||||
std::option::Option::Some(value) => {
|
std::option::Option::Some(value) => {
|
||||||
let result = entity_kind_from_code(value);
|
let result = entity_kind_from_code(value);
|
||||||
@@ -510,11 +595,11 @@ fn optional_entity_kind_from_code(
|
|||||||
|
|
||||||
fn entity_kind_from_code(
|
fn entity_kind_from_code(
|
||||||
code: &str,
|
code: &str,
|
||||||
) -> std::result::Result<ks_store::PostgresReplayEntityKind, std::string::String> {
|
) -> std::result::Result<ks_store::ReplayEntityKind, std::string::String> {
|
||||||
return match code {
|
return match code {
|
||||||
"mint" => std::result::Result::Ok(ks_store::PostgresReplayEntityKind::Mint),
|
"mint" => std::result::Result::Ok(ks_store::ReplayEntityKind::Mint),
|
||||||
"owner" => std::result::Result::Ok(ks_store::PostgresReplayEntityKind::Owner),
|
"owner" => std::result::Result::Ok(ks_store::ReplayEntityKind::Owner),
|
||||||
"account_key" => std::result::Result::Ok(ks_store::PostgresReplayEntityKind::AccountKey),
|
"account_key" => std::result::Result::Ok(ks_store::ReplayEntityKind::AccountKey),
|
||||||
_ => std::result::Result::Err(format!("unsupported replay entity kind: {code}")),
|
_ => std::result::Result::Err(format!("unsupported replay entity kind: {code}")),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -524,7 +609,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn program_scope_parser_accepts_logs() {
|
fn program_scope_parser_accepts_logs() {
|
||||||
let result = super::program_scope_from_code("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]
|
#[test]
|
||||||
@@ -551,16 +636,16 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn program_row_exposes_registered_code() {
|
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(),
|
program_id: ks_program_ids::SYSTEM_PROGRAM_ID.to_owned(),
|
||||||
transaction_count: 1,
|
transaction_count: 1,
|
||||||
outer_instruction_count: 1,
|
top_level_instruction_count: 1,
|
||||||
inner_instruction_count: 0,
|
inner_instruction_count: 0,
|
||||||
log_count: 1,
|
log_count: 1,
|
||||||
min_slot: 1,
|
min_slot: 1,
|
||||||
max_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"));
|
assert_eq!(converted.program_code.as_deref(), std::option::Option::Some("system"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1051
kb-app-demo-desktop/src/demo_wallet.rs
Normal file
1051
kb-app-demo-desktop/src/demo_wallet.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/demo_ws.rs
|
// file: kb-app-demo-desktop/src/demo_ws.rs
|
||||||
// version: 23
|
// version: 24
|
||||||
|
|
||||||
//! Standard Solana WebSocket demo commands backed by `ks_onchain_transport::WsSession`.
|
//! Standard Solana WebSocket demo commands backed by `ks_onchain_transport::WsSession`.
|
||||||
|
|
||||||
@@ -1031,7 +1031,7 @@ mod tests {
|
|||||||
<crate::DemoWsUnsubscribeRequest as TS>::decl(&config),
|
<crate::DemoWsUnsubscribeRequest as TS>::decl(&config),
|
||||||
<crate::DemoBackfillProgressPayload as TS>::decl(&config),
|
<crate::DemoBackfillProgressPayload as TS>::decl(&config),
|
||||||
<crate::DemoBackfillSummaryPayload 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 {
|
for declaration in declarations {
|
||||||
assert!(!declaration.contains("bigint"));
|
assert!(!declaration.contains("bigint"));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/lib.rs
|
// file: kb-app-demo-desktop/src/lib.rs
|
||||||
// version: 37
|
// version: 50
|
||||||
|
|
||||||
//! Tauri desktop demo application for `khadhroony-bot3`.
|
//! Tauri desktop demo application for `khadhroony-bot3`.
|
||||||
|
|
||||||
@@ -24,12 +24,13 @@ mod demo_http;
|
|||||||
mod demo_spl_ata;
|
mod demo_spl_ata;
|
||||||
mod demo_spl_token;
|
mod demo_spl_token;
|
||||||
mod demo_spl_token_2022;
|
mod demo_spl_token_2022;
|
||||||
mod demo_sql_common;
|
mod demo_store_common;
|
||||||
mod demo_sql_diag;
|
mod demo_store_core;
|
||||||
mod demo_sql_pg_core;
|
mod demo_store_diag;
|
||||||
mod demo_sql_pg_raw;
|
mod demo_store_raw;
|
||||||
mod demo_sql_replay_candidates;
|
mod demo_store_replay_candidates;
|
||||||
mod demo_transport;
|
mod demo_transport;
|
||||||
|
mod demo_wallet;
|
||||||
mod demo_ws;
|
mod demo_ws;
|
||||||
mod desktop_config;
|
mod desktop_config;
|
||||||
mod frontend_log;
|
mod frontend_log;
|
||||||
@@ -132,6 +133,8 @@ pub(crate) use self::demo_decode_replay::DemoDecodeReplayRequest;
|
|||||||
pub(crate) use self::demo_decode_replay::DemoDecodeReplayRunGuard;
|
pub(crate) use self::demo_decode_replay::DemoDecodeReplayRunGuard;
|
||||||
/// Internal demo decode replay item.
|
/// Internal demo decode replay item.
|
||||||
pub(crate) use self::demo_decode_replay::DemoDecodeReplaySummaryPayload;
|
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.
|
/// Internal demo decode replay item.
|
||||||
pub(crate) use self::demo_decode_replay::DemoTransactionAnnotationRequest;
|
pub(crate) use self::demo_decode_replay::DemoTransactionAnnotationRequest;
|
||||||
/// Internal demo decode replay item.
|
/// 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;
|
pub(crate) use self::demo_decode_replay::demo_decode_replay_execute;
|
||||||
/// Returns available decoders and default replay bounds.
|
/// Returns available decoders and default replay bounds.
|
||||||
pub(crate) use self::demo_decode_replay::demo_decode_replay_options;
|
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;
|
pub(crate) use self::demo_devnet_common::DemoExecutionDevnetStoreReadinessPayload;
|
||||||
/// Restores the shared single-execution state flag when a Devnet run finishes.
|
/// Restores the shared single-execution state flag when a Devnet run finishes.
|
||||||
pub(crate) use self::demo_devnet_common::DemoExecutionRunGuard;
|
pub(crate) use self::demo_devnet_common::DemoExecutionRunGuard;
|
||||||
/// Converts the reusable scenario readiness report to the desktop TS-RS payload.
|
/// Prepares the Store selected by one compatible Devnet profile.
|
||||||
pub(crate) use self::demo_devnet_common::devnet_store_readiness_payload;
|
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.
|
/// Serializes a diagnostic value to indented JSON without panicking.
|
||||||
pub(crate) use self::demo_devnet_common::pretty_json;
|
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.
|
/// Bridges Metadata execution progress to the Metadata desktop window.
|
||||||
pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataObserver;
|
pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataObserver;
|
||||||
/// Progress event emitted by Metadata execution workflows.
|
/// 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;
|
pub(crate) use self::demo_execution_solana_core::DemoExecutionSolanaCoreSummaryPayload;
|
||||||
/// Maps a confirmation status to its stable UI code.
|
/// Maps a confirmation status to its stable UI code.
|
||||||
pub(crate) use self::demo_execution_solana_core::confirmation_status_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.
|
/// Requests cooperative cancellation before the next execution stage.
|
||||||
pub(crate) use self::demo_execution_solana_core::demo_execution_solana_core_cancel;
|
pub(crate) use self::demo_execution_solana_core::demo_execution_solana_core_cancel;
|
||||||
/// Executes one bounded System transfer simulation or Devnet submission.
|
/// 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;
|
pub(crate) use self::demo_execution_solana_core::demo_execution_spl_memo_execute;
|
||||||
/// Maps an execution cluster to its stable UI code.
|
/// Maps an execution cluster to its stable UI code.
|
||||||
pub(crate) use self::demo_execution_solana_core::execution_cluster_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.
|
/// Frontend payload for one independent Devnet SPL validation scenario.
|
||||||
pub(crate) use self::demo_execution_spl::DevnetSplValidationScenarioPayload;
|
pub(crate) use self::demo_execution_spl::DevnetSplValidationScenarioPayload;
|
||||||
/// Returns the complete ordered Devnet SPL validation inventory for milestone 0.4.6.
|
/// 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;
|
pub(crate) use self::demo_spl_ata::DemoSplAtaDerivationPayload;
|
||||||
/// Request used to derive the representative wallet ATA before execution.
|
/// Request used to derive the representative wallet ATA before execution.
|
||||||
pub(crate) use self::demo_spl_ata::DemoSplAtaDerivationRequest;
|
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.
|
/// Bounded ATA lifecycle journal request.
|
||||||
pub(crate) use self::demo_spl_ata::DemoSplAtaJournalRequest;
|
pub(crate) use self::demo_spl_ata::DemoSplAtaJournalRequest;
|
||||||
/// One UI-safe ATA lifecycle journal row.
|
/// 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;
|
pub(crate) use self::demo_spl_token::DemoExecutionSplTokenRequest;
|
||||||
/// UI-safe result of one checked SPL Token transfer orchestration.
|
/// UI-safe result of one checked SPL Token transfer orchestration.
|
||||||
pub(crate) use self::demo_spl_token::DemoExecutionSplTokenSummaryPayload;
|
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.
|
/// Bounded exact filters for the classic SPL Token materialized journal.
|
||||||
pub(crate) use self::demo_spl_token::DemoSplTokenJournalRequest;
|
pub(crate) use self::demo_spl_token::DemoSplTokenJournalRequest;
|
||||||
/// UI-safe materialized classic SPL Token journal row.
|
/// 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;
|
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.
|
/// Loads public Token-2022 fixture values without reading private key bytes.
|
||||||
pub(crate) use self::demo_spl_token_2022::demo_spl_token_2022_fixture;
|
pub(crate) use self::demo_spl_token_2022::demo_spl_token_2022_fixture;
|
||||||
/// UI-safe SQL table diagnostic snapshot.
|
/// Fixed Store block size used by desktop listings.
|
||||||
pub(crate) use self::demo_sql_common::DemoSqlTableSnapshot;
|
pub(crate) use self::demo_store_common::DEMO_STORE_BLOCK_SIZE;
|
||||||
/// Connects to the configured PostgreSQL store.
|
/// UI-safe Store block pagination metadata.
|
||||||
pub(crate) use self::demo_sql_common::connect_postgres_store;
|
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.
|
/// Formats diagnostic statuses.
|
||||||
pub(crate) use self::demo_sql_common::debug_status;
|
pub(crate) use self::demo_store_common::debug_status;
|
||||||
/// Initializes the PostgreSQL schema during startup.
|
/// Calculates refreshed Store block metadata for desktop listings.
|
||||||
pub(crate) use self::demo_sql_common::initialize_postgres_schema_for_startup;
|
pub(crate) use self::demo_store_common::demo_store_block_metadata;
|
||||||
/// Builds PostgreSQL options from the active profile.
|
/// Builds one fixed-size Store block request for desktop listings.
|
||||||
pub(crate) use self::demo_sql_common::postgres_store_options_from_profile;
|
pub(crate) use self::demo_store_common::demo_store_block_request;
|
||||||
/// Converts one PostgreSQL table diagnostic.
|
/// Opens and verifies the configured store during startup.
|
||||||
pub(crate) use self::demo_sql_common::table_snapshot_from_pg;
|
pub(crate) use self::demo_store_common::initialize_store_for_startup;
|
||||||
/// Converts PostgreSQL table diagnostics.
|
/// Clones the persistent application store.
|
||||||
pub(crate) use self::demo_sql_common::table_snapshots_from_pg;
|
pub(crate) use self::demo_store_common::shared_store;
|
||||||
/// SQL diagnostic payload.
|
/// Clones the shared persistent store held for one configured profile.
|
||||||
pub(crate) use self::demo_sql_diag::DemoSqlDiagPayload;
|
pub(crate) use self::demo_store_common::shared_store_for_profile;
|
||||||
/// Loads read-only SQL diagnostics from the active profile.
|
/// Returns the safe connection descriptor exposed by the active store backend.
|
||||||
pub(crate) use self::demo_sql_diag::load_demo_sql_diag;
|
pub(crate) use self::demo_store_common::store_connection_descriptor;
|
||||||
/// PostgreSQL core-table payload.
|
/// Converts one logical store resource diagnostic.
|
||||||
pub(crate) use self::demo_sql_pg_core::DemoSqlPgCorePayload;
|
pub(crate) use self::demo_store_common::store_resource_snapshot;
|
||||||
/// Loads read-only core table diagnostics from PostgreSQL.
|
/// Converts logical store resource diagnostics.
|
||||||
pub(crate) use self::demo_sql_pg_core::load_demo_sql_pg_core;
|
pub(crate) use self::demo_store_common::store_resource_snapshots;
|
||||||
/// PostgreSQL raw-table payload.
|
/// Backend-agnostic Core store diagnostic payload.
|
||||||
pub(crate) use self::demo_sql_pg_raw::DemoSqlPgRawPayload;
|
pub(crate) use self::demo_store_core::DemoStoreCorePayload;
|
||||||
/// Loads read-only raw table diagnostics from PostgreSQL.
|
/// Loads read-only Core resource diagnostics from the active store.
|
||||||
pub(crate) use self::demo_sql_pg_raw::load_demo_sql_pg_raw;
|
pub(crate) use self::demo_store_core::load_demo_store_core;
|
||||||
/// SQL replay entity request.
|
/// Backend-agnostic store diagnostic payload.
|
||||||
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayEntityRequest;
|
pub(crate) use self::demo_store_diag::DemoStoreDiagPayload;
|
||||||
/// SQL replay entity row.
|
/// Loads read-only Store diagnostics from the active profile.
|
||||||
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayEntityRow;
|
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.
|
/// Known program option.
|
||||||
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayKnownProgramOption;
|
pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayKnownProgramOption;
|
||||||
/// SQL replay options payload.
|
/// Store replay options payload.
|
||||||
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayOptionsPayload;
|
pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayOptionsPayload;
|
||||||
/// SQL replay program request.
|
/// Store replay program page payload.
|
||||||
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayProgramRequest;
|
pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayProgramPagePayload;
|
||||||
/// SQL replay program row.
|
/// Store replay program request.
|
||||||
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayProgramRow;
|
pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayProgramRequest;
|
||||||
/// SQL replay transaction request.
|
/// Store replay program row.
|
||||||
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayTransactionRequest;
|
pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayProgramRow;
|
||||||
/// SQL replay transaction row.
|
/// Store replay transaction page payload.
|
||||||
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayTransactionRow;
|
pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayTransactionPagePayload;
|
||||||
/// Loads static browser options from the active PostgreSQL profile.
|
/// Store replay transaction request.
|
||||||
pub(crate) use self::demo_sql_replay_candidates::demo_sql_replay_options;
|
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/`.
|
/// Writes a bounded replay-candidate CSV file into `./data/exports_csv/`.
|
||||||
pub(crate) use self::demo_sql_replay_candidates::export_demo_sql_replay_csv;
|
pub(crate) use self::demo_store_replay_candidates::export_demo_store_replay_csv;
|
||||||
/// Loads bounded mint, owner or account-key summaries from PostgreSQL core tables.
|
/// Loads bounded mint, owner or account-key summaries from Core store resources.
|
||||||
pub(crate) use self::demo_sql_replay_candidates::load_demo_sql_replay_entities;
|
pub(crate) use self::demo_store_replay_candidates::load_demo_store_replay_entities;
|
||||||
/// Loads bounded program summaries from PostgreSQL.
|
/// Loads bounded program summaries from the active store.
|
||||||
pub(crate) use self::demo_sql_replay_candidates::load_demo_sql_replay_programs;
|
pub(crate) use self::demo_store_replay_candidates::load_demo_store_replay_programs;
|
||||||
/// Loads bounded transaction replay candidates from PostgreSQL.
|
/// Loads bounded transaction replay candidates from the active store.
|
||||||
pub(crate) use self::demo_sql_replay_candidates::load_demo_sql_replay_transactions;
|
pub(crate) use self::demo_store_replay_candidates::load_demo_store_replay_transactions;
|
||||||
/// UI-safe endpoint role shared by HTTP and WebSocket diagnostics.
|
/// UI-safe endpoint role shared by HTTP and WebSocket diagnostics.
|
||||||
pub(crate) use self::demo_transport::DemoEndpointRolePayload;
|
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.
|
/// UI-safe WebSocket endpoint snapshot.
|
||||||
pub(crate) use self::demo_ws::DemoWsEndpointPayload;
|
pub(crate) use self::demo_ws::DemoWsEndpointPayload;
|
||||||
/// WebSocket execution response payload.
|
/// WebSocket execution response payload.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/splash.rs
|
// file: kb-app-demo-desktop/src/splash.rs
|
||||||
// version: 5
|
// version: 6
|
||||||
|
|
||||||
//! Splash-window payloads and startup sequencing helpers.
|
//! 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::Some("success"),
|
||||||
std::option::Option::None,
|
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(
|
crate::emit_splash_order(
|
||||||
&window,
|
&window,
|
||||||
"add_msg",
|
"add_msg",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/tauri.rs
|
// file: kb-app-demo-desktop/src/tauri.rs
|
||||||
// version: 38
|
// version: 47
|
||||||
|
|
||||||
//! Tauri runtime assembly and private command wrappers.
|
//! Tauri runtime assembly and private command wrappers.
|
||||||
|
|
||||||
@@ -36,6 +36,16 @@ pub fn run() -> ks_core::Result<()> {
|
|||||||
load_project_readme,
|
load_project_readme,
|
||||||
open_demo_config_window,
|
open_demo_config_window,
|
||||||
load_demo_config,
|
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,
|
open_demo_http_window,
|
||||||
demo_http_list_pool_clients,
|
demo_http_list_pool_clients,
|
||||||
demo_http_options,
|
demo_http_options,
|
||||||
@@ -61,18 +71,18 @@ pub fn run() -> ks_core::Result<()> {
|
|||||||
demo_decode_replay_cancel,
|
demo_decode_replay_cancel,
|
||||||
demo_decode_replay_diagnostics,
|
demo_decode_replay_diagnostics,
|
||||||
demo_decode_replay_annotations,
|
demo_decode_replay_annotations,
|
||||||
open_demo_sql_diag_window,
|
open_demo_store_diag_window,
|
||||||
load_demo_sql_diag,
|
load_demo_store_diag,
|
||||||
open_demo_sql_pg_raw_window,
|
open_demo_store_raw_window,
|
||||||
load_demo_sql_pg_raw,
|
load_demo_store_raw,
|
||||||
open_demo_sql_pg_core_window,
|
open_demo_store_core_window,
|
||||||
load_demo_sql_pg_core,
|
load_demo_store_core,
|
||||||
open_demo_sql_replay_candidates_window,
|
open_demo_store_replay_candidates_window,
|
||||||
export_demo_sql_replay_csv,
|
export_demo_store_replay_csv,
|
||||||
demo_sql_replay_options,
|
demo_store_replay_options,
|
||||||
load_demo_sql_replay_transactions,
|
load_demo_store_replay_transactions,
|
||||||
load_demo_sql_replay_programs,
|
load_demo_store_replay_programs,
|
||||||
load_demo_sql_replay_entities,
|
load_demo_store_replay_entities,
|
||||||
open_demo_execution_solana_core_window,
|
open_demo_execution_solana_core_window,
|
||||||
demo_execution_devnet_prepare_profile,
|
demo_execution_devnet_prepare_profile,
|
||||||
demo_execution_solana_core_options,
|
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());
|
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]
|
#[tauri::command]
|
||||||
fn open_demo_backfill_window(
|
fn open_demo_backfill_window(
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
@@ -425,116 +513,116 @@ async fn demo_decode_replay_diagnostics(
|
|||||||
async fn demo_decode_replay_annotations(
|
async fn demo_decode_replay_annotations(
|
||||||
state: tauri::State<'_, crate::AppState>,
|
state: tauri::State<'_, crate::AppState>,
|
||||||
request: crate::DemoTransactionAnnotationRequest,
|
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;
|
return crate::demo_decode_replay_annotations(state, request).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn open_demo_sql_diag_window(
|
fn open_demo_store_diag_window(
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
) -> std::result::Result<(), std::string::String> {
|
) -> std::result::Result<(), std::string::String> {
|
||||||
return open_or_focus_demo_window(
|
return open_or_focus_demo_window(
|
||||||
app_handle,
|
app_handle,
|
||||||
"demo_sql_diag",
|
"demo_store_diag",
|
||||||
"demo_sql_diag.html",
|
"demo_store_diag.html",
|
||||||
"SQL diagnostics",
|
"Store diagnostics",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn load_demo_sql_diag(
|
async fn load_demo_store_diag(
|
||||||
state: tauri::State<'_, crate::AppState>,
|
state: tauri::State<'_, crate::AppState>,
|
||||||
) -> std::result::Result<crate::DemoSqlDiagPayload, std::string::String> {
|
) -> std::result::Result<crate::DemoStoreDiagPayload, std::string::String> {
|
||||||
return crate::load_demo_sql_diag(state).await;
|
return crate::load_demo_store_diag(state).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn open_demo_sql_pg_raw_window(
|
fn open_demo_store_raw_window(
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
) -> std::result::Result<(), std::string::String> {
|
) -> std::result::Result<(), std::string::String> {
|
||||||
return open_or_focus_demo_window(
|
return open_or_focus_demo_window(
|
||||||
app_handle,
|
app_handle,
|
||||||
"demo_sql_pg_raw",
|
"demo_store_raw",
|
||||||
"demo_sql_pg_raw.html",
|
"demo_store_raw.html",
|
||||||
"PostgreSQL canonical acquisition",
|
"Raw store",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn load_demo_sql_pg_raw(
|
async fn load_demo_store_raw(
|
||||||
state: tauri::State<'_, crate::AppState>,
|
state: tauri::State<'_, crate::AppState>,
|
||||||
) -> std::result::Result<crate::DemoSqlPgRawPayload, std::string::String> {
|
) -> std::result::Result<crate::DemoStoreRawPayload, std::string::String> {
|
||||||
return crate::load_demo_sql_pg_raw(state).await;
|
return crate::load_demo_store_raw(state).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn open_demo_sql_pg_core_window(
|
fn open_demo_store_core_window(
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
) -> std::result::Result<(), std::string::String> {
|
) -> std::result::Result<(), std::string::String> {
|
||||||
return open_or_focus_demo_window(
|
return open_or_focus_demo_window(
|
||||||
app_handle,
|
app_handle,
|
||||||
"demo_sql_pg_core",
|
"demo_store_core",
|
||||||
"demo_sql_pg_core.html",
|
"demo_store_core.html",
|
||||||
"PostgreSQL core store",
|
"Core store",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn load_demo_sql_pg_core(
|
async fn load_demo_store_core(
|
||||||
state: tauri::State<'_, crate::AppState>,
|
state: tauri::State<'_, crate::AppState>,
|
||||||
) -> std::result::Result<crate::DemoSqlPgCorePayload, std::string::String> {
|
) -> std::result::Result<crate::DemoStoreCorePayload, std::string::String> {
|
||||||
return crate::load_demo_sql_pg_core(state).await;
|
return crate::load_demo_store_core(state).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn open_demo_sql_replay_candidates_window(
|
fn open_demo_store_replay_candidates_window(
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
) -> std::result::Result<(), std::string::String> {
|
) -> std::result::Result<(), std::string::String> {
|
||||||
return open_or_focus_demo_window(
|
return open_or_focus_demo_window(
|
||||||
app_handle,
|
app_handle,
|
||||||
"demo_sql_replay_candidates",
|
"demo_store_replay_candidates",
|
||||||
"demo_sql_replay_candidates.html",
|
"demo_store_replay_candidates.html",
|
||||||
"SQL replay candidates",
|
"Store replay candidates",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn export_demo_sql_replay_csv(
|
async fn export_demo_store_replay_csv(
|
||||||
file_name: std::string::String,
|
file_name: std::string::String,
|
||||||
content: std::string::String,
|
content: std::string::String,
|
||||||
) -> std::result::Result<std::string::String, 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]
|
#[tauri::command]
|
||||||
async fn demo_sql_replay_options(
|
async fn demo_store_replay_options(
|
||||||
state: tauri::State<'_, crate::AppState>,
|
state: tauri::State<'_, crate::AppState>,
|
||||||
) -> std::result::Result<crate::DemoSqlReplayOptionsPayload, std::string::String> {
|
) -> std::result::Result<crate::DemoStoreReplayOptionsPayload, std::string::String> {
|
||||||
return crate::demo_sql_replay_options(state).await;
|
return crate::demo_store_replay_options(state).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn load_demo_sql_replay_transactions(
|
async fn load_demo_store_replay_transactions(
|
||||||
state: tauri::State<'_, crate::AppState>,
|
state: tauri::State<'_, crate::AppState>,
|
||||||
request: crate::DemoSqlReplayTransactionRequest,
|
request: crate::DemoStoreReplayTransactionRequest,
|
||||||
) -> std::result::Result<std::vec::Vec<crate::DemoSqlReplayTransactionRow>, std::string::String> {
|
) -> std::result::Result<crate::DemoStoreReplayTransactionPagePayload, std::string::String> {
|
||||||
return crate::load_demo_sql_replay_transactions(state, request).await;
|
return crate::load_demo_store_replay_transactions(state, request).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn load_demo_sql_replay_programs(
|
async fn load_demo_store_replay_programs(
|
||||||
state: tauri::State<'_, crate::AppState>,
|
state: tauri::State<'_, crate::AppState>,
|
||||||
request: crate::DemoSqlReplayProgramRequest,
|
request: crate::DemoStoreReplayProgramRequest,
|
||||||
) -> std::result::Result<std::vec::Vec<crate::DemoSqlReplayProgramRow>, std::string::String> {
|
) -> std::result::Result<crate::DemoStoreReplayProgramPagePayload, std::string::String> {
|
||||||
return crate::load_demo_sql_replay_programs(state, request).await;
|
return crate::load_demo_store_replay_programs(state, request).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn load_demo_sql_replay_entities(
|
async fn load_demo_store_replay_entities(
|
||||||
state: tauri::State<'_, crate::AppState>,
|
state: tauri::State<'_, crate::AppState>,
|
||||||
request: crate::DemoSqlReplayEntityRequest,
|
request: crate::DemoStoreReplayEntityRequest,
|
||||||
) -> std::result::Result<std::vec::Vec<crate::DemoSqlReplayEntityRow>, std::string::String> {
|
) -> std::result::Result<crate::DemoStoreReplayEntityPagePayload, std::string::String> {
|
||||||
return crate::load_demo_sql_replay_entities(state, request).await;
|
return crate::load_demo_store_replay_entities(state, request).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -769,7 +857,7 @@ async fn demo_execution_spl_token_execute(
|
|||||||
async fn demo_spl_token_journal(
|
async fn demo_spl_token_journal(
|
||||||
state: tauri::State<'_, crate::AppState>,
|
state: tauri::State<'_, crate::AppState>,
|
||||||
request: crate::DemoSplTokenJournalRequest,
|
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;
|
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(
|
async fn demo_spl_ata_journal(
|
||||||
state: tauri::State<'_, crate::AppState>,
|
state: tauri::State<'_, crate::AppState>,
|
||||||
request: crate::DemoSplAtaJournalRequest,
|
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;
|
return crate::demo_spl_ata_journal(state, request).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Khadhroony Bot3 Demo Desktop",
|
"productName": "Khadhroony Bot3 Demo Desktop",
|
||||||
"version": "0.5.1",
|
"version": "0.5.3",
|
||||||
"identifier": "com.sasedev.kb-app-demo-desktop",
|
"identifier": "com.sasedev.kb-app-demo-desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/vite.config.ts
|
// file: kb-app-demo-desktop/vite.config.ts
|
||||||
// version: 9
|
// version: 11
|
||||||
|
|
||||||
import { defineConfig, normalizePath } from "vite";
|
import { defineConfig, normalizePath } from "vite";
|
||||||
import { NodePackageImporter } from "sass-embedded";
|
import { NodePackageImporter } from "sass-embedded";
|
||||||
@@ -24,15 +24,16 @@ export default defineConfig(() => ({
|
|||||||
"splash": normalizePath(resolve(__dirname, 'frontend/splash.html')),
|
"splash": normalizePath(resolve(__dirname, 'frontend/splash.html')),
|
||||||
"main": normalizePath(resolve(__dirname, 'frontend/main.html')),
|
"main": normalizePath(resolve(__dirname, 'frontend/main.html')),
|
||||||
"demo_config": normalizePath(resolve(__dirname, 'frontend/demo_config.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_http": normalizePath(resolve(__dirname, 'frontend/demo_http.html')),
|
||||||
"demo_ws": normalizePath(resolve(__dirname, 'frontend/demo_ws.html')),
|
"demo_ws": normalizePath(resolve(__dirname, 'frontend/demo_ws.html')),
|
||||||
"demo_backfill": normalizePath(resolve(__dirname, 'frontend/demo_backfill.html')),
|
"demo_backfill": normalizePath(resolve(__dirname, 'frontend/demo_backfill.html')),
|
||||||
"demo_core_extraction": normalizePath(resolve(__dirname, 'frontend/demo_core_extraction.html')),
|
"demo_core_extraction": normalizePath(resolve(__dirname, 'frontend/demo_core_extraction.html')),
|
||||||
"demo_decode_replay": normalizePath(resolve(__dirname, 'frontend/demo_decode_replay.html')),
|
"demo_decode_replay": normalizePath(resolve(__dirname, 'frontend/demo_decode_replay.html')),
|
||||||
"demo_sql_diag": normalizePath(resolve(__dirname, 'frontend/demo_sql_diag.html')),
|
"demo_store_diag": normalizePath(resolve(__dirname, 'frontend/demo_store_diag.html')),
|
||||||
"demo_sql_pg_raw": normalizePath(resolve(__dirname, 'frontend/demo_sql_pg_raw.html')),
|
"demo_store_raw": normalizePath(resolve(__dirname, 'frontend/demo_store_raw.html')),
|
||||||
"demo_sql_pg_core": normalizePath(resolve(__dirname, 'frontend/demo_sql_pg_core.html')),
|
"demo_store_core": normalizePath(resolve(__dirname, 'frontend/demo_store_core.html')),
|
||||||
"demo_sql_replay_candidates": normalizePath(resolve(__dirname, 'frontend/demo_sql_replay_candidates.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_solana_core": normalizePath(resolve(__dirname, 'frontend/demo_execution_solana_core.html')),
|
||||||
"demo_execution_spl": normalizePath(resolve(__dirname, 'frontend/demo_execution_spl.html')),
|
"demo_execution_spl": normalizePath(resolve(__dirname, 'frontend/demo_execution_spl.html')),
|
||||||
"demo_execution_metatdata": normalizePath(resolve(__dirname, 'frontend/demo_execution_metadata.html')),
|
"demo_execution_metatdata": normalizePath(resolve(__dirname, 'frontend/demo_execution_metadata.html')),
|
||||||
|
|||||||
@@ -1,8 +1,40 @@
|
|||||||
<!-- file: ks-config/CHANGELOG.md -->
|
<!-- file: ks-config/CHANGELOG.md -->
|
||||||
<!-- version: 20 -->
|
<!-- version: 26 -->
|
||||||
|
|
||||||
# CHANGELOG — ks-config
|
# 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 qu’il résolve d’abord `${KS_WALLETS_DIRECTORY:-wallets}` comme le chemin de chargement runtime ;
|
||||||
|
- rend le test indépendant d’un éventuel override `KS_WALLETS_DIRECTORY` en vérifiant la relation `temporary_wallet_dir = wallet_dir/temporary/local_devnet` plutôt qu’une 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`
|
## `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` ;
|
- réconcilie schémas, exemples sous `config/exemples/`, guide de configuration et documentation de crate avant archivage du plan `0.5.1` ;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ks-config/README.md -->
|
<!-- file: ks-config/README.md -->
|
||||||
<!-- version: 12 -->
|
<!-- version: 14 -->
|
||||||
|
|
||||||
# ks-config
|
# ks-config
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
- charger l'environnement du workspace et résoudre les placeholders `${KS_*}` / `${KB_*}` ;
|
- 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/` ;
|
- 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 ;
|
- 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 ;
|
- résoudre les defaults de chaque document ou les overrides d'une composition ;
|
||||||
- refuser une référence de fichier ou de profil inexistante ;
|
- refuser une référence de fichier ou de profil inexistante ;
|
||||||
- reconstruire temporairement `AppConfig/ProfileConfig` pour les consommateurs existants ;
|
- 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.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`.
|
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
|
## Documents
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ks-config/USAGE.md -->
|
<!-- file: ks-config/USAGE.md -->
|
||||||
<!-- version: 10 -->
|
<!-- version: 13 -->
|
||||||
|
|
||||||
# Utilisation de ks-config
|
# Utilisation de ks-config
|
||||||
|
|
||||||
@@ -9,6 +9,22 @@
|
|||||||
|
|
||||||
`ks-config` ne connaît pas la liste des futurs workers ou applications.
|
`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
|
## Charger les defaults partagés
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
@@ -72,13 +88,13 @@ Les endpoints WebSocket source sélectionnent une classe `ws_endpoint_defaults`;
|
|||||||
|
|
||||||
## Store
|
## 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
|
||||||
|
|
||||||
`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
|
## Exécution
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-config/src/composition.rs
|
// file: ks-config/src/composition.rs
|
||||||
// version: 3
|
// version: 5
|
||||||
|
|
||||||
//! Binary composition configuration and resolution into the shared runtime profile contract.
|
//! Binary composition configuration and resolution into the shared runtime profile contract.
|
||||||
|
|
||||||
@@ -18,9 +18,9 @@ pub struct CompositionConfigDocument {
|
|||||||
/// Active binary composition profile name.
|
/// Active binary composition profile name.
|
||||||
pub active_profile: std::string::String,
|
pub active_profile: std::string::String,
|
||||||
/// Shared configuration documents referenced by this composition.
|
/// Shared configuration documents referenced by this composition.
|
||||||
pub sources: CompositionConfigSources,
|
pub sources: crate::CompositionConfigSources,
|
||||||
/// Named binary composition profiles.
|
/// 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.
|
/// Paths of shared configuration documents consumed by one binary composition.
|
||||||
@@ -65,7 +65,7 @@ pub struct CompositionProfileConfig {
|
|||||||
#[derive(Clone, Eq, PartialEq)]
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
pub struct ComposedAppConfig {
|
pub struct ComposedAppConfig {
|
||||||
/// Parsed binary composition document.
|
/// Parsed binary composition document.
|
||||||
pub composition: CompositionConfigDocument,
|
pub composition: crate::CompositionConfigDocument,
|
||||||
/// Resolved runtime application configuration.
|
/// Resolved runtime application configuration.
|
||||||
pub app_config: crate::AppConfig,
|
pub app_config: crate::AppConfig,
|
||||||
/// Resolved logging source path.
|
/// 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.
|
/// Validates raw binary composition JSON against its embedded schema.
|
||||||
pub fn validate_composition_json_schema(raw_json: &str) -> ks_core::Result<()> {
|
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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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.
|
/// Parses and validates one binary composition document.
|
||||||
pub fn parse_composition_json(raw_json: &str) -> ks_core::Result<CompositionConfigDocument> {
|
pub fn parse_composition_json(raw_json: &str) -> ks_core::Result<crate::CompositionConfigDocument> {
|
||||||
match validate_composition_json_schema(raw_json) {
|
match crate::validate_composition_json_schema(raw_json) {
|
||||||
std::result::Result::Ok(()) => (),
|
std::result::Result::Ok(()) => (),
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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::Ok(value) => value,
|
||||||
std::result::Result::Err(_) => {
|
std::result::Result::Err(_) => {
|
||||||
return std::result::Result::Err(ks_core::Error::new(
|
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::Ok(()) => std::result::Result::Ok(document),
|
||||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
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.
|
/// Reads and parses one binary composition document from a filesystem path.
|
||||||
pub fn read_composition_json_file(
|
pub fn read_composition_json_file(
|
||||||
path: &std::path::Path,
|
path: &std::path::Path,
|
||||||
) -> ks_core::Result<CompositionConfigDocument> {
|
) -> ks_core::Result<crate::CompositionConfigDocument> {
|
||||||
let raw_json = match std::fs::read_to_string(path) {
|
let raw_json = match std::fs::read_to_string(path) {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => {
|
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.
|
/// Loads workspace environment values, resolves placeholders and parses one binary composition document.
|
||||||
pub fn read_composition_json_file_with_environment(
|
pub fn read_composition_json_file_with_environment(
|
||||||
path: &std::path::Path,
|
path: &std::path::Path,
|
||||||
workspace_root: &std::path::Path,
|
workspace_root: &std::path::Path,
|
||||||
) -> ks_core::Result<CompositionConfigDocument> {
|
) -> ks_core::Result<crate::CompositionConfigDocument> {
|
||||||
match crate::load_workspace_environment(workspace_root) {
|
match crate::load_workspace_environment(workspace_root) {
|
||||||
std::result::Result::Ok(_) => (),
|
std::result::Result::Ok(_) => (),
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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.
|
/// Returns one named binary composition profile.
|
||||||
pub fn composition_profile<'a>(
|
pub fn composition_profile<'a>(
|
||||||
document: &'a CompositionConfigDocument,
|
document: &'a crate::CompositionConfigDocument,
|
||||||
profile_name: &str,
|
profile_name: &str,
|
||||||
) -> ks_core::Result<&'a CompositionProfileConfig> {
|
) -> ks_core::Result<&'a crate::CompositionProfileConfig> {
|
||||||
for profile in &document.profiles {
|
for profile in &document.profiles {
|
||||||
if profile.name == profile_name {
|
if profile.name == profile_name {
|
||||||
return std::result::Result::Ok(profile);
|
return std::result::Result::Ok(profile);
|
||||||
@@ -208,9 +208,9 @@ pub fn composition_profile<'a>(
|
|||||||
|
|
||||||
/// Returns the active binary composition profile.
|
/// Returns the active binary composition profile.
|
||||||
pub fn active_composition_profile(
|
pub fn active_composition_profile(
|
||||||
document: &CompositionConfigDocument,
|
document: &crate::CompositionConfigDocument,
|
||||||
) -> ks_core::Result<&CompositionProfileConfig> {
|
) -> ks_core::Result<&crate::CompositionProfileConfig> {
|
||||||
return composition_profile(document, document.active_profile.as_str());
|
return crate::composition_profile(document, document.active_profile.as_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolves a source path relative to the workspace root unless already absolute.
|
/// 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.
|
/// Resolves all shared source selections into the runtime application contract.
|
||||||
pub fn compose_app_config(
|
pub fn compose_app_config(
|
||||||
document: &CompositionConfigDocument,
|
document: &crate::CompositionConfigDocument,
|
||||||
transport: &crate::TransportConfigDocument,
|
transport: &crate::TransportConfigDocument,
|
||||||
listeners: &crate::ListenersConfigDocument,
|
listeners: &crate::ListenersConfigDocument,
|
||||||
store: &crate::StoreConfigDocument,
|
store: &crate::StoreConfigDocument,
|
||||||
@@ -247,23 +247,23 @@ pub fn compose_app_config(
|
|||||||
) -> ks_core::Result<crate::AppConfig> {
|
) -> ks_core::Result<crate::AppConfig> {
|
||||||
let mut profiles = std::vec::Vec::<crate::ProfileConfig>::new();
|
let mut profiles = std::vec::Vec::<crate::ProfileConfig>::new();
|
||||||
for profile in &document.profiles {
|
for profile in &document.profiles {
|
||||||
let transport_name = resolve_profile_selection(
|
let transport_name = crate::resolve_profile_selection(
|
||||||
profile.transport_profile.as_deref(),
|
profile.transport_profile.as_deref(),
|
||||||
transport.default_profile.as_str(),
|
transport.default_profile.as_str(),
|
||||||
);
|
);
|
||||||
let listeners_name = resolve_profile_selection(
|
let listeners_name = crate::resolve_profile_selection(
|
||||||
profile.listeners_profile.as_deref(),
|
profile.listeners_profile.as_deref(),
|
||||||
listeners.default_profile.as_str(),
|
listeners.default_profile.as_str(),
|
||||||
);
|
);
|
||||||
let store_name = resolve_profile_selection(
|
let store_name = crate::resolve_profile_selection(
|
||||||
profile.store_profile.as_deref(),
|
profile.store_profile.as_deref(),
|
||||||
store.default_profile.as_str(),
|
store.default_profile.as_str(),
|
||||||
);
|
);
|
||||||
let wallet_name = resolve_profile_selection(
|
let wallet_name = crate::resolve_profile_selection(
|
||||||
profile.wallet_profile.as_deref(),
|
profile.wallet_profile.as_deref(),
|
||||||
wallet.default_profile.as_str(),
|
wallet.default_profile.as_str(),
|
||||||
);
|
);
|
||||||
let execution_name = resolve_profile_selection(
|
let execution_name = crate::resolve_profile_selection(
|
||||||
profile.execution_profile.as_deref(),
|
profile.execution_profile.as_deref(),
|
||||||
execution.default_profile.as_str(),
|
execution.default_profile.as_str(),
|
||||||
);
|
);
|
||||||
@@ -313,24 +313,34 @@ pub fn compose_app_config(
|
|||||||
pub fn read_composed_app_config_with_environment(
|
pub fn read_composed_app_config_with_environment(
|
||||||
composition_path: &std::path::Path,
|
composition_path: &std::path::Path,
|
||||||
workspace_root: &std::path::Path,
|
workspace_root: &std::path::Path,
|
||||||
) -> ks_core::Result<ComposedAppConfig> {
|
) -> ks_core::Result<crate::ComposedAppConfig> {
|
||||||
let composition =
|
let composition = match crate::read_composition_json_file_with_environment(
|
||||||
match read_composition_json_file_with_environment(composition_path, workspace_root) {
|
composition_path,
|
||||||
std::result::Result::Ok(value) => value,
|
workspace_root,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
) {
|
||||||
};
|
std::result::Result::Ok(value) => value,
|
||||||
let logging_path =
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
resolve_composition_source_path(workspace_root, composition.sources.logging.as_str());
|
};
|
||||||
let transport_path =
|
let logging_path = crate::resolve_composition_source_path(
|
||||||
resolve_composition_source_path(workspace_root, composition.sources.transport.as_str());
|
workspace_root,
|
||||||
let listeners_path =
|
composition.sources.logging.as_str(),
|
||||||
resolve_composition_source_path(workspace_root, composition.sources.listeners.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 =
|
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 =
|
let wallet_path =
|
||||||
resolve_composition_source_path(workspace_root, composition.sources.wallet.as_str());
|
crate::resolve_composition_source_path(workspace_root, composition.sources.wallet.as_str());
|
||||||
let execution_path =
|
let execution_path = crate::resolve_composition_source_path(
|
||||||
resolve_composition_source_path(workspace_root, composition.sources.execution.as_str());
|
workspace_root,
|
||||||
|
composition.sources.execution.as_str(),
|
||||||
|
);
|
||||||
let transport =
|
let transport =
|
||||||
match crate::read_transport_json_file_with_environment(&transport_path, workspace_root) {
|
match crate::read_transport_json_file_with_environment(&transport_path, workspace_root) {
|
||||||
std::result::Result::Ok(value) => value,
|
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::Ok(()) => (),
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
}
|
}
|
||||||
let app_config =
|
let app_config = match crate::compose_app_config(
|
||||||
match compose_app_config(&composition, &transport, &listeners, &store, &wallet, &execution)
|
&composition,
|
||||||
{
|
&transport,
|
||||||
std::result::Result::Ok(value) => value,
|
&listeners,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
&store,
|
||||||
};
|
&wallet,
|
||||||
return std::result::Result::Ok(ComposedAppConfig {
|
&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,
|
composition,
|
||||||
app_config,
|
app_config,
|
||||||
logging_path,
|
logging_path,
|
||||||
@@ -421,9 +436,9 @@ pub fn read_default_shared_app_config_with_environment(
|
|||||||
} else {
|
} else {
|
||||||
"shared_default".to_string()
|
"shared_default".to_string()
|
||||||
};
|
};
|
||||||
let composition = CompositionConfigDocument {
|
let composition = crate::CompositionConfigDocument {
|
||||||
active_profile: profile_name.clone(),
|
active_profile: profile_name.clone(),
|
||||||
sources: CompositionConfigSources {
|
sources: crate::CompositionConfigSources {
|
||||||
logging: DEFAULT_LOGGING_CONFIG_PATH.to_string(),
|
logging: DEFAULT_LOGGING_CONFIG_PATH.to_string(),
|
||||||
transport: DEFAULT_TRANSPORT_CONFIG_PATH.to_string(),
|
transport: DEFAULT_TRANSPORT_CONFIG_PATH.to_string(),
|
||||||
listeners: DEFAULT_LISTENERS_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(),
|
wallet: DEFAULT_WALLET_CONFIG_PATH.to_string(),
|
||||||
execution: DEFAULT_EXECUTION_CONFIG_PATH.to_string(),
|
execution: DEFAULT_EXECUTION_CONFIG_PATH.to_string(),
|
||||||
},
|
},
|
||||||
profiles: vec![CompositionProfileConfig {
|
profiles: vec![crate::CompositionProfileConfig {
|
||||||
name: profile_name.clone(),
|
name: profile_name.clone(),
|
||||||
application: std::option::Option::None,
|
application: std::option::Option::None,
|
||||||
logging_profile: 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,
|
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.
|
/// 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") {
|
match crate::require_non_empty(&document.active_profile, "composition.active_profile") {
|
||||||
std::result::Result::Ok(()) => (),
|
std::result::Result::Ok(()) => (),
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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::Ok(_) => std::result::Result::Ok(()),
|
||||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
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(
|
fn validate_logging_references_with_environment(
|
||||||
path: &std::path::Path,
|
path: &std::path::Path,
|
||||||
workspace_root: &std::path::Path,
|
workspace_root: &std::path::Path,
|
||||||
composition: &CompositionConfigDocument,
|
composition: &crate::CompositionConfigDocument,
|
||||||
) -> ks_core::Result<()> {
|
) -> ks_core::Result<()> {
|
||||||
match crate::load_workspace_environment(workspace_root) {
|
match crate::load_workspace_environment(workspace_root) {
|
||||||
std::result::Result::Ok(_) => (),
|
std::result::Result::Ok(_) => (),
|
||||||
@@ -566,7 +590,7 @@ fn validate_logging_references_with_environment(
|
|||||||
}
|
}
|
||||||
for profile in &composition.profiles {
|
for profile in &composition.profiles {
|
||||||
let selected =
|
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) {
|
if !names.contains(selected) {
|
||||||
return std::result::Result::Err(ks_core::Error::new(
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
"composition_logging_profile_not_found",
|
"composition_logging_profile_not_found",
|
||||||
@@ -586,17 +610,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_and_example_compositions_validate() {
|
fn default_and_example_compositions_validate() {
|
||||||
assert!(super::parse_composition_json(DEFAULT_COMPOSITION).is_ok());
|
assert!(crate::parse_composition_json(DEFAULT_COMPOSITION).is_ok());
|
||||||
assert!(super::parse_composition_json(EXAMPLE_COMPOSITION).is_ok());
|
assert!(crate::parse_composition_json(EXAMPLE_COMPOSITION).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn local_devnet_composition_uses_shared_defaults() {
|
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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => panic!("default composition must parse: {error}"),
|
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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => {
|
std::result::Result::Err(error) => {
|
||||||
panic!("local Devnet composition must resolve: {error}")
|
panic!("local Devnet composition must resolve: {error}")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-config/src/environment.rs
|
// file: ks-config/src/environment.rs
|
||||||
// version: 6
|
// version: 8
|
||||||
|
|
||||||
//! Environment-file loading and configuration placeholder resolution.
|
//! Environment-file loading and configuration placeholder resolution.
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ pub struct EnvironmentLoadReport {
|
|||||||
/// fallback text declared with `${NAME:-fallback}` placeholders.
|
/// fallback text declared with `${NAME:-fallback}` placeholders.
|
||||||
pub fn load_workspace_environment(
|
pub fn load_workspace_environment(
|
||||||
workspace_root: &std::path::Path,
|
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 explicit_path = std::env::var("KS_ENV_FILE").ok();
|
||||||
let selected_path = match explicit_path {
|
let selected_path = match explicit_path {
|
||||||
std::option::Option::Some(path) if !path.trim().is_empty() => {
|
std::option::Option::Some(path) if !path.trim().is_empty() => {
|
||||||
@@ -26,10 +26,10 @@ pub fn load_workspace_environment(
|
|||||||
_ => workspace_root.join(".env"),
|
_ => workspace_root.join(".env"),
|
||||||
};
|
};
|
||||||
if !selected_path.exists() {
|
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) {
|
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),
|
loaded_path: std::option::Option::Some(selected_path),
|
||||||
}),
|
}),
|
||||||
std::result::Result::Err(_) => std::result::Result::Err(ks_core::Error::new(
|
std::result::Result::Err(_) => std::result::Result::Err(ks_core::Error::new(
|
||||||
@@ -82,14 +82,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn fallback_is_used_when_variable_is_absent() {
|
fn fallback_is_used_when_variable_is_absent() {
|
||||||
let resolved =
|
let resolved =
|
||||||
super::resolve_environment_placeholders("${KS_CONFIG_TEST_MISSING:-fallback}");
|
crate::resolve_environment_placeholders("${KS_CONFIG_TEST_MISSING:-fallback}");
|
||||||
assert_eq!(resolved, "fallback");
|
assert_eq!(resolved, "fallback");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unresolved_required_placeholder_is_preserved() {
|
fn unresolved_required_placeholder_is_preserved() {
|
||||||
let resolved =
|
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");
|
assert_eq!(resolved, "prefix-${KS_CONFIG_TEST_MISSING}-suffix");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-config/src/execution.rs
|
// file: ks-config/src/execution.rs
|
||||||
// version: 3
|
// version: 5
|
||||||
|
|
||||||
//! Independent execution policy configuration document loading and validation.
|
//! 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.
|
/// Default execution profile used when a composition does not override it.
|
||||||
pub default_profile: std::string::String,
|
pub default_profile: std::string::String,
|
||||||
/// Named execution policy profiles.
|
/// Named execution policy profiles.
|
||||||
pub profiles: std::vec::Vec<ExecutionProfileConfig>,
|
pub profiles: std::vec::Vec<crate::ExecutionProfileConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Named execution policy profile.
|
/// Named execution policy profile.
|
||||||
@@ -31,7 +31,7 @@ pub fn execution_json_schema_text() -> &'static str {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parses and validates one execution configuration document.
|
/// 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) {
|
let schema = match serde_json::from_str::<serde_json::Value>(EXECUTION_JSON_SCHEMA) {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => {
|
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",
|
"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::Ok(value) => value,
|
||||||
std::result::Result::Err(_) => {
|
std::result::Result::Err(_) => {
|
||||||
return std::result::Result::Err(ks_core::Error::new(
|
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::Ok(()) => std::result::Result::Ok(document),
|
||||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
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(
|
pub fn read_execution_json_file_with_environment(
|
||||||
path: &std::path::Path,
|
path: &std::path::Path,
|
||||||
workspace_root: &std::path::Path,
|
workspace_root: &std::path::Path,
|
||||||
) -> ks_core::Result<ExecutionConfigDocument> {
|
) -> ks_core::Result<crate::ExecutionConfigDocument> {
|
||||||
match crate::load_workspace_environment(workspace_root) {
|
match crate::load_workspace_environment(workspace_root) {
|
||||||
std::result::Result::Ok(_) => (),
|
std::result::Result::Ok(_) => (),
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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.
|
/// Returns one named execution profile.
|
||||||
pub fn execution_profile<'a>(
|
pub fn execution_profile<'a>(
|
||||||
document: &'a ExecutionConfigDocument,
|
document: &'a crate::ExecutionConfigDocument,
|
||||||
profile_name: &str,
|
profile_name: &str,
|
||||||
) -> ks_core::Result<&'a ExecutionProfileConfig> {
|
) -> ks_core::Result<&'a crate::ExecutionProfileConfig> {
|
||||||
for profile in &document.profiles {
|
for profile in &document.profiles {
|
||||||
if profile.name == profile_name {
|
if profile.name == profile_name {
|
||||||
return std::result::Result::Ok(profile);
|
return std::result::Result::Ok(profile);
|
||||||
@@ -119,13 +119,15 @@ pub fn execution_profile<'a>(
|
|||||||
|
|
||||||
/// Returns the default execution profile.
|
/// Returns the default execution profile.
|
||||||
pub fn default_execution_profile(
|
pub fn default_execution_profile(
|
||||||
document: &ExecutionConfigDocument,
|
document: &crate::ExecutionConfigDocument,
|
||||||
) -> ks_core::Result<&ExecutionProfileConfig> {
|
) -> ks_core::Result<&crate::ExecutionProfileConfig> {
|
||||||
return execution_profile(document, document.default_profile.as_str());
|
return crate::execution_profile(document, document.default_profile.as_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates one typed execution configuration document.
|
/// 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") {
|
match crate::require_non_empty(&document.default_profile, "execution.default_profile") {
|
||||||
std::result::Result::Ok(()) => (),
|
std::result::Result::Ok(()) => (),
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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),
|
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::Ok(_) => std::result::Result::Ok(()),
|
||||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
@@ -167,7 +169,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_and_example_execution_configs_validate() {
|
fn default_and_example_execution_configs_validate() {
|
||||||
assert!(super::parse_execution_json(DEFAULT_EXECUTION).is_ok());
|
assert!(crate::parse_execution_json(DEFAULT_EXECUTION).is_ok());
|
||||||
assert!(super::parse_execution_json(EXAMPLE_EXECUTION).is_ok());
|
assert!(crate::parse_execution_json(EXAMPLE_EXECUTION).is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-config/src/lib.rs
|
// file: ks-config/src/lib.rs
|
||||||
// version: 9
|
// version: 11
|
||||||
|
|
||||||
//! Khadhroony Solana shared configuration contracts and composition helpers.
|
//! Khadhroony Solana shared configuration contracts and composition helpers.
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
@@ -125,16 +125,12 @@ pub use self::settings::HttpEndpointConfig;
|
|||||||
pub use self::settings::ListenerConfig;
|
pub use self::settings::ListenerConfig;
|
||||||
/// Exposes the log listener configuration type.
|
/// Exposes the log listener configuration type.
|
||||||
pub use self::settings::LogListenerConfig;
|
pub use self::settings::LogListenerConfig;
|
||||||
/// Exposes the PostgreSQL configuration type.
|
|
||||||
pub use self::settings::PostgresConfig;
|
|
||||||
/// Exposes the profile configuration type.
|
/// Exposes the profile configuration type.
|
||||||
pub use self::settings::ProfileConfig;
|
pub use self::settings::ProfileConfig;
|
||||||
/// Exposes the program listener configuration type.
|
/// Exposes the program listener configuration type.
|
||||||
pub use self::settings::ProgramListenerConfig;
|
pub use self::settings::ProgramListenerConfig;
|
||||||
/// Exposes the Solana configuration type.
|
/// Exposes the Solana configuration type.
|
||||||
pub use self::settings::SolanaConfig;
|
pub use self::settings::SolanaConfig;
|
||||||
/// Exposes the SQLite configuration type.
|
|
||||||
pub use self::settings::SqliteConfig;
|
|
||||||
/// Exposes the wallet configuration type.
|
/// Exposes the wallet configuration type.
|
||||||
pub use self::settings::WalletConfig;
|
pub use self::settings::WalletConfig;
|
||||||
/// Exposes the WebSocket endpoint configuration type.
|
/// 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;
|
pub(crate) use self::settings::validate_listeners;
|
||||||
/// Keeps `validate_wallet` available to internal configuration modules.
|
/// Keeps `validate_wallet` available to internal configuration modules.
|
||||||
pub(crate) use self::settings::validate_wallet;
|
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.
|
/// Keeps `validate_ws_endpoint` available to internal configuration modules.
|
||||||
pub(crate) use self::settings::validate_ws_endpoint;
|
pub(crate) use self::settings::validate_ws_endpoint;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-config/src/listeners.rs
|
// file: ks-config/src/listeners.rs
|
||||||
// version: 3
|
// version: 5
|
||||||
|
|
||||||
//! Independent Solana listener configuration document loading and validation.
|
//! 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.
|
/// Default listener profile used by consumers that do not provide an explicit selection.
|
||||||
pub default_profile: std::string::String,
|
pub default_profile: std::string::String,
|
||||||
/// Named listener profiles available in this document.
|
/// Named listener profiles available in this document.
|
||||||
pub profiles: std::vec::Vec<ListenersProfileConfig>,
|
pub profiles: std::vec::Vec<crate::ListenersProfileConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Named listener profile.
|
/// 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.
|
/// Validates raw listener JSON against the embedded listener schema.
|
||||||
pub fn validate_listeners_json_schema(raw_json: &str) -> ks_core::Result<()> {
|
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::Ok(schema) => schema,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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.
|
/// Parses and validates one independent listener configuration document.
|
||||||
pub fn parse_listeners_json(raw_json: &str) -> ks_core::Result<ListenersConfigDocument> {
|
pub fn parse_listeners_json(raw_json: &str) -> ks_core::Result<crate::ListenersConfigDocument> {
|
||||||
match validate_listeners_json_schema(raw_json) {
|
match crate::validate_listeners_json_schema(raw_json) {
|
||||||
std::result::Result::Ok(()) => (),
|
std::result::Result::Ok(()) => (),
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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::Ok(document) => document,
|
||||||
std::result::Result::Err(_) => {
|
std::result::Result::Err(_) => {
|
||||||
return std::result::Result::Err(ks_core::Error::new(
|
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::Ok(()) => std::result::Result::Ok(document),
|
||||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
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.
|
/// Reads and parses one listener configuration document from a filesystem path.
|
||||||
pub fn read_listeners_json_file(
|
pub fn read_listeners_json_file(
|
||||||
path: &std::path::Path,
|
path: &std::path::Path,
|
||||||
) -> ks_core::Result<ListenersConfigDocument> {
|
) -> ks_core::Result<crate::ListenersConfigDocument> {
|
||||||
let raw_json = match std::fs::read_to_string(path) {
|
let raw_json = match std::fs::read_to_string(path) {
|
||||||
std::result::Result::Ok(content) => content,
|
std::result::Result::Ok(content) => content,
|
||||||
std::result::Result::Err(error) => {
|
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.
|
/// Loads workspace environment values, resolves placeholders and parses listener configuration.
|
||||||
pub fn read_listeners_json_file_with_environment(
|
pub fn read_listeners_json_file_with_environment(
|
||||||
path: &std::path::Path,
|
path: &std::path::Path,
|
||||||
workspace_root: &std::path::Path,
|
workspace_root: &std::path::Path,
|
||||||
) -> ks_core::Result<ListenersConfigDocument> {
|
) -> ks_core::Result<crate::ListenersConfigDocument> {
|
||||||
match crate::load_workspace_environment(workspace_root) {
|
match crate::load_workspace_environment(workspace_root) {
|
||||||
std::result::Result::Ok(_) => (),
|
std::result::Result::Ok(_) => (),
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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);
|
let resolved = crate::resolve_environment_placeholders(&raw_json);
|
||||||
return parse_listeners_json(&resolved);
|
return crate::parse_listeners_json(&resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns one named listener profile.
|
/// Returns one named listener profile.
|
||||||
pub fn listeners_profile<'a>(
|
pub fn listeners_profile<'a>(
|
||||||
document: &'a ListenersConfigDocument,
|
document: &'a crate::ListenersConfigDocument,
|
||||||
profile_name: &str,
|
profile_name: &str,
|
||||||
) -> ks_core::Result<&'a ListenersProfileConfig> {
|
) -> ks_core::Result<&'a crate::ListenersProfileConfig> {
|
||||||
for profile in &document.profiles {
|
for profile in &document.profiles {
|
||||||
if profile.name == profile_name {
|
if profile.name == profile_name {
|
||||||
return std::result::Result::Ok(profile);
|
return std::result::Result::Ok(profile);
|
||||||
@@ -159,13 +159,13 @@ pub fn listeners_profile<'a>(
|
|||||||
|
|
||||||
/// Returns the default listener profile declared by the document.
|
/// Returns the default listener profile declared by the document.
|
||||||
pub fn default_listeners_profile(
|
pub fn default_listeners_profile(
|
||||||
document: &ListenersConfigDocument,
|
document: &crate::ListenersConfigDocument,
|
||||||
) -> ks_core::Result<&ListenersProfileConfig> {
|
) -> ks_core::Result<&crate::ListenersProfileConfig> {
|
||||||
return listeners_profile(document, document.default_profile.as_str());
|
return crate::listeners_profile(document, document.default_profile.as_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts one named listener profile into the resolved runtime listener contract.
|
/// 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 {
|
return crate::ListenerConfig {
|
||||||
enabled: profile.enabled,
|
enabled: profile.enabled,
|
||||||
default_commitment: profile.default_commitment.clone(),
|
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.
|
/// 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") {
|
match crate::require_non_empty(&document.default_profile, "default_profile") {
|
||||||
std::result::Result::Ok(()) => (),
|
std::result::Result::Ok(()) => (),
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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(),
|
profile.name.clone(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let resolved = resolved_listener_config(profile);
|
let resolved = crate::resolved_listener_config(profile);
|
||||||
match crate::validate_listeners(&resolved) {
|
match crate::validate_listeners(&resolved) {
|
||||||
std::result::Result::Ok(()) => (),
|
std::result::Result::Ok(()) => (),
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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::Ok(_) => std::result::Result::Ok(()),
|
||||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
@@ -219,7 +221,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_and_example_listener_configs_validate() {
|
fn default_and_example_listener_configs_validate() {
|
||||||
assert!(super::parse_listeners_json(DEFAULT_LISTENERS).is_ok());
|
assert!(crate::parse_listeners_json(DEFAULT_LISTENERS).is_ok());
|
||||||
assert!(super::parse_listeners_json(EXAMPLE_LISTENERS).is_ok());
|
assert!(crate::parse_listeners_json(EXAMPLE_LISTENERS).is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-config/src/schema.rs
|
// file: ks-config/src/schema.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
//! Generic JSON Schema validation helpers for binary-owned configuration fragments.
|
//! Generic JSON Schema validation helpers for binary-owned configuration fragments.
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ mod tests {
|
|||||||
fn validation_error_never_echoes_instance_secret() {
|
fn validation_error_never_echoes_instance_secret() {
|
||||||
let schema = r#"{"type":"object","additionalProperties":false,"required":["value"],"properties":{"value":{"type":"integer"}}}"#;
|
let schema = r#"{"type":"object","additionalProperties":false,"required":["value"],"properties":{"value":{"type":"integer"}}}"#;
|
||||||
let instance = serde_json::json!({"value": "SECRET-CANARY"});
|
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,
|
schema,
|
||||||
&instance,
|
&instance,
|
||||||
"test_fragment_validation_failed",
|
"test_fragment_validation_failed",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-config/src/sensitivity.rs
|
// file: ks-config/src/sensitivity.rs
|
||||||
// version: 1
|
// version: 3
|
||||||
|
|
||||||
//! Application environment-variable sensitivity classification.
|
//! 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.
|
/// one `KS_SECRET_*` placeholder is classified as secret even when the surrounding URL is public.
|
||||||
pub fn classify_environment_template(
|
pub fn classify_environment_template(
|
||||||
raw: &str,
|
raw: &str,
|
||||||
) -> std::option::Option<EnvironmentValueSensitivity> {
|
) -> std::option::Option<crate::EnvironmentValueSensitivity> {
|
||||||
let bytes = raw.as_bytes();
|
let bytes = raw.as_bytes();
|
||||||
let mut sensitivity = std::option::Option::None;
|
let mut sensitivity = std::option::Option::None;
|
||||||
let mut index = 0_usize;
|
let mut index = 0_usize;
|
||||||
@@ -36,7 +36,8 @@ pub fn classify_environment_template(
|
|||||||
std::option::Option::Some((name, _)) => name,
|
std::option::Option::Some((name, _)) => name,
|
||||||
std::option::Option::None => expression,
|
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;
|
index = end + 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -49,15 +50,15 @@ pub fn classify_environment_template(
|
|||||||
/// Classifies a Khadhroony-owned environment variable by its namespace.
|
/// Classifies a Khadhroony-owned environment variable by its namespace.
|
||||||
pub fn classify_environment_variable(
|
pub fn classify_environment_variable(
|
||||||
name: &str,
|
name: &str,
|
||||||
) -> std::option::Option<EnvironmentValueSensitivity> {
|
) -> std::option::Option<crate::EnvironmentValueSensitivity> {
|
||||||
if name.starts_with("KS_SECRET_") || name.starts_with("KB_SECRET_") {
|
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_") {
|
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_") {
|
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;
|
return std::option::Option::None;
|
||||||
}
|
}
|
||||||
@@ -67,14 +68,14 @@ pub fn diagnostic_environment_value(
|
|||||||
name: &str,
|
name: &str,
|
||||||
value: &str,
|
value: &str,
|
||||||
) -> std::option::Option<std::string::String> {
|
) -> std::option::Option<std::string::String> {
|
||||||
return match classify_environment_variable(name) {
|
return match crate::classify_environment_variable(name) {
|
||||||
std::option::Option::Some(EnvironmentValueSensitivity::Secret) => {
|
std::option::Option::Some(crate::EnvironmentValueSensitivity::Secret) => {
|
||||||
std::option::Option::Some("<redacted>".to_string())
|
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("<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::Some(value.to_string())
|
||||||
},
|
},
|
||||||
std::option::Option::None => std::option::Option::None,
|
std::option::Option::None => std::option::Option::None,
|
||||||
@@ -82,23 +83,23 @@ pub fn diagnostic_environment_value(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn merge_sensitivity(
|
fn merge_sensitivity(
|
||||||
left: std::option::Option<EnvironmentValueSensitivity>,
|
left: std::option::Option<crate::EnvironmentValueSensitivity>,
|
||||||
right: std::option::Option<EnvironmentValueSensitivity>,
|
right: std::option::Option<crate::EnvironmentValueSensitivity>,
|
||||||
) -> std::option::Option<EnvironmentValueSensitivity> {
|
) -> std::option::Option<crate::EnvironmentValueSensitivity> {
|
||||||
if left == std::option::Option::Some(EnvironmentValueSensitivity::Secret)
|
if left == std::option::Option::Some(crate::EnvironmentValueSensitivity::Secret)
|
||||||
|| right == std::option::Option::Some(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)
|
if left == std::option::Option::Some(crate::EnvironmentValueSensitivity::Internal)
|
||||||
|| right == std::option::Option::Some(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)
|
if left == std::option::Option::Some(crate::EnvironmentValueSensitivity::Public)
|
||||||
|| right == std::option::Option::Some(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;
|
return std::option::Option::None;
|
||||||
}
|
}
|
||||||
@@ -108,19 +109,19 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn composed_values_inherit_the_strongest_placeholder_sensitivity() {
|
fn composed_values_inherit_the_strongest_placeholder_sensitivity() {
|
||||||
assert_eq!(
|
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}"
|
"https://rpc.invalid/?api-key=${KS_SECRET_HELIUS_API_KEY}&mint=${KS_PUBLIC_DEVNET_TOKEN_2022_MINT}"
|
||||||
),
|
),
|
||||||
std::option::Option::Some(super::EnvironmentValueSensitivity::Secret)
|
std::option::Option::Some(super::EnvironmentValueSensitivity::Secret)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
super::classify_environment_template(
|
crate::classify_environment_template(
|
||||||
"${KS_LOGS_DIRECTORY:-logs}/${KS_PUBLIC_SPL_TOKEN_PROGRAM_ID}"
|
"${KS_LOGS_DIRECTORY:-logs}/${KS_PUBLIC_SPL_TOKEN_PROGRAM_ID}"
|
||||||
),
|
),
|
||||||
std::option::Option::Some(super::EnvironmentValueSensitivity::Internal)
|
std::option::Option::Some(super::EnvironmentValueSensitivity::Internal)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
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)
|
std::option::Option::Some(super::EnvironmentValueSensitivity::Public)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -128,15 +129,15 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn generic_diagnostic_never_reveals_secret_or_internal_values() {
|
fn generic_diagnostic_never_reveals_secret_or_internal_values() {
|
||||||
assert_eq!(
|
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())
|
std::option::Option::Some("<redacted>".to_string())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
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())
|
std::option::Option::Some("<configured>".to_string())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
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())
|
std::option::Option::Some("Token111".to_string())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -144,17 +145,17 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn namespaces_map_to_secret_internal_and_public() {
|
fn namespaces_map_to_secret_internal_and_public() {
|
||||||
assert_eq!(
|
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)
|
std::option::Option::Some(super::EnvironmentValueSensitivity::Secret)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
super::classify_environment_variable("KS_LOGS_DIRECTORY"),
|
crate::classify_environment_variable("KS_LOGS_DIRECTORY"),
|
||||||
std::option::Option::Some(super::EnvironmentValueSensitivity::Internal)
|
std::option::Option::Some(super::EnvironmentValueSensitivity::Internal)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
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)
|
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
Reference in New Issue
Block a user