v0.5.2-pre.006-fix-004
This commit is contained in:
@@ -1,8 +1,17 @@
|
|||||||
<!-- file: kb-app-demo-desktop/CHANGELOG.md -->
|
<!-- file: kb-app-demo-desktop/CHANGELOG.md -->
|
||||||
<!-- version: 56 -->
|
<!-- version: 57 -->
|
||||||
|
|
||||||
# CHANGELOG — kb-app-demo-desktop
|
# CHANGELOG — kb-app-demo-desktop
|
||||||
|
|
||||||
|
## `0.5.2-pre.006-delta-fix-004`
|
||||||
|
|
||||||
|
- complète la fenêtre `Wallets` avec inspection publique, import et export des deux formats `ks-wallet` réellement supportés : Solana CLI JSON 64 octets et keypair Base58 64 octets ;
|
||||||
|
- garde le password hors Tauri IPC : création, import et export utilisent uniquement `KB_SECRET_DEMO_WALLET_PASSWORD` côté backend ;
|
||||||
|
- permet d'inspecter un ancien fichier keypair sans le convertir, d'en extraire uniquement la pubkey puis de l'envoyer directement vers l'explorateur Devnet/Mainnet ;
|
||||||
|
- peuple les sélecteurs de formats depuis l'inventaire exposé par `ks-wallet` afin d'éviter une liste frontend indépendante du backend ;
|
||||||
|
- aligne visuellement `Profil RPC`, `Adresse publique` et `Transactions`, puis affiche le profil/cluster sélectionné sous la ligne de contrôles ;
|
||||||
|
- prépare la validation suivante où les démos d'exécution Devnet devront consommer une capacité `.kswallet` explicite au lieu de recréer directement leur wallet JSON temporaire.
|
||||||
|
|
||||||
## `0.5.2-pre.006-delta-fix-003`
|
## `0.5.2-pre.006-delta-fix-003`
|
||||||
|
|
||||||
- ajoute dans `Wallets` un sélecteur explicite de profil RPC pour l’explorateur on-chain ; le profil actif reste la valeur initiale mais Devnet/Mainnet peuvent être interrogés sans redémarrer l’application ni modifier `active_profile` ;
|
- ajoute dans `Wallets` un sélecteur explicite de profil RPC pour l’explorateur on-chain ; le profil actif reste la valeur initiale mais Devnet/Mainnet peuvent être interrogés sans redémarrer l’application ni modifier `active_profile` ;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: kb-app-demo-desktop/README.md -->
|
<!-- file: kb-app-demo-desktop/README.md -->
|
||||||
<!-- version: 19 -->
|
<!-- version: 20 -->
|
||||||
|
|
||||||
# kb-app-demo-desktop
|
# kb-app-demo-desktop
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
## Fonctionnalités
|
## Fonctionnalités
|
||||||
|
|
||||||
- configuration et diagnostics de logging ;
|
- configuration et diagnostics de logging ;
|
||||||
- fenêtre Wallets dédiée : inventaire sûr des identités `.kswallet`, création d’un conteneur persistant via un secret backend-only, inspection explicite d’un fichier externe et exploration publique du solde SOL, des comptes SPL Token / Token-2022 et des transactions récentes sur un profil RPC explicitement sélectionnable ;
|
- 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 ;
|
||||||
- transport HTTP JSON-RPC et WebSocket ;
|
- transport HTTP JSON-RPC et WebSocket ;
|
||||||
- diagnostics PostgreSQL ;
|
- diagnostics PostgreSQL ;
|
||||||
- backfill, extraction Core et replay, avec enregistrement générique de Solana Program Metadata ;
|
- backfill, extraction Core et replay, avec enregistrement générique de Solana Program Metadata ;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<!-- file: kb-app-demo-desktop/TODO.md -->
|
<!-- file: kb-app-demo-desktop/TODO.md -->
|
||||||
<!-- version: 16 -->
|
<!-- version: 17 -->
|
||||||
|
|
||||||
# TODO — kb-app-demo-desktop
|
# TODO — kb-app-demo-desktop
|
||||||
|
|
||||||
## Évolutions générales
|
## É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.4` — réduire les panneaux d'exécution à l'adaptation UI/Tauri en déplaçant toute orchestration de campagne encore réutilisable vers `ks-pipeline-demo-scenarios`.
|
- [ ] `0.5.4` — réduire les panneaux d'exécution à l'adaptation UI/Tauri en déplaçant toute orchestration de campagne encore réutilisable vers `ks-pipeline-demo-scenarios`.
|
||||||
- [ ] UX — empêcher les doubles déclenchements pendant une préparation, simulation ou soumission active.
|
- [ ] UX — empêcher les doubles déclenchements pendant une préparation, simulation ou soumission active.
|
||||||
- [ ] Observabilité — afficher explicitement les comptes relus et les projections matérialisées.
|
- [ ] Observabilité — afficher explicitement les comptes relus et les projections matérialisées.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: kb-app-demo-desktop/USAGE.md -->
|
<!-- file: kb-app-demo-desktop/USAGE.md -->
|
||||||
<!-- version: 19 -->
|
<!-- version: 20 -->
|
||||||
|
|
||||||
# Utilisation de kb-app-demo-desktop
|
# Utilisation de kb-app-demo-desktop
|
||||||
|
|
||||||
@@ -11,10 +11,12 @@ 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.
|
Le backend desktop peut lister les `.kswallet` du répertoire configuré, créer un wallet natif protégé, inspecter un fichier sélectionné explicitement ailleurs et explorer l’état on-chain d’une adresse publique. Les DTO frontend ne contiennent jamais de matériau secret. Aucun password, chemin interne du store, sel, nonce, ciphertext ou byte privé n’est renvoyé par Tauri.
|
||||||
|
|
||||||
La fenêtre `Wallets`, accessible depuis le menu `Démos` de `main`, fournit quatre surfaces :
|
La fenêtre `Wallets`, accessible depuis le menu `Démos` de `main`, fournit maintenant six surfaces :
|
||||||
|
|
||||||
- inventaire des wallets natifs du profil actif ;
|
- inventaire des wallets natifs du profil actif ;
|
||||||
- création d’un nouveau `.kswallet` dans le store du profil actif ;
|
- création d’un nouveau `.kswallet` dans le store du profil actif ;
|
||||||
|
- inspection et import d'un keypair externe dans un format supporté ;
|
||||||
|
- export authentifié d'un `.kswallet` vers un format supporté ;
|
||||||
- explorateur public on-chain ;
|
- explorateur public on-chain ;
|
||||||
- inspection structurale d’un `.kswallet` explicite situé hors du store.
|
- inspection structurale d’un `.kswallet` explicite situé hors du store.
|
||||||
|
|
||||||
@@ -31,6 +33,19 @@ Le secret peut aussi être placé dans le fichier `.env` local chargé par `ks-c
|
|||||||
|
|
||||||
La création demande uniquement l’alias. Elle crée `<alias>.kswallet` dans le répertoire wallet résolu du profil actif et ne modifie pas automatiquement `wallet_alias` dans la configuration. Un changement de valeur de `KB_SECRET_DEMO_WALLET_PASSWORD` avant une autre session permet d’utiliser un autre mot de passe pour de nouveaux wallets ; ce secret de démonstration n’est pas un mot de passe global du format `.kswallet`.
|
La création demande uniquement l’alias. Elle crée `<alias>.kswallet` dans le répertoire wallet résolu du profil actif et ne modifie pas automatiquement `wallet_alias` dans la configuration. Un changement de valeur de `KB_SECRET_DEMO_WALLET_PASSWORD` avant une autre session permet d’utiliser un autre mot de passe pour de nouveaux wallets ; ce secret de démonstration n’est pas un mot de passe global du format `.kswallet`.
|
||||||
|
|
||||||
|
### Inspecter, importer et exporter les formats keypair
|
||||||
|
|
||||||
|
Les sélecteurs de format sont alimentés par `ks-wallet` et exposent actuellement :
|
||||||
|
|
||||||
|
- `Solana CLI keypair JSON (64 octets)` ;
|
||||||
|
- `Solana private key Base58 (64 octets)`.
|
||||||
|
|
||||||
|
L'action **Inspecter la keypair** ne crée aucun `.kswallet` : le backend valide le fichier avec le codec choisi et ne renvoie que la pubkey. Cette pubkey peut être envoyée directement à l'explorateur on-chain, ce qui permet par exemple de contrôler un ancien `wallets/temporary/local_devnet/*.json` avant de décider de le migrer.
|
||||||
|
|
||||||
|
L'action **Importer en .kswallet** demande en plus un alias destination et utilise `KB_SECRET_DEMO_WALLET_PASSWORD` uniquement côté backend. La source reste inchangée. L'export fonctionne dans le sens inverse : choix d'un alias natif, du format et d'un chemin destination explicite ; le backend authentifie le `.kswallet` avec le même secret de démonstration et refuse d'écraser un fichier existant.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
### Explorer une adresse sur un profil RPC sélectionné
|
### 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.
|
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 -->
|
<!-- file: kb-app-demo-desktop/frontend/demo_wallet.html -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 4 -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
|
|
||||||
@@ -90,6 +90,62 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-xl-7">
|
||||||
|
<div class="card shadow-sm border-0 h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="h5 card-title mb-2">Importer ou inspecter une keypair externe</h2>
|
||||||
|
<p class="text-body-secondary">Le fichier source reste inchangé. L'inspection valide la keypair et ne retourne que sa pubkey ; l'import crée ensuite un nouveau <code>.kswallet</code> dans le store du profil actif.</p>
|
||||||
|
<label for="walletImportSourcePathInput" class="form-label">Chemin source</label>
|
||||||
|
<input id="walletImportSourcePathInput" class="form-control font-monospace" type="text" autocomplete="off" placeholder="/chemin/vers/local-devnet-operator.json">
|
||||||
|
<div class="row g-3 mt-1">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label for="walletImportFormatSelect" class="form-label">Format source</label>
|
||||||
|
<select id="walletImportFormatSelect" class="form-select"></select>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label for="walletImportAliasInput" class="form-label">Alias destination</label>
|
||||||
|
<input id="walletImportAliasInput" class="form-control font-monospace" type="text" autocomplete="off" maxlength="64" placeholder="local-devnet-operator">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap gap-2 mt-3">
|
||||||
|
<button id="inspectWalletTransferButton" class="btn btn-outline-primary" type="button">Inspecter la keypair</button>
|
||||||
|
<button id="importWalletTransferButton" class="btn btn-primary" type="button">Importer en .kswallet</button>
|
||||||
|
<button id="exploreWalletTransferButton" class="btn btn-outline-secondary" type="button" disabled>Explorer la pubkey</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<span class="fw-semibold">Pubkey détectée :</span>
|
||||||
|
<code id="walletTransferPublicKey" class="ms-1 text-break">—</code>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<span class="fw-semibold">Statut :</span>
|
||||||
|
<code id="walletTransferStatus" class="ms-1">En attente</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-xl-5">
|
||||||
|
<div class="card shadow-sm border-0 h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="h5 card-title mb-2">Exporter un wallet natif</h2>
|
||||||
|
<p class="text-body-secondary">L'export exige le même secret backend que la création et refuse d'écraser un fichier existant.</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">
|
||||||
|
<div class="d-flex flex-wrap gap-2 mt-3">
|
||||||
|
<button id="exportWalletTransferButton" class="btn btn-primary" type="button">Exporter</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<span class="fw-semibold">Statut :</span>
|
||||||
|
<code id="walletExportStatus" class="ms-1">En attente</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card shadow-sm border-0">
|
<div class="card shadow-sm border-0">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -104,7 +160,6 @@
|
|||||||
<div class="col-12 col-lg-3">
|
<div class="col-12 col-lg-3">
|
||||||
<label for="walletOnchainProfileSelect" class="form-label">Profil RPC</label>
|
<label for="walletOnchainProfileSelect" class="form-label">Profil RPC</label>
|
||||||
<select id="walletOnchainProfileSelect" class="form-select"></select>
|
<select id="walletOnchainProfileSelect" class="form-select"></select>
|
||||||
<div class="small text-body-secondary mt-1">Cluster : <code id="walletOnchainCluster">—</code></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-7">
|
<div class="col-12 col-lg-7">
|
||||||
<label for="walletAddressInput" class="form-label">Adresse publique</label>
|
<label for="walletAddressInput" class="form-label">Adresse publique</label>
|
||||||
@@ -115,6 +170,7 @@
|
|||||||
<input id="walletSignatureLimitInput" class="form-control" type="number" min="1" max="100" value="20">
|
<input id="walletSignatureLimitInput" class="form-control" type="number" min="1" max="100" value="20">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="small text-body-secondary mt-2">Profil sélectionné : <code id="walletOnchainSelectedProfile">—</code> · Cluster : <code id="walletOnchainCluster">—</code></div>
|
||||||
<div class="row g-3 mt-2">
|
<div class="row g-3 mt-2">
|
||||||
<div class="col-12 col-md-4">
|
<div class="col-12 col-md-4">
|
||||||
<div class="border rounded p-3 h-100">
|
<div class="border rounded p-3 h-100">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/frontend/ts/demo_wallet.ts
|
// file: kb-app-demo-desktop/frontend/ts/demo_wallet.ts
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
import * as bootstrap from "bootstrap";
|
import * as bootstrap from "bootstrap";
|
||||||
import "simplebar";
|
import "simplebar";
|
||||||
@@ -9,11 +9,16 @@ import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./fr
|
|||||||
import { renderJsonViewer } from "./json_viewer.ts";
|
import { renderJsonViewer } from "./json_viewer.ts";
|
||||||
import type { DemoWalletBalancePayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletBalancePayload.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 { 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 { DemoWalletIdentityPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletIdentityPayload.ts";
|
import type { DemoWalletIdentityPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletIdentityPayload.ts";
|
||||||
|
import type { DemoWalletImportRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletImportRequest.ts";
|
||||||
import type { DemoWalletInventoryPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletInventoryPayload.ts";
|
import type { DemoWalletInventoryPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletInventoryPayload.ts";
|
||||||
import type { DemoWalletOnchainProfilePayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletOnchainProfilePayload.ts";
|
import type { DemoWalletOnchainProfilePayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletOnchainProfilePayload.ts";
|
||||||
import type { DemoWalletRpcExecutionPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletRpcExecutionPayload.ts";
|
import type { DemoWalletRpcExecutionPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletRpcExecutionPayload.ts";
|
||||||
import type { DemoWalletRpcRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletRpcRequest.ts";
|
import type { DemoWalletRpcRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletRpcRequest.ts";
|
||||||
|
import type { DemoWalletTransferFormatPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletTransferFormatPayload.ts";
|
||||||
|
import type { DemoWalletTransferInspectionPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletTransferInspectionPayload.ts";
|
||||||
|
import type { DemoWalletTransferInspectionRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletTransferInspectionRequest.ts";
|
||||||
|
|
||||||
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||||
@@ -37,6 +42,7 @@ interface ParsedSignatureInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let walletInventory: DemoWalletInventoryPayload | null = null;
|
let walletInventory: DemoWalletInventoryPayload | null = null;
|
||||||
|
let inspectedTransferPublicKey = "";
|
||||||
|
|
||||||
function setText(selector: string, value: string): void {
|
function setText(selector: string, value: string): void {
|
||||||
const element = document.querySelector<HTMLElement>(selector);
|
const element = document.querySelector<HTMLElement>(selector);
|
||||||
@@ -63,6 +69,7 @@ function selectedOnchainProfilePayload(): DemoWalletOnchainProfilePayload | null
|
|||||||
|
|
||||||
function updateOnchainClusterLabel(): void {
|
function updateOnchainClusterLabel(): void {
|
||||||
const selected = selectedOnchainProfilePayload();
|
const selected = selectedOnchainProfilePayload();
|
||||||
|
setText("#walletOnchainSelectedProfile", selected?.name ?? "—");
|
||||||
setText("#walletOnchainCluster", selected?.cluster ?? "—");
|
setText("#walletOnchainCluster", selected?.cluster ?? "—");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +109,65 @@ function parseJsonResponse(response: DemoWalletRpcExecutionPayload): unknown {
|
|||||||
return JSON.parse(response.response_json) as unknown;
|
return JSON.parse(response.response_json) as unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderTransferFormats(formats: ReadonlyArray<DemoWalletTransferFormatPayload>): void {
|
||||||
|
for (const selector of ["#walletImportFormatSelect", "#walletExportFormatSelect"]) {
|
||||||
|
const select = document.querySelector<HTMLSelectElement>(selector);
|
||||||
|
if (!select) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const previous = select.value;
|
||||||
|
select.replaceChildren();
|
||||||
|
for (const format of formats) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = format.code;
|
||||||
|
option.textContent = `${format.label} (.${format.default_extension})`;
|
||||||
|
option.dataset.defaultExtension = format.default_extension;
|
||||||
|
select.appendChild(option);
|
||||||
|
}
|
||||||
|
if (formats.some(format => format.code === previous)) {
|
||||||
|
select.value = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderExportWalletAliases(wallets: ReadonlyArray<DemoWalletIdentityPayload>): void {
|
||||||
|
const select = document.querySelector<HTMLSelectElement>("#walletExportAliasSelect");
|
||||||
|
if (!select) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previous = select.value;
|
||||||
|
select.replaceChildren();
|
||||||
|
if (wallets.length === 0) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = "";
|
||||||
|
option.textContent = "Aucun wallet natif";
|
||||||
|
select.appendChild(option);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const wallet of wallets) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = wallet.alias;
|
||||||
|
option.textContent = `${wallet.alias} — ${wallet.public_key}`;
|
||||||
|
select.appendChild(option);
|
||||||
|
}
|
||||||
|
if (wallets.some(wallet => wallet.alias === previous)) {
|
||||||
|
select.value = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedTransferFormat(selector: string): string {
|
||||||
|
return document.querySelector<HTMLSelectElement>(selector)?.value.trim() ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTransferPublicKey(publicKey: string): void {
|
||||||
|
inspectedTransferPublicKey = publicKey;
|
||||||
|
setText("#walletTransferPublicKey", publicKey.length > 0 ? publicKey : "—");
|
||||||
|
const exploreButton = document.querySelector<HTMLButtonElement>("#exploreWalletTransferButton");
|
||||||
|
if (exploreButton) {
|
||||||
|
exploreButton.disabled = publicKey.length === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderWalletRows(wallets: ReadonlyArray<DemoWalletIdentityPayload>): void {
|
function renderWalletRows(wallets: ReadonlyArray<DemoWalletIdentityPayload>): void {
|
||||||
const body = document.querySelector<HTMLTableSectionElement>("#walletInventoryTableBody");
|
const body = document.querySelector<HTMLTableSectionElement>("#walletInventoryTableBody");
|
||||||
if (!body) {
|
if (!body) {
|
||||||
@@ -158,6 +224,8 @@ async function refreshWalletInventory(): Promise<void> {
|
|||||||
setText("#walletProfile", payload.profile);
|
setText("#walletProfile", payload.profile);
|
||||||
setText("#walletCluster", payload.cluster);
|
setText("#walletCluster", payload.cluster);
|
||||||
renderOnchainProfiles(payload.onchain_profiles, payload.profile);
|
renderOnchainProfiles(payload.onchain_profiles, payload.profile);
|
||||||
|
renderTransferFormats(payload.transfer_formats);
|
||||||
|
renderExportWalletAliases(payload.wallets);
|
||||||
setText("#walletCreateSecretStatus", payload.create_password_configured ? "configuré" : "absent");
|
setText("#walletCreateSecretStatus", payload.create_password_configured ? "configuré" : "absent");
|
||||||
setText("#walletSelectedAlias", payload.selected_alias ?? "—");
|
setText("#walletSelectedAlias", payload.selected_alias ?? "—");
|
||||||
setText("#walletInventoryCount", String(payload.wallets.length));
|
setText("#walletInventoryCount", String(payload.wallets.length));
|
||||||
@@ -169,6 +237,7 @@ async function refreshWalletInventory(): Promise<void> {
|
|||||||
setText("#walletInventoryStatus", `Erreur : ${message}`);
|
setText("#walletInventoryStatus", `Erreur : ${message}`);
|
||||||
setText("#walletInventoryCount", "0");
|
setText("#walletInventoryCount", "0");
|
||||||
renderWalletRows([]);
|
renderWalletRows([]);
|
||||||
|
renderExportWalletAliases([]);
|
||||||
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `Wallet inventory loading failed: ${message}`);
|
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `Wallet inventory loading failed: ${message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,6 +270,89 @@ async function createWallet(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function inspectTransferFile(): Promise<void> {
|
||||||
|
const sourcePath = inputValue("#walletImportSourcePathInput");
|
||||||
|
const format = selectedTransferFormat("#walletImportFormatSelect");
|
||||||
|
if (sourcePath.length === 0 || format.length === 0) {
|
||||||
|
setText("#walletTransferStatus", "Chemin source et format requis");
|
||||||
|
setTransferPublicKey("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const request: DemoWalletTransferInspectionRequest = { source_path: sourcePath, format };
|
||||||
|
setText("#walletTransferStatus", "Inspection...");
|
||||||
|
setTransferPublicKey("");
|
||||||
|
try {
|
||||||
|
const payload = await invoke<DemoWalletTransferInspectionPayload>("demo_wallet_inspect_transfer_file", { request });
|
||||||
|
setTransferPublicKey(payload.public_key);
|
||||||
|
setText("#walletTransferStatus", `Keypair valide — ${payload.format}`);
|
||||||
|
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", "external transfer keypair inspected");
|
||||||
|
} catch (caughtError) {
|
||||||
|
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||||
|
setText("#walletTransferStatus", `Erreur : ${message}`);
|
||||||
|
setTransferPublicKey("");
|
||||||
|
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `External transfer inspection failed: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importTransferFile(): Promise<void> {
|
||||||
|
const sourcePath = inputValue("#walletImportSourcePathInput");
|
||||||
|
const format = selectedTransferFormat("#walletImportFormatSelect");
|
||||||
|
const alias = inputValue("#walletImportAliasInput");
|
||||||
|
if (sourcePath.length === 0 || format.length === 0 || alias.length === 0) {
|
||||||
|
setText("#walletTransferStatus", "Chemin source, format et alias requis");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const request: DemoWalletImportRequest = { alias, source_path: sourcePath, format };
|
||||||
|
setText("#walletTransferStatus", "Import...");
|
||||||
|
try {
|
||||||
|
const payload = await invoke<DemoWalletIdentityPayload>("demo_wallet_import_file", { request });
|
||||||
|
setTransferPublicKey(payload.public_key);
|
||||||
|
setText("#walletTransferStatus", `Importé : ${payload.alias}.kswallet`);
|
||||||
|
const addressInput = document.querySelector<HTMLInputElement>("#walletAddressInput");
|
||||||
|
if (addressInput) {
|
||||||
|
addressInput.value = payload.public_key;
|
||||||
|
}
|
||||||
|
await refreshWalletInventory();
|
||||||
|
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", `external keypair imported: ${payload.alias}`);
|
||||||
|
} catch (caughtError) {
|
||||||
|
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||||
|
setText("#walletTransferStatus", `Erreur : ${message}`);
|
||||||
|
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `External keypair import failed: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportWallet(): Promise<void> {
|
||||||
|
const alias = document.querySelector<HTMLSelectElement>("#walletExportAliasSelect")?.value.trim() ?? "";
|
||||||
|
const format = selectedTransferFormat("#walletExportFormatSelect");
|
||||||
|
const destinationPath = inputValue("#walletExportDestinationPathInput");
|
||||||
|
if (alias.length === 0 || format.length === 0 || destinationPath.length === 0) {
|
||||||
|
setText("#walletExportStatus", "Wallet, format et destination requis");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const request: DemoWalletExportRequest = { alias, destination_path: destinationPath, format };
|
||||||
|
setText("#walletExportStatus", "Export...");
|
||||||
|
try {
|
||||||
|
await invoke<void>("demo_wallet_export_file", { request });
|
||||||
|
setText("#walletExportStatus", `Exporté : ${alias}`);
|
||||||
|
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", `native wallet exported: ${alias}`);
|
||||||
|
} catch (caughtError) {
|
||||||
|
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||||
|
setText("#walletExportStatus", `Erreur : ${message}`);
|
||||||
|
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `Native wallet export failed: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exploreInspectedTransfer(): void {
|
||||||
|
if (inspectedTransferPublicKey.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const addressInput = document.querySelector<HTMLInputElement>("#walletAddressInput");
|
||||||
|
if (addressInput) {
|
||||||
|
addressInput.value = inspectedTransferPublicKey;
|
||||||
|
}
|
||||||
|
document.querySelector<HTMLElement>("#walletAddressInput")?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
|
}
|
||||||
|
|
||||||
function parseTokenAccounts(value: unknown, program: string): ParsedTokenAccount[] {
|
function parseTokenAccounts(value: unknown, program: string): ParsedTokenAccount[] {
|
||||||
if (typeof value !== "object" || value === null || !("value" in value)) {
|
if (typeof value !== "object" || value === null || !("value" in value)) {
|
||||||
return [];
|
return [];
|
||||||
@@ -504,6 +656,26 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
void createWallet();
|
void createWallet();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
document.querySelector<HTMLButtonElement>("#inspectWalletTransferButton")?.addEventListener("click", () => {
|
||||||
|
void inspectTransferFile();
|
||||||
|
});
|
||||||
|
document.querySelector<HTMLButtonElement>("#importWalletTransferButton")?.addEventListener("click", () => {
|
||||||
|
void importTransferFile();
|
||||||
|
});
|
||||||
|
document.querySelector<HTMLButtonElement>("#exploreWalletTransferButton")?.addEventListener("click", () => {
|
||||||
|
exploreInspectedTransfer();
|
||||||
|
});
|
||||||
|
document.querySelector<HTMLButtonElement>("#exportWalletTransferButton")?.addEventListener("click", () => {
|
||||||
|
void exportWallet();
|
||||||
|
});
|
||||||
|
document.querySelector<HTMLInputElement>("#walletImportSourcePathInput")?.addEventListener("input", () => {
|
||||||
|
setTransferPublicKey("");
|
||||||
|
setText("#walletTransferStatus", "En attente");
|
||||||
|
});
|
||||||
|
document.querySelector<HTMLSelectElement>("#walletImportFormatSelect")?.addEventListener("change", () => {
|
||||||
|
setTransferPublicKey("");
|
||||||
|
setText("#walletTransferStatus", "En attente");
|
||||||
|
});
|
||||||
document.querySelector<HTMLSelectElement>("#walletOnchainProfileSelect")?.addEventListener("change", () => {
|
document.querySelector<HTMLSelectElement>("#walletOnchainProfileSelect")?.addEventListener("change", () => {
|
||||||
updateOnchainClusterLabel();
|
updateOnchainClusterLabel();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/demo_wallet.rs
|
// file: kb-app-demo-desktop/src/demo_wallet.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
//! UI-safe wallet management and public on-chain exploration.
|
//! UI-safe wallet management and public on-chain exploration.
|
||||||
|
|
||||||
@@ -21,6 +21,8 @@ pub(crate) struct DemoWalletInventoryPayload {
|
|||||||
pub(crate) cluster: std::string::String,
|
pub(crate) cluster: std::string::String,
|
||||||
/// Profiles available for explicit read-only on-chain exploration.
|
/// Profiles available for explicit read-only on-chain exploration.
|
||||||
pub(crate) onchain_profiles: std::vec::Vec<DemoWalletOnchainProfilePayload>,
|
pub(crate) onchain_profiles: std::vec::Vec<DemoWalletOnchainProfilePayload>,
|
||||||
|
/// 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.
|
/// Canonical classic SPL Token program identifier used by the explorer.
|
||||||
pub(crate) token_program_id: std::string::String,
|
pub(crate) token_program_id: std::string::String,
|
||||||
/// Canonical Token-2022 program identifier used by the explorer.
|
/// Canonical Token-2022 program identifier used by the explorer.
|
||||||
@@ -61,6 +63,77 @@ pub(crate) struct DemoWalletCreateRequest {
|
|||||||
pub(crate) alias: std::string::String,
|
pub(crate) alias: std::string::String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One transfer format selectable by the wallet demo.
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
|
#[ts(
|
||||||
|
export,
|
||||||
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletTransferFormatPayload.ts"
|
||||||
|
)]
|
||||||
|
pub(crate) struct DemoWalletTransferFormatPayload {
|
||||||
|
/// Stable machine-readable transfer format code.
|
||||||
|
pub(crate) code: std::string::String,
|
||||||
|
/// Human-readable transfer format label.
|
||||||
|
pub(crate) label: std::string::String,
|
||||||
|
/// Conventional extension suggested for explicit export paths.
|
||||||
|
pub(crate) default_extension: std::string::String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request to inspect one external keypair transfer file without importing it.
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||||
|
#[ts(
|
||||||
|
export,
|
||||||
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletTransferInspectionRequest.ts"
|
||||||
|
)]
|
||||||
|
pub(crate) struct DemoWalletTransferInspectionRequest {
|
||||||
|
/// Explicit path of the external transfer file.
|
||||||
|
pub(crate) source_path: std::string::String,
|
||||||
|
/// Stable transfer format code selected by the operator.
|
||||||
|
pub(crate) format: std::string::String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe public identity extracted from one external keypair transfer file.
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
|
#[ts(
|
||||||
|
export,
|
||||||
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletTransferInspectionPayload.ts"
|
||||||
|
)]
|
||||||
|
pub(crate) struct DemoWalletTransferInspectionPayload {
|
||||||
|
/// Public key derived from the validated secret keypair.
|
||||||
|
pub(crate) public_key: std::string::String,
|
||||||
|
/// Stable transfer format code used for validation.
|
||||||
|
pub(crate) format: std::string::String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request to import one external keypair into the active native wallet store.
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||||
|
#[ts(
|
||||||
|
export,
|
||||||
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletImportRequest.ts"
|
||||||
|
)]
|
||||||
|
pub(crate) struct DemoWalletImportRequest {
|
||||||
|
/// Alias assigned to the newly created native wallet.
|
||||||
|
pub(crate) alias: std::string::String,
|
||||||
|
/// Explicit path of the external source file.
|
||||||
|
pub(crate) source_path: std::string::String,
|
||||||
|
/// Stable transfer format code selected by the operator.
|
||||||
|
pub(crate) format: std::string::String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request to export one native wallet to an explicit external file.
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||||
|
#[ts(
|
||||||
|
export,
|
||||||
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletExportRequest.ts"
|
||||||
|
)]
|
||||||
|
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,
|
||||||
|
/// Stable transfer format code selected by the operator.
|
||||||
|
pub(crate) format: std::string::String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Exact SOL balance returned for one public wallet address.
|
/// Exact SOL balance returned for one public wallet address.
|
||||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
#[ts(
|
#[ts(
|
||||||
@@ -157,6 +230,7 @@ pub(crate) async fn demo_wallet_inventory(
|
|||||||
profile: profile.name.clone(),
|
profile: profile.name.clone(),
|
||||||
cluster: profile.wallet.cluster.clone(),
|
cluster: profile.wallet.cluster.clone(),
|
||||||
onchain_profiles: onchain_profile_payloads(state),
|
onchain_profiles: onchain_profile_payloads(state),
|
||||||
|
transfer_formats: wallet_transfer_format_payloads(),
|
||||||
token_program_id: ks_program_ids::SPL_TOKEN_PROGRAM_ID.to_string(),
|
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(),
|
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: std::env::var_os(DEMO_WALLET_PASSWORD_ENV).is_some(),
|
||||||
@@ -174,17 +248,9 @@ pub(crate) async fn demo_wallet_create_native(
|
|||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||||
};
|
};
|
||||||
let password_text = match std::env::var(DEMO_WALLET_PASSWORD_ENV) {
|
let password = match demo_wallet_password() {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(_) => {
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
return std::result::Result::Err(format!(
|
|
||||||
"backend secret {DEMO_WALLET_PASSWORD_ENV} is not configured as UTF-8"
|
|
||||||
));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let password = match ks_wallet::WalletPassword::new(password_text) {
|
|
||||||
std::result::Result::Ok(value) => value,
|
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
|
||||||
};
|
};
|
||||||
let profile = state.active_profile();
|
let profile = state.active_profile();
|
||||||
let manager = match wallet_manager_for_profile(profile) {
|
let manager = match wallet_manager_for_profile(profile) {
|
||||||
@@ -207,6 +273,100 @@ pub(crate) async fn demo_wallet_create_native(
|
|||||||
return std::result::Result::Ok(payload);
|
return std::result::Result::Ok(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inspects one external transfer file and returns only the derived public identity.
|
||||||
|
pub(crate) async fn demo_wallet_inspect_transfer_file(
|
||||||
|
request: crate::DemoWalletTransferInspectionRequest,
|
||||||
|
) -> std::result::Result<DemoWalletTransferInspectionPayload, std::string::String> {
|
||||||
|
let format = match wallet_transfer_format_from_code(request.format.as_str()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let inspection = match ks_wallet::inspect_transfer_file(
|
||||||
|
std::path::PathBuf::from(request.source_path),
|
||||||
|
format,
|
||||||
|
)
|
||||||
|
.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(DemoWalletTransferInspectionPayload {
|
||||||
|
public_key: inspection.public_key().to_string(),
|
||||||
|
format: inspection.format().code().to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Imports one external keypair into a new password-protected native wallet.
|
||||||
|
pub(crate) async fn demo_wallet_import_file(
|
||||||
|
state: &crate::AppState,
|
||||||
|
request: crate::DemoWalletImportRequest,
|
||||||
|
) -> std::result::Result<DemoWalletIdentityPayload, std::string::String> {
|
||||||
|
let alias = match ks_wallet::WalletAlias::parse(request.alias.trim().to_string()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||||
|
};
|
||||||
|
let format = match wallet_transfer_format_from_code(request.format.as_str()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let password = match demo_wallet_password() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let profile = state.active_profile();
|
||||||
|
let manager = match wallet_manager_for_profile(profile) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let wallet = match manager
|
||||||
|
.import_file(alias, password, std::path::PathBuf::from(request.source_path), format)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||||
|
};
|
||||||
|
let identity = wallet.identity();
|
||||||
|
let payload = DemoWalletIdentityPayload {
|
||||||
|
selected: profile.wallet.wallet_alias.as_deref()
|
||||||
|
== std::option::Option::Some(identity.alias.as_str()),
|
||||||
|
alias: identity.alias.as_str().to_string(),
|
||||||
|
public_key: identity.public_key,
|
||||||
|
format_version: 1,
|
||||||
|
};
|
||||||
|
wallet.lock();
|
||||||
|
return std::result::Result::Ok(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exports one native wallet after backend-only password authentication.
|
||||||
|
pub(crate) async fn demo_wallet_export_file(
|
||||||
|
state: &crate::AppState,
|
||||||
|
request: crate::DemoWalletExportRequest,
|
||||||
|
) -> std::result::Result<(), std::string::String> {
|
||||||
|
let alias = match ks_wallet::WalletAlias::parse(request.alias.trim().to_string()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||||
|
};
|
||||||
|
let format = match wallet_transfer_format_from_code(request.format.as_str()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let password = match 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(state.active_profile()) {
|
||||||
|
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
|
||||||
|
{
|
||||||
|
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Inspects one explicitly selected native wallet file and returns only its safe identity.
|
/// Inspects one explicitly selected native wallet file and returns only its safe identity.
|
||||||
pub(crate) async fn demo_wallet_inspect_file(
|
pub(crate) async fn demo_wallet_inspect_file(
|
||||||
state: &crate::AppState,
|
state: &crate::AppState,
|
||||||
@@ -329,6 +489,45 @@ 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() {
|
||||||
|
formats.push(crate::DemoWalletTransferFormatPayload {
|
||||||
|
code: format.code().to_string(),
|
||||||
|
label: format.label().to_string(),
|
||||||
|
default_extension: format.default_extension().to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return formats;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wallet_transfer_format_from_code(
|
||||||
|
code: &str,
|
||||||
|
) -> std::result::Result<ks_wallet::WalletTransferFormat, std::string::String> {
|
||||||
|
let code = code.trim();
|
||||||
|
for format in ks_wallet::WalletTransferFormat::supported() {
|
||||||
|
if format.code() == code {
|
||||||
|
return std::result::Result::Ok(format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(format!("wallet transfer format '{code}' is not supported"));
|
||||||
|
}
|
||||||
|
|
||||||
fn onchain_profile<'a>(
|
fn onchain_profile<'a>(
|
||||||
state: &'a crate::AppState,
|
state: &'a crate::AppState,
|
||||||
profile_name: &str,
|
profile_name: &str,
|
||||||
@@ -470,6 +669,15 @@ mod tests {
|
|||||||
"walletTokensTableBody",
|
"walletTokensTableBody",
|
||||||
"walletTransactionsTableBody",
|
"walletTransactionsTableBody",
|
||||||
"walletTransactionJson",
|
"walletTransactionJson",
|
||||||
|
"walletImportSourcePathInput",
|
||||||
|
"walletImportFormatSelect",
|
||||||
|
"walletImportAliasInput",
|
||||||
|
"inspectWalletTransferButton",
|
||||||
|
"importWalletTransferButton",
|
||||||
|
"walletExportAliasSelect",
|
||||||
|
"walletExportFormatSelect",
|
||||||
|
"walletExportDestinationPathInput",
|
||||||
|
"exportWalletTransferButton",
|
||||||
"walletExternalPathInput",
|
"walletExternalPathInput",
|
||||||
"inspectWalletFileButton",
|
"inspectWalletFileButton",
|
||||||
"walletInspectionJson",
|
"walletInspectionJson",
|
||||||
@@ -477,6 +685,9 @@ mod tests {
|
|||||||
assert!(html.contains(required));
|
assert!(html.contains(required));
|
||||||
}
|
}
|
||||||
assert!(script.contains("demo_wallet_create_native"));
|
assert!(script.contains("demo_wallet_create_native"));
|
||||||
|
assert!(script.contains("demo_wallet_inspect_transfer_file"));
|
||||||
|
assert!(script.contains("demo_wallet_import_file"));
|
||||||
|
assert!(script.contains("demo_wallet_export_file"));
|
||||||
assert!(script.contains("demo_wallet_balance"));
|
assert!(script.contains("demo_wallet_balance"));
|
||||||
assert!(script.contains("demo_wallet_inventory"));
|
assert!(script.contains("demo_wallet_inventory"));
|
||||||
assert!(script.contains("demo_wallet_rpc_execute"));
|
assert!(script.contains("demo_wallet_rpc_execute"));
|
||||||
@@ -490,6 +701,9 @@ mod tests {
|
|||||||
assert!(main_script.contains("open_demo_wallet_window"));
|
assert!(main_script.contains("open_demo_wallet_window"));
|
||||||
assert!(tauri_runtime.contains("open_demo_wallet_window"));
|
assert!(tauri_runtime.contains("open_demo_wallet_window"));
|
||||||
assert!(tauri_runtime.contains("demo_wallet_create_native"));
|
assert!(tauri_runtime.contains("demo_wallet_create_native"));
|
||||||
|
assert!(tauri_runtime.contains("demo_wallet_inspect_transfer_file"));
|
||||||
|
assert!(tauri_runtime.contains("demo_wallet_import_file"));
|
||||||
|
assert!(tauri_runtime.contains("demo_wallet_export_file"));
|
||||||
assert!(tauri_runtime.contains("demo_wallet_balance"));
|
assert!(tauri_runtime.contains("demo_wallet_balance"));
|
||||||
assert!(tauri_runtime.contains("demo_wallet_rpc_execute"));
|
assert!(tauri_runtime.contains("demo_wallet_rpc_execute"));
|
||||||
assert!(tauri_runtime.contains("demo_wallet.html"));
|
assert!(tauri_runtime.contains("demo_wallet.html"));
|
||||||
@@ -507,6 +721,7 @@ mod tests {
|
|||||||
cluster: "devnet".to_string(),
|
cluster: "devnet".to_string(),
|
||||||
active: false,
|
active: false,
|
||||||
}],
|
}],
|
||||||
|
transfer_formats: super::wallet_transfer_format_payloads(),
|
||||||
token_program_id: ks_program_ids::SPL_TOKEN_PROGRAM_ID.to_string(),
|
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(),
|
token_2022_program_id: ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string(),
|
||||||
create_password_configured: true,
|
create_password_configured: true,
|
||||||
@@ -549,6 +764,17 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wallet_transfer_format_inventory_matches_reusable_wallet_boundary() {
|
||||||
|
let formats = super::wallet_transfer_format_payloads();
|
||||||
|
assert_eq!(formats.len(), 2);
|
||||||
|
assert_eq!(formats[0].code, "solana_cli_json");
|
||||||
|
assert_eq!(formats[1].code, "solana_private_key_base58");
|
||||||
|
assert!(super::wallet_transfer_format_from_code("solana_cli_json").is_ok());
|
||||||
|
assert!(super::wallet_transfer_format_from_code("solana_private_key_base58").is_ok());
|
||||||
|
assert!(super::wallet_transfer_format_from_code("unknown").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lamport_formatting_is_exact_without_float_conversion() {
|
fn lamport_formatting_is_exact_without_float_conversion() {
|
||||||
assert_eq!(super::format_lamports_as_sol(0), "0");
|
assert_eq!(super::format_lamports_as_sol(0), "0");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/lib.rs
|
// file: kb-app-demo-desktop/src/lib.rs
|
||||||
// version: 40
|
// version: 41
|
||||||
|
|
||||||
//! Tauri desktop demo application for `khadhroony-bot3`.
|
//! Tauri desktop demo application for `khadhroony-bot3`.
|
||||||
|
|
||||||
@@ -367,8 +367,12 @@ pub(crate) use self::demo_transport::DemoEndpointRolePayload;
|
|||||||
pub(crate) use self::demo_wallet::DemoWalletBalancePayload;
|
pub(crate) use self::demo_wallet::DemoWalletBalancePayload;
|
||||||
/// Request to create one native wallet from the backend-only demo secret.
|
/// Request to create one native wallet from the backend-only demo secret.
|
||||||
pub(crate) use self::demo_wallet::DemoWalletCreateRequest;
|
pub(crate) use self::demo_wallet::DemoWalletCreateRequest;
|
||||||
|
/// 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.
|
/// UI-safe native wallet identity exposed by the desktop adapter.
|
||||||
pub(crate) use self::demo_wallet::DemoWalletIdentityPayload;
|
pub(crate) use self::demo_wallet::DemoWalletIdentityPayload;
|
||||||
|
/// Request to import one external keypair into the native wallet store.
|
||||||
|
pub(crate) use self::demo_wallet::DemoWalletImportRequest;
|
||||||
/// UI-safe native wallet inventory exposed by the desktop adapter.
|
/// UI-safe native wallet inventory exposed by the desktop adapter.
|
||||||
pub(crate) use self::demo_wallet::DemoWalletInventoryPayload;
|
pub(crate) use self::demo_wallet::DemoWalletInventoryPayload;
|
||||||
/// One selectable profile for read-only wallet on-chain exploration.
|
/// One selectable profile for read-only wallet on-chain exploration.
|
||||||
@@ -377,12 +381,24 @@ pub(crate) use self::demo_wallet::DemoWalletOnchainProfilePayload;
|
|||||||
pub(crate) use self::demo_wallet::DemoWalletRpcExecutionPayload;
|
pub(crate) use self::demo_wallet::DemoWalletRpcExecutionPayload;
|
||||||
/// Request for one wallet-scoped public JSON-RPC read.
|
/// Request for one wallet-scoped public JSON-RPC read.
|
||||||
pub(crate) use self::demo_wallet::DemoWalletRpcRequest;
|
pub(crate) use self::demo_wallet::DemoWalletRpcRequest;
|
||||||
|
/// One secret transfer format selectable by the wallet demo.
|
||||||
|
pub(crate) use self::demo_wallet::DemoWalletTransferFormatPayload;
|
||||||
|
/// Safe public identity extracted from one external keypair transfer file.
|
||||||
|
pub(crate) use self::demo_wallet::DemoWalletTransferInspectionPayload;
|
||||||
|
/// Request to inspect one external keypair transfer file without importing it.
|
||||||
|
pub(crate) use self::demo_wallet::DemoWalletTransferInspectionRequest;
|
||||||
/// Loads the exact SOL balance of one public wallet address.
|
/// Loads the exact SOL balance of one public wallet address.
|
||||||
pub(crate) use self::demo_wallet::demo_wallet_balance;
|
pub(crate) use self::demo_wallet::demo_wallet_balance;
|
||||||
/// Creates one password-protected native wallet without exposing the password to Tauri IPC.
|
/// Creates one password-protected native wallet without exposing the password to Tauri IPC.
|
||||||
pub(crate) use self::demo_wallet::demo_wallet_create_native;
|
pub(crate) use self::demo_wallet::demo_wallet_create_native;
|
||||||
|
/// Exports one native wallet through an explicitly selected transfer format.
|
||||||
|
pub(crate) use self::demo_wallet::demo_wallet_export_file;
|
||||||
|
/// Imports one external keypair into a new native wallet.
|
||||||
|
pub(crate) use self::demo_wallet::demo_wallet_import_file;
|
||||||
/// Inspects an explicitly selected native wallet file without exposing its path in the result.
|
/// Inspects an explicitly selected native wallet file without exposing its path in the result.
|
||||||
pub(crate) use self::demo_wallet::demo_wallet_inspect_file;
|
pub(crate) use self::demo_wallet::demo_wallet_inspect_file;
|
||||||
|
/// Inspects one external transfer file and returns only its derived public identity.
|
||||||
|
pub(crate) use self::demo_wallet::demo_wallet_inspect_transfer_file;
|
||||||
/// Returns native wallet identities without paths, passwords or protected payloads.
|
/// Returns native wallet identities without paths, passwords or protected payloads.
|
||||||
pub(crate) use self::demo_wallet::demo_wallet_inventory;
|
pub(crate) use self::demo_wallet::demo_wallet_inventory;
|
||||||
/// Executes one bounded read-only wallet explorer RPC through an explicitly selected profile.
|
/// Executes one bounded read-only wallet explorer RPC through an explicitly selected profile.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/tauri.rs
|
// file: kb-app-demo-desktop/src/tauri.rs
|
||||||
// version: 42
|
// version: 43
|
||||||
|
|
||||||
//! Tauri runtime assembly and private command wrappers.
|
//! Tauri runtime assembly and private command wrappers.
|
||||||
|
|
||||||
@@ -39,6 +39,9 @@ pub fn run() -> ks_core::Result<()> {
|
|||||||
open_demo_wallet_window,
|
open_demo_wallet_window,
|
||||||
demo_wallet_inventory,
|
demo_wallet_inventory,
|
||||||
demo_wallet_create_native,
|
demo_wallet_create_native,
|
||||||
|
demo_wallet_inspect_transfer_file,
|
||||||
|
demo_wallet_import_file,
|
||||||
|
demo_wallet_export_file,
|
||||||
demo_wallet_balance,
|
demo_wallet_balance,
|
||||||
demo_wallet_inspect_file,
|
demo_wallet_inspect_file,
|
||||||
demo_wallet_rpc_execute,
|
demo_wallet_rpc_execute,
|
||||||
@@ -263,6 +266,29 @@ async fn demo_wallet_create_native(
|
|||||||
return crate::demo_wallet_create_native(state.inner(), request).await;
|
return crate::demo_wallet_create_native(state.inner(), request).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn demo_wallet_inspect_transfer_file(
|
||||||
|
request: crate::DemoWalletTransferInspectionRequest,
|
||||||
|
) -> std::result::Result<crate::DemoWalletTransferInspectionPayload, std::string::String> {
|
||||||
|
return crate::demo_wallet_inspect_transfer_file(request).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn demo_wallet_import_file(
|
||||||
|
state: tauri::State<'_, crate::AppState>,
|
||||||
|
request: crate::DemoWalletImportRequest,
|
||||||
|
) -> std::result::Result<crate::DemoWalletIdentityPayload, std::string::String> {
|
||||||
|
return crate::demo_wallet_import_file(state.inner(), request).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn demo_wallet_export_file(
|
||||||
|
state: tauri::State<'_, crate::AppState>,
|
||||||
|
request: crate::DemoWalletExportRequest,
|
||||||
|
) -> std::result::Result<(), std::string::String> {
|
||||||
|
return crate::demo_wallet_export_file(state.inner(), request).await;
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn demo_wallet_balance(
|
async fn demo_wallet_balance(
|
||||||
state: tauri::State<'_, crate::AppState>,
|
state: tauri::State<'_, crate::AppState>,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<!-- file: ks-pipeline-demo-scenarios/TODO.md -->
|
<!-- file: ks-pipeline-demo-scenarios/TODO.md -->
|
||||||
<!-- version: 43 -->
|
<!-- version: 44 -->
|
||||||
|
|
||||||
# TODO — ks-pipeline-demo-scenarios
|
# TODO — ks-pipeline-demo-scenarios
|
||||||
|
|
||||||
## Évolutions générales
|
## Évolutions générales
|
||||||
|
|
||||||
|
- [ ] `0.5.2` — refactorer les runners Devnet qui appellent encore directement `load_profile_temporary_wallet()` afin qu'ils puissent recevoir une capacité signer issue d'un `.kswallet` explicitement déverrouillé, sans fallback silencieux.
|
||||||
- [ ] `0.5.4` — centraliser les dispatchs, compositions decoder/materializer et critères de complétion encore réutilisables depuis le desktop.
|
- [ ] `0.5.4` — centraliser les dispatchs, compositions decoder/materializer et critères de complétion encore réutilisables depuis le desktop.
|
||||||
- [ ] `0.5.4` — produire la matrice active decoder/materializer/executor/scénario/preuve sans transformer les surfaces réservées en dettes implicites.
|
- [ ] `0.5.4` — produire la matrice active decoder/materializer/executor/scénario/preuve sans transformer les surfaces réservées en dettes implicites.
|
||||||
- [ ] Ajouter au CLI uniquement les préparations de fixtures explicitement justifiées.
|
- [ ] Ajouter au CLI uniquement les préparations de fixtures explicitement justifiées.
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
<!-- file: ks-wallet/CHANGELOG.md -->
|
<!-- file: ks-wallet/CHANGELOG.md -->
|
||||||
<!-- version: 19 -->
|
<!-- version: 20 -->
|
||||||
|
|
||||||
# CHANGELOG — ks-wallet
|
# CHANGELOG — ks-wallet
|
||||||
|
|
||||||
|
## `0.5.2-pre.006-delta-fix-004`
|
||||||
|
|
||||||
|
- ajoute `WalletTransferFormat::supported()`, `code()`, `label()` et `default_extension()` afin que les consommateurs puissent présenter exactement les formats réellement compilés ;
|
||||||
|
- ajoute `inspect_transfer_file()` et `WalletTransferInspection` pour valider un fichier secret externe et n'en exposer que la pubkey dérivée et le format sélectionné, sans import ni conversion ;
|
||||||
|
- conserve les codecs secrets dans `ks-wallet` et permet au desktop de diagnostiquer les anciens keypairs de `wallets/temporary/**` un par un avant toute migration ;
|
||||||
|
- ajoute un test externe couvrant l'inspection publique JSON/Base58 et l'inventaire des formats supportés.
|
||||||
|
|
||||||
## `0.5.2-pre.006`
|
## `0.5.2-pre.006`
|
||||||
|
|
||||||
- clôt les TODO de sélection non sensible par alias et d'adaptation des consommateurs : `ks-config` ne porte que `wallet_alias`, les scénarios exigent un unlock explicite pour une sélection persistante et le desktop ne projette que des DTO sûrs ;
|
- clôt les TODO de sélection non sensible par alias et d'adaptation des consommateurs : `ks-config` ne porte que `wallet_alias`, les scénarios exigent un unlock explicite pour une sélection persistante et le desktop ne projette que des DTO sûrs ;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ks-wallet/README.md -->
|
<!-- file: ks-wallet/README.md -->
|
||||||
<!-- version: 11 -->
|
<!-- version: 12 -->
|
||||||
|
|
||||||
# ks-wallet
|
# ks-wallet
|
||||||
|
|
||||||
@@ -21,6 +21,8 @@ La crate fournit actuellement :
|
|||||||
- ouverture d'un fichier explicitement sélectionné avec `WalletManager::unlock_file()` sans l'enregistrer dans le store ;
|
- ouverture d'un fichier explicitement sélectionné avec `WalletManager::unlock_file()` sans l'enregistrer dans le store ;
|
||||||
- changement du mot de passe avec `WalletManager::change_password()` en conservant exactement la même keypair/pubkey ;
|
- changement du mot de passe avec `WalletManager::change_password()` en conservant exactement la même keypair/pubkey ;
|
||||||
- migration non destructive de `<alias>.json` vers `<alias>.kswallet` avec `WalletManager::migrate_legacy()` ;
|
- migration non destructive de `<alias>.json` vers `<alias>.kswallet` avec `WalletManager::migrate_legacy()` ;
|
||||||
|
- inspection sûre d'un fichier de transfert via `inspect_transfer_file()`, qui valide le keypair sans l'importer et ne retourne que sa pubkey publique ;
|
||||||
|
- inventaire stable des formats via `WalletTransferFormat::supported()` avec code, libellé et extension conventionnelle ;
|
||||||
- import de fichiers secrets avec `WalletManager::import_file()` en `SolanaCliJson` ou `SolanaPrivateKeyBase58` ;
|
- import de fichiers secrets avec `WalletManager::import_file()` en `SolanaCliJson` ou `SolanaPrivateKeyBase58` ;
|
||||||
- export de fichiers secrets avec `WalletManager::export_file()`, uniquement après authentification du mot de passe du `.kswallet` ;
|
- export de fichiers secrets avec `WalletManager::export_file()`, uniquement après authentification du mot de passe du `.kswallet` ;
|
||||||
- refus des collisions d’alias et de pubkey lors d’un import ;
|
- refus des collisions d’alias et de pubkey lors d’un import ;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ks-wallet/TODO.md -->
|
<!-- file: ks-wallet/TODO.md -->
|
||||||
<!-- version: 14 -->
|
<!-- version: 15 -->
|
||||||
|
|
||||||
# TODO — ks-wallet
|
# TODO — ks-wallet
|
||||||
|
|
||||||
@@ -35,7 +35,8 @@
|
|||||||
|
|
||||||
- [ ] inventorier et classifier non destructivement les fichiers keypair JSON sous `wallets/temporary/**` avant toute conversion de masse : distinguer wallets/signers, autorités, mints, recipients et autres keypairs de fixtures ;
|
- [ ] inventorier et classifier non destructivement les fichiers keypair JSON sous `wallets/temporary/**` avant toute conversion de masse : distinguer wallets/signers, autorités, mints, recipients et autres keypairs de fixtures ;
|
||||||
- [ ] proposer une migration sélective des seuls signers devant devenir persistants vers `.kswallet`, en préservant les fichiers JSON sources et les pubkeys ;
|
- [ ] proposer une migration sélective des seuls signers devant devenir persistants vers `.kswallet`, en préservant les fichiers JSON sources et les pubkeys ;
|
||||||
- [ ] permettre aux outils de diagnostic d’extraire la pubkey publique des keypairs legacy compatibles pour explorer leur historique on-chain sans exiger leur conversion préalable en `.kswallet`.
|
- [x] permettre l'inspection explicite d'un fichier keypair legacy compatible et l'extraction de sa pubkey publique sans conversion préalable en `.kswallet`.
|
||||||
|
- [ ] ajouter un scanner borné de répertoire pour inventorier plusieurs candidats legacy avant migration, sans conversion automatique ni classification métier inventée à partir du secret seul.
|
||||||
|
|
||||||
## Version ultérieure non déterminée — adaptateurs de transfert
|
## Version ultérieure non déterminée — adaptateurs de transfert
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ks-wallet/USAGE.md -->
|
<!-- file: ks-wallet/USAGE.md -->
|
||||||
<!-- version: 11 -->
|
<!-- version: 12 -->
|
||||||
|
|
||||||
# Utilisation de ks-wallet
|
# Utilisation de ks-wallet
|
||||||
|
|
||||||
@@ -317,6 +317,27 @@ println!("pubkey={}", handle.public_key());
|
|||||||
|
|
||||||
La matrice des formats vérifiés et reportés est maintenue dans [`../docs/WALLET_FORMAT_COMPATIBILITY.md`](../docs/WALLET_FORMAT_COMPATIBILITY.md).
|
La matrice des formats vérifiés et reportés est maintenue dans [`../docs/WALLET_FORMAT_COMPATIBILITY.md`](../docs/WALLET_FORMAT_COMPATIBILITY.md).
|
||||||
|
|
||||||
|
### Inspecter un keypair externe sans l'importer
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let inspection = match ks_wallet::inspect_transfer_file(
|
||||||
|
source_path,
|
||||||
|
ks_wallet::WalletTransferFormat::SolanaCliJson,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
println!("pubkey={}", inspection.public_key());
|
||||||
|
```
|
||||||
|
|
||||||
|
`inspect_transfer_file()` applique les mêmes validations bornées que l'import mais ne crée aucun `.kswallet`. Le résultat ne contient que la pubkey dérivée et le format explicitement sélectionné. `WalletTransferFormat::supported()` fournit la liste compilée des formats afin qu'un consommateur n'ait pas à maintenir une seconde matrice.
|
||||||
|
|
||||||
|
Cette inspection confirme uniquement qu'un fichier contient un keypair Solana valide dans le format choisi. Le wire secret ne permet pas de déduire de manière fiable le rôle historique de la clé (`wallet`, `mint`, `authority`, `recipient`, etc.).
|
||||||
|
|
||||||
### Migrer un legacy `<alias>.json`
|
### Migrer un legacy `<alias>.json`
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-wallet/src/lib.rs
|
// file: ks-wallet/src/lib.rs
|
||||||
// version: 8
|
// version: 9
|
||||||
|
|
||||||
//! Wallet boundary for local key storage and transaction signing.
|
//! Wallet boundary for local key storage and transaction signing.
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
@@ -24,6 +24,10 @@ pub use self::manager::WalletManager;
|
|||||||
pub use self::password::WalletPassword;
|
pub use self::password::WalletPassword;
|
||||||
/// Explicit secret import/export format supported by the wallet boundary.
|
/// Explicit secret import/export format supported by the wallet boundary.
|
||||||
pub use self::transfer::WalletTransferFormat;
|
pub use self::transfer::WalletTransferFormat;
|
||||||
|
/// Safe public identity extracted from one validated transfer file.
|
||||||
|
pub use self::transfer::WalletTransferInspection;
|
||||||
|
/// Validates one external transfer file and returns only its public identity.
|
||||||
|
pub use self::transfer::inspect_transfer_file;
|
||||||
/// Authenticated signing capability for one unlocked persistent wallet.
|
/// Authenticated signing capability for one unlocked persistent wallet.
|
||||||
pub use self::unlocked::UnlockedWallet;
|
pub use self::unlocked::UnlockedWallet;
|
||||||
/// Solana keypair kept private inside the wallet boundary.
|
/// Solana keypair kept private inside the wallet boundary.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-wallet/src/transfer.rs
|
// file: ks-wallet/src/transfer.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
//! Explicit migration and secret import/export adapters.
|
//! Explicit migration and secret import/export adapters.
|
||||||
|
|
||||||
@@ -23,6 +23,74 @@ pub enum WalletTransferFormat {
|
|||||||
SolanaPrivateKeyBase58,
|
SolanaPrivateKeyBase58,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WalletTransferFormat {
|
||||||
|
/// Returns every transfer format currently supported for both import and export.
|
||||||
|
pub const fn supported() -> [Self; 2] {
|
||||||
|
return [Self::SolanaCliJson, Self::SolanaPrivateKeyBase58];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the stable machine-readable code of this transfer format.
|
||||||
|
pub const fn code(self) -> &'static str {
|
||||||
|
return match self {
|
||||||
|
Self::SolanaCliJson => "solana_cli_json",
|
||||||
|
Self::SolanaPrivateKeyBase58 => "solana_private_key_base58",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the human-readable label of this transfer format.
|
||||||
|
pub const fn label(self) -> &'static str {
|
||||||
|
return match self {
|
||||||
|
Self::SolanaCliJson => "Solana CLI keypair JSON (64 bytes)",
|
||||||
|
Self::SolanaPrivateKeyBase58 => "Solana private key Base58 (64 bytes)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the conventional file extension used by this transfer format.
|
||||||
|
pub const fn default_extension(self) -> &'static str {
|
||||||
|
return match self {
|
||||||
|
Self::SolanaCliJson => "json",
|
||||||
|
Self::SolanaPrivateKeyBase58 => "txt",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe result of validating one external secret-transfer file.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct WalletTransferInspection {
|
||||||
|
public_key: std::string::String,
|
||||||
|
format: WalletTransferFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalletTransferInspection {
|
||||||
|
/// Returns the public key derived from the validated external keypair.
|
||||||
|
pub fn public_key(&self) -> &str {
|
||||||
|
return self.public_key.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the transfer format used to validate the external keypair.
|
||||||
|
pub fn format(&self) -> WalletTransferFormat {
|
||||||
|
return self.format;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates one external transfer file and returns only its public identity.
|
||||||
|
///
|
||||||
|
/// The secret bytes never leave `ks-wallet`; callers receive only the derived
|
||||||
|
/// Solana public key and the format that was explicitly selected for validation.
|
||||||
|
pub async fn inspect_transfer_file(
|
||||||
|
source_path: impl std::convert::AsRef<std::path::Path>,
|
||||||
|
format: WalletTransferFormat,
|
||||||
|
) -> ks_core::Result<WalletTransferInspection> {
|
||||||
|
let keypair = match read_transfer_keypair(source_path.as_ref(), format).await {
|
||||||
|
std::result::Result::Ok(keypair) => keypair,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(WalletTransferInspection {
|
||||||
|
public_key: keypair.pubkey().to_string(),
|
||||||
|
format,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
impl crate::WalletManager {
|
impl crate::WalletManager {
|
||||||
/// Imports one secret file into a new password-protected native wallet.
|
/// Imports one secret file into a new password-protected native wallet.
|
||||||
///
|
///
|
||||||
@@ -136,7 +204,7 @@ impl crate::WalletManager {
|
|||||||
action = "export_wallet_secret",
|
action = "export_wallet_secret",
|
||||||
wallet_alias = alias.as_str(),
|
wallet_alias = alias.as_str(),
|
||||||
public_key = public_key.as_str(),
|
public_key = public_key.as_str(),
|
||||||
transfer_format = transfer_format_code(format),
|
transfer_format = format.code(),
|
||||||
"exported wallet secret after password authentication"
|
"exported wallet secret after password authentication"
|
||||||
);
|
);
|
||||||
return std::result::Result::Ok(());
|
return std::result::Result::Ok(());
|
||||||
@@ -189,7 +257,7 @@ async fn persist_imported_keypair(
|
|||||||
action = "import_wallet_secret",
|
action = "import_wallet_secret",
|
||||||
wallet_alias = alias.as_str(),
|
wallet_alias = alias.as_str(),
|
||||||
public_key = %public_key,
|
public_key = %public_key,
|
||||||
transfer_format = transfer_format_code(format),
|
transfer_format = format.code(),
|
||||||
"imported wallet secret into native protected storage"
|
"imported wallet secret into native protected storage"
|
||||||
);
|
);
|
||||||
return std::result::Result::Ok(crate::UnlockedWallet::new(alias, keypair));
|
return std::result::Result::Ok(crate::UnlockedWallet::new(alias, keypair));
|
||||||
@@ -656,10 +724,3 @@ fn native_path(manager: &crate::WalletManager, alias: &crate::WalletAlias) -> st
|
|||||||
crate::KSWALLET_FILE_EXTENSION
|
crate::KSWALLET_FILE_EXTENSION
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn transfer_format_code(format: crate::WalletTransferFormat) -> &'static str {
|
|
||||||
return match format {
|
|
||||||
crate::WalletTransferFormat::SolanaCliJson => "solana_cli_json",
|
|
||||||
crate::WalletTransferFormat::SolanaPrivateKeyBase58 => "solana_private_key_base58",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-wallet/tests/transfer.rs
|
// file: ks-wallet/tests/transfer.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
//! External migration and secret import/export contract tests.
|
//! External migration and secret import/export contract tests.
|
||||||
|
|
||||||
@@ -277,3 +277,42 @@ async fn import_rejects_invalid_sources_without_creating_native_destination() {
|
|||||||
assert_eq!(base58_error.code(), "wallet_import_base58_invalid");
|
assert_eq!(base58_error.code(), "wallet_import_base58_invalid");
|
||||||
assert!(!directory.path().join("invalid-base58.kswallet").exists());
|
assert!(!directory.path().join("invalid-base58.kswallet").exists());
|
||||||
}
|
}
|
||||||
|
#[tokio::test]
|
||||||
|
async fn transfer_inspection_exposes_only_public_identity_for_supported_formats() {
|
||||||
|
let directory = tempfile::tempdir()
|
||||||
|
.unwrap_or_else(|error| panic!("temporary directory must exist: {error}"));
|
||||||
|
make_directory_private(directory.path());
|
||||||
|
let keypair = solana_keypair::Keypair::new();
|
||||||
|
let expected_public_key = keypair.pubkey().to_string();
|
||||||
|
let mut keypair_bytes = keypair.to_bytes();
|
||||||
|
let mut json = serde_json::to_vec(keypair_bytes.as_slice())
|
||||||
|
.unwrap_or_else(|error| panic!("Solana JSON fixture must encode: {error}"));
|
||||||
|
let json_path = directory.path().join("inspect.json");
|
||||||
|
write_private_file(&json_path, json.as_slice());
|
||||||
|
let json_inspection = ks_wallet::inspect_transfer_file(
|
||||||
|
&json_path,
|
||||||
|
ks_wallet::WalletTransferFormat::SolanaCliJson,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|error| panic!("Solana JSON inspection must succeed: {error}"));
|
||||||
|
assert_eq!(json_inspection.public_key(), expected_public_key);
|
||||||
|
assert_eq!(json_inspection.format().code(), "solana_cli_json");
|
||||||
|
json.zeroize();
|
||||||
|
let mut base58 = bs58::encode(keypair_bytes.as_slice()).into_string();
|
||||||
|
let base58_path = directory.path().join("inspect.txt");
|
||||||
|
write_private_file(&base58_path, base58.as_bytes());
|
||||||
|
let base58_inspection = ks_wallet::inspect_transfer_file(
|
||||||
|
&base58_path,
|
||||||
|
ks_wallet::WalletTransferFormat::SolanaPrivateKeyBase58,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|error| panic!("Base58 inspection must succeed: {error}"));
|
||||||
|
assert_eq!(base58_inspection.public_key(), expected_public_key);
|
||||||
|
assert_eq!(base58_inspection.format().code(), "solana_private_key_base58");
|
||||||
|
let formats = ks_wallet::WalletTransferFormat::supported();
|
||||||
|
assert_eq!(formats.len(), 2);
|
||||||
|
assert_eq!(formats[0].default_extension(), "json");
|
||||||
|
assert_eq!(formats[1].default_extension(), "txt");
|
||||||
|
base58.zeroize();
|
||||||
|
keypair_bytes.zeroize();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user