v0.5.2-pre.006-fix-010
This commit is contained in:
@@ -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 d’export `data/wallets/`, ni les contrats de configuration introduits par les fixes précédents.
|
||||
|
||||
## `0.5.2-pre.006-delta-fix-008`
|
||||
|
||||
- ajoute dans `demo_wallet` une sélection session-only du `.kswallet` d'exécution par profil Devnet ; l'alias est authentifié côté backend avant activation, ne réécrit pas `wallet.config.json` et devient prioritaire sur `wallet_alias` pour les démos suivantes ;
|
||||
- propage cette sélection runtime à tous les adaptateurs desktop System, Memo, ATA, SPL Token, Token-2022 et Metadata, tout en conservant le fallback temporaire uniquement lorsqu'aucun alias runtime ou configuré n'existe ;
|
||||
- contraint les exports secrets de la démo à `./data/wallets/`, remplace le chemin arbitraire par un nom de fichier borné, impose l'extension du format et sécurise le répertoire en `0700` sur Unix ;
|
||||
- affiche explicitement les alias configuré, runtime et effectif afin que la validation Devnet puisse confirmer le passage de `persistence="temporary"` à `persistence="persistent"`.
|
||||
|
||||
## `0.5.2-pre.006-delta-fix-005`
|
||||
|
||||
- résout côté backend le signer Devnet sélectionné avant les démos System, Memo, ATA, SPL Token, Token-2022, Solana Program Metadata, Token-2022 Metadata ainsi que toutes les campagnes Metaplex qualifiées, y compris leurs parcours multi-signers ;
|
||||
- utilise `KB_SECRET_DEMO_WALLET_PASSWORD` uniquement lorsque `wallet_alias` sélectionne un `.kswallet`, sans transmettre le password à Tauri ou TypeScript ;
|
||||
- conserve les répertoires de fixtures sous `temporary_wallet_dir` tandis que `demo_wallet` inventorie et importe désormais dans la racine persistante `wallet_dir`.
|
||||
|
||||
## `0.5.2-pre.006-delta-fix-004`
|
||||
|
||||
- complète la fenêtre `Wallets` avec inspection publique, import et export des deux formats `ks-wallet` réellement supportés : Solana CLI JSON 64 octets et keypair Base58 64 octets ;
|
||||
|
||||
@@ -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 d’un 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 d’un `.kswallet` externe et exploration publique du solde SOL, des comptes SPL Token / Token-2022 et des transactions récentes sur un profil RPC explicitement sélectionnable ;
|
||||
- fenêtre Wallets dédiée : inventaire sûr des identités `.kswallet`, création d’un conteneur persistant via un secret backend-only, sélection session-only du wallet d'exécution Devnet, inspection/import/export des formats keypair supportés sous `data/wallets/`, extraction sûre de pubkey depuis un keypair legacy, inspection explicite d’un `.kswallet` externe et exploration publique du solde SOL, des comptes SPL Token / Token-2022 et des transactions récentes sur un profil RPC explicitement sélectionnable ;
|
||||
- transport HTTP JSON-RPC et WebSocket ;
|
||||
- diagnostics PostgreSQL ;
|
||||
- backfill, extraction Core et replay, avec enregistrement générique de Solana Program Metadata ;
|
||||
|
||||
@@ -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 d’un `.kswallet` dans `demo_wallet`, confirmer `persistence="persistent"` dans les logs puis retrouver les signatures produites depuis l’explorateur.
|
||||
- [ ] `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.
|
||||
|
||||
@@ -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 @@ L’application 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 d’une adresse publique. Les DTO frontend ne contiennent jamais de matériau secret. Aucun password, chemin interne du store, sel, nonce, ciphertext ou byte privé n’est renvoyé par Tauri.
|
||||
|
||||
La fenêtre `Wallets`, accessible depuis le menu `Démos` de `main`, fournit maintenant 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 d’un nouveau `.kswallet` dans le store du profil actif ;
|
||||
- inspection et import d'un keypair externe dans un format supporté ;
|
||||
- export authentifié d'un `.kswallet` vers un format supporté ;
|
||||
@@ -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é
|
||||
|
||||
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.
|
||||
|
||||
@@ -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électionné</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>
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"] {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user