v0.5.2-pre.006
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
"get_recent_performance_samples",
|
"get_recent_performance_samples",
|
||||||
"get_recent_prioritization_fees",
|
"get_recent_prioritization_fees",
|
||||||
"get_signature_statuses",
|
"get_signature_statuses",
|
||||||
|
"get_signatures_for_address",
|
||||||
"get_slot",
|
"get_slot",
|
||||||
"get_slot_leader",
|
"get_slot_leader",
|
||||||
"get_slot_leaders",
|
"get_slot_leaders",
|
||||||
|
|||||||
@@ -6,9 +6,10 @@
|
|||||||
"name": "local_devnet",
|
"name": "local_devnet",
|
||||||
"directory": "temporary/local_devnet",
|
"directory": "temporary/local_devnet",
|
||||||
"cluster": "devnet",
|
"cluster": "devnet",
|
||||||
"temporary_wallet_enabled": true,
|
"wallet_alias": "example-operator",
|
||||||
"temporary_wallet_alias": "local-devnet-operator",
|
"temporary_wallet_enabled": false,
|
||||||
"temporary_wallet_persist": true
|
"temporary_wallet_alias": "example-temporary-disabled",
|
||||||
|
"temporary_wallet_persist": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
"get_recent_performance_samples",
|
"get_recent_performance_samples",
|
||||||
"get_recent_prioritization_fees",
|
"get_recent_prioritization_fees",
|
||||||
"get_signature_statuses",
|
"get_signature_statuses",
|
||||||
|
"get_signatures_for_address",
|
||||||
"get_slot",
|
"get_slot",
|
||||||
"get_slot_leader",
|
"get_slot_leader",
|
||||||
"get_slot_leaders",
|
"get_slot_leaders",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"name": "local_devnet",
|
"name": "local_devnet",
|
||||||
"directory": "temporary/local_devnet",
|
"directory": "temporary/local_devnet",
|
||||||
"cluster": "devnet",
|
"cluster": "devnet",
|
||||||
|
"wallet_alias": null,
|
||||||
"temporary_wallet_enabled": true,
|
"temporary_wallet_enabled": true,
|
||||||
"temporary_wallet_alias": "local-devnet-operator",
|
"temporary_wallet_alias": "local-devnet-operator",
|
||||||
"temporary_wallet_persist": true
|
"temporary_wallet_persist": true
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
"name": "mainnet_research",
|
"name": "mainnet_research",
|
||||||
"directory": "temporary/mainnet_research",
|
"directory": "temporary/mainnet_research",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet-beta",
|
||||||
|
"wallet_alias": null,
|
||||||
"temporary_wallet_enabled": false,
|
"temporary_wallet_enabled": false,
|
||||||
"temporary_wallet_alias": "mainnet_research-disabled",
|
"temporary_wallet_alias": "mainnet_research-disabled",
|
||||||
"temporary_wallet_persist": false
|
"temporary_wallet_persist": false
|
||||||
@@ -22,6 +24,7 @@
|
|||||||
"name": "mainnet",
|
"name": "mainnet",
|
||||||
"directory": "temporary/mainnet",
|
"directory": "temporary/mainnet",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet-beta",
|
||||||
|
"wallet_alias": null,
|
||||||
"temporary_wallet_enabled": false,
|
"temporary_wallet_enabled": false,
|
||||||
"temporary_wallet_alias": "mainnet-disabled",
|
"temporary_wallet_alias": "mainnet-disabled",
|
||||||
"temporary_wallet_persist": false
|
"temporary_wallet_persist": false
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/DEVNET_EXECUTION_GUIDE.md -->
|
<!-- file: docs/DEVNET_EXECUTION_GUIDE.md -->
|
||||||
<!-- version: 26 -->
|
<!-- version: 27 -->
|
||||||
|
|
||||||
# Guide d’exécution Devnet
|
# Guide d’exé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` ;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/guides/CONFIGURATION.md -->
|
<!-- file: docs/guides/CONFIGURATION.md -->
|
||||||
<!-- version: 10 -->
|
<!-- version: 11 -->
|
||||||
|
|
||||||
# Guide de configuration
|
# Guide de configuration
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ Les classes actuelles couvrent le RPC WS standard et le RPC WS à capacité plus
|
|||||||
|
|
||||||
## Wallet et politique d'exécution
|
## Wallet et politique d'exécution
|
||||||
|
|
||||||
Les propriétés de stockage/identité du wallet appartiennent à `wallet.config.json`.
|
Les propriétés de stockage/identité du wallet appartiennent à `wallet.config.json`. Un profil peut définir le champ optionnel `wallet_alias` pour sélectionner un wallet natif persistant par alias. Son omission ou `null` signifie qu’aucun wallet persistant n’est sélectionné. Le champ est non sensible et ne contient jamais de mot de passe, de keypair ou de chemin de fichier individuel.
|
||||||
|
|
||||||
Les autorisations :
|
Les autorisations :
|
||||||
|
|
||||||
|
|||||||
@@ -1,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
|
||||||
|
|
||||||
|
|||||||
@@ -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 l’explorateur on-chain ; le profil actif reste la valeur initiale mais Devnet/Mainnet peuvent être interrogés sans redémarrer l’application ni modifier `active_profile` ;
|
||||||
|
- route `getBalance`, `getTokenAccountsByOwner`, `getSignaturesForAddress` et `getTransaction` à travers le profil sélectionné et non plus obligatoirement à travers le pool HTTP actif ;
|
||||||
|
- borne la commande RPC dédiée à la démo wallet aux trois lectures génériques nécessaires aux tokens et à l’historique, tandis que le solde garde son adaptateur typé ;
|
||||||
|
- conserve la gestion locale des `.kswallet` sur le profil applicatif actif : la sélection RPC de l’explorateur ne déplace ni ne recrée les fichiers wallet.
|
||||||
|
|
||||||
|
## `0.5.2-pre.006-delta-fix-002`
|
||||||
|
|
||||||
|
- complète la fenêtre `Wallets` avec la création de `.kswallet` dans le store du profil actif ; le password est lu uniquement côté backend depuis `KB_SECRET_DEMO_WALLET_PASSWORD` et ne traverse jamais HTML, TypeScript ou les arguments Tauri ;
|
||||||
|
- ajoute un explorateur public on-chain réutilisant le transport HTTP configuré : solde SOL exact, comptes SPL Token et Token-2022, signatures récentes impliquant l’adresse et détail `getTransaction` via le JsonViewer commun ;
|
||||||
|
- étend le rôle `http_queries` Devnet avec `get_signatures_for_address` afin que l’explorateur fonctionne sur `local_devnet` comme sur les profils Mainnet déjà dotés des rôles d’historique ;
|
||||||
|
- ajoute des DTO TS-RS applicatifs sûrs, des canaris frontend/backend de non-divulgation et des tests statiques de câblage pour les nouvelles commandes.
|
||||||
|
|
||||||
|
## `0.5.2-pre.006-delta-fix-001`
|
||||||
|
|
||||||
|
- ajoute la fenêtre frontend `demo_wallet.html` et son contrôleur `demo_wallet.ts` sur le même gabarit Bootstrap/Simplebar que les autres démonstrations ;
|
||||||
|
- ajoute `open_demo_wallet_window`, l'entrée `Wallets` dans le menu `Démos` de `main.html`, l'entrée Vite multipage et la capability Tauri `demo_wallet` ;
|
||||||
|
- permet de rafraîchir l'inventaire sûr des `.kswallet` et d'inspecter explicitement un fichier externe par chemin sans afficher ce chemin dans le résultat ;
|
||||||
|
- ajoute un test statique de câblage frontend afin que la présence du backend `demo_wallet` ne puisse plus être validée sans fenêtre réellement ouvrable.
|
||||||
|
|
||||||
|
## `0.5.2-pre.006`
|
||||||
|
|
||||||
|
- ajoute des DTO TS-RS possédés par le desktop pour l'inventaire des wallets natifs : alias, pubkey déclarée, version et état de sélection uniquement ;
|
||||||
|
- ajoute des commandes Tauri lecture seule pour lister le store configuré et inspecter un `.kswallet` sélectionné explicitement hors store, sans retourner son chemin ;
|
||||||
|
- expose l'alias persistant sélectionné uniquement dans le diagnostic wallet borné et ajoute des canaris interdisant password, ciphertext, nonce, sel et chemins de stockage dans les DTO.
|
||||||
|
|
||||||
## `0.5.1-pre.008`
|
## `0.5.1-pre.008`
|
||||||
|
|
||||||
- Fix: remplace les snapshots `ks-onchain-transport` retournés directement par les commandes Tauri HTTP/WS par des DTO desktop sanitisés, fait supprimer l’URL des snapshots backend et supprime `endpointUrl` des payloads d’exécution/statut et ajoute des canaris empêchant toute URL résolue de traverser le frontend. Supprime également le chemin local du fixture Token-2022 des payloads fonctionnels et des erreurs affichées.
|
- Fix: remplace les snapshots `ks-onchain-transport` retournés directement par les commandes Tauri HTTP/WS par des DTO desktop sanitisés, fait supprimer l’URL des snapshots backend et supprime `endpointUrl` des payloads d’exécution/statut et ajoute des canaris empêchant toute URL résolue de traverser le frontend. Supprime également le chemin local du fixture Token-2022 des payloads fonctionnels et des erreurs affichées.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: kb-app-demo-desktop/README.md -->
|
<!-- file: kb-app-demo-desktop/README.md -->
|
||||||
<!-- version: 15 -->
|
<!-- version: 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 d’un conteneur persistant via un secret backend-only, inspection explicite d’un 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 ;
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
|
|
||||||
L’application expose une interface opérateur pour diagnostiquer, simuler et valider les couches de bot3.
|
L’application expose une interface opérateur pour diagnostiquer, simuler et valider les couches de bot3.
|
||||||
|
|
||||||
|
## Surface wallet sûre
|
||||||
|
|
||||||
|
Le backend desktop peut lister les `.kswallet` du répertoire configuré, créer un wallet natif protégé, inspecter un fichier sélectionné explicitement ailleurs et explorer l’état on-chain d’une adresse publique. Les DTO frontend ne contiennent jamais de matériau secret. Aucun password, chemin interne du store, sel, nonce, ciphertext ou byte privé n’est renvoyé par Tauri.
|
||||||
|
|
||||||
|
La fenêtre `Wallets`, accessible depuis le menu `Démos` de `main`, fournit quatre surfaces :
|
||||||
|
|
||||||
|
- inventaire des wallets natifs du profil actif ;
|
||||||
|
- création d’un nouveau `.kswallet` dans le store du profil actif ;
|
||||||
|
- explorateur public on-chain ;
|
||||||
|
- inspection structurale d’un `.kswallet` explicite situé hors du store.
|
||||||
|
|
||||||
|
### Créer un wallet natif depuis la démo
|
||||||
|
|
||||||
|
Le mot de passe de création ne doit pas traverser le frontend ou Tauri IPC. La démo lit uniquement côté backend le secret applicatif :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export KB_SECRET_DEMO_WALLET_PASSWORD='mot-de-passe-de-demo'
|
||||||
|
cargo tauri dev -c kb-app-demo-desktop/tauri.conf.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Le secret peut aussi être placé dans le fichier `.env` local chargé par `ks-config`. Le vrai `.env` reste non versionné et ne doit jamais être archivé. L’UI ne reçoit qu’un booléen indiquant si ce secret backend est configuré.
|
||||||
|
|
||||||
|
La création demande uniquement l’alias. Elle crée `<alias>.kswallet` dans le répertoire wallet résolu du profil actif et ne modifie pas automatiquement `wallet_alias` dans la configuration. Un changement de valeur de `KB_SECRET_DEMO_WALLET_PASSWORD` avant une autre session permet d’utiliser un autre mot de passe pour de nouveaux wallets ; ce secret de démonstration n’est pas un mot de passe global du format `.kswallet`.
|
||||||
|
|
||||||
|
### Explorer une adresse sur un profil RPC sélectionné
|
||||||
|
|
||||||
|
L’explorateur on-chain ne déverrouille aucun wallet : seule la pubkey publique est nécessaire. Le bouton `Explorer` d’un wallet inventorié remplit l’adresse automatiquement ; toute autre pubkey Solana peut également être saisie.
|
||||||
|
|
||||||
|
Les lectures utilisent le transport HTTP du profil choisi dans le sélecteur `Profil RPC` de la fenêtre Wallets. Le profil actif est sélectionné par défaut, mais `local_devnet`, `mainnet_research` ou tout autre profil HTTP résolu peut être choisi sans redémarrer l’application :
|
||||||
|
|
||||||
|
- `getBalance` pour le solde SOL exact en lamports ;
|
||||||
|
- `getTokenAccountsByOwner` pour les comptes SPL Token classique et Token-2022 ;
|
||||||
|
- `getSignaturesForAddress` pour les signatures récentes des transactions qui référencent l’adresse ;
|
||||||
|
- `getTransaction` à la demande pour afficher le détail JSON d’une signature.
|
||||||
|
|
||||||
|
La liste est donc intitulée **transactions impliquant l’adresse** : elle ne prétend pas que le wallet a initié ou signé chaque transaction retournée. Le profil `local_devnet` interroge Devnet ; `mainnet_research` ou `mainnet` interrogent Mainnet Beta. La sélection utilisée par l’explorateur est locale à cette fenêtre : elle ne modifie pas `active_profile`, la configuration du workspace ni le profil utilisé par les autres démos.
|
||||||
|
|
||||||
## Lancer le desktop
|
## Lancer le desktop
|
||||||
|
|
||||||
Depuis la racine du workspace :
|
Depuis la racine du workspace :
|
||||||
@@ -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 l’interface applicative frontend/backend. Elle
|
|||||||
```typescript
|
```typescript
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
await invoke("open_demo_wallet_window");
|
||||||
await invoke("open_demo_http_window");
|
await invoke("open_demo_http_window");
|
||||||
await invoke("open_demo_ws_window");
|
await invoke("open_demo_ws_window");
|
||||||
await invoke("open_demo_decode_replay_window");
|
await invoke("open_demo_decode_replay_window");
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
245
kb-app-demo-desktop/frontend/demo_wallet.html
Normal file
245
kb-app-demo-desktop/frontend/demo_wallet.html
Normal 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">© 2026 SASEDEV</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script type="module" src="ts/demo_wallet.ts" defer></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: kb-app-demo-desktop/frontend/main.html -->
|
<!-- file: kb-app-demo-desktop/frontend/main.html -->
|
||||||
<!-- version: 11 -->
|
<!-- version: 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>
|
||||||
|
|||||||
532
kb-app-demo-desktop/frontend/ts/demo_wallet.ts
Normal file
532
kb-app-demo-desktop/frontend/ts/demo_wallet.ts
Normal 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();
|
||||||
|
});
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
559
kb-app-demo-desktop/src/demo_wallet.rs
Normal file
559
kb-app-demo-desktop/src/demo_wallet.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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')),
|
||||||
|
|||||||
@@ -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` ;
|
||||||
|
|||||||
@@ -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 ;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 d’un 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` ;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 l’application 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 l’application 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 l’environnement
|
## Initialiser l’environnement
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
|||||||
@@ -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![
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 ;
|
||||||
|
|||||||
@@ -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 d’extraire 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user