v0.5.2-pre.006-fix-010

This commit is contained in:
2026-08-11 12:03:35 +02:00
parent 1ee1d0297d
commit 56572cec40
67 changed files with 2484 additions and 539 deletions

View File

@@ -1,8 +1,26 @@
<!-- file: kb-app-demo-desktop/CHANGELOG.md -->
<!-- version: 57 -->
<!-- version: 60 -->
# CHANGELOG — kb-app-demo-desktop
## `0.5.2-pre.006-delta-fix-009`
- corrige les adaptateurs Tauri Devnet qui transmettaient directement `tauri::State<AppState>` aux helpers wallet/projection attendant `&AppState`; les commandes empruntent désormais explicitement `&state` pour System, SPL et les trois familles Metadata ;
- ne modifie ni la sélection runtime du `.kswallet`, ni les chemins dexport `data/wallets/`, ni les contrats de configuration introduits par les fixes précédents.
## `0.5.2-pre.006-delta-fix-008`
- ajoute dans `demo_wallet` une sélection session-only du `.kswallet` d'exécution par profil Devnet ; l'alias est authentifié côté backend avant activation, ne réécrit pas `wallet.config.json` et devient prioritaire sur `wallet_alias` pour les démos suivantes ;
- propage cette sélection runtime à tous les adaptateurs desktop System, Memo, ATA, SPL Token, Token-2022 et Metadata, tout en conservant le fallback temporaire uniquement lorsqu'aucun alias runtime ou configuré n'existe ;
- contraint les exports secrets de la démo à `./data/wallets/`, remplace le chemin arbitraire par un nom de fichier borné, impose l'extension du format et sécurise le répertoire en `0700` sur Unix ;
- affiche explicitement les alias configuré, runtime et effectif afin que la validation Devnet puisse confirmer le passage de `persistence="temporary"` à `persistence="persistent"`.
## `0.5.2-pre.006-delta-fix-005`
- résout côté backend le signer Devnet sélectionné avant les démos System, Memo, ATA, SPL Token, Token-2022, Solana Program Metadata, Token-2022 Metadata ainsi que toutes les campagnes Metaplex qualifiées, y compris leurs parcours multi-signers ;
- utilise `KB_SECRET_DEMO_WALLET_PASSWORD` uniquement lorsque `wallet_alias` sélectionne un `.kswallet`, sans transmettre le password à Tauri ou TypeScript ;
- conserve les répertoires de fixtures sous `temporary_wallet_dir` tandis que `demo_wallet` inventorie et importe désormais dans la racine persistante `wallet_dir`.
## `0.5.2-pre.006-delta-fix-004`
- complète la fenêtre `Wallets` avec inspection publique, import et export des deux formats `ks-wallet` réellement supportés : Solana CLI JSON 64 octets et keypair Base58 64 octets ;

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/README.md -->
<!-- version: 20 -->
<!-- version: 21 -->
# kb-app-demo-desktop
@@ -8,7 +8,7 @@
## Fonctionnalités
- configuration et diagnostics de logging ;
- fenêtre Wallets dédiée : inventaire sûr des identités `.kswallet`, création dun conteneur persistant via un secret backend-only, inspection/import/export des formats keypair supportés, extraction sûre de pubkey depuis un keypair legacy, inspection explicite dun `.kswallet` externe et exploration publique du solde SOL, des comptes SPL Token / Token-2022 et des transactions récentes sur un profil RPC explicitement sélectionnable ;
- fenêtre Wallets dédiée : inventaire sûr des identités `.kswallet`, création dun conteneur persistant via un secret backend-only, sélection session-only du wallet d'exécution Devnet, inspection/import/export des formats keypair supportés sous `data/wallets/`, extraction sûre de pubkey depuis un keypair legacy, inspection explicite dun `.kswallet` externe et exploration publique du solde SOL, des comptes SPL Token / Token-2022 et des transactions récentes sur un profil RPC explicitement sélectionnable ;
- transport HTTP JSON-RPC et WebSocket ;
- diagnostics PostgreSQL ;
- backfill, extraction Core et replay, avec enregistrement générique de Solana Program Metadata ;

View File

@@ -1,11 +1,11 @@
<!-- file: kb-app-demo-desktop/TODO.md -->
<!-- version: 17 -->
<!-- version: 19 -->
# TODO — kb-app-demo-desktop
## Évolutions générales
- [ ] `0.5.2` — valider les démos Devnet Solana Core/SPL/Metadata avec un `.kswallet` sélectionné et déverrouillé côté backend, sans transmettre le password au frontend.
- [ ] `0.5.2` — valider sur Devnet la sélection session-only dun `.kswallet` dans `demo_wallet`, confirmer `persistence="persistent"` dans les logs puis retrouver les signatures produites depuis lexplorateur.
- [ ] `0.5.4` — réduire les panneaux d'exécution à l'adaptation UI/Tauri en déplaçant toute orchestration de campagne encore réutilisable vers `ks-pipeline-demo-scenarios`.
- [ ] UX — empêcher les doubles déclenchements pendant une préparation, simulation ou soumission active.
- [ ] Observabilité — afficher explicitement les comptes relus et les projections matérialisées.

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/USAGE.md -->
<!-- version: 20 -->
<!-- version: 21 -->
# Utilisation de kb-app-demo-desktop
@@ -11,9 +11,10 @@ Lapplication expose une interface opérateur pour diagnostiquer, simuler et v
Le backend desktop peut lister les `.kswallet` du répertoire configuré, créer un wallet natif protégé, inspecter un fichier sélectionné explicitement ailleurs et explorer létat on-chain dune adresse publique. Les DTO frontend ne contiennent jamais de matériau secret. Aucun password, chemin interne du store, sel, nonce, ciphertext ou byte privé nest renvoyé par Tauri.
La fenêtre `Wallets`, accessible depuis le menu `Démos` de `main`, fournit maintenant six surfaces :
La fenêtre `Wallets`, accessible depuis le menu `Démos` de `main`, fournit maintenant sept surfaces :
- inventaire des wallets natifs du profil actif ;
- sélection session-only du `.kswallet` utilisé par les démos Devnet, sans réécrire la configuration ;
- création dun nouveau `.kswallet` dans le store du profil actif ;
- inspection et import d'un keypair externe dans un format supporté ;
- export authentifié d'un `.kswallet` vers un format supporté ;
@@ -42,10 +43,22 @@ Les sélecteurs de format sont alimentés par `ks-wallet` et exposent actuelleme
L'action **Inspecter la keypair** ne crée aucun `.kswallet` : le backend valide le fichier avec le codec choisi et ne renvoie que la pubkey. Cette pubkey peut être envoyée directement à l'explorateur on-chain, ce qui permet par exemple de contrôler un ancien `wallets/temporary/local_devnet/*.json` avant de décider de le migrer.
L'action **Importer en .kswallet** demande en plus un alias destination et utilise `KB_SECRET_DEMO_WALLET_PASSWORD` uniquement côté backend. La source reste inchangée. L'export fonctionne dans le sens inverse : choix d'un alias natif, du format et d'un chemin destination explicite ; le backend authentifie le `.kswallet` avec le même secret de démonstration et refuse d'écraser un fichier existant.
L'action **Importer en .kswallet** demande en plus un alias destination et utilise `KB_SECRET_DEMO_WALLET_PASSWORD` uniquement côté backend. La source reste inchangée. L'export fonctionne dans le sens inverse : choix d'un alias natif, du format et d'un nom de fichier ; le backend authentifie le `.kswallet` avec le même secret de démonstration, refuse d'écraser un fichier existant et contraint la destination à `./data/wallets/`. Le répertoire d'export privé est créé avec des permissions `0700` sur Unix et `ks-wallet` conserve les fichiers exportés en `0600`.
Un keypair JSON de 64 octets ne contient pas son rôle métier. L'inspection peut dire « keypair Solana valide » et donner sa pubkey, mais elle ne peut pas décider localement si cette clé a servi de wallet opérateur, mint, authority ou autre fixture. Cette classification nécessite le contexte du scénario ou une lecture on-chain séparée.
### Sélectionner le `.kswallet` utilisé par les démos Devnet
La configuration persistante continue d'utiliser `wallet_alias`, mais la fenêtre Wallets permet également de choisir un alias pour un profil Devnet uniquement pendant la session desktop. Cette sélection est authentifiée immédiatement avec `KB_SECRET_DEMO_WALLET_PASSWORD`, ne réécrit aucun JSON et s'applique aux exécutions System, Memo, ATA, SPL Token, Token-2022 et Metadata suivantes.
Le resolver applique l'ordre suivant :
1. override session desktop pour le profil, lorsqu'il existe ;
2. `wallet_alias` du profil ;
3. wallet temporaire historique uniquement si les deux précédents sont absents.
Un override valide ne peut donc pas retomber silencieusement sur `temporary_wallet_alias`. Le log `ks-pipeline-demo-scenarios.wallet` doit alors afficher `persistence="persistent"` avec l'alias et la pubkey sélectionnés.
### Explorer une adresse sur un profil RPC sélectionné
Lexplorateur on-chain ne déverrouille aucun wallet : seule la pubkey publique est nécessaire. Le bouton `Explorer` dun wallet inventorié remplit ladresse automatiquement ; toute autre pubkey Solana peut également être saisie.

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/demo_wallet.html -->
<!-- version: 4 -->
<!-- version: 5 -->
<!DOCTYPE html>
<html lang="fr">
@@ -49,6 +49,33 @@
<dt class="col-sm-3">Wallets trouvés</dt>
<dd class="col-sm-9"><code id="walletInventoryCount">0</code></dd>
</dl>
<div class="border rounded p-3 mb-3">
<h2 class="h6 mb-2">Wallet d'exécution des démos Devnet</h2>
<p class="small text-body-secondary mb-3">La sélection est propre à cette session desktop. Elle ne modifie pas <code>wallet.config.json</code> et remplace <code>wallet_alias</code> uniquement pour les démos du profil Devnet choisi.</p>
<div class="row g-3 align-items-end">
<div class="col-12 col-lg-4">
<label for="walletExecutionProfileSelect" class="form-label">Profil Devnet</label>
<select id="walletExecutionProfileSelect" class="form-select"></select>
</div>
<div class="col-12 col-lg-5">
<label for="walletExecutionAliasSelect" class="form-label">Wallet d'exécution</label>
<select id="walletExecutionAliasSelect" class="form-select"></select>
</div>
<div class="col-12 col-lg-3">
<button id="applyWalletExecutionSelectionButton" class="btn btn-outline-primary w-100" type="button">Appliquer</button>
</div>
</div>
<dl class="row small mb-0 mt-3">
<dt class="col-sm-4">Configuration</dt>
<dd class="col-sm-8"><code id="walletExecutionConfiguredAlias"></code></dd>
<dt class="col-sm-4">Override session</dt>
<dd class="col-sm-8"><code id="walletExecutionRuntimeAlias"></code></dd>
<dt class="col-sm-4">Effectif</dt>
<dd class="col-sm-8"><code id="walletExecutionEffectiveAlias">temporaire</code></dd>
<dt class="col-sm-4">Statut</dt>
<dd class="col-sm-8"><code id="walletExecutionSelectionStatus">En attente</code></dd>
</dl>
</div>
<div class="table-responsive app-table-full-width">
<table class="table table-sm align-middle mb-0" id="walletInventoryTable">
<thead>
@@ -56,7 +83,7 @@
<th>Alias</th>
<th>Pubkey déclarée</th>
<th>Format</th>
<th>Sélection</th>
<th>Exécution</th>
<th>Action</th>
</tr>
</thead>
@@ -128,13 +155,16 @@
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h2 class="h5 card-title mb-2">Exporter un wallet natif</h2>
<p class="text-body-secondary">L'export exige le même secret backend que la création et refuse d'écraser un fichier existant.</p>
<p class="text-body-secondary">L'export exige le même secret backend que la création, refuse d'écraser un fichier existant et écrit uniquement dans <code>./data/wallets/</code>.</p>
<label for="walletExportAliasSelect" class="form-label">Wallet</label>
<select id="walletExportAliasSelect" class="form-select"></select>
<label for="walletExportFormatSelect" class="form-label mt-3">Format d'export</label>
<select id="walletExportFormatSelect" class="form-select"></select>
<label for="walletExportDestinationPathInput" class="form-label mt-3">Chemin destination</label>
<input id="walletExportDestinationPathInput" class="form-control font-monospace" type="text" autocomplete="off" placeholder="/chemin/vers/export.json">
<label for="walletExportFileNameInput" class="form-label mt-3">Nom du fichier</label>
<div class="input-group">
<span id="walletExportDirectory" class="input-group-text font-monospace">data/wallets/</span>
<input id="walletExportFileNameInput" class="form-control font-monospace" type="text" autocomplete="off" maxlength="128" placeholder="operator.json">
</div>
<div class="d-flex flex-wrap gap-2 mt-3">
<button id="exportWalletTransferButton" class="btn btn-primary" type="button">Exporter</button>
</div>
@@ -298,4 +328,4 @@
<script type="module" src="ts/demo_wallet.ts" defer></script>
</body>
</html>
</html>

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_wallet.ts
// version: 4
// version: 5
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -10,6 +10,8 @@ import { renderJsonViewer } from "./json_viewer.ts";
import type { DemoWalletBalancePayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletBalancePayload.ts";
import type { DemoWalletCreateRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletCreateRequest.ts";
import type { DemoWalletExportRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletExportRequest.ts";
import type { DemoWalletExecutionProfilePayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletExecutionProfilePayload.ts";
import type { DemoWalletExecutionSelectionRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletExecutionSelectionRequest.ts";
import type { DemoWalletIdentityPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletIdentityPayload.ts";
import type { DemoWalletImportRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletImportRequest.ts";
import type { DemoWalletInventoryPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletInventoryPayload.ts";
@@ -96,6 +98,102 @@ function renderOnchainProfiles(profiles: ReadonlyArray<DemoWalletOnchainProfileP
updateOnchainClusterLabel();
}
function selectedExecutionProfilePayload(): DemoWalletExecutionProfilePayload | null {
const profile = document.querySelector<HTMLSelectElement>("#walletExecutionProfileSelect")?.value.trim() ?? "";
if (!walletInventory || profile.length === 0) {
return null;
}
return walletInventory.execution_profiles.find(candidate => candidate.name === profile) ?? null;
}
function renderExecutionProfiles(profiles: ReadonlyArray<DemoWalletExecutionProfilePayload>): void {
const select = document.querySelector<HTMLSelectElement>("#walletExecutionProfileSelect");
if (!select) {
return;
}
const previous = select.value;
select.replaceChildren();
for (const profile of profiles) {
const option = document.createElement("option");
option.value = profile.name;
option.textContent = profile.name;
select.appendChild(option);
}
if (profiles.some(profile => profile.name === previous)) {
select.value = previous;
} else if (profiles.length > 0) {
select.value = profiles[0].name;
}
}
function renderExecutionWalletAliases(wallets: ReadonlyArray<DemoWalletIdentityPayload>): void {
const select = document.querySelector<HTMLSelectElement>("#walletExecutionAliasSelect");
if (!select) {
return;
}
select.replaceChildren();
const configuredOption = document.createElement("option");
configuredOption.value = "";
configuredOption.textContent = "Utiliser la configuration du profil";
select.appendChild(configuredOption);
for (const wallet of wallets) {
const option = document.createElement("option");
option.value = wallet.alias;
option.textContent = `${wallet.alias}${wallet.public_key}`;
select.appendChild(option);
}
}
function updateExecutionSelectionDisplay(): void {
const profile = selectedExecutionProfilePayload();
const aliasSelect = document.querySelector<HTMLSelectElement>("#walletExecutionAliasSelect");
if (!profile) {
setText("#walletExecutionConfiguredAlias", "—");
setText("#walletExecutionRuntimeAlias", "—");
setText("#walletExecutionEffectiveAlias", "temporaire");
if (aliasSelect) {
aliasSelect.value = "";
}
return;
}
setText("#walletExecutionConfiguredAlias", profile.configured_alias ?? "—");
setText("#walletExecutionRuntimeAlias", profile.runtime_alias ?? "—");
setText("#walletExecutionEffectiveAlias", profile.effective_alias ?? "temporaire");
if (aliasSelect) {
const requested = profile.runtime_alias ?? "";
if (Array.from(aliasSelect.options).some(option => option.value === requested)) {
aliasSelect.value = requested;
} else {
aliasSelect.value = "";
}
}
}
async function applyExecutionWalletSelection(): Promise<void> {
const profile = document.querySelector<HTMLSelectElement>("#walletExecutionProfileSelect")?.value.trim() ?? "";
const aliasText = document.querySelector<HTMLSelectElement>("#walletExecutionAliasSelect")?.value.trim() ?? "";
if (profile.length === 0) {
setText("#walletExecutionSelectionStatus", "Profil Devnet requis");
return;
}
const request: DemoWalletExecutionSelectionRequest = {
profile,
alias: aliasText.length === 0 ? null : aliasText,
};
setText("#walletExecutionSelectionStatus", "Validation...");
try {
const payload = await invoke<DemoWalletInventoryPayload>("demo_wallet_select_execution_wallet", { request });
applyWalletInventoryPayload(payload);
const effective = payload.execution_profiles.find(candidate => candidate.name === profile)?.effective_alias ?? null;
setText("#walletExecutionSelectionStatus", effective ? `Persistant : ${effective}` : "Configuration du profil");
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", `execution wallet selection updated for ${profile}`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletExecutionSelectionStatus", `Erreur : ${message}`);
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `Execution wallet selection failed: ${message}`);
}
}
async function executeWalletRpc(profile: string, method: string, params: unknown[]): Promise<DemoWalletRpcExecutionPayload> {
const request: DemoWalletRpcRequest = {
profile,
@@ -159,6 +257,28 @@ function selectedTransferFormat(selector: string): string {
return document.querySelector<HTMLSelectElement>(selector)?.value.trim() ?? "";
}
function selectedExportFormatPayload(): DemoWalletTransferFormatPayload | null {
const code = selectedTransferFormat("#walletExportFormatSelect");
if (!walletInventory || code.length === 0) {
return null;
}
return walletInventory.transfer_formats.find(format => format.code === code) ?? null;
}
function updateExportFileName(force: boolean): void {
const input = document.querySelector<HTMLInputElement>("#walletExportFileNameInput");
const alias = document.querySelector<HTMLSelectElement>("#walletExportAliasSelect")?.value.trim() ?? "";
const format = selectedExportFormatPayload();
if (!input || alias.length === 0 || !format) {
return;
}
const generated = `${alias}.${format.default_extension}`;
if (force || input.value.trim().length === 0 || input.dataset.autoGenerated === "true") {
input.value = generated;
input.dataset.autoGenerated = "true";
}
}
function setTransferPublicKey(publicKey: string): void {
inspectedTransferPublicKey = publicKey;
setText("#walletTransferPublicKey", publicKey.length > 0 ? publicKey : "—");
@@ -196,7 +316,8 @@ function renderWalletRows(wallets: ReadonlyArray<DemoWalletIdentityPayload>): vo
publicKeyCell.textContent = wallet.public_key;
publicKeyCell.className = "font-monospace text-break";
formatCell.textContent = `v${wallet.format_version}`;
selectedCell.textContent = wallet.selected ? "Oui" : "Non";
const executionSelected = selectedExecutionProfilePayload()?.effective_alias === wallet.alias;
selectedCell.textContent = executionSelected ? "Oui" : "Non";
exploreButton.type = "button";
exploreButton.className = "btn btn-sm btn-outline-primary";
exploreButton.textContent = "Explorer";
@@ -208,7 +329,7 @@ function renderWalletRows(wallets: ReadonlyArray<DemoWalletIdentityPayload>): vo
document.querySelector<HTMLElement>("#walletAddressInput")?.scrollIntoView({ behavior: "smooth", block: "center" });
});
actionCell.appendChild(exploreButton);
if (wallet.selected) {
if (executionSelected) {
row.classList.add("table-primary");
}
row.append(aliasCell, publicKeyCell, formatCell, selectedCell, actionCell);
@@ -216,21 +337,30 @@ function renderWalletRows(wallets: ReadonlyArray<DemoWalletIdentityPayload>): vo
}
}
function applyWalletInventoryPayload(payload: DemoWalletInventoryPayload): void {
walletInventory = payload;
setText("#walletProfile", payload.profile);
setText("#walletCluster", payload.cluster);
renderOnchainProfiles(payload.onchain_profiles, payload.profile);
renderExecutionProfiles(payload.execution_profiles);
renderTransferFormats(payload.transfer_formats);
renderExportWalletAliases(payload.wallets);
renderExecutionWalletAliases(payload.wallets);
setText("#walletCreateSecretStatus", payload.create_password_configured ? "configuré" : "absent");
setText("#walletSelectedAlias", payload.selected_alias ?? "—");
setText("#walletInventoryCount", String(payload.wallets.length));
setText("#walletExportDirectory", `${payload.export_directory}/`);
setText("#walletInventoryStatus", "OK");
renderWalletRows(payload.wallets);
updateExecutionSelectionDisplay();
updateExportFileName(false);
}
async function refreshWalletInventory(): Promise<void> {
setText("#walletInventoryStatus", "Chargement...");
try {
const payload = await invoke<DemoWalletInventoryPayload>("demo_wallet_inventory");
walletInventory = payload;
setText("#walletProfile", payload.profile);
setText("#walletCluster", payload.cluster);
renderOnchainProfiles(payload.onchain_profiles, payload.profile);
renderTransferFormats(payload.transfer_formats);
renderExportWalletAliases(payload.wallets);
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);
applyWalletInventoryPayload(payload);
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", "wallet inventory refreshed");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
@@ -238,6 +368,7 @@ async function refreshWalletInventory(): Promise<void> {
setText("#walletInventoryCount", "0");
renderWalletRows([]);
renderExportWalletAliases([]);
renderExecutionWalletAliases([]);
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `Wallet inventory loading failed: ${message}`);
}
}
@@ -324,16 +455,16 @@ async function importTransferFile(): Promise<void> {
async function exportWallet(): Promise<void> {
const alias = document.querySelector<HTMLSelectElement>("#walletExportAliasSelect")?.value.trim() ?? "";
const format = selectedTransferFormat("#walletExportFormatSelect");
const destinationPath = inputValue("#walletExportDestinationPathInput");
if (alias.length === 0 || format.length === 0 || destinationPath.length === 0) {
setText("#walletExportStatus", "Wallet, format et destination requis");
const fileName = inputValue("#walletExportFileNameInput");
if (alias.length === 0 || format.length === 0 || fileName.length === 0) {
setText("#walletExportStatus", "Wallet, format et nom de fichier requis");
return;
}
const request: DemoWalletExportRequest = { alias, destination_path: destinationPath, format };
const request: DemoWalletExportRequest = { alias, file_name: fileName, format };
setText("#walletExportStatus", "Export...");
try {
await invoke<void>("demo_wallet_export_file", { request });
setText("#walletExportStatus", `Exporté : ${alias}`);
const exportedPath = await invoke<string>("demo_wallet_export_file", { request });
setText("#walletExportStatus", `Exporté : ${exportedPath}`);
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", `native wallet exported: ${alias}`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
@@ -647,6 +778,16 @@ document.addEventListener("DOMContentLoaded", () => {
document.querySelector<HTMLButtonElement>("#refreshWalletInventoryButton")?.addEventListener("click", () => {
void refreshWalletInventory();
});
document.querySelector<HTMLButtonElement>("#applyWalletExecutionSelectionButton")?.addEventListener("click", () => {
void applyExecutionWalletSelection();
});
document.querySelector<HTMLSelectElement>("#walletExecutionProfileSelect")?.addEventListener("change", () => {
updateExecutionSelectionDisplay();
if (walletInventory) {
renderWalletRows(walletInventory.wallets);
}
setText("#walletExecutionSelectionStatus", "En attente");
});
document.querySelector<HTMLButtonElement>("#createWalletButton")?.addEventListener("click", () => {
void createWallet();
});
@@ -668,6 +809,16 @@ document.addEventListener("DOMContentLoaded", () => {
document.querySelector<HTMLButtonElement>("#exportWalletTransferButton")?.addEventListener("click", () => {
void exportWallet();
});
document.querySelector<HTMLSelectElement>("#walletExportAliasSelect")?.addEventListener("change", () => {
updateExportFileName(false);
});
document.querySelector<HTMLSelectElement>("#walletExportFormatSelect")?.addEventListener("change", () => {
updateExportFileName(false);
});
document.querySelector<HTMLInputElement>("#walletExportFileNameInput")?.addEventListener("input", event => {
const input = event.currentTarget as HTMLInputElement;
input.dataset.autoGenerated = "false";
});
document.querySelector<HTMLInputElement>("#walletImportSourcePathInput")?.addEventListener("input", () => {
setTransferPublicKey("");
setText("#walletTransferStatus", "En attente");

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/app_state.rs
// version: 16
// version: 17
//! Shared Tauri application state and startup initialization.
@@ -30,6 +30,8 @@ pub(crate) struct AppState {
demo_decode_replay_campaign_id: std::sync::Mutex<std::option::Option<std::string::String>>,
demo_execution_solana_core_running: std::sync::atomic::AtomicBool,
demo_execution_solana_core_cancel_requested: std::sync::atomic::AtomicBool,
demo_execution_wallet_alias_overrides:
std::sync::Mutex<std::collections::BTreeMap<std::string::String, std::string::String>>,
startup_sequence_started: std::sync::atomic::AtomicBool,
}
@@ -115,6 +117,9 @@ impl crate::AppState {
demo_decode_replay_campaign_id: std::sync::Mutex::new(std::option::Option::None),
demo_execution_solana_core_running: std::sync::atomic::AtomicBool::new(false),
demo_execution_solana_core_cancel_requested: std::sync::atomic::AtomicBool::new(false),
demo_execution_wallet_alias_overrides: std::sync::Mutex::new(
std::collections::BTreeMap::new(),
),
startup_sequence_started: std::sync::atomic::AtomicBool::new(false),
});
}
@@ -169,6 +174,46 @@ impl crate::AppState {
return &self.active_profile;
}
/// Returns the session-only execution-wallet alias override for one profile.
pub(crate) fn demo_execution_wallet_alias_override(
&self,
profile_name: &str,
) -> std::result::Result<std::option::Option<std::string::String>, std::string::String> {
let lock_result = self.demo_execution_wallet_alias_overrides.lock();
let guard = match lock_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(
"demo execution wallet selection lock is poisoned".to_string(),
);
},
};
return std::result::Result::Ok(guard.get(profile_name).cloned());
}
/// Sets or clears the session-only execution-wallet alias override for one profile.
pub(crate) fn set_demo_execution_wallet_alias_override(
&self,
profile_name: std::string::String,
alias: std::option::Option<std::string::String>,
) -> std::result::Result<(), std::string::String> {
let lock_result = self.demo_execution_wallet_alias_overrides.lock();
let mut guard = match lock_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(
"demo execution wallet selection lock is poisoned".to_string(),
);
},
};
if let std::option::Option::Some(alias) = alias {
guard.insert(profile_name, alias);
} else {
guard.remove(profile_name.as_str());
}
return std::result::Result::Ok(());
}
/// Returns the configured HTTP endpoint pool.
pub(crate) fn http_pool(&self) -> &ks_onchain_transport::HttpEndpointPool {
return &self.http_pool;

View File

@@ -1,10 +1,12 @@
// file: kb-app-demo-desktop/src/demo_devnet_common.rs
// version: 4
// version: 6
//! Shared Devnet demo UI contracts.
use ts_rs::TS; // rust-rules: derive-import
const DEMO_WALLET_PASSWORD_ENV: &str = "KB_SECRET_DEMO_WALLET_PASSWORD";
/// Readiness report for one Devnet profile PostgreSQL store.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
@@ -73,3 +75,58 @@ fn bounded_u32(value: usize) -> u32 {
std::result::Result::Err(_) => u32::MAX,
};
}
/// Returns whether the backend-only demo wallet password is configured.
pub(crate) fn demo_wallet_password_configured() -> bool {
return std::env::var_os(DEMO_WALLET_PASSWORD_ENV).is_some();
}
/// Loads the backend-only demo wallet password without exposing it to Tauri DTOs.
pub(crate) fn demo_wallet_password()
-> std::result::Result<ks_wallet::WalletPassword, std::string::String> {
let password_text = match std::env::var(DEMO_WALLET_PASSWORD_ENV) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(format!(
"backend secret {DEMO_WALLET_PASSWORD_ENV} is not configured as UTF-8"
));
},
};
return match ks_wallet::WalletPassword::new(password_text) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
}
/// Resolves the temporary or persistent signer selected by one Devnet profile.
pub(crate) async fn resolve_devnet_execution_wallet(
state: &crate::AppState,
profile: &ks_config::ProfileConfig,
) -> std::result::Result<ks_pipeline_demo_scenarios::DevnetExecutionWallet, std::string::String> {
let alias_override = match state.demo_execution_wallet_alias_override(profile.name.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut effective_profile = profile.clone();
if let std::option::Option::Some(alias) = alias_override {
effective_profile.wallet.wallet_alias = std::option::Option::Some(alias);
}
let password = if effective_profile.wallet.wallet_alias.is_some() {
match demo_wallet_password() {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
} else {
std::option::Option::None
};
return match ks_pipeline_demo_scenarios::resolve_devnet_execution_wallet(
&effective_profile,
crate::workspace_root_dir().as_path(),
password,
)
.await
{
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
}

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_execution_metadata_metaplex_token_metadata.rs
// version: 8
// version: 11
//! Desktop adapters for generic and qualified Metaplex Token Metadata execution workflows.
@@ -235,22 +235,26 @@ pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_execute(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let workspace_root = crate::workspace_root_dir();
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_token_metadata(
&http_pool,
&store,
&profile,
workspace_root.as_path(),
&execution_request,
decoders.as_slice(),
materializers.as_slice(),
&observer,
)
.await
{
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary =
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_token_metadata_with_wallet(
&http_pool,
&store,
&profile,
&execution_wallet,
&execution_request,
decoders.as_slice(),
materializers.as_slice(),
&observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let transaction_signature =
summary.send_result.as_ref().map(|value| return value.signature.0.clone());
let confirmation_status =
@@ -417,7 +421,7 @@ async fn prepare_create_fixture_for_family(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let configured = std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str());
let wallet_dir = if configured.is_absolute() {
configured
} else {
@@ -1048,7 +1052,8 @@ pub(crate) async fn demo_execution_metadata_metaplex_execute_campaign(
return std::result::Result::Err(error.to_string());
}
let workspace_root = crate::workspace_root_dir();
let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let configured_wallet_dir =
std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str());
let wallet_dir = if configured_wallet_dir.is_absolute() {
configured_wallet_dir
} else {
@@ -1070,8 +1075,8 @@ pub(crate) async fn demo_execution_metadata_metaplex_execute_campaign(
wallet_dir,
&http_pool,
&store,
&state,
&profile,
workspace_root.as_path(),
&observer,
)
.await
@@ -1133,8 +1138,8 @@ fn execute_campaign_projection<'a, S, O>(
wallet_dir: std::path::PathBuf,
http_pool: &'a ks_onchain_transport::HttpEndpointPool,
store: &'a S,
state: &'a crate::AppState,
profile: &'a ks_config::ProfileConfig,
workspace_root: &'a std::path::Path,
observer: &'a O,
) -> CampaignProjectionFuture<'a>
where
@@ -1150,8 +1155,8 @@ where
wallet_dir,
http_pool,
store,
state,
profile,
workspace_root,
observer,
));
}
@@ -1161,8 +1166,8 @@ async fn execute_campaign_projection_unboxed<S, O>(
wallet_dir: std::path::PathBuf,
http_pool: &ks_onchain_transport::HttpEndpointPool,
store: &S,
state: &crate::AppState,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
observer: &O,
) -> ks_core::Result<CampaignProjection>
where
@@ -1174,6 +1179,15 @@ where
{
let base_options =
ks_pipeline_demo_scenarios::MetaplexCreateFixturePreparationOptions::new(wallet_dir);
let execution_wallet = match crate::resolve_devnet_execution_wallet(state, profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"demo_wallet_resolution_failed",
error,
));
},
};
return match campaign_id {
"create_mint_nft" => {
execute_create_mint_projection(
@@ -1184,7 +1198,7 @@ where
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
observer,
)
.await
@@ -1198,7 +1212,7 @@ where
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
observer,
)
.await
@@ -1212,7 +1226,7 @@ where
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
observer,
)
.await
@@ -1226,7 +1240,7 @@ where
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
observer,
)
.await
@@ -1240,17 +1254,17 @@ where
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
observer,
)
.await
},
"collection_verify" => {
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_collection_verify_campaign(
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_collection_verify_campaign_with_wallet(
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
&base_options,
observer,
).await {
@@ -1285,11 +1299,11 @@ where
},
"print_burn" => {
let summary =
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_print_burn_campaign(
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_print_burn_campaign_with_wallet(
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
&base_options,
observer,
)
@@ -1327,11 +1341,11 @@ where
ks_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
);
let summary =
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_pnft_lifecycle_campaign(
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_pnft_lifecycle_campaign_with_wallet(
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
&options,
observer,
)
@@ -1361,11 +1375,11 @@ where
})
},
"escrow" => {
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_escrow_campaign(
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_escrow_campaign_with_wallet(
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
&base_options,
observer,
)
@@ -1410,11 +1424,11 @@ where
ks_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft,
);
let summary =
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_maintenance_campaign(
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_maintenance_campaign_with_wallet(
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
&options,
observer,
)
@@ -1465,19 +1479,20 @@ where
let options = base_options.clone().with_asset_family(
ks_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft,
);
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_use_probe(
http_pool,
store,
profile,
workspace_root,
&options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary =
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_use_probe_with_wallet(
http_pool,
store,
profile,
&execution_wallet,
&options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut executions = vec![
metaplex_execution_json("fixture.create", &summary.fixture.create),
metaplex_execution_json("fixture.mint", &summary.fixture.mint),
@@ -1513,7 +1528,7 @@ async fn execute_create_mint_projection<S, O>(
http_pool: &ks_onchain_transport::HttpEndpointPool,
store: &S,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
wallet: &ks_pipeline_demo_scenarios::DevnetExecutionWallet,
observer: &O,
) -> ks_core::Result<CampaignProjection>
where
@@ -1523,19 +1538,15 @@ where
+ Sync,
O: ks_pipeline_demo_scenarios::SolanaExecutionObserver,
{
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_create_mint_campaign(
http_pool,
store,
profile,
workspace_root,
&options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary =
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_create_mint_campaign_with_wallet(
http_pool, store, profile, wallet, &options, observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let executions = vec![
metaplex_execution_json("create", &summary.create),
metaplex_execution_json("mint", &summary.mint),

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_execution_solana_core.rs
// version: 17
// version: 20
//! Tauri adapter for bounded Solana Core execution on Devnet.
@@ -509,12 +509,15 @@ pub(crate) async fn demo_execution_solana_core_execute(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let workspace_root = crate::workspace_root_dir();
let summary = match ks_pipeline_demo_scenarios::execute_devnet_system_transfer(
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary = match ks_pipeline_demo_scenarios::execute_devnet_system_transfer_with_wallet(
&http_pool,
&store,
&profile,
workspace_root.as_path(),
&execution_wallet,
&pipeline_request,
decoders.as_slice(),
materializers.as_slice(),
@@ -582,12 +585,15 @@ pub(crate) async fn demo_execution_spl_memo_execute(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let workspace_root = crate::workspace_root_dir();
let summary = match ks_pipeline_demo_scenarios::execute_devnet_memo(
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary = match ks_pipeline_demo_scenarios::execute_devnet_memo_with_wallet(
&http_pool,
&store,
&profile,
workspace_root.as_path(),
&execution_wallet,
&pipeline_request,
decoders.as_slice(),
materializers.as_slice(),

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_spl_ata.rs
// version: 13
// version: 16
//! Thin Tauri adapter for ATA execution, derivation and lifecycle journal reads.
@@ -279,21 +279,26 @@ pub(crate) async fn demo_execution_spl_ata_execute(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_associated_token_account(
&http_pool,
&store,
&profile,
crate::workspace_root_dir().as_path(),
&pipeline_request,
decoders.as_slice(),
materializers.as_slice(),
&observer,
)
.await
{
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary =
match ks_pipeline_demo_scenarios::execute_devnet_spl_associated_token_account_with_wallet(
&http_pool,
&store,
&profile,
&execution_wallet,
&pipeline_request,
decoders.as_slice(),
materializers.as_slice(),
&observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
return std::result::Result::Ok(summary_payload(summary));
}
@@ -346,7 +351,7 @@ pub(crate) async fn demo_spl_ata_journal(
async fn load_profile_wallet(
profile: &ks_config::ProfileConfig,
) -> std::result::Result<ks_wallet::TemporaryWallet, std::string::String> {
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let configured = std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str());
let directory = if configured.is_absolute() {
configured
} else {

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_spl_token.rs
// version: 10
// version: 13
//! Thin Tauri adapter for classic SPL Token execution and materialized journals.
@@ -223,12 +223,15 @@ pub(crate) async fn demo_execution_spl_token_execute(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let workspace_root = crate::workspace_root_dir();
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_token(
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_token_with_wallet(
&http_pool,
&store,
&profile,
workspace_root.as_path(),
&execution_wallet,
&pipeline_request,
decoders.as_slice(),
materializers.as_slice(),

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_spl_token_2022.rs
// version: 15
// version: 18
//! Thin Tauri adapter for independent public Token-2022 Devnet validation scenarios.
@@ -192,12 +192,15 @@ pub(crate) async fn demo_execution_spl_token_2022_execute(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let workspace_root = crate::workspace_root_dir();
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_token_2022(
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_token_2022_with_wallet(
&http_pool,
&store,
&profile,
workspace_root.as_path(),
&execution_wallet,
&pipeline_request,
decoders.as_slice(),
materializers.as_slice(),
@@ -220,7 +223,7 @@ pub(crate) fn demo_spl_token_2022_fixture(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let configured = std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str());
let wallet_dir = if configured.is_absolute() {
configured
} else {

View File

@@ -1,11 +1,12 @@
// file: kb-app-demo-desktop/src/demo_wallet.rs
// version: 5
// version: 7
//! UI-safe wallet management and public on-chain exploration.
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import
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.
@@ -21,6 +22,10 @@ pub(crate) struct DemoWalletInventoryPayload {
pub(crate) cluster: std::string::String,
/// Profiles available for explicit read-only on-chain exploration.
pub(crate) onchain_profiles: std::vec::Vec<DemoWalletOnchainProfilePayload>,
/// Devnet profiles and their session/config execution-wallet selections.
pub(crate) execution_profiles: std::vec::Vec<DemoWalletExecutionProfilePayload>,
/// Workspace-relative directory used for secret exports from this demo.
pub(crate) export_directory: std::string::String,
/// Secret transfer formats supported by the reusable wallet boundary.
pub(crate) transfer_formats: std::vec::Vec<DemoWalletTransferFormatPayload>,
/// Canonical classic SPL Token program identifier used by the explorer.
@@ -119,7 +124,7 @@ pub(crate) struct DemoWalletImportRequest {
pub(crate) format: std::string::String,
}
/// Request to export one native wallet to an explicit external file.
/// Request to export one native wallet to the application-owned export directory.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[ts(
export,
@@ -128,12 +133,42 @@ pub(crate) struct DemoWalletImportRequest {
pub(crate) struct DemoWalletExportRequest {
/// Alias of the native wallet to export.
pub(crate) alias: std::string::String,
/// Explicit destination path created without overwrite.
pub(crate) destination_path: std::string::String,
/// File name created inside the application-owned wallet export directory.
pub(crate) file_name: std::string::String,
/// Stable transfer format code selected by the operator.
pub(crate) format: std::string::String,
}
/// One Devnet profile and its effective desktop-session execution-wallet selection.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletExecutionProfilePayload.ts"
)]
pub(crate) struct DemoWalletExecutionProfilePayload {
/// Resolved Devnet profile name.
pub(crate) name: std::string::String,
/// Alias persisted in wallet configuration, when present.
pub(crate) configured_alias: std::option::Option<std::string::String>,
/// Session-only desktop override, when present.
pub(crate) runtime_alias: std::option::Option<std::string::String>,
/// Effective alias used by execution demos, or `None` for the temporary fallback.
pub(crate) effective_alias: std::option::Option<std::string::String>,
}
/// Request to set or clear one session-only execution-wallet selection.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletExecutionSelectionRequest.ts"
)]
pub(crate) struct DemoWalletExecutionSelectionRequest {
/// Devnet profile whose execution wallet is selected.
pub(crate) profile: std::string::String,
/// Native wallet alias, or `None` to return to profile configuration.
pub(crate) alias: std::option::Option<std::string::String>,
}
/// Exact SOL balance returned for one public wallet address.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
@@ -226,19 +261,87 @@ pub(crate) async fn demo_wallet_inventory(
for handle in handles {
wallets.push(wallet_identity_payload(&handle, selected_alias.as_deref()));
}
let execution_profiles = match execution_profile_payloads(state) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(DemoWalletInventoryPayload {
profile: profile.name.clone(),
cluster: profile.wallet.cluster.clone(),
onchain_profiles: onchain_profile_payloads(state),
execution_profiles,
export_directory: "data/wallets".to_string(),
transfer_formats: wallet_transfer_format_payloads(),
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(),
create_password_configured: crate::demo_wallet_password_configured(),
selected_alias,
wallets,
});
}
/// Sets or clears the session-only native wallet used by Devnet execution demos.
pub(crate) async fn demo_wallet_select_execution_wallet(
state: &crate::AppState,
request: crate::DemoWalletExecutionSelectionRequest,
) -> std::result::Result<DemoWalletInventoryPayload, std::string::String> {
let profile = match devnet_execution_profile(state, request.profile.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let profile_name = profile.name.clone();
if let std::option::Option::Some(alias_text) = request.alias {
let alias = match ks_wallet::WalletAlias::parse(alias_text.trim().to_string()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let password = match crate::demo_wallet_password() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
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.unlock(&alias, password).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let public_key = wallet.public_key();
wallet.lock();
if let std::result::Result::Err(error) = state.set_demo_execution_wallet_alias_override(
profile_name.clone(),
std::option::Option::Some(alias.as_str().to_string()),
) {
return std::result::Result::Err(error);
}
tracing::info!(
target: crate::TRACING_TARGET,
action = "select_demo_execution_wallet",
profile = profile_name.as_str(),
wallet_alias = alias.as_str(),
public_key = %public_key,
selection_source = "runtime",
"selected authenticated native wallet for desktop execution demos"
);
} else {
if let std::result::Result::Err(error) = state.set_demo_execution_wallet_alias_override(
profile_name.clone(),
std::option::Option::None,
) {
return std::result::Result::Err(error);
}
tracing::info!(
target: crate::TRACING_TARGET,
action = "select_demo_execution_wallet",
profile = profile_name.as_str(),
selection_source = "configuration",
"cleared desktop execution-wallet override"
);
}
return demo_wallet_inventory(state).await;
}
/// Creates one password-protected native wallet without sending the password through Tauri IPC.
pub(crate) async fn demo_wallet_create_native(
state: &crate::AppState,
@@ -248,7 +351,7 @@ pub(crate) async fn demo_wallet_create_native(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let password = match demo_wallet_password() {
let password = match crate::demo_wallet_password() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -309,7 +412,7 @@ pub(crate) async fn demo_wallet_import_file(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let password = match demo_wallet_password() {
let password = match crate::demo_wallet_password() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -341,7 +444,7 @@ pub(crate) async fn demo_wallet_import_file(
pub(crate) async fn demo_wallet_export_file(
state: &crate::AppState,
request: crate::DemoWalletExportRequest,
) -> std::result::Result<(), std::string::String> {
) -> std::result::Result<std::string::String, 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()),
@@ -350,7 +453,15 @@ pub(crate) async fn demo_wallet_export_file(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let password = match demo_wallet_password() {
let file_name = match validated_wallet_export_file_name(request.file_name.as_str(), format) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let export_directory = match prepare_wallet_export_directory().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let password = match crate::demo_wallet_password() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -358,13 +469,21 @@ pub(crate) async fn demo_wallet_export_file(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return match manager
.export_file(&alias, password, std::path::PathBuf::from(request.destination_path), format)
.await
let destination = export_directory.join(file_name.as_str());
if let std::result::Result::Err(error) =
manager.export_file(&alias, password, destination, format).await
{
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
return std::result::Result::Err(error.to_string());
}
tracing::info!(
target: crate::TRACING_TARGET,
action = "export_wallet_secret",
wallet_alias = alias.as_str(),
transfer_format = format.code(),
export_file_name = file_name.as_str(),
"exported native wallet secret to application-owned data directory"
);
return std::result::Result::Ok(format!("data/wallets/{file_name}"));
}
/// Inspects one explicitly selected native wallet file and returns only its safe identity.
@@ -489,21 +608,6 @@ pub(crate) async fn demo_wallet_rpc_execute(
});
}
fn demo_wallet_password() -> std::result::Result<ks_wallet::WalletPassword, std::string::String> {
let password_text = match std::env::var(DEMO_WALLET_PASSWORD_ENV) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(format!(
"backend secret {DEMO_WALLET_PASSWORD_ENV} is not configured as UTF-8"
));
},
};
return match ks_wallet::WalletPassword::new(password_text) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
}
fn wallet_transfer_format_payloads() -> std::vec::Vec<crate::DemoWalletTransferFormatPayload> {
let mut formats = std::vec::Vec::new();
for format in ks_wallet::WalletTransferFormat::supported() {
@@ -528,6 +632,127 @@ fn wallet_transfer_format_from_code(
return std::result::Result::Err(format!("wallet transfer format '{code}' is not supported"));
}
fn execution_profile_payloads(
state: &crate::AppState,
) -> std::result::Result<std::vec::Vec<crate::DemoWalletExecutionProfilePayload>, std::string::String>
{
let mut profiles = std::vec::Vec::new();
for profile in &state.app_config().profiles {
if profile.wallet.cluster != "devnet" {
continue;
}
let runtime_alias = match state.demo_execution_wallet_alias_override(profile.name.as_str())
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let effective_alias = match runtime_alias.as_ref() {
std::option::Option::Some(value) => std::option::Option::Some(value.clone()),
std::option::Option::None => profile.wallet.wallet_alias.clone(),
};
profiles.push(crate::DemoWalletExecutionProfilePayload {
name: profile.name.clone(),
configured_alias: profile.wallet.wallet_alias.clone(),
runtime_alias,
effective_alias,
});
}
return std::result::Result::Ok(profiles);
}
fn devnet_execution_profile<'a>(
state: &'a crate::AppState,
profile_name: &str,
) -> std::result::Result<&'a ks_config::ProfileConfig, std::string::String> {
let profile = match onchain_profile(state, profile_name) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if profile.wallet.cluster != "devnet" {
return std::result::Result::Err(
"wallet execution selection requires a Devnet profile".to_string(),
);
}
return std::result::Result::Ok(profile);
}
fn validated_wallet_export_file_name(
file_name: &str,
format: ks_wallet::WalletTransferFormat,
) -> std::result::Result<std::string::String, std::string::String> {
let file_name = file_name.trim();
if file_name.is_empty() || file_name.len() > 128 {
return std::result::Result::Err(
"wallet export file name must contain between 1 and 128 bytes".to_string(),
);
}
if file_name == "." || file_name == ".." || file_name.starts_with('.') {
return std::result::Result::Err("wallet export file name is invalid".to_string());
}
if !file_name.chars().all(|value| {
return value.is_ascii_alphanumeric() || value == '-' || value == '_' || value == '.';
}) {
return std::result::Result::Err(
"wallet export file name contains unsupported characters".to_string(),
);
}
let expected_extension = format.default_extension();
let path = std::path::Path::new(file_name);
if path.extension().is_none() {
return std::result::Result::Ok(format!("{file_name}.{expected_extension}"));
}
if path.extension().and_then(std::ffi::OsStr::to_str)
!= std::option::Option::Some(expected_extension)
{
return std::result::Result::Err(format!(
"wallet export file extension must be '.{expected_extension}' for format '{}'",
format.code()
));
}
return std::result::Result::Ok(file_name.to_string());
}
async fn prepare_wallet_export_directory()
-> std::result::Result<std::path::PathBuf, std::string::String> {
let data_directory = crate::workspace_root_dir().join("data");
if let std::result::Result::Ok(metadata) = tokio::fs::symlink_metadata(&data_directory).await {
if metadata.file_type().is_symlink() || !metadata.is_dir() {
return std::result::Result::Err(
"wallet export data directory must be a regular directory".to_string(),
);
}
}
if let std::result::Result::Err(error) = tokio::fs::create_dir_all(&data_directory).await {
return std::result::Result::Err(format!(
"cannot create wallet export data directory: {error}"
));
}
let export_directory = data_directory.join("wallets");
if let std::result::Result::Ok(metadata) = tokio::fs::symlink_metadata(&export_directory).await
{
if metadata.file_type().is_symlink() || !metadata.is_dir() {
return std::result::Result::Err(
"wallet export directory must be a regular directory".to_string(),
);
}
}
if let std::result::Result::Err(error) = tokio::fs::create_dir_all(&export_directory).await {
return std::result::Result::Err(format!("cannot create wallet export directory: {error}"));
}
#[cfg(unix)]
{
let permissions = std::fs::Permissions::from_mode(0o700);
if let std::result::Result::Err(error) =
tokio::fs::set_permissions(&export_directory, permissions).await
{
return std::result::Result::Err(format!(
"cannot secure wallet export directory permissions: {error}"
));
}
}
return std::result::Result::Ok(export_directory);
}
fn onchain_profile<'a>(
state: &'a crate::AppState,
profile_name: &str,
@@ -676,14 +901,19 @@ mod tests {
"importWalletTransferButton",
"walletExportAliasSelect",
"walletExportFormatSelect",
"walletExportDestinationPathInput",
"walletExportFileNameInput",
"walletExportDirectory",
"exportWalletTransferButton",
"walletExecutionProfileSelect",
"walletExecutionAliasSelect",
"applyWalletExecutionSelectionButton",
"walletExternalPathInput",
"inspectWalletFileButton",
"walletInspectionJson",
] {
assert!(html.contains(required));
}
assert!(script.contains("demo_wallet_select_execution_wallet"));
assert!(script.contains("demo_wallet_create_native"));
assert!(script.contains("demo_wallet_inspect_transfer_file"));
assert!(script.contains("demo_wallet_import_file"));
@@ -700,6 +930,7 @@ mod tests {
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_select_execution_wallet"));
assert!(tauri_runtime.contains("demo_wallet_create_native"));
assert!(tauri_runtime.contains("demo_wallet_inspect_transfer_file"));
assert!(tauri_runtime.contains("demo_wallet_import_file"));
@@ -721,6 +952,13 @@ mod tests {
cluster: "devnet".to_string(),
active: false,
}],
execution_profiles: std::vec![super::DemoWalletExecutionProfilePayload {
name: "local_devnet".to_string(),
configured_alias: std::option::Option::None,
runtime_alias: std::option::Option::Some("selected-wallet".to_string()),
effective_alias: std::option::Option::Some("selected-wallet".to_string()),
}],
export_directory: "data/wallets".to_string(),
transfer_formats: super::wallet_transfer_format_payloads(),
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(),
@@ -752,6 +990,34 @@ mod tests {
}
}
#[test]
fn wallet_export_file_name_is_bounded_and_format_specific() {
let json = super::validated_wallet_export_file_name(
"operator",
ks_wallet::WalletTransferFormat::SolanaCliJson,
);
assert_eq!(json, std::result::Result::Ok("operator.json".to_string()));
let base58 = super::validated_wallet_export_file_name(
"operator.txt",
ks_wallet::WalletTransferFormat::SolanaPrivateKeyBase58,
);
assert_eq!(base58, std::result::Result::Ok("operator.txt".to_string()));
assert!(
super::validated_wallet_export_file_name(
"../operator.json",
ks_wallet::WalletTransferFormat::SolanaCliJson,
)
.is_err()
);
assert!(
super::validated_wallet_export_file_name(
"operator.txt",
ks_wallet::WalletTransferFormat::SolanaCliJson,
)
.is_err()
);
}
#[test]
fn wallet_rpc_surface_is_read_only_and_bounded() {
for allowed in ["getTokenAccountsByOwner", "getSignaturesForAddress", "getTransaction"] {

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/lib.rs
// version: 41
// version: 43
//! Tauri desktop demo application for `khadhroony-bot3`.
@@ -151,10 +151,16 @@ pub(crate) use self::demo_decode_replay::demo_decode_replay_options;
pub(crate) use self::demo_devnet_common::DemoExecutionDevnetStoreReadinessPayload;
/// Restores the shared single-execution state flag when a Devnet run finishes.
pub(crate) use self::demo_devnet_common::DemoExecutionRunGuard;
/// Loads the backend-only demo wallet password.
pub(crate) use self::demo_devnet_common::demo_wallet_password;
/// Returns whether the backend-only demo wallet password is configured.
pub(crate) use self::demo_devnet_common::demo_wallet_password_configured;
/// Converts the reusable scenario readiness report to the desktop TS-RS payload.
pub(crate) use self::demo_devnet_common::devnet_store_readiness_payload;
/// Serializes a diagnostic value to indented JSON without panicking.
pub(crate) use self::demo_devnet_common::pretty_json;
/// Resolves the temporary or persistent signer selected by one Devnet profile.
pub(crate) use self::demo_devnet_common::resolve_devnet_execution_wallet;
/// Bridges Metadata execution progress to the Metadata desktop window.
pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataObserver;
/// Progress event emitted by Metadata execution workflows.
@@ -367,6 +373,10 @@ pub(crate) use self::demo_transport::DemoEndpointRolePayload;
pub(crate) use self::demo_wallet::DemoWalletBalancePayload;
/// Request to create one native wallet from the backend-only demo secret.
pub(crate) use self::demo_wallet::DemoWalletCreateRequest;
/// One Devnet profile and its effective execution-wallet selection.
pub(crate) use self::demo_wallet::DemoWalletExecutionProfilePayload;
/// Request to set or clear one session-only execution-wallet selection.
pub(crate) use self::demo_wallet::DemoWalletExecutionSelectionRequest;
/// Request to export one native wallet secret through the backend-only demo secret.
pub(crate) use self::demo_wallet::DemoWalletExportRequest;
/// UI-safe native wallet identity exposed by the desktop adapter.
@@ -403,6 +413,8 @@ pub(crate) use self::demo_wallet::demo_wallet_inspect_transfer_file;
pub(crate) use self::demo_wallet::demo_wallet_inventory;
/// Executes one bounded read-only wallet explorer RPC through an explicitly selected profile.
pub(crate) use self::demo_wallet::demo_wallet_rpc_execute;
/// Sets or clears the session-only native wallet used by Devnet execution demos.
pub(crate) use self::demo_wallet::demo_wallet_select_execution_wallet;
/// UI-safe WebSocket endpoint snapshot.
pub(crate) use self::demo_ws::DemoWsEndpointPayload;
/// WebSocket execution response payload.

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/tauri.rs
// version: 43
// version: 44
//! Tauri runtime assembly and private command wrappers.
@@ -38,6 +38,7 @@ pub fn run() -> ks_core::Result<()> {
load_demo_config,
open_demo_wallet_window,
demo_wallet_inventory,
demo_wallet_select_execution_wallet,
demo_wallet_create_native,
demo_wallet_inspect_transfer_file,
demo_wallet_import_file,
@@ -258,6 +259,14 @@ async fn demo_wallet_inventory(
return crate::demo_wallet_inventory(state.inner()).await;
}
#[tauri::command]
async fn demo_wallet_select_execution_wallet(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoWalletExecutionSelectionRequest,
) -> std::result::Result<crate::DemoWalletInventoryPayload, std::string::String> {
return crate::demo_wallet_select_execution_wallet(state.inner(), request).await;
}
#[tauri::command]
async fn demo_wallet_create_native(
state: tauri::State<'_, crate::AppState>,
@@ -285,7 +294,7 @@ async fn demo_wallet_import_file(
async fn demo_wallet_export_file(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoWalletExportRequest,
) -> std::result::Result<(), std::string::String> {
) -> std::result::Result<std::string::String, std::string::String> {
return crate::demo_wallet_export_file(state.inner(), request).await;
}