v0.5.2-pre.006

This commit is contained in:
2026-08-10 23:14:17 +02:00
parent e3acb8ceb1
commit 066d969f5a
51 changed files with 1937 additions and 88 deletions

View File

@@ -17,6 +17,10 @@ KS_SECRET_POSTGRES_TEST_URL=postgres://solana:solana@localhost:5432/khadhroony_t
# Optional binary composition override for the desktop demo. # 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

View File

@@ -1,5 +1,5 @@
# file: Cargo.toml # file: Cargo.toml
# version: 62 # version: 63
[workspace] [workspace]
resolver = "3" resolver = "3"
@@ -18,7 +18,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.5.2-pre.5" version = "0.5.2-pre.6"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3" repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3"

View File

@@ -1,5 +1,5 @@
<!-- file: config/README.md --> <!-- file: config/README.md -->
<!-- version: 25 --> <!-- version: 26 -->
# 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.

View File

@@ -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",

View File

@@ -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
} }
] ]
} }

View File

@@ -477,6 +477,15 @@
"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"
}, },

View File

@@ -50,6 +50,15 @@
"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"
}, },

View File

@@ -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",

View File

@@ -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

View File

@@ -1,5 +1,5 @@
<!-- file: docs/DEVNET_EXECUTION_GUIDE.md --> <!-- file: docs/DEVNET_EXECUTION_GUIDE.md -->
<!-- version: 26 --> <!-- version: 27 -->
# Guide dexécution Devnet # Guide dexécution Devnet
@@ -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` ;

View File

@@ -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 quaucun wallet persistant nest sélectionné. Le champ est non sensible et ne contient jamais de mot de passe, de keypair ou de chemin de fichier individuel.
Les autorisations : Les autorisations :

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/V0_5_2_KS_WALLET_RESTRUCTURING_PLAN.md --> <!-- file: docs/plans/V0_5_2_KS_WALLET_RESTRUCTURING_PLAN.md -->
<!-- version: 8 --> <!-- version: 9 -->
# Plan `0.5.2` — restructuration de `ks-wallet` # Plan `0.5.2` — restructuration de `ks-wallet`
@@ -537,13 +537,15 @@ La tranche `pre.003` reste structurelle ; le cycle de vie protégé est activé
### `0.5.2-pre.006` — configuration et consommateurs ### `0.5.2-pre.006` — configuration et consommateurs
- sélection par alias dans `ks-config` sans secret ; - [x] `wallet_alias` optionnel/nullable dans les profils `wallet.config.json`, propagé dans `WalletConfig` sans secret et rétrocompatible avec les documents `0.5.1` qui omettent ce champ ;
- adaptation `ks-pipeline-demo-scenarios` ; - [x] sélection/inspection/unlock du wallet natif configuré dans `ks-pipeline-demo-scenarios`, avec password fourni uniquement à l'API runtime ;
- adaptation desktop uniquement via DTO sûrs ; - [x] fallback temporaire interdit lorsqu'un alias persistant est explicitement sélectionné ;
- résolution explicite des workflows CLI legacy ; - [x] DTO desktop possédés par `kb-app-demo-desktop` pour liste/inspection : alias, pubkey déclarée, version et sélection uniquement ;
- tests d'intégration et canaris de non-divulgation. - [x] aucune commande Tauri de password/unlock et aucun type runtime sensible `ks-wallet` sérialisé vers le frontend ;
- [x] workflow CLI Token-2022 explicitement limité au format Solana CLI JSON et rejet des `.kswallet` ;
- [x] tests de configuration, sélection persistante, unlock explicite, refus du fallback et canaris de non-divulgation.
Cette tranche ne doit pas devenir la refonte générale des scénarios prévue pour `0.5.4`. Cette tranche ne devient pas la refonte générale des scénarios prévue pour `0.5.4`. Les campagnes existantes restent temporaires lorsque `wallet_alias = null`; leur migration fonctionnelle vers un wallet persistant avec acquisition sécurisée du password appartient au consommateur qui choisit d'activer cette sélection.
### `0.5.2-pre.007` — finalisation ### `0.5.2-pre.007` — finalisation

View File

@@ -1,8 +1,35 @@
<!-- file: kb-app-demo-desktop/CHANGELOG.md --> <!-- file: kb-app-demo-desktop/CHANGELOG.md -->
<!-- version: 52 --> <!-- version: 56 -->
# CHANGELOG — kb-app-demo-desktop # CHANGELOG — kb-app-demo-desktop
## `0.5.2-pre.006-delta-fix-003`
- ajoute dans `Wallets` un sélecteur explicite de profil RPC pour lexplorateur on-chain ; le profil actif reste la valeur initiale mais Devnet/Mainnet peuvent être interrogés sans redémarrer lapplication ni modifier `active_profile` ;
- route `getBalance`, `getTokenAccountsByOwner`, `getSignaturesForAddress` et `getTransaction` à travers le profil sélectionné et non plus obligatoirement à travers le pool HTTP actif ;
- borne la commande RPC dédiée à la démo wallet aux trois lectures génériques nécessaires aux tokens et à lhistorique, tandis que le solde garde son adaptateur typé ;
- conserve la gestion locale des `.kswallet` sur le profil applicatif actif : la sélection RPC de lexplorateur ne déplace ni ne recrée les fichiers wallet.
## `0.5.2-pre.006-delta-fix-002`
- complète la fenêtre `Wallets` avec la création de `.kswallet` dans le store du profil actif ; le password est lu uniquement côté backend depuis `KB_SECRET_DEMO_WALLET_PASSWORD` et ne traverse jamais HTML, TypeScript ou les arguments Tauri ;
- ajoute un explorateur public on-chain réutilisant le transport HTTP configuré : solde SOL exact, comptes SPL Token et Token-2022, signatures récentes impliquant ladresse et détail `getTransaction` via le JsonViewer commun ;
- étend le rôle `http_queries` Devnet avec `get_signatures_for_address` afin que lexplorateur fonctionne sur `local_devnet` comme sur les profils Mainnet déjà dotés des rôles dhistorique ;
- ajoute des DTO TS-RS applicatifs sûrs, des canaris frontend/backend de non-divulgation et des tests statiques de câblage pour les nouvelles commandes.
## `0.5.2-pre.006-delta-fix-001`
- ajoute la fenêtre frontend `demo_wallet.html` et son contrôleur `demo_wallet.ts` sur le même gabarit Bootstrap/Simplebar que les autres démonstrations ;
- ajoute `open_demo_wallet_window`, l'entrée `Wallets` dans le menu `Démos` de `main.html`, l'entrée Vite multipage et la capability Tauri `demo_wallet` ;
- permet de rafraîchir l'inventaire sûr des `.kswallet` et d'inspecter explicitement un fichier externe par chemin sans afficher ce chemin dans le résultat ;
- ajoute un test statique de câblage frontend afin que la présence du backend `demo_wallet` ne puisse plus être validée sans fenêtre réellement ouvrable.
## `0.5.2-pre.006`
- ajoute des DTO TS-RS possédés par le desktop pour l'inventaire des wallets natifs : alias, pubkey déclarée, version et état de sélection uniquement ;
- ajoute des commandes Tauri lecture seule pour lister le store configuré et inspecter un `.kswallet` sélectionné explicitement hors store, sans retourner son chemin ;
- expose l'alias persistant sélectionné uniquement dans le diagnostic wallet borné et ajoute des canaris interdisant password, ciphertext, nonce, sel et chemins de stockage dans les DTO.
## `0.5.1-pre.008` ## `0.5.1-pre.008`
- Fix: remplace les snapshots `ks-onchain-transport` retournés directement par les commandes Tauri HTTP/WS par des DTO desktop sanitisés, fait supprimer lURL des snapshots backend et supprime `endpointUrl` des payloads dexécution/statut et ajoute des canaris empêchant toute URL résolue de traverser le frontend. Supprime également le chemin local du fixture Token-2022 des payloads fonctionnels et des erreurs affichées. - Fix: remplace les snapshots `ks-onchain-transport` retournés directement par les commandes Tauri HTTP/WS par des DTO desktop sanitisés, fait supprimer lURL des snapshots backend et supprime `endpointUrl` des payloads dexécution/statut et ajoute des canaris empêchant toute URL résolue de traverser le frontend. Supprime également le chemin local du fixture Token-2022 des payloads fonctionnels et des erreurs affichées.

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/README.md --> <!-- file: kb-app-demo-desktop/README.md -->
<!-- version: 15 --> <!-- version: 19 -->
# kb-app-demo-desktop # kb-app-demo-desktop
@@ -8,6 +8,7 @@
## 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 dun conteneur persistant via un secret backend-only, inspection explicite dun fichier 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 PostgreSQL ;
- 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 ;

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/USAGE.md --> <!-- file: kb-app-demo-desktop/USAGE.md -->
<!-- version: 15 --> <!-- version: 19 -->
# Utilisation de kb-app-demo-desktop # Utilisation de kb-app-demo-desktop
@@ -7,6 +7,43 @@
Lapplication expose une interface opérateur pour diagnostiquer, simuler et valider les couches de bot3. Lapplication expose une interface opérateur pour diagnostiquer, simuler et valider les couches de bot3.
## Surface wallet sûre
Le backend desktop peut lister les `.kswallet` du répertoire configuré, créer un wallet natif protégé, inspecter un fichier sélectionné explicitement ailleurs et explorer létat on-chain dune adresse publique. Les DTO frontend ne contiennent jamais de matériau secret. Aucun password, chemin interne du store, sel, nonce, ciphertext ou byte privé nest renvoyé par Tauri.
La fenêtre `Wallets`, accessible depuis le menu `Démos` de `main`, fournit quatre surfaces :
- inventaire des wallets natifs du profil actif ;
- création dun nouveau `.kswallet` dans le store du profil actif ;
- explorateur public on-chain ;
- inspection structurale dun `.kswallet` explicite situé hors du store.
### Créer un wallet natif depuis la démo
Le mot de passe de création ne doit pas traverser le frontend ou Tauri IPC. La démo lit uniquement côté backend le secret applicatif :
```bash
export KB_SECRET_DEMO_WALLET_PASSWORD='mot-de-passe-de-demo'
cargo tauri dev -c kb-app-demo-desktop/tauri.conf.json
```
Le secret peut aussi être placé dans le fichier `.env` local chargé par `ks-config`. Le vrai `.env` reste non versionné et ne doit jamais être archivé. LUI ne reçoit quun booléen indiquant si ce secret backend est configuré.
La création demande uniquement lalias. Elle crée `<alias>.kswallet` dans le répertoire wallet résolu du profil actif et ne modifie pas automatiquement `wallet_alias` dans la configuration. Un changement de valeur de `KB_SECRET_DEMO_WALLET_PASSWORD` avant une autre session permet dutiliser un autre mot de passe pour de nouveaux wallets ; ce secret de démonstration nest pas un mot de passe global du format `.kswallet`.
### Explorer une adresse sur un profil RPC sélectionné
Lexplorateur on-chain ne déverrouille aucun wallet : seule la pubkey publique est nécessaire. Le bouton `Explorer` dun wallet inventorié remplit ladresse automatiquement ; toute autre pubkey Solana peut également être saisie.
Les lectures utilisent le transport HTTP du profil choisi dans le sélecteur `Profil RPC` de la fenêtre Wallets. Le profil actif est sélectionné par défaut, mais `local_devnet`, `mainnet_research` ou tout autre profil HTTP résolu peut être choisi sans redémarrer lapplication :
- `getBalance` pour le solde SOL exact en lamports ;
- `getTokenAccountsByOwner` pour les comptes SPL Token classique et Token-2022 ;
- `getSignaturesForAddress` pour les signatures récentes des transactions qui référencent ladresse ;
- `getTransaction` à la demande pour afficher le détail JSON dune signature.
La liste est donc intitulée **transactions impliquant ladresse** : elle ne prétend pas que le wallet a initié ou signé chaque transaction retournée. Le profil `local_devnet` interroge Devnet ; `mainnet_research` ou `mainnet` interrogent Mainnet Beta. La sélection utilisée par lexplorateur est locale à cette fenêtre : elle ne modifie pas `active_profile`, la configuration du workspace ni le profil utilisé par les autres démos.
## Lancer le desktop ## Lancer le desktop
Depuis la racine du workspace : Depuis la racine du workspace :
@@ -54,6 +91,7 @@ Les autres fenêtres sont ouvertes par les commandes Tauri dédiées :
- diagnostics SQL ; - diagnostics SQL ;
- candidats replay ; - candidats replay ;
- configuration ; - configuration ;
- wallets ;
- extraction Core ; - extraction Core ;
- decode replay ; - decode replay ;
- exécution Solana Core ; - exécution Solana Core ;
@@ -68,6 +106,7 @@ Les commandes Tauri constituent linterface 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");

View File

@@ -6,6 +6,7 @@
"splash", "splash",
"main", "main",
"demo_config", "demo_config",
"demo_wallet",
"demo_http", "demo_http",
"demo_ws", "demo_ws",
"demo_backfill", "demo_backfill",

View File

@@ -0,0 +1,245 @@
<!-- file: kb-app-demo-desktop/frontend/demo_wallet.html -->
<!-- version: 3 -->
<!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="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>Sélectionné</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">
<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 class="small text-body-secondary mt-1">Cluster : <code id="walletOnchainCluster"></code></div>
</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="row g-3 mt-2">
<div class="col-12 col-md-4">
<div class="border rounded p-3 h-100">
<div class="small text-body-secondary">SOL</div>
<div class="fs-4 fw-semibold"><span id="walletSolBalance"></span> SOL</div>
<div class="small"><code id="walletLamportBalance"></code> lamports</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="border rounded p-3 h-100">
<div class="small text-body-secondary">Comptes token</div>
<div class="fs-4 fw-semibold" id="walletTokenCount"></div>
<div class="small" id="walletTokenStatus">En attente</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="border rounded p-3 h-100">
<div class="small text-body-secondary">Transactions récentes</div>
<div class="fs-4 fw-semibold" id="walletTransactionCount"></div>
<div class="small" id="walletTransactionStatus">En attente</div>
</div>
</div>
</div>
<div class="mt-3">
<span class="fw-semibold">Statut global :</span>
<code id="walletOnchainStatus" class="ms-1">En attente</code>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5 card-title mb-3">Tokens détenus</h2>
<div class="table-responsive app-table-full-width">
<table class="table table-sm align-middle mb-0">
<thead>
<tr>
<th>Programme</th>
<th>Mint</th>
<th>Compte token</th>
<th>Montant</th>
<th>Raw</th>
</tr>
</thead>
<tbody id="walletTokensTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5 card-title mb-3">Transactions impliquant l'adresse</h2>
<div class="table-responsive app-table-full-width">
<table class="table table-sm align-middle mb-0">
<thead>
<tr>
<th>Signature</th>
<th>Slot</th>
<th>Date</th>
<th>Statut</th>
<th>Confirmation</th>
<th>Action</th>
</tr>
</thead>
<tbody id="walletTransactionsTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5 card-title mb-3">Détail de transaction</h2>
<div id="walletTransactionJson" class="app-config-box app-json-viewer mb-0">{"status":"idle","message":"Sélectionner une transaction."}</div>
</div>
</div>
</div>
<div class="col-12 col-xl-5">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h2 class="h5 card-title mb-3">Inspecter un fichier externe</h2>
<p class="text-body-secondary">Le chemin saisi est transmis au backend uniquement pour cette inspection et n'est pas retourné dans le résultat.</p>
<label for="walletExternalPathInput" class="form-label">Chemin du fichier <code>.kswallet</code></label>
<input id="walletExternalPathInput" class="form-control font-monospace" type="text" autocomplete="off" placeholder="/chemin/vers/operator.kswallet">
<div class="d-flex flex-wrap gap-2 mt-3">
<button id="inspectWalletFileButton" class="btn btn-primary" type="button">Inspecter</button>
<button id="clearWalletInspectionButton" class="btn btn-outline-secondary" type="button">Effacer</button>
</div>
<div class="mt-3">
<span class="fw-semibold">Statut :</span>
<code id="walletInspectionStatus" class="ms-1">En attente</code>
</div>
</div>
</div>
</div>
<div class="col-12 col-xl-7">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h2 class="h5 card-title mb-3">Identité inspectée</h2>
<div id="walletInspectionJson" class="app-config-box app-json-viewer mb-0">{"status":"idle","message":"Aucun fichier inspecté."}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<footer class="app-footer bg-dark text-light">
<div class="container h-100 d-flex align-items-center">
<div class="row flex-grow-1 align-items-center">
<div class="col-12 text-center text-small my-1 my-md-0">&copy; 2026 SASEDEV</div>
</div>
</div>
</footer>
<script type="module" src="ts/demo_wallet.ts" defer></script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/main.html --> <!-- file: kb-app-demo-desktop/frontend/main.html -->
<!-- version: 11 --> <!-- version: 12 -->
<!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>
@@ -99,4 +100,4 @@
<script type="module" src="ts/main.ts" defer></script> <script type="module" src="ts/main.ts" defer></script>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,532 @@
// file: kb-app-demo-desktop/frontend/ts/demo_wallet.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.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 { DemoWalletIdentityPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletIdentityPayload.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";
(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;
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("#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();
}
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 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}`;
selectedCell.textContent = wallet.selected ? "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 (wallet.selected) {
row.classList.add("table-primary");
}
row.append(aliasCell, publicKeyCell, formatCell, selectedCell, actionCell);
body.appendChild(row);
}
}
async function refreshWalletInventory(): Promise<void> {
setText("#walletInventoryStatus", "Chargement...");
try {
const payload = await invoke<DemoWalletInventoryPayload>("demo_wallet_inventory");
walletInventory = payload;
setText("#walletProfile", payload.profile);
setText("#walletCluster", payload.cluster);
renderOnchainProfiles(payload.onchain_profiles, payload.profile);
setText("#walletCreateSecretStatus", payload.create_password_configured ? "configuré" : "absent");
setText("#walletSelectedAlias", payload.selected_alias ?? "—");
setText("#walletInventoryCount", String(payload.wallets.length));
setText("#walletInventoryStatus", "OK");
renderWalletRows(payload.wallets);
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([]);
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}`);
}
}
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>("#createWalletButton")?.addEventListener("click", () => {
void createWallet();
});
document.querySelector<HTMLInputElement>("#createWalletAliasInput")?.addEventListener("keydown", event => {
if (event.key === "Enter") {
event.preventDefault();
void createWallet();
}
});
document.querySelector<HTMLSelectElement>("#walletOnchainProfileSelect")?.addEventListener("change", () => {
updateOnchainClusterLabel();
});
document.querySelector<HTMLButtonElement>("#loadWalletOnchainButton")?.addEventListener("click", () => {
void loadWalletOnchain();
});
document.querySelector<HTMLInputElement>("#walletAddressInput")?.addEventListener("keydown", event => {
if (event.key === "Enter") {
event.preventDefault();
void loadWalletOnchain();
}
});
document.querySelector<HTMLButtonElement>("#inspectWalletFileButton")?.addEventListener("click", () => {
void inspectWalletFile();
});
document.querySelector<HTMLButtonElement>("#clearWalletInspectionButton")?.addEventListener("click", () => {
clearWalletInspection();
});
document.querySelector<HTMLInputElement>("#walletExternalPathInput")?.addEventListener("keydown", event => {
if (event.key === "Enter") {
event.preventDefault();
void inspectWalletFile();
}
});
void refreshWalletInventory();
});

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/main.ts // file: kb-app-demo-desktop/frontend/ts/main.ts
// version: 6 // version: 7
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) {

View File

@@ -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: 6
//! Configuration demo payloads with explicit public and bounded diagnostic projections. //! Configuration demo payloads with explicit public and bounded diagnostic projections.
@@ -203,6 +203,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.
@@ -387,6 +389,7 @@ fn diagnostic_config_payload(state: &crate::AppState) -> DemoConfigDiagnosticPay
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,
@@ -547,6 +550,7 @@ mod tests {
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,

View File

@@ -0,0 +1,559 @@
// file: kb-app-demo-desktop/src/demo_wallet.rs
// version: 4
//! UI-safe wallet management and public on-chain exploration.
use ts_rs::TS; // rust-rules: derive-import
const DEMO_WALLET_PASSWORD_ENV: &str = "KB_SECRET_DEMO_WALLET_PASSWORD";
const LAMPORTS_PER_SOL: u64 = 1_000_000_000;
/// UI-safe inventory of native wallets visible to the active profile.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletInventoryPayload.ts"
)]
pub(crate) struct DemoWalletInventoryPayload {
/// Active resolved application profile.
pub(crate) profile: std::string::String,
/// Active wallet cluster.
pub(crate) cluster: std::string::String,
/// Profiles available for explicit read-only on-chain exploration.
pub(crate) onchain_profiles: std::vec::Vec<DemoWalletOnchainProfilePayload>,
/// Canonical classic SPL Token program identifier used by the explorer.
pub(crate) token_program_id: std::string::String,
/// Canonical Token-2022 program identifier used by the explorer.
pub(crate) token_2022_program_id: std::string::String,
/// Whether the backend-only demo password environment variable is present.
pub(crate) create_password_configured: bool,
/// Persistent alias selected by configuration, when present.
pub(crate) selected_alias: std::option::Option<std::string::String>,
/// Structurally valid native wallet identities discovered in the configured directory.
pub(crate) wallets: std::vec::Vec<DemoWalletIdentityPayload>,
}
/// One UI-safe native wallet identity.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletIdentityPayload.ts"
)]
pub(crate) struct DemoWalletIdentityPayload {
/// Non-sensitive wallet alias.
pub(crate) alias: std::string::String,
/// Public key declared by the native wallet header.
pub(crate) public_key: std::string::String,
/// Native wallet format version.
pub(crate) format_version: u16,
/// Whether this alias is selected by the active profile.
pub(crate) selected: bool,
}
/// Request to create one native wallet in the active profile store.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletCreateRequest.ts"
)]
pub(crate) struct DemoWalletCreateRequest {
/// Alias of the new persistent wallet.
pub(crate) alias: std::string::String,
}
/// Exact SOL balance returned for one public wallet address.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletBalancePayload.ts"
)]
pub(crate) struct DemoWalletBalancePayload {
/// Explicitly selected resolved application profile.
pub(crate) profile: std::string::String,
/// Cluster selected by that profile.
pub(crate) cluster: std::string::String,
/// Public address queried on-chain.
pub(crate) address: std::string::String,
/// Exact lamport balance represented as a decimal string.
pub(crate) lamports: std::string::String,
/// Exact SOL balance represented without floating-point conversion.
pub(crate) sol: std::string::String,
/// Endpoint selected for the balance query.
pub(crate) endpoint_name: std::string::String,
/// Provider selected for the balance query.
pub(crate) provider: std::string::String,
}
/// One selectable profile for read-only wallet on-chain exploration.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletOnchainProfilePayload.ts"
)]
pub(crate) struct DemoWalletOnchainProfilePayload {
/// Resolved profile name.
pub(crate) name: std::string::String,
/// Cluster selected by that profile.
pub(crate) cluster: std::string::String,
/// Whether this is the application-wide active profile.
pub(crate) active: bool,
}
/// Request for one wallet-scoped public JSON-RPC read on an explicitly selected profile.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletRpcRequest.ts"
)]
pub(crate) struct DemoWalletRpcRequest {
/// Resolved profile name used for transport selection.
pub(crate) profile: std::string::String,
/// Allowed read-only Solana JSON-RPC method.
pub(crate) method: std::string::String,
/// Complete positional JSON-RPC parameters encoded as one JSON array.
pub(crate) params_json: std::string::String,
}
/// Result of one wallet-scoped public JSON-RPC read.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletRpcExecutionPayload.ts"
)]
pub(crate) struct DemoWalletRpcExecutionPayload {
/// Resolved profile name used by the request.
pub(crate) profile: std::string::String,
/// Cluster selected by that profile.
pub(crate) cluster: std::string::String,
/// Selected endpoint name.
pub(crate) endpoint_name: std::string::String,
/// Selected provider code.
pub(crate) provider: std::string::String,
/// JSON-RPC method name.
pub(crate) method: std::string::String,
/// Pretty JSON response text.
pub(crate) response_json: std::string::String,
}
/// Returns the native wallet inventory visible to the active profile without secret material.
pub(crate) async fn demo_wallet_inventory(
state: &crate::AppState,
) -> std::result::Result<DemoWalletInventoryPayload, std::string::String> {
let profile = state.active_profile();
let manager = match wallet_manager_for_profile(profile) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let handles = match manager.scan().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let selected_alias = profile.wallet.wallet_alias.clone();
let mut wallets = std::vec::Vec::with_capacity(handles.len());
for handle in handles {
wallets.push(wallet_identity_payload(&handle, selected_alias.as_deref()));
}
return std::result::Result::Ok(DemoWalletInventoryPayload {
profile: profile.name.clone(),
cluster: profile.wallet.cluster.clone(),
onchain_profiles: onchain_profile_payloads(state),
token_program_id: ks_program_ids::SPL_TOKEN_PROGRAM_ID.to_string(),
token_2022_program_id: ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string(),
create_password_configured: std::env::var_os(DEMO_WALLET_PASSWORD_ENV).is_some(),
selected_alias,
wallets,
});
}
/// Creates one password-protected native wallet without sending the password through Tauri IPC.
pub(crate) async fn demo_wallet_create_native(
state: &crate::AppState,
request: crate::DemoWalletCreateRequest,
) -> std::result::Result<DemoWalletIdentityPayload, std::string::String> {
let alias = match ks_wallet::WalletAlias::parse(request.alias.trim().to_string()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_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"
));
},
};
let password = match ks_wallet::WalletPassword::new(password_text) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let profile = state.active_profile();
let manager = match wallet_manager_for_profile(profile) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let wallet = match manager.create(alias, password).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let identity = wallet.identity();
let payload = DemoWalletIdentityPayload {
selected: profile.wallet.wallet_alias.as_deref()
== std::option::Option::Some(identity.alias.as_str()),
alias: identity.alias.as_str().to_string(),
public_key: identity.public_key,
format_version: 1,
};
wallet.lock();
return std::result::Result::Ok(payload);
}
/// Inspects one explicitly selected native wallet file and returns only its safe identity.
pub(crate) async fn demo_wallet_inspect_file(
state: &crate::AppState,
path: std::string::String,
) -> std::result::Result<DemoWalletIdentityPayload, std::string::String> {
let profile = state.active_profile();
let manager = match wallet_manager_for_profile(profile) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let handle = match manager.inspect_file(std::path::PathBuf::from(path)).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(wallet_identity_payload(
&handle,
profile.wallet.wallet_alias.as_deref(),
));
}
/// Loads the exact SOL balance of one public address through an explicitly selected profile.
pub(crate) async fn demo_wallet_balance(
state: &crate::AppState,
profile: std::string::String,
address: std::string::String,
) -> std::result::Result<crate::DemoWalletBalancePayload, std::string::String> {
let selected_profile = match onchain_profile(state, &profile) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pool = match ks_onchain_transport::HttpEndpointPool::from_profile(selected_profile) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let address = address.trim().to_string();
if address.is_empty() {
return std::result::Result::Err("wallet address must not be empty".to_string());
}
let role = match best_http_role_for_method(&pool, "getBalance") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let client = match pool.select_client_for_role_and_method(&role, "getBalance") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let result = match client
.get_balance(
&ks_lib::MdPubkey(address.clone()),
&ks_onchain_transport::GetBalanceConfig::confirmed(),
)
.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::DemoWalletBalancePayload {
profile: selected_profile.name.clone(),
cluster: selected_profile.wallet.cluster.clone(),
address,
lamports: result.lamports.to_string(),
sol: format_lamports_as_sol(result.lamports),
endpoint_name: client.endpoint_name().to_string(),
provider: client.provider().to_string(),
});
}
/// Executes one bounded public wallet explorer RPC through an explicitly selected profile.
pub(crate) async fn demo_wallet_rpc_execute(
state: &crate::AppState,
request: crate::DemoWalletRpcRequest,
) -> std::result::Result<crate::DemoWalletRpcExecutionPayload, std::string::String> {
let selected_profile = match onchain_profile(state, &request.profile) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let method = request.method.trim().to_string();
if !wallet_rpc_method_allowed(&method) {
return std::result::Result::Err(format!(
"wallet explorer RPC method '{method}' is not allowed"
));
}
let params = match serde_json::from_str::<std::vec::Vec<serde_json::Value>>(
request.params_json.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(format!(
"wallet explorer params must be a JSON array: {error}"
));
},
};
let pool = match ks_onchain_transport::HttpEndpointPool::from_profile(selected_profile) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let role = match best_http_role_for_method(&pool, &method) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let client = match pool.select_client_for_role_and_method(&role, &method) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let response = match client.execute_json_rpc_result_raw(method.clone(), params).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let response_json = match serde_json::to_string_pretty(&response) {
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::DemoWalletRpcExecutionPayload {
profile: selected_profile.name.clone(),
cluster: selected_profile.wallet.cluster.clone(),
endpoint_name: client.endpoint_name().to_string(),
provider: client.provider().to_string(),
method,
response_json,
});
}
fn onchain_profile<'a>(
state: &'a crate::AppState,
profile_name: &str,
) -> std::result::Result<&'a ks_config::ProfileConfig, std::string::String> {
let profile_name = profile_name.trim();
if profile_name.is_empty() {
return std::result::Result::Err("wallet on-chain profile must not be empty".to_string());
}
for profile in &state.app_config().profiles {
if profile.name == profile_name {
return std::result::Result::Ok(profile);
}
}
return std::result::Result::Err(format!(
"wallet on-chain profile '{profile_name}' is not configured"
));
}
fn onchain_profile_payloads(
state: &crate::AppState,
) -> std::vec::Vec<crate::DemoWalletOnchainProfilePayload> {
let mut profiles = std::vec::Vec::new();
for profile in &state.app_config().profiles {
let has_enabled_http =
profile.solana.http_endpoints.iter().any(|endpoint| return endpoint.enabled);
if !has_enabled_http {
continue;
}
profiles.push(crate::DemoWalletOnchainProfilePayload {
name: profile.name.clone(),
cluster: profile.wallet.cluster.clone(),
active: profile.name == state.active_profile().name,
});
}
return profiles;
}
fn wallet_rpc_method_allowed(method: &str) -> bool {
return method == "getTokenAccountsByOwner"
|| method == "getSignaturesForAddress"
|| method == "getTransaction";
}
fn wallet_manager_for_profile(
profile: &ks_config::ProfileConfig,
) -> std::result::Result<ks_wallet::WalletManager, std::string::String> {
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let directory = if configured.is_absolute() {
configured
} else {
crate::workspace_root_dir().join(configured)
};
return match ks_wallet::WalletManager::new(directory) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
}
fn wallet_identity_payload(
handle: &ks_wallet::WalletFileHandle,
selected_alias: std::option::Option<&str>,
) -> crate::DemoWalletIdentityPayload {
return crate::DemoWalletIdentityPayload {
selected: selected_alias == std::option::Option::Some(handle.alias().as_str()),
alias: handle.alias().as_str().to_string(),
public_key: handle.public_key().to_string(),
format_version: handle.format_version(),
};
}
fn best_http_role_for_method(
pool: &ks_onchain_transport::HttpEndpointPool,
method: &str,
) -> std::result::Result<std::string::String, std::string::String> {
let request_kind = ks_onchain_transport::request_kind_from_method(method);
let mut selected = std::option::Option::<(u32, std::string::String)>::None;
for endpoint in pool.snapshot() {
for role in endpoint.roles {
if !role.enabled {
continue;
}
let supports_kind = role
.request_kinds
.iter()
.any(|kind| return kind == &request_kind || kind == "*");
if !supports_kind {
continue;
}
let replace = match &selected {
std::option::Option::Some((priority, selected_role)) => {
role.priority < *priority
|| (role.priority == *priority && role.role < *selected_role)
},
std::option::Option::None => true,
};
if replace {
selected = std::option::Option::Some((role.priority, role.role));
}
}
}
return match selected {
std::option::Option::Some((_, role)) => std::result::Result::Ok(role),
std::option::Option::None => std::result::Result::Err(format!(
"active HTTP profile has no role for request kind '{request_kind}'"
)),
};
}
fn format_lamports_as_sol(lamports: u64) -> std::string::String {
let whole = lamports / LAMPORTS_PER_SOL;
let fraction = lamports % LAMPORTS_PER_SOL;
if fraction == 0 {
return whole.to_string();
}
let fraction_text = format!("{fraction:09}");
let trimmed = fraction_text.trim_end_matches('0');
return format!("{whole}.{trimmed}");
}
#[cfg(test)]
mod tests {
#[test]
fn wallet_frontend_is_wired_to_management_and_onchain_read_commands() {
let html = include_str!("../frontend/demo_wallet.html");
let script = include_str!("../frontend/ts/demo_wallet.ts");
let main_html = include_str!("../frontend/main.html");
let main_script = include_str!("../frontend/ts/main.ts");
let tauri_runtime = include_str!("tauri.rs");
let capability = include_str!("../capabilities/default.json");
let vite_config = include_str!("../vite.config.ts");
for required in [
"createWalletAliasInput",
"createWalletButton",
"refreshWalletInventoryButton",
"walletInventoryTableBody",
"walletOnchainProfileSelect",
"walletAddressInput",
"loadWalletOnchainButton",
"walletTokensTableBody",
"walletTransactionsTableBody",
"walletTransactionJson",
"walletExternalPathInput",
"inspectWalletFileButton",
"walletInspectionJson",
] {
assert!(html.contains(required));
}
assert!(script.contains("demo_wallet_create_native"));
assert!(script.contains("demo_wallet_balance"));
assert!(script.contains("demo_wallet_inventory"));
assert!(script.contains("demo_wallet_rpc_execute"));
assert!(script.contains("demo_wallet_inspect_file"));
assert!(!script.contains("demo_http_execute_request"));
assert!(script.contains("getTokenAccountsByOwner"));
assert!(script.contains("getSignaturesForAddress"));
assert!(script.contains("getTransaction"));
assert!(!html.to_ascii_lowercase().contains("type=\"password\""));
assert!(main_html.contains("openDemoWalletLink"));
assert!(main_script.contains("open_demo_wallet_window"));
assert!(tauri_runtime.contains("open_demo_wallet_window"));
assert!(tauri_runtime.contains("demo_wallet_create_native"));
assert!(tauri_runtime.contains("demo_wallet_balance"));
assert!(tauri_runtime.contains("demo_wallet_rpc_execute"));
assert!(tauri_runtime.contains("demo_wallet.html"));
assert!(capability.contains("\"demo_wallet\""));
assert!(vite_config.contains("frontend/demo_wallet.html"));
}
#[test]
fn wallet_inventory_payload_contains_only_explicit_safe_fields() {
let payload = super::DemoWalletInventoryPayload {
profile: "mainnet_research".to_string(),
cluster: "mainnet-beta".to_string(),
onchain_profiles: std::vec![super::DemoWalletOnchainProfilePayload {
name: "local_devnet".to_string(),
cluster: "devnet".to_string(),
active: false,
}],
token_program_id: ks_program_ids::SPL_TOKEN_PROGRAM_ID.to_string(),
token_2022_program_id: ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string(),
create_password_configured: true,
selected_alias: std::option::Option::Some("selected-wallet".to_string()),
wallets: std::vec![super::DemoWalletIdentityPayload {
alias: "selected-wallet".to_string(),
public_key: "11111111111111111111111111111111".to_string(),
format_version: 1,
selected: true,
}],
};
let serialized = match serde_json::to_string(&payload) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("wallet inventory must serialize: {error}"),
};
for forbidden in [
"canary-wallet-password",
"ciphertext",
"nonce",
"salt",
"wallet_path",
"storage_path",
".kswallet",
"http://",
"https://",
] {
assert!(!serialized.contains(forbidden));
}
}
#[test]
fn wallet_rpc_surface_is_read_only_and_bounded() {
for allowed in ["getTokenAccountsByOwner", "getSignaturesForAddress", "getTransaction"] {
assert!(super::wallet_rpc_method_allowed(allowed));
}
for forbidden in
["requestAirdrop", "sendTransaction", "simulateTransaction", "getProgramAccounts"]
{
assert!(!super::wallet_rpc_method_allowed(forbidden));
}
}
#[test]
fn lamport_formatting_is_exact_without_float_conversion() {
assert_eq!(super::format_lamports_as_sol(0), "0");
assert_eq!(super::format_lamports_as_sol(1), "0.000000001");
assert_eq!(super::format_lamports_as_sol(1_500_000_000), "1.5");
assert_eq!(super::format_lamports_as_sol(u64::MAX), "18446744073.709551615");
}
}

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/lib.rs // file: kb-app-demo-desktop/src/lib.rs
// version: 37 // version: 40
//! Tauri desktop demo application for `khadhroony-bot3`. //! Tauri desktop demo application for `khadhroony-bot3`.
@@ -30,6 +30,7 @@ mod demo_sql_pg_core;
mod demo_sql_pg_raw; mod demo_sql_pg_raw;
mod demo_sql_replay_candidates; mod demo_sql_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;
@@ -362,6 +363,30 @@ pub(crate) use self::demo_sql_replay_candidates::load_demo_sql_replay_programs;
pub(crate) use self::demo_sql_replay_candidates::load_demo_sql_replay_transactions; pub(crate) use self::demo_sql_replay_candidates::load_demo_sql_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;
/// UI-safe native wallet identity exposed by the desktop adapter.
pub(crate) use self::demo_wallet::DemoWalletIdentityPayload;
/// 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;
/// 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;
/// Inspects an explicitly selected native wallet file without exposing its path in the result.
pub(crate) use self::demo_wallet::demo_wallet_inspect_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;
/// 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.

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/tauri.rs // file: kb-app-demo-desktop/src/tauri.rs
// version: 38 // version: 42
//! Tauri runtime assembly and private command wrappers. //! Tauri runtime assembly and private command wrappers.
@@ -36,6 +36,12 @@ 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_create_native,
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,
@@ -235,6 +241,53 @@ 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_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_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,

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/vite.config.ts // file: kb-app-demo-desktop/vite.config.ts
// version: 9 // version: 10
import { defineConfig, normalizePath } from "vite"; import { defineConfig, normalizePath } from "vite";
import { NodePackageImporter } from "sass-embedded"; import { NodePackageImporter } from "sass-embedded";
@@ -24,6 +24,7 @@ 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')),

View File

@@ -1,8 +1,14 @@
<!-- file: ks-config/CHANGELOG.md --> <!-- file: ks-config/CHANGELOG.md -->
<!-- version: 20 --> <!-- version: 21 -->
# CHANGELOG — ks-config # CHANGELOG — ks-config
## `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` ;

View File

@@ -1,5 +1,5 @@
<!-- file: ks-config/README.md --> <!-- file: ks-config/README.md -->
<!-- version: 12 --> <!-- version: 13 -->
# 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 ;

View File

@@ -1,5 +1,5 @@
<!-- file: ks-config/USAGE.md --> <!-- file: ks-config/USAGE.md -->
<!-- version: 10 --> <!-- version: 11 -->
# 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

View File

@@ -1,5 +1,5 @@
// file: ks-config/src/lib.rs // file: ks-config/src/lib.rs
// version: 9 // version: 10
//! Khadhroony Solana shared configuration contracts and composition helpers. //! Khadhroony Solana shared configuration contracts and composition helpers.
#![warn(missing_docs)] #![warn(missing_docs)]
@@ -234,5 +234,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;

View File

@@ -1,5 +1,5 @@
// file: ks-config/src/settings.rs // file: ks-config/src/settings.rs
// version: 23 // version: 24
//! Resolved runtime configuration models retained during the `0.5.1` source-document split. //! Resolved runtime configuration models retained during the `0.5.1` source-document split.
@@ -226,6 +226,8 @@ pub struct WalletConfig {
pub wallet_dir: std::string::String, pub wallet_dir: std::string::String,
/// Selected cluster name. /// Selected cluster name.
pub cluster: std::string::String, pub cluster: std::string::String,
/// Optional persistent native wallet alias selected by this profile.
pub wallet_alias: std::option::Option<std::string::String>,
/// Enables the managed temporary wallet for this profile. /// Enables the managed temporary wallet for this profile.
pub temporary_wallet_enabled: bool, pub temporary_wallet_enabled: bool,
/// Alias used for the managed temporary wallet. /// Alias used for the managed temporary wallet.
@@ -736,7 +738,13 @@ pub(crate) fn validate_wallet(config: &WalletConfig) -> ks_core::Result<()> {
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),
} }
match validate_wallet_alias(&config.temporary_wallet_alias) { if let std::option::Option::Some(alias) = config.wallet_alias.as_deref() {
match validate_wallet_alias(alias, "wallet.wallet_alias") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
match validate_wallet_alias(&config.temporary_wallet_alias, "wallet.temporary_wallet_alias") {
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),
} }
@@ -755,11 +763,11 @@ pub(crate) fn validate_wallet(config: &WalletConfig) -> ks_core::Result<()> {
return std::result::Result::Ok(()); return std::result::Result::Ok(());
} }
fn validate_wallet_alias(value: &str) -> ks_core::Result<()> { pub(crate) fn validate_wallet_alias(value: &str, field: &str) -> ks_core::Result<()> {
if value.is_empty() || value.len() > 64 { if value.is_empty() || value.len() > 64 {
return std::result::Result::Err(ks_core::Error::new( return std::result::Result::Err(ks_core::Error::new(
"wallet_alias_length_invalid", "wallet_alias_length_invalid",
"temporary wallet alias length must be between 1 and 64 bytes", format!("{field} length must be between 1 and 64 bytes"),
)); ));
} }
if !value.bytes().enumerate().all(|(index, byte)| { if !value.bytes().enumerate().all(|(index, byte)| {
@@ -767,7 +775,7 @@ fn validate_wallet_alias(value: &str) -> ks_core::Result<()> {
}) { }) {
return std::result::Result::Err(ks_core::Error::new( return std::result::Result::Err(ks_core::Error::new(
"wallet_alias_invalid", "wallet_alias_invalid",
"temporary wallet alias contains invalid characters", format!("{field} contains invalid characters"),
)); ));
} }
return std::result::Result::Ok(()); return std::result::Result::Ok(());
@@ -1016,6 +1024,16 @@ mod tests {
assert!(result.is_err()); assert!(result.is_err());
} }
#[test]
fn parser_rejects_invalid_persistent_wallet_alias() {
let mut value = parse_default_value();
value["profiles"][0]["wallet"]["wallet_alias"] =
serde_json::Value::String("../wallet".to_string());
let raw_json = value_to_json(&value);
let result = super::parse_config_json(&raw_json);
assert!(result.is_err());
}
#[test] #[test]
fn parser_rejects_invalid_temporary_wallet_alias() { fn parser_rejects_invalid_temporary_wallet_alias() {
let mut value = parse_default_value(); let mut value = parse_default_value();

View File

@@ -1,5 +1,5 @@
// file: ks-config/src/wallet.rs // file: ks-config/src/wallet.rs
// version: 3 // version: 4
//! Independent wallet configuration document loading and validation. //! Independent wallet configuration document loading and validation.
@@ -25,6 +25,8 @@ pub struct WalletProfileConfig {
pub directory: std::string::String, pub directory: std::string::String,
/// Selected cluster name. /// Selected cluster name.
pub cluster: std::string::String, pub cluster: std::string::String,
/// Optional persistent native wallet alias selected by this profile.
pub wallet_alias: std::option::Option<std::string::String>,
/// Enables the managed temporary wallet for this profile. /// Enables the managed temporary wallet for this profile.
pub temporary_wallet_enabled: bool, pub temporary_wallet_enabled: bool,
/// Alias used for the managed temporary wallet. /// Alias used for the managed temporary wallet.
@@ -150,6 +152,7 @@ pub fn resolved_wallet_profile(
let resolved = crate::WalletConfig { let resolved = crate::WalletConfig {
wallet_dir: wallet_dir.display().to_string(), wallet_dir: wallet_dir.display().to_string(),
cluster: profile.cluster.clone(), cluster: profile.cluster.clone(),
wallet_alias: profile.wallet_alias.clone(),
temporary_wallet_enabled: profile.temporary_wallet_enabled, temporary_wallet_enabled: profile.temporary_wallet_enabled,
temporary_wallet_alias: profile.temporary_wallet_alias.clone(), temporary_wallet_alias: profile.temporary_wallet_alias.clone(),
temporary_wallet_persist: profile.temporary_wallet_persist, temporary_wallet_persist: profile.temporary_wallet_persist,
@@ -186,6 +189,12 @@ pub fn validate_wallet_document(document: &WalletConfigDocument) -> ks_core::Res
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),
} }
if let std::option::Option::Some(alias) = profile.wallet_alias.as_deref() {
match crate::validate_wallet_alias(alias, "wallet.profile.wallet_alias") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
if !names.insert(profile.name.clone()) { if !names.insert(profile.name.clone()) {
return std::result::Result::Err(ks_core::Error::new( return std::result::Result::Err(ks_core::Error::new(
"wallet_profile_name_duplicate", "wallet_profile_name_duplicate",
@@ -210,7 +219,55 @@ mod tests {
#[test] #[test]
fn default_and_example_wallet_configs_validate() { fn default_and_example_wallet_configs_validate() {
assert!(super::parse_wallet_json(DEFAULT_WALLET).is_ok()); let default = super::parse_wallet_json(DEFAULT_WALLET);
assert!(super::parse_wallet_json(EXAMPLE_WALLET).is_ok()); assert!(default.is_ok());
let example = super::parse_wallet_json(EXAMPLE_WALLET);
assert!(example.is_ok());
let example = match example {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(
example.profiles[0].wallet_alias.as_deref(),
std::option::Option::Some("example-operator")
);
}
#[test]
fn wallet_profile_without_persistent_alias_remains_backward_compatible() {
let mut value = match serde_json::from_str::<serde_json::Value>(DEFAULT_WALLET) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("default wallet JSON failed: {error}"),
};
let profile = match value["profiles"][0].as_object_mut() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("default wallet profile must be an object"),
};
assert!(profile.remove("wallet_alias").is_some());
let raw = match serde_json::to_string(&value) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("wallet JSON encode failed: {error}"),
};
let parsed = super::parse_wallet_json(raw.as_str());
assert!(parsed.is_ok());
let parsed = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert!(parsed.profiles[0].wallet_alias.is_none());
}
#[test]
fn wallet_profile_rejects_invalid_persistent_alias() {
let mut value = match serde_json::from_str::<serde_json::Value>(EXAMPLE_WALLET) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("example wallet JSON failed: {error}"),
};
value["profiles"][0]["wallet_alias"] = serde_json::Value::String("../wallet".to_string());
let raw = match serde_json::to_string(&value) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("wallet JSON encode failed: {error}"),
};
assert!(super::parse_wallet_json(raw.as_str()).is_err());
} }
} }

View File

@@ -1,8 +1,15 @@
<!-- file: ks-pipeline-demo-scenarios/CHANGELOG.md --> <!-- file: ks-pipeline-demo-scenarios/CHANGELOG.md -->
<!-- version: 78 --> <!-- version: 79 -->
# CHANGELOG — ks-pipeline-demo-scenarios # CHANGELOG — ks-pipeline-demo-scenarios
## `0.5.2-pre.006`
- ajoute l'inspection et l'unlock explicites du wallet natif sélectionné par `profile.wallet.wallet_alias`, avec `WalletPassword` fourni uniquement au runtime ;
- renomme le chemin historique en `load_profile_temporary_wallet` et interdit qu'un alias persistant configuré retombe silencieusement sur le wallet temporaire ;
- refuse les fichiers `.kswallet` dans le workflow CLI Token-2022 fondé sur `solana-keygen` / `spl-token`, exige explicitement un export `SolanaCliJson` et ne révèle plus le chemin complet dun wallet absent ;
- ajoute des tests de sélection persistante, unlock, refus de fallback et rejet du conteneur natif dans le workflow CLI.
## `0.5.1-pre.007` ## `0.5.1-pre.007`
- supprime la composition par défaut dédiée aux scénarios et charge directement les defaults partagés de `ks-config` ; - supprime la composition par défaut dédiée aux scénarios et charge directement les defaults partagés de `ks-config` ;

View File

@@ -1,5 +1,5 @@
# file: ks-pipeline-demo-scenarios/Cargo.toml # file: ks-pipeline-demo-scenarios/Cargo.toml
# version: 12 # version: 13
[package] [package]
name = "ks-pipeline-demo-scenarios" name = "ks-pipeline-demo-scenarios"
@@ -43,5 +43,8 @@ tokio.workspace = true
tracing.workspace = true tracing.workspace = true
uuid.workspace = true uuid.workspace = true
[dev-dependencies]
tempfile.workspace = true
[lints] [lints]
workspace = true workspace = true

View File

@@ -1,5 +1,5 @@
<!-- file: ks-pipeline-demo-scenarios/README.md --> <!-- file: ks-pipeline-demo-scenarios/README.md -->
<!-- version: 30 --> <!-- version: 31 -->
# ks-pipeline-demo-scenarios # ks-pipeline-demo-scenarios
@@ -8,7 +8,8 @@
La crate contient : La crate contient :
- la bibliothèque Rust `ks_pipeline_demo_scenarios` ; - la bibliothèque Rust `ks_pipeline_demo_scenarios` ;
- le binaire ciblé `ks-pipeline-demo-scenarios-cli`. - le binaire ciblé `ks-pipeline-demo-scenarios-cli` ;
- les adaptateurs de sélection wallet qui respectent `wallet_alias` sans stocker de password dans la configuration.
## Responsabilités ## Responsabilités

View File

@@ -1,5 +1,5 @@
<!-- file: ks-pipeline-demo-scenarios/USAGE.md --> <!-- file: ks-pipeline-demo-scenarios/USAGE.md -->
<!-- version: 31 --> <!-- version: 32 -->
# Utilisation de ks-pipeline-demo-scenarios # Utilisation de ks-pipeline-demo-scenarios
@@ -7,6 +7,12 @@
La bibliothèque expose des scénarios Devnet contrôlés. Le binaire permet leur exécution hors de lapplication desktop lorsque la commande correspondante est disponible. La bibliothèque expose des scénarios Devnet contrôlés. Le binaire permet leur exécution hors de lapplication desktop lorsque la commande correspondante est disponible.
## Wallet sélectionné par le profil
`inspect_selected_profile_wallet()` résout l'alias persistant non sensible configuré dans `ks-config`. `unlock_selected_profile_wallet()` exige ensuite un `ks_wallet::WalletPassword` fourni explicitement par le consommateur. Les scénarios historiques qui utilisent encore le wallet temporaire refusent un fallback silencieux lorsqu'un `wallet_alias` persistant est configuré.
Le CLI `prepare-token-2022-fixture` reste un workflow externe Solana CLI : son `--wallet` doit viser un keypair JSON exporté explicitement et refuse un fichier `.kswallet`.
## Initialiser lenvironnement ## Initialiser lenvironnement
```rust ```rust

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/bin/ks_pipeline_demo_scenarios_cli.rs // file: ks-pipeline-demo-scenarios/src/bin/ks_pipeline_demo_scenarios_cli.rs
// version: 3 // version: 4
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -102,6 +102,13 @@ fn parse_prepare_options(
)); ));
}, },
}; };
if wallet_path.extension().and_then(|value| return value.to_str())
== std::option::Option::Some(ks_wallet::KSWALLET_FILE_EXTENSION)
{
return std::result::Result::Err(ks_core::Error::config(
"native .kswallet files cannot be passed to Solana CLI fixture preparation; export SolanaCliJson explicitly first",
));
}
let wallet_dir = match wallet_dir { let wallet_dir = match wallet_dir {
std::option::Option::Some(value) => value, std::option::Option::Some(value) => value,
std::option::Option::None => match wallet_path.parent() { std::option::Option::None => match wallet_path.parent() {
@@ -127,6 +134,19 @@ fn usage_error<T>(message: &str) -> ks_core::Result<T> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#[test]
fn prepare_options_reject_native_wallet_path() {
let arguments = vec![
"prepare-token-2022-fixture".to_string(),
"--rpc-url".to_string(),
"https://api.devnet.solana.com".to_string(),
"--wallet".to_string(),
"operator.kswallet".to_string(),
];
let options = super::parse_prepare_options(arguments.as_slice());
assert!(options.is_err());
}
#[test] #[test]
fn prepare_options_accept_explicit_values() { fn prepare_options_accept_explicit_values() {
let arguments = vec![ let arguments = vec![

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/lib.rs // file: ks-pipeline-demo-scenarios/src/lib.rs
// version: 29 // version: 30
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -231,10 +231,14 @@ pub(crate) use self::solana::emit;
pub(crate) use self::solana::ensure_not_cancelled; pub(crate) use self::solana::ensure_not_cancelled;
/// Executes one bounded System Program transfer on Devnet. /// Executes one bounded System Program transfer on Devnet.
pub use self::solana::execute_devnet_system_transfer; pub use self::solana::execute_devnet_system_transfer;
/// Loads the persistent wallet configured by one execution profile. /// Inspects the persistent native wallet alias selected by one execution profile.
pub(crate) use self::solana::load_profile_wallet; pub use self::solana::inspect_selected_profile_wallet;
/// Loads the managed temporary wallet used by legacy Devnet scenarios.
pub(crate) use self::solana::load_profile_temporary_wallet;
/// Formats one failed simulation without discarding runtime diagnostics. /// Formats one failed simulation without discarding runtime diagnostics.
pub(crate) use self::solana::simulation_failure_message; pub(crate) use self::solana::simulation_failure_message;
/// Unlocks the persistent native wallet alias selected by one execution profile.
pub use self::solana::unlock_selected_profile_wallet;
/// Formats safety violations for one denied execution plan. /// Formats safety violations for one denied execution plan.
pub(crate) use self::solana::violation_message; pub(crate) use self::solana::violation_message;
/// Complete request for one Devnet Associated Token Account execution. /// Complete request for one Devnet Associated Token Account execution.

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/devnet_execution.rs // file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/devnet_execution.rs
// version: 22 // version: 23
//! Real Devnet simulation and submission for current Metaplex Token Metadata operations. //! Real Devnet simulation and submission for current Metaplex Token Metadata operations.
@@ -839,7 +839,7 @@ where
), ),
)); ));
} }
let wallet = match crate::load_profile_wallet(profile, workspace_root).await { let wallet = match crate::load_profile_temporary_wallet(profile, workspace_root).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),
}; };

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/fixture.rs // file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/fixture.rs
// version: 21 // version: 22
//! Native Metaplex Token Metadata fixture preparation for Devnet demos. //! Native Metaplex Token Metadata fixture preparation for Devnet demos.
@@ -168,7 +168,7 @@ pub async fn prepare_metaplex_create_fixture(
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }
} }
let operator = match crate::load_profile_wallet(profile, workspace_root).await { let operator = match crate::load_profile_temporary_wallet(profile, workspace_root).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),
}; };

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/metadata/solana_program/devnet_execution.rs // file: ks-pipeline-demo-scenarios/src/metadata/solana_program/devnet_execution.rs
// version: 4 // version: 5
//! Real Devnet simulation and controlled submission for Solana Program Metadata. //! Real Devnet simulation and controlled submission for Solana Program Metadata.
@@ -355,7 +355,7 @@ where
), ),
)); ));
} }
let wallet = match crate::load_profile_wallet(profile, workspace_root).await { let wallet = match crate::load_profile_temporary_wallet(profile, workspace_root).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),
}; };

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/metadata/solana_program/fixture.rs // file: ks-pipeline-demo-scenarios/src/metadata/solana_program/fixture.rs
// version: 8 // version: 9
//! Native Solana Program Metadata fixture preparation for Devnet journeys. //! Native Solana Program Metadata fixture preparation for Devnet journeys.
@@ -143,7 +143,7 @@ pub async fn prepare_solana_program_metadata_fixture(
"Solana Program Metadata fixture preparation requires a Devnet endpoint", "Solana Program Metadata fixture preparation requires a Devnet endpoint",
)); ));
} }
let operator = match crate::load_profile_wallet(profile, workspace_root).await { let operator = match crate::load_profile_temporary_wallet(profile, workspace_root).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),
}; };

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/solana.rs // file: ks-pipeline-demo-scenarios/src/solana.rs
// version: 16 // version: 17
//! Devnet Solana execution orchestration with canonical post-validation. //! Devnet Solana execution orchestration with canonical post-validation.
@@ -305,7 +305,7 @@ where
if let std::result::Result::Err(error) = cancellation_result { if let std::result::Result::Err(error) = cancellation_result {
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }
let wallet = match load_profile_wallet(profile, workspace_root).await { let wallet = match load_profile_temporary_wallet(profile, workspace_root).await {
std::result::Result::Ok(wallet) => wallet, std::result::Result::Ok(wallet) => wallet,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
@@ -983,17 +983,92 @@ fn validate_devnet_profile(
return std::result::Result::Ok(()); return std::result::Result::Ok(());
} }
pub(crate) async fn load_profile_wallet( /// Resolves the wallet directory selected by one profile relative to the workspace root.
fn profile_wallet_directory(
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
) -> std::path::PathBuf {
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
if configured.is_absolute() {
return configured;
}
return workspace_root.join(configured);
}
/// Inspects the persistent native wallet alias selected by one profile, when configured.
pub async fn inspect_selected_profile_wallet(
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
) -> ks_core::Result<std::option::Option<ks_wallet::WalletFileHandle>> {
let configured_alias = match profile.wallet.wallet_alias.as_deref() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
let alias = match ks_wallet::WalletAlias::parse(configured_alias) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let manager =
match ks_wallet::WalletManager::new(profile_wallet_directory(profile, workspace_root)) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let handle = match manager.lookup(&alias).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return match handle {
std::option::Option::Some(value) => {
std::result::Result::Ok(std::option::Option::Some(value))
},
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
"wallet_selected_alias_not_found",
"selected native wallet alias was not found in the configured wallet directory",
)),
};
}
/// Unlocks the persistent native wallet selected by one profile using an explicit password.
pub async fn unlock_selected_profile_wallet(
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
password: ks_wallet::WalletPassword,
) -> ks_core::Result<ks_wallet::UnlockedWallet> {
let configured_alias = match profile.wallet.wallet_alias.as_deref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"wallet_selected_alias_missing",
"profile does not select a persistent native wallet alias",
));
},
};
let alias = match ks_wallet::WalletAlias::parse(configured_alias) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let manager =
match ks_wallet::WalletManager::new(profile_wallet_directory(profile, workspace_root)) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return manager.unlock(&alias, password).await;
}
pub(crate) async fn load_profile_temporary_wallet(
profile: &ks_config::ProfileConfig, profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path, workspace_root: &std::path::Path,
) -> ks_core::Result<ks_wallet::TemporaryWallet> { ) -> ks_core::Result<ks_wallet::TemporaryWallet> {
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str()); if profile.wallet.wallet_alias.is_some() {
let directory = if configured.is_absolute() { return std::result::Result::Err(ks_core::Error::new(
configured "wallet_persistent_selection_requires_explicit_unlock",
} else { "profile selects a persistent native wallet; temporary-wallet fallback is forbidden",
workspace_root.join(configured) ));
}; }
let store = match ks_wallet::TemporaryWalletStore::new(directory) { let store = match ks_wallet::TemporaryWalletStore::new(profile_wallet_directory(
profile,
workspace_root,
)) {
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),
}; };
@@ -1223,6 +1298,18 @@ pub(crate) fn emit<O>(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#[cfg(unix)]
fn make_directory_private(path: &std::path::Path) {
use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import
let permissions = std::fs::Permissions::from_mode(0o700);
if let std::result::Result::Err(error) = std::fs::set_permissions(path, permissions) {
panic!("temporary wallet directory permissions failed: {error}");
}
}
#[cfg(not(unix))]
fn make_directory_private(_path: &std::path::Path) {}
fn example_devnet_profile() -> ks_config::ProfileConfig { fn example_devnet_profile() -> ks_config::ProfileConfig {
let config = match ks_config::parse_config_json(include_str!( let config = match ks_config::parse_config_json(include_str!(
"../../test-fixtures/config/resolved.app.config.json" "../../test-fixtures/config/resolved.app.config.json"
@@ -1240,6 +1327,71 @@ mod tests {
return ks_lib::MdPubkey("Vote111111111111111111111111111111111111111".to_string()); return ks_lib::MdPubkey("Vote111111111111111111111111111111111111111".to_string());
} }
#[tokio::test]
async fn selected_persistent_wallet_requires_explicit_password_path() {
let temporary = match tempfile::tempdir() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("temporary directory failed: {error}"),
};
make_directory_private(temporary.path());
let mut profile = example_devnet_profile();
profile.wallet.wallet_dir = temporary.path().display().to_string();
profile.wallet.wallet_alias = std::option::Option::Some("selected-operator".to_string());
let manager = match ks_wallet::WalletManager::new(temporary.path()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("wallet manager failed: {error}"),
};
let alias = match ks_wallet::WalletAlias::parse("selected-operator") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("wallet alias failed: {error}"),
};
let create_password = match ks_wallet::WalletPassword::new("selected-password".to_string())
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("wallet password failed: {error}"),
};
let created = match manager.create(alias, create_password).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("wallet creation failed: {error}"),
};
let expected_public_key = created.public_key();
created.lock();
let inspected =
match super::inspect_selected_profile_wallet(&profile, temporary.path()).await {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => {
panic!("selected wallet must be present")
},
std::result::Result::Err(error) => panic!("wallet inspection failed: {error}"),
};
assert_eq!(inspected.alias().as_str(), "selected-operator");
assert_eq!(inspected.public_key(), expected_public_key.as_str());
let unlock_password = match ks_wallet::WalletPassword::new("selected-password".to_string())
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("wallet password failed: {error}"),
};
let unlocked = match super::unlock_selected_profile_wallet(
&profile,
temporary.path(),
unlock_password,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("wallet unlock failed: {error}"),
};
assert_eq!(unlocked.public_key(), expected_public_key);
let fallback = super::load_profile_temporary_wallet(&profile, temporary.path()).await;
assert!(fallback.is_err());
}
#[test]
fn profile_without_persistent_alias_has_no_selected_native_wallet() {
let profile = example_devnet_profile();
assert!(profile.wallet.wallet_alias.is_none());
}
#[test] #[test]
fn request_and_profile_validation_are_conservative() { fn request_and_profile_validation_are_conservative() {
let profile = example_devnet_profile(); let profile = example_devnet_profile();

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/spl/associated_token_account.rs // file: ks-pipeline-demo-scenarios/src/spl/associated_token_account.rs
// version: 15 // version: 16
//! Devnet ATA execution with stateful and canonical post-validation. //! Devnet ATA execution with stateful and canonical post-validation.
@@ -491,7 +491,7 @@ where
), ),
)); ));
} }
let wallet = match crate::load_profile_wallet(profile, workspace_root).await { let wallet = match crate::load_profile_temporary_wallet(profile, workspace_root).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),
}; };
@@ -1094,7 +1094,7 @@ mod tests {
.parent() .parent()
.map(std::path::Path::to_path_buf) .map(std::path::Path::to_path_buf)
.unwrap_or_else(|| panic!("workspace root missing")); .unwrap_or_else(|| panic!("workspace root missing"));
let wallet = crate::load_profile_wallet(&profile, workspace_root.as_path()) let wallet = crate::load_profile_temporary_wallet(&profile, workspace_root.as_path())
.await .await
.unwrap_or_else(|error| panic!("profile wallet failed: {error}")); .unwrap_or_else(|error| panic!("profile wallet failed: {error}"));
let wallet_pubkey = wallet.public_key(); let wallet_pubkey = wallet.public_key();

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/spl/memo.rs // file: ks-pipeline-demo-scenarios/src/spl/memo.rs
// version: 11 // version: 12
//! Devnet SPL Memo v4 execution with canonical post-validation. //! Devnet SPL Memo v4 execution with canonical post-validation.
@@ -155,7 +155,7 @@ where
), ),
)); ));
} }
let wallet = match crate::load_profile_wallet(profile, workspace_root).await { let wallet = match crate::load_profile_temporary_wallet(profile, workspace_root).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),
}; };

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/spl/token/execution.rs // file: ks-pipeline-demo-scenarios/src/spl/token/execution.rs
// version: 15 // version: 16
//! Devnet classic SPL Token execution with stateful and canonical post-validation. //! Devnet classic SPL Token execution with stateful and canonical post-validation.
@@ -479,7 +479,7 @@ where
failed_readiness_message(&readiness), failed_readiness_message(&readiness),
)); ));
} }
let wallet = match crate::load_profile_wallet(profile, workspace_root).await { let wallet = match crate::load_profile_temporary_wallet(profile, workspace_root).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),
}; };

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/spl/token/lifecycle.rs // file: ks-pipeline-demo-scenarios/src/spl/token/lifecycle.rs
// version: 14 // version: 15
//! Controlled Devnet lifecycle for freshly prepared classic SPL Token accounts. //! Controlled Devnet lifecycle for freshly prepared classic SPL Token accounts.
@@ -319,7 +319,7 @@ where
"lifecycle account preparation endpoint is not Devnet", "lifecycle account preparation endpoint is not Devnet",
)); ));
} }
let payer = match crate::load_profile_wallet(profile, workspace_root).await { let payer = match crate::load_profile_temporary_wallet(profile, workspace_root).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),
}; };
@@ -460,7 +460,7 @@ where
"the destructive lifecycle requires submit=true and explicit operator confirmation", "the destructive lifecycle requires submit=true and explicit operator confirmation",
)); ));
} }
let profile_wallet = match crate::load_profile_wallet(profile, workspace_root).await { let profile_wallet = match crate::load_profile_temporary_wallet(profile, workspace_root).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),
}; };

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/spl/token_2022/devnet_execution.rs // file: ks-pipeline-demo-scenarios/src/spl/token_2022/devnet_execution.rs
// version: 9 // version: 10
//! Devnet Token-2022 execution with stateful and canonical post-validation. //! Devnet Token-2022 execution with stateful and canonical post-validation.
@@ -469,7 +469,7 @@ where
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 wallet = match crate::load_profile_wallet(profile, workspace_root).await { let wallet = match crate::load_profile_temporary_wallet(profile, workspace_root).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),
}; };

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/spl/token_2022/fixture.rs // file: ks-pipeline-demo-scenarios/src/spl/token_2022/fixture.rs
// version: 5 // version: 6
//! Idempotent Token-2022 public scenario fixture preparation. //! Idempotent Token-2022 public scenario fixture preparation.
@@ -181,16 +181,22 @@ pub async fn prepare_token_2022_fixture(
} }
fn validate_options(options: &crate::Token2022FixturePreparationOptions) -> ks_core::Result<()> { fn validate_options(options: &crate::Token2022FixturePreparationOptions) -> ks_core::Result<()> {
if options.wallet_path.extension().and_then(|value| return value.to_str())
== std::option::Option::Some(ks_wallet::KSWALLET_FILE_EXTENSION)
{
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 CLI fixture preparation requires an explicit Solana CLI JSON export; native .kswallet files are never passed to external CLI tools",
));
}
if options.rpc_url.trim().is_empty() { if options.rpc_url.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config( return std::result::Result::Err(ks_core::Error::config(
"Token-2022 fixture RPC URL must not be empty", "Token-2022 fixture RPC URL must not be empty",
)); ));
} }
if !options.wallet_path.is_file() { if !options.wallet_path.is_file() {
return std::result::Result::Err(ks_core::Error::config(format!( return std::result::Result::Err(ks_core::Error::config(
"Token-2022 fixture wallet does not exist: {}", "Token-2022 fixture wallet does not exist",
options.wallet_path.display() ));
)));
} }
if options.decimals > 18 { if options.decimals > 18 {
return std::result::Result::Err(ks_core::Error::config( return std::result::Result::Err(ks_core::Error::config(
@@ -448,6 +454,18 @@ mod tests {
assert!(fixture.contains("export KS_PUBLIC_DEVNET_WALLET_ADDRESS=authority\n")); assert!(fixture.contains("export KS_PUBLIC_DEVNET_WALLET_ADDRESS=authority\n"));
} }
#[test]
fn options_reject_native_wallet_container_for_legacy_cli_workflow() {
let options = super::Token2022FixturePreparationOptions {
rpc_url: "https://api.devnet.solana.com".to_string(),
wallet_path: std::path::PathBuf::from("operator.kswallet"),
wallet_dir: std::path::PathBuf::from("wallets"),
decimals: 9,
};
let error = super::validate_options(&options);
assert!(error.is_err());
}
#[test] #[test]
fn options_reject_excessive_decimals() { fn options_reject_excessive_decimals() {
let options = super::Token2022FixturePreparationOptions { let options = super::Token2022FixturePreparationOptions {

View File

@@ -1,5 +1,5 @@
// file: ks-pipeline-demo-scenarios/src/spl/token_2022/metadata/fixture.rs // file: ks-pipeline-demo-scenarios/src/spl/token_2022/metadata/fixture.rs
// version: 5 // version: 6
//! Native Token-2022 mint preparation for the embedded Token Metadata Devnet campaign. //! Native Token-2022 mint preparation for the embedded Token Metadata Devnet campaign.
@@ -96,7 +96,7 @@ pub async fn prepare_token_2022_metadata_fixture(
"Token-2022 metadata fixture requires enabled Devnet submission and explicit operator confirmation", "Token-2022 metadata fixture requires enabled Devnet submission and explicit operator confirmation",
)); ));
} }
let operator = match crate::load_profile_wallet(profile, workspace_root).await { let operator = match crate::load_profile_temporary_wallet(profile, workspace_root).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),
}; };

View File

@@ -1,8 +1,13 @@
<!-- file: ks-wallet/CHANGELOG.md --> <!-- file: ks-wallet/CHANGELOG.md -->
<!-- version: 18 --> <!-- version: 19 -->
# CHANGELOG — ks-wallet # CHANGELOG — ks-wallet
## `0.5.2-pre.006`
- clôt les TODO de sélection non sensible par alias et d'adaptation des consommateurs : `ks-config` ne porte que `wallet_alias`, les scénarios exigent un unlock explicite pour une sélection persistante et le desktop ne projette que des DTO sûrs ;
- conserve pour la finalisation `pre.007` le nettoyage des anciens chemins locaux encore présents dans les logs du store temporaire legacy et la documentation sécurité finale.
## `0.5.2-pre.005` ## `0.5.2-pre.005`
- ajoute `WalletTransferFormat` avec `SolanaCliJson` et `SolanaPrivateKeyBase58` comme formats secrets explicites ; - ajoute `WalletTransferFormat` avec `SolanaCliJson` et `SolanaPrivateKeyBase58` comme formats secrets explicites ;

View File

@@ -1,5 +1,5 @@
<!-- file: ks-wallet/TODO.md --> <!-- file: ks-wallet/TODO.md -->
<!-- version: 12 --> <!-- version: 14 -->
# TODO — ks-wallet # TODO — ks-wallet
@@ -25,12 +25,18 @@
- [x] ne jamais synthétiser une recovery phrase supposée préserver une keypair arbitraire sans mnemonic/seed d'origine. - [x] ne jamais synthétiser une recovery phrase supposée préserver une keypair arbitraire sans mnemonic/seed d'origine.
- [x] exiger le mot de passe valide pour tout export contenant le secret. - [x] exiger le mot de passe valide pour tout export contenant le secret.
- [x] définir les collisions d'alias et de pubkey sans écrasement silencieux. - [x] définir les collisions d'alias et de pubkey sans écrasement silencieux.
- [ ] normaliser la sélection par alias dans `ks-config` sans secret. - [x] normaliser la sélection par alias dans `ks-config` sans secret.
- [ ] adapter les consommateurs et le desktop uniquement via des surfaces non sensibles. - [x] adapter les consommateurs et le desktop uniquement via des surfaces non sensibles.
- [ ] retirer secrets et chemins locaux inutiles des logs, erreurs, diagnostics et DTO. - [ ] retirer secrets et chemins locaux inutiles des logs, erreurs, diagnostics et DTO.
- [x] compléter les tests `pre.005` de migration non destructive, round-trip Solana CLI JSON/Base58, permissions privées, refus d'écrasement, collision de pubkey et export refusé avec mauvais mot de passe ; les tests de concurrence restent à réconcilier dans la finalisation selon les surfaces réellement utilisées. - [x] compléter les tests `pre.005` de migration non destructive, round-trip Solana CLI JSON/Base58, permissions privées, refus d'écrasement, collision de pubkey et export refusé avec mauvais mot de passe ; les tests de concurrence restent à réconcilier dans la finalisation selon les surfaces réellement utilisées.
- [ ] produire le guide de sécurité et la documentation finale avant clôture de `0.5.2`. - [ ] produire le guide de sécurité et la documentation finale avant clôture de `0.5.2`.
## Version ultérieure non déterminée — migration des keypairs de fixtures
- [ ] inventorier et classifier non destructivement les fichiers keypair JSON sous `wallets/temporary/**` avant toute conversion de masse : distinguer wallets/signers, autorités, mints, recipients et autres keypairs de fixtures ;
- [ ] proposer une migration sélective des seuls signers devant devenir persistants vers `.kswallet`, en préservant les fichiers JSON sources et les pubkeys ;
- [ ] permettre aux outils de diagnostic dextraire la pubkey publique des keypairs legacy compatibles pour explorer leur historique on-chain sans exiger leur conversion préalable en `.kswallet`.
## Version ultérieure non déterminée — adaptateurs de transfert ## Version ultérieure non déterminée — adaptateurs de transfert
- [ ] caractériser le wire Solana exact accepté par l'import `Private key` de Backpack avant d'ajouter un codec de marque ou un alias de format. - [ ] caractériser le wire Solana exact accepté par l'import `Private key` de Backpack avant d'ajouter un codec de marque ou un alias de format.