v0.1.0-pre.053

This commit is contained in:
2026-07-25 23:57:48 +02:00
parent 930056d422
commit 4e76263f88
23 changed files with 4575 additions and 82 deletions

View File

@@ -0,0 +1,73 @@
<!-- file: kb-app-demo-desktop/frontend/demo_execution_solana_core.html -->
<!-- version: 6 -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Exécution Solana Core Devnet</title>
<link href="sass/main.scss" rel="stylesheet"/>
</head>
<body class="app-body">
<header class="app-header bg-dark text-light">
<nav class="container-fluid h-100 d-flex align-items-center justify-content-between px-4">
<div>
<h1 class="h4 mb-0">Exécution Solana Core Devnet</h1><small class="text-light-emphasis">System Program uniquement — simulation-first et replay post-exécution</small>
</div>
<div class="d-flex align-items-center gap-2"><span class="badge text-bg-success" id="executionStatusBadge">Prêt</span><button class="btn btn-sm btn-outline-warning" disabled="" id="cancelExecutionButton" type="button">Annuler</button></div>
</nav>
</header>
<main class="app-main">
<div class="osb-scrollable pt-1 pb-4" data-simplebar="">
<div class="container-fluid py-4 px-4">
<div class="alert alert-warning">Cette fenêtre est strictement réservée aux opérations Solana Core. Les opérations SPL sont disponibles dans « Exécution SPL Devnet ».</div>
<div class="row g-4">
<div class="col-12 col-xl-5">
<div class="card shadow-sm border-0"><div class="card-body"><div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3"><h2 class="h5 mb-0">System Program</h2><span class="badge text-bg-primary">solana_native_classifier</span></div>
<div class="mb-3"><label class="form-label" for="executionProfileSelect">Profil Devnet</label><select class="form-select" id="executionProfileSelect"></select>
<div class="form-text" id="executionProfileHelp"></div>
</div>
<div class="mb-3"><label class="form-label" for="executionRecipientInput">Destinataire</label>
<div class="input-group"><input autocomplete="off" class="form-control font-monospace" id="executionRecipientInput" type="text"/><button class="btn btn-outline-secondary" id="generateRecipientButton" type="button">Générer</button></div>
</div>
<div class="row g-3">
<div class="col-6"><label class="form-label" for="executionLamportsInput">Lamports</label><input class="form-control" id="executionLamportsInput" min="1" step="1" type="number"/></div>
<div class="col-6"><label class="form-label" for="executionAirdropInput">Airdrop maximal</label><input class="form-control" id="executionAirdropInput" min="0" step="1" type="number" value="0"/></div>
</div>
<hr/>
<div class="form-check form-switch mb-2"><input class="form-check-input" id="executionOperatorConfirmedInput" type="checkbox"/><label class="form-check-label" for="executionOperatorConfirmedInput">Confirmation opérateur explicite</label></div>
<div class="form-check form-switch mb-2"><input class="form-check-input" id="executionForceReplayInput" type="checkbox"/><label class="form-check-label" for="executionForceReplayInput">Forcer le replay post-exécution</label></div>
<div class="form-check form-switch mb-3"><input class="form-check-input" id="executionMaterializeInput" type="checkbox"/><label class="form-check-label" for="executionMaterializeInput">Matérialiser après décodage</label></div>
<div class="d-grid gap-2 d-md-flex"><button class="btn btn-outline-primary flex-fill" id="simulateExecutionButton" type="button">Simuler uniquement</button><button class="btn btn-danger flex-fill" id="submitExecutionButton" type="button">Signer et envoyer</button></div>
</div></div>
<div class="card shadow-sm border-0 mt-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h2 class="h5 mb-0">Journal</h2><button class="btn btn-sm btn-outline-secondary" id="clearExecutionLogButton" type="button">Effacer</button>
</div><textarea class="form-control font-monospace" id="executionLogOutput" readonly="" rows="18"></textarea>
</div>
</div>
</div>
<div class="col-12 col-xl-7">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5 mb-3">Résumé</h2><textarea class="form-control font-monospace" id="executionSummaryOutput" readonly="" rows="14">Aucun résultat.</textarea>
</div>
</div>
<div class="card shadow-sm border-0 mt-4">
<div class="card-body">
<h2 class="h5">Plan exact</h2><textarea class="form-control font-monospace" id="executionPlanOutput" readonly="" rows="12">Aucun plan.</textarea>
<h2 class="h5 mt-3">Simulation exacte</h2><textarea class="form-control font-monospace" id="executionSimulationOutput" readonly="" rows="12">Aucune simulation.</textarea>
<h2 class="h5 mt-3">Confirmation et replay</h2><textarea class="form-control font-monospace" id="executionDiagnosticsOutput" readonly="" rows="12">Aucun diagnostic.</textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<script defer="" src="ts/demo_execution_solana_core.ts" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,433 @@
<!-- file: kb-app-demo-desktop/frontend/demo_execution_spl.html -->
<!-- version: 8 -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>Exécution SPL Devnet</title>
<link href="sass/main.scss" rel="stylesheet" />
</head>
<body class="app-body">
<header class="app-header bg-dark text-light">
<nav class="container-fluid h-100 d-flex align-items-center justify-content-between px-4">
<div class="d-flex align-items-center gap-3">
<div class="navbar-brand d-flex align-items-center mb-0">
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot3</span>
</div>
<div>
<h1 class="h4 mb-0">Exécution SPL Devnet</h1>
<small class="text-light-emphasis">Memo, SPL Token classique, Token-2022 et ATA — simulation-first et replay post-exécution</small>
</div>
</div>
<div class="d-flex align-items-center gap-2">
<span class="badge text-bg-success" id="executionStatusBadge">Prêt</span>
<button class="btn btn-sm btn-outline-warning" disabled="" id="cancelExecutionButton" type="button">Annuler</button>
</div>
</nav>
</header>
<main class="app-main">
<div class="osb-scrollable pt-1 pb-4" data-simplebar="">
<div class="container-fluid py-4 px-4">
<div class="alert alert-warning" role="alert">
Cette fenêtre est limitée à Devnet. La clé privée du wallet source reste dans le backend Rust et nest jamais transmise à la WebView.
</div>
<div class="row g-4" id="splExecutionWorkspace">
<div class="col-12 col-xl-5" id="splExecutionControls">
<div class="accordion shadow-sm" id="walletPreparationAccordion">
<div class="accordion-item">
<h2 class="accordion-header" id="walletPreparationHeading"><button aria-controls="walletPreparationCollapse" aria-expanded="true" class="accordion-button" data-bs-target="#walletPreparationCollapse" data-bs-toggle="collapse" type="button"><span class="d-flex flex-wrap align-items-center justify-content-between gap-2 w-100 me-2"><span>Préparation commune du wallet Devnet</span><span class="badge text-bg-info">Pré-requis</span></span></button></h2>
<div aria-labelledby="walletPreparationHeading" class="accordion-collapse collapse show" id="walletPreparationCollapse">
<div class="accordion-body">
<div class="mb-3">
<label class="form-label" for="executionProfileSelect">Profil Devnet</label>
<select class="form-select" id="executionProfileSelect"></select>
<div class="form-text" id="executionProfileHelp"></div>
</div>
<div class="mb-3">
<label class="form-label" for="executionRecipientInput">Destinataire</label>
<div class="input-group">
<input autocomplete="off" class="form-control font-monospace" id="executionRecipientInput" type="text" />
<button class="btn btn-outline-secondary" id="generateRecipientButton" type="button">Générer</button>
</div>
<div class="form-text">Une adresse générée ici est jetable : sa clé privée nest ni conservée ni exposée.</div>
</div>
<div class="row g-3">
<div class="col-12 col-md-6">
<label class="form-label" for="executionLamportsInput">Lamports transférés</label>
<input class="form-control" id="executionLamportsInput" min="1" step="1" type="number" />
</div>
<div class="col-12 col-md-6">
<label class="form-label" for="executionAirdropInput">Airdrop maximal</label>
<input class="form-control" id="executionAirdropInput" min="0" step="1" type="number" value="0" />
</div>
</div>
<hr />
<div class="form-check form-switch mb-2">
<input class="form-check-input" id="executionOperatorConfirmedInput" type="checkbox" />
<label class="form-check-label" for="executionOperatorConfirmedInput">Confirmation opérateur explicite</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" id="executionForceReplayInput" type="checkbox" />
<label class="form-check-label" for="executionForceReplayInput">Forcer le replay post-exécution</label>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" id="executionMaterializeInput" type="checkbox" />
<label class="form-check-label" for="executionMaterializeInput">Matérialiser après décodage</label>
</div>
<div class="d-grid gap-2 d-md-flex">
<button class="btn btn-outline-primary flex-fill" id="simulateExecutionButton" type="button">Simuler uniquement</button>
<button class="btn btn-danger flex-fill" id="submitExecutionButton" type="button">Signer et envoyer</button>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex align-items-center justify-content-between mt-4 mb-2">
<h2 class="h5 mb-0">Exécutions par décodeur</h2><small class="text-body-secondary">Ouvrir une seule famille à la fois</small>
</div>
<div class="accordion shadow-sm mt-4" id="decoderExecutionAccordion">
<div class="accordion-item">
<h2 class="accordion-header" id="splMemoDecoderHeading"><button aria-controls="splMemoDecoderCollapse" aria-expanded="false" class="accordion-button collapsed" data-bs-target="#splMemoDecoderCollapse" data-bs-toggle="collapse" type="button"><span class="d-flex flex-wrap align-items-center justify-content-between gap-2 w-100 me-2"><span>Décodeur spl_memo — Memo v4</span><span class="badge text-bg-primary">Exécutable</span></span></button></h2>
<div aria-labelledby="splMemoDecoderHeading" class="accordion-collapse collapse" data-bs-parent="#decoderExecutionAccordion" id="splMemoDecoderCollapse">
<div class="accordion-body">
<div class="mb-3">
<label class="form-label" for="memoMessageInput">Annotation UTF-8 exacte</label>
<textarea class="form-control" id="memoMessageInput" maxlength="566" rows="5">khadhroony-bot2 Memo v4 validation</textarea>
<div class="form-text">566 octets UTF-8 maximum. La matérialisation `transaction_annotation` et le second replay didempotence sont obligatoires après envoi.</div>
</div>
<div class="form-check form-switch mb-3">
<input checked="" class="form-check-input" id="memoWalletSignerInput" type="checkbox" />
<label class="form-check-label" for="memoWalletSignerInput">Inclure le wallet comme signer Memo readonly</label>
</div>
<div class="alert alert-secondary py-2 small" role="note">
Ce parcours réutilise le profil, la confirmation opérateur et loption de force replay du panneau System ci-dessus. Aucune clé privée ne traverse la WebView.
</div>
<div class="d-grid gap-2 d-md-flex">
<button class="btn btn-outline-info flex-fill" id="simulateMemoButton" type="button">Simuler Memo</button>
<button class="btn btn-danger flex-fill" id="submitMemoButton" type="button">Signer et envoyer Memo</button>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="splTokenDecoderHeading"><button aria-controls="splTokenDecoderCollapse" aria-expanded="false" class="accordion-button collapsed" data-bs-target="#splTokenDecoderCollapse" data-bs-toggle="collapse" type="button"><span class="d-flex flex-wrap align-items-center justify-content-between gap-2 w-100 me-2"><span>Décodeur spl_token — TransferChecked</span><span class="badge text-bg-primary">Exécutable</span></span></button></h2>
<div aria-labelledby="splTokenDecoderHeading" class="accordion-collapse collapse" data-bs-parent="#decoderExecutionAccordion" id="splTokenDecoderCollapse">
<div class="accordion-body">
<div class="alert alert-secondary py-2 small" role="note">
Le mint et les decimals sont explicites. Le préflight vérifie les deux comptes, le solde et lautorité avant toute simulation. Lenvoi réutilise le profil et la confirmation opérateur communs.
</div>
<div class="mb-3">
<label class="form-label" for="tokenSourceInput">Compte source</label>
<input autocomplete="off" class="form-control font-monospace" id="tokenSourceInput" type="text" />
</div>
<div class="mb-3">
<label class="form-label" for="tokenMintInput">Mint</label>
<input autocomplete="off" class="form-control font-monospace" id="tokenMintInput" type="text" />
</div>
<div class="mb-3">
<label class="form-label" for="tokenDestinationInput">Compte destination</label>
<input autocomplete="off" class="form-control font-monospace" id="tokenDestinationInput" type="text" />
</div>
<div class="mb-3">
<label class="form-label" for="tokenAuthorityInput">Autorité simple</label>
<input autocomplete="off" class="form-control font-monospace" id="tokenAuthorityInput" type="text" />
<div class="form-text">Doit correspondre au wallet persistant du profil pour un envoi.</div>
</div>
<div class="row g-3 mb-3">
<div class="col-12 col-md-7">
<label class="form-label" for="tokenAmountRawInput">Montant brut exact</label>
<input class="form-control font-monospace" id="tokenAmountRawInput" inputmode="numeric" type="text" value="1" />
</div>
<div class="col-12 col-md-5">
<label class="form-label" for="tokenDecimalsInput">Decimals</label>
<input class="form-control" id="tokenDecimalsInput" max="255" min="0" step="1" type="number" value="9" />
</div>
</div>
<div class="d-grid gap-2 d-md-flex">
<button class="btn btn-outline-primary flex-fill" id="simulateTokenButton" type="button">Préflight et simuler</button>
<button class="btn btn-danger flex-fill" id="submitTokenButton" type="button">Signer et envoyer Token</button>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="splAtaDecoderHeading"><button aria-controls="splAtaDecoderCollapse" aria-expanded="false" class="accordion-button collapsed" data-bs-target="#splAtaDecoderCollapse" data-bs-toggle="collapse" type="button"><span class="d-flex flex-wrap align-items-center justify-content-between gap-2 w-100 me-2"><span>Décodeur spl_associated_token_account — Create / CreateIdempotent</span><span class="badge text-bg-primary">Exécutable</span></span></button></h2>
<div aria-labelledby="splAtaDecoderHeading" class="accordion-collapse collapse" data-bs-parent="#decoderExecutionAccordion" id="splAtaDecoderCollapse">
<div class="accordion-body">
<div class="row g-3 mb-3">
<div class="col-12 col-md-6">
<label class="form-label" for="ataTokenProgramSelect">Token Program cible</label>
<select class="form-select" id="ataTokenProgramSelect">
<option value="classic">SPL Token classique</option>
<option value="token_2022">Token-2022</option>
</select>
</div>
<div class="col-12 col-md-6">
<label class="form-label" for="ataModeSelect">Mode</label>
<select class="form-select" id="ataModeSelect">
<option value="create_idempotent">CreateIdempotent</option>
<option value="create">Create</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="ataMintInput">Mint</label>
<input autocomplete="off" class="form-control font-monospace" id="ataMintInput" type="text" />
</div>
<div class="mb-3">
<label class="form-label" for="ataPayerInput">Payer readonly</label>
<input class="form-control font-monospace" id="ataPayerInput" readonly="" type="text" />
</div>
<div class="mb-3">
<label class="form-label" for="ataWalletOwnerInput">Wallet owner readonly</label>
<input class="form-control font-monospace" id="ataWalletOwnerInput" readonly="" type="text" />
</div>
<div class="mb-3">
<label class="form-label" for="ataDerivedInput">ATA dérivé readonly</label>
<input class="form-control font-monospace" id="ataDerivedInput" readonly="" type="text" />
<div class="form-text">Actualisé depuis le backend avec les seeds wallet, Token Program et mint.</div>
</div>
<div class="d-grid gap-2 d-md-flex">
<button class="btn btn-outline-secondary flex-fill" id="deriveAtaButton" type="button">Dériver</button>
<button class="btn btn-outline-primary flex-fill" id="simulateAtaButton" type="button">Préflight et simuler</button>
<button class="btn btn-danger flex-fill" id="submitAtaButton" type="button">Confirmer et envoyer</button>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="splToken2022DecoderHeading"><button aria-controls="splToken2022DecoderCollapse" aria-expanded="false" class="accordion-button collapsed" data-bs-target="#splToken2022DecoderCollapse" data-bs-toggle="collapse" type="button"><span class="d-flex flex-wrap align-items-center justify-content-between gap-2 w-100 me-2"><span>Exécution spl_token_2022 — scénarios publics</span><span class="badge text-bg-success">Exécutable</span></span></button></h2>
<div aria-labelledby="splToken2022DecoderHeading" class="accordion-collapse collapse" data-bs-parent="#decoderExecutionAccordion" id="splToken2022DecoderCollapse">
<div class="accordion-body">
<div class="alert alert-info">Chaque opération est simulée sur le message exact avant toute signature. Les scénarios sont indépendants et réutilisent la fixture chargée dans les deux terminaux.</div>
<div class="d-grid mb-3"><button class="btn btn-outline-secondary" id="loadToken2022FixtureButton" type="button">Charger la fixture Token-2022</button></div>
<div class="form-text mb-3" id="token2022FixtureHelp">Aucune fixture chargée.</div>
<div class="mb-3"><label class="form-label" for="token2022ScenarioSelect">Scénario</label><select class="form-select" id="token2022ScenarioSelect">
<option value="token_2022_mint_to_checked">MintToChecked</option>
<option value="token_2022_transfer_checked">TransferChecked</option>
<option value="token_2022_approve_checked">ApproveChecked</option>
<option value="token_2022_revoke">Revoke</option>
<option value="token_2022_burn_checked">BurnChecked</option>
<option value="token_2022_freeze_account">FreezeAccount</option>
<option value="token_2022_thaw_account">ThawAccount</option>
<option value="token_2022_close_destination">CloseAccount</option>
</select></div>
<div class="mb-3"><label class="form-label" for="token2022SourceInput">Compte source / compte cible</label><input class="form-control font-monospace" id="token2022SourceInput" type="text" /></div>
<div class="mb-3"><label class="form-label" for="token2022MintInput">Mint Token-2022</label><input class="form-control font-monospace" id="token2022MintInput" type="text" /></div>
<div class="mb-3"><label class="form-label" for="token2022DestinationInput">Compte destination / destination lamports</label><input class="form-control font-monospace" id="token2022DestinationInput" type="text" /></div>
<div class="mb-3"><label class="form-label" for="token2022DelegateInput">Delegate</label><input class="form-control font-monospace" id="token2022DelegateInput" type="text" /></div>
<div class="mb-3"><label class="form-label" for="token2022AuthorityInput">Autorité</label><input class="form-control font-monospace" id="token2022AuthorityInput" type="text" /></div>
<div class="mb-3"><label class="form-label" for="token2022FreezeAuthorityInput">Freeze authority</label><input class="form-control font-monospace" id="token2022FreezeAuthorityInput" type="text" /></div>
<div class="row g-3 mb-3">
<div class="col-8"><label class="form-label" for="token2022AmountInput">Montant brut</label><input class="form-control" id="token2022AmountInput" type="text" value="1" /></div>
<div class="col-4"><label class="form-label" for="token2022DecimalsInput">Décimales</label><input class="form-control" id="token2022DecimalsInput" max="255" min="0" type="number" value="9" /></div>
</div>
<div class="d-grid gap-2 d-md-flex">
<button class="btn btn-outline-primary flex-fill" id="simulateToken2022Button" type="button">Préflight et simuler</button>
<button class="btn btn-danger flex-fill" id="submitToken2022Button" type="button">Confirmer et envoyer</button>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="splElgamalRegistryDecoderHeading"><button aria-controls="splElgamalRegistryDecoderCollapse" aria-expanded="false" class="accordion-button collapsed" data-bs-target="#splElgamalRegistryDecoderCollapse" data-bs-toggle="collapse" type="button"><span class="d-flex flex-wrap align-items-center justify-content-between gap-2 w-100 me-2"><span>Décodeur spl_elgamal_registry — ElGamal Registry</span><span class="badge text-bg-primary">Plan exact</span></span></button></h2>
<div aria-labelledby="splElgamalRegistryDecoderHeading" class="accordion-collapse collapse" data-bs-parent="#decoderExecutionAccordion" id="splElgamalRegistryDecoderCollapse">
<div class="accordion-body">
<div class="alert alert-info">Première démo : dériver le Registry PDA et construire le plan exact avec un Proof Context State existant.</div>
<div class="mb-3"><label class="form-label" for="elgamalFeePayerInput">Fee payer</label><input class="form-control font-monospace" id="elgamalFeePayerInput" type="text" /></div>
<div class="mb-3"><label class="form-label" for="elgamalOwnerInput">Owner du registre</label><input class="form-control font-monospace" id="elgamalOwnerInput" type="text" /></div>
<div class="mb-3"><label class="form-label" for="elgamalOperationSelect">Opération</label><select class="form-select" id="elgamalOperationSelect">
<option value="create_registry">CreateRegistry</option>
<option value="update_registry">UpdateRegistry</option>
</select></div>
<div class="mb-3"><label class="form-label" for="elgamalProofContextInput">Proof Context State account</label><input class="form-control font-monospace" id="elgamalProofContextInput" type="text" /></div><button class="btn btn-outline-primary w-100" id="buildElgamalRegistryPlanButton" type="button">Construire le plan Registry</button>
</div>
</div>
</div>
</div>
<div class="alert alert-secondary mt-4 mb-0">
<strong>Scénarios avec preuve.</strong> CreateRegistry, UpdateRegistry et ConfigureAccountWithRegistry restent dans le panneau ElGamal/Confidential et exigent un Proof Context State valide. Ils ne sont pas présentés comme des scénarios publics exécutables.
</div>
</div>
<div class="col-12 col-xl-7" id="splExecutionResults">
<div class="d-flex align-items-center justify-content-between mb-2">
<h2 class="h5 mb-0">Résultats détaillés</h2><small class="text-body-secondary">Plan, simulation, confirmation et journaux métier</small>
</div>
<div class="accordion mt-4" id="executionDetailsAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" data-bs-target="#executionPlanCollapse" data-bs-toggle="collapse" type="button">Plan exact</button>
</h2>
<div class="accordion-collapse collapse" data-bs-parent="#executionDetailsAccordion" id="executionPlanCollapse">
<div class="accordion-body">
<textarea class="form-control font-monospace" id="executionPlanOutput" readonly="" rows="16">Aucun plan.</textarea>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" data-bs-target="#executionSimulationCollapse" data-bs-toggle="collapse" type="button">Simulation exacte</button>
</h2>
<div class="accordion-collapse collapse" data-bs-parent="#executionDetailsAccordion" id="executionSimulationCollapse">
<div class="accordion-body">
<textarea class="form-control font-monospace" id="executionSimulationOutput" readonly="" rows="16">Aucune simulation.</textarea>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" data-bs-target="#executionDiagnosticsCollapse" data-bs-toggle="collapse" type="button">Confirmation et replay</button>
</h2>
<div class="accordion-collapse collapse" data-bs-parent="#executionDetailsAccordion" id="executionDiagnosticsCollapse">
<div class="accordion-body">
<textarea class="form-control font-monospace" id="executionDiagnosticsOutput" readonly="" rows="16">Aucun diagnostic.</textarea>
</div>
</div>
</div>
</div>
<div class="accordion shadow-sm mt-4" id="splBusinessJournalsAccordion">
<div class="accordion-item">
<h2 class="accordion-header" id="splTokenJournalHeading"><button aria-controls="splTokenJournalCollapse" aria-expanded="false" class="accordion-button collapsed" data-bs-target="#splTokenJournalCollapse" data-bs-toggle="collapse" type="button">Journal métier SPL Token</button></h2>
<div aria-labelledby="splTokenJournalHeading" class="accordion-collapse collapse" data-bs-parent="#splBusinessJournalsAccordion" id="splTokenJournalCollapse">
<div class="accordion-body p-0" id="splTokenJournalHost">
<div class="card border-0 rounded-0">
<div class="card-body">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
<div>
<h2 class="h5 mb-0">Journal métier SPL Token</h2>
<div class="form-text">Événements commités uniquement, sans solde final reconstruit ni OHLC.</div>
</div>
<button class="btn btn-sm btn-outline-primary" id="refreshTokenJournalButton" type="button">Actualiser</button>
</div>
<div class="row g-2 mb-3">
<div class="col-12 col-lg-6">
<label class="form-label" for="tokenJournalSignatureInput">Signature contient</label>
<input class="form-control font-monospace" id="tokenJournalSignatureInput" type="text" />
</div>
<div class="col-12 col-lg-6">
<label class="form-label" for="tokenJournalMintInput">Mint exact</label>
<input class="form-control font-monospace" id="tokenJournalMintInput" type="text" />
</div>
<div class="col-12 col-lg-6">
<label class="form-label" for="tokenJournalAccountInput">Compte exact</label>
<input class="form-control font-monospace" id="tokenJournalAccountInput" type="text" />
</div>
<div class="col-6 col-lg-3">
<label class="form-label" for="tokenJournalFamilySelect">Famille</label>
<select class="form-select" id="tokenJournalFamilySelect">
<option value="">Toutes</option>
<option value="token_account">token_account</option>
<option value="admin">admin</option>
<option value="risk">risk</option>
</select>
</div>
<div class="col-6 col-lg-3">
<label class="form-label" for="tokenJournalLimitInput">Limite</label>
<input class="form-control" id="tokenJournalLimitInput" max="500" min="1" step="1" type="number" value="100" />
</div>
<div class="col-12">
<label class="form-label" for="tokenJournalOperationInput">Opération exacte</label>
<input class="form-control font-monospace" id="tokenJournalOperationInput" placeholder="transfer_checked" type="text" />
</div>
</div>
<div class="table-responsive">
<table class="table table-sm table-hover align-middle">
<thead>
<tr>
<th>Slot</th>
<th>Opération</th>
<th>Famille</th>
<th>Montant brut</th>
<th>Signature</th>
</tr>
</thead>
<tbody id="tokenJournalRows">
<tr>
<td class="text-body-secondary" colspan="5">Aucune lecture.</td>
</tr>
</tbody>
</table>
</div>
<label class="form-label" for="tokenJournalDetailOutput">Détail JSON de la ligne sélectionnée</label>
<textarea class="form-control font-monospace" id="tokenJournalDetailOutput" readonly="" rows="14">Aucun événement sélectionné.</textarea>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="ataLifecycleJournalHeading"><button aria-controls="ataLifecycleJournalCollapse" aria-expanded="false" class="accordion-button collapsed" data-bs-target="#ataLifecycleJournalCollapse" data-bs-toggle="collapse" type="button">Journal lifecycle ATA</button></h2>
<div aria-labelledby="ataLifecycleJournalHeading" class="accordion-collapse collapse" data-bs-parent="#splBusinessJournalsAccordion" id="ataLifecycleJournalCollapse">
<div class="accordion-body p-0" id="ataLifecycleJournalHost">
<div class="card border-0 rounded-0">
<div class="card-body">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
<div>
<h2 class="h5 mb-0">Journal lifecycle ATA</h2>
<div class="form-text">Création, réutilisation idempotente et récupération nested, sans duplication des CPI Token.</div>
</div>
<button class="btn btn-sm btn-outline-primary" id="refreshAtaJournalButton" type="button">Actualiser</button>
</div>
<div class="row g-2 mb-3">
<div class="col-12 col-lg-6">
<label class="form-label" for="ataJournalSignatureInput">Signature contient</label>
<input class="form-control font-monospace" id="ataJournalSignatureInput" type="text" />
</div>
<div class="col-12 col-lg-6">
<label class="form-label" for="ataJournalMintInput">Mint exact</label>
<input class="form-control font-monospace" id="ataJournalMintInput" type="text" />
</div>
<div class="col-12 col-lg-6">
<label class="form-label" for="ataJournalAddressInput">ATA exact</label>
<input class="form-control font-monospace" id="ataJournalAddressInput" type="text" />
</div>
<div class="col-8 col-lg-4">
<label class="form-label" for="ataJournalOperationInput">Opération</label>
<input class="form-control font-monospace" id="ataJournalOperationInput" type="text" />
</div>
<div class="col-4 col-lg-2">
<label class="form-label" for="ataJournalLimitInput">Limite</label>
<input class="form-control" id="ataJournalLimitInput" max="500" min="1" type="number" value="100" />
</div>
</div>
<textarea class="form-control font-monospace" id="ataJournalOutput" readonly="" rows="14">Aucune lecture.</textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card shadow-sm border-0 mt-4">
<div class="card-body">
<h2 class="h5 mb-3">Résumé</h2>
<textarea class="form-control font-monospace" id="executionSummaryOutput" readonly="" rows="14">Aucun résultat.</textarea>
</div>
</div>
<div class="card shadow-sm border-0 mt-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h2 class="h5 mb-0">Journal</h2>
<button class="btn btn-sm btn-outline-secondary" id="clearExecutionLogButton" type="button">Effacer</button>
</div>
<textarea class="form-control font-monospace" id="executionLogOutput" readonly="" rows="18"></textarea>
</div>
</div>
</div>
</div>
</main>
<footer class="app-footer bg-dark text-light">
<div class="container h-100 d-flex align-items-center">
<div class="row flex-grow-1 align-items-center">
<div class="col-12 text-center text-small my-1 my-md-0">© 2026 SASEDEV</div>
</div>
</div>
</footer>
<script defer="" src="ts/demo_execution_spl.ts" type="module"></script>
</body>
</html>

View File

@@ -14,10 +14,10 @@
<header class="app-header">
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
<div class="container my-0">
<a class="navbar-brand d-flex align-items-center" href="main.html">
<div class="navbar-brand d-flex align-items-center">
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot3</span>
</span>
</div>
<div class="ms-auto d-flex align-items-center gap-2">
<span class="badge text-bg-primary">SQL diagnostics</span>
</div>
@@ -29,7 +29,7 @@
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
<div class="container py-4">
<div class="row g-4">
<div class="col-12">
<div class="col-12 col-xl-6">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
@@ -61,7 +61,7 @@
</div>
</div>
<div class="col-12">
<div class="col-12 col-xl-6">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5 card-title mb-3">Tables raw/core connues</h2>

View File

@@ -14,10 +14,10 @@
<header class="app-header">
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
<div class="container my-0">
<a class="navbar-brand d-flex align-items-center" href="main.html">
<div class="navbar-brand d-flex align-items-center">
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot3</span>
</span>
</div>
<div class="ms-auto d-flex align-items-center gap-2">
<span class="badge text-bg-primary">PostgreSQL core</span>
</div>
@@ -29,7 +29,7 @@
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
<div class="container py-4">
<div class="row g-4">
<div class="col-12">
<div class="col-12 col-xl-6">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
@@ -48,7 +48,7 @@
</div>
</div>
<div class="col-12">
<div class="col-12 col-xl-6">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5 card-title mb-3">Tables core</h2>

View File

@@ -14,10 +14,10 @@
<header class="app-header">
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
<div class="container my-0">
<a class="navbar-brand d-flex align-items-center" href="main.html">
<div class="navbar-brand d-flex align-items-center">
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot3</span>
</span>
</div>
<div class="ms-auto d-flex align-items-center gap-2">
<span class="badge text-bg-primary">PostgreSQL acquisition</span>
</div>
@@ -29,7 +29,7 @@
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
<div class="container py-4">
<div class="row g-4">
<div class="col-12">
<div class="col-12 col-xl-6">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
@@ -48,7 +48,7 @@
</div>
</div>
<div class="col-12">
<div class="col-12 col-xl-6">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5 card-title mb-3">Transactions canoniques et observations</h2>

View File

@@ -14,10 +14,10 @@
<header class="app-header">
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
<div class="container-fluid px-4 my-0">
<a class="navbar-brand d-flex align-items-center" href="main.html">
<div class="navbar-brand d-flex align-items-center">
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Candidats de replay SQL</span>
</span>
</div>
<div class="ms-auto d-flex align-items-center gap-2">
<span id="replayCandidateProfileBadge" class="badge text-bg-info">Profil —</span>
<span id="replayCandidateStatusBadge" class="badge text-bg-secondary">Initialisation</span>

View File

@@ -30,6 +30,8 @@
<li><hr class="dropdown-divider"></li>
<li><a id="openDemoCoreExtractionLink" class="dropdown-item" href="#">Extraction core</a></li>
<li><a id="openDemoDecodeReplayLink" class="dropdown-item" href="#">Décodage contextualisé</a></li>
<li><a id="openDemoExecutionSolanaCoreLink" class="dropdown-item" href="#">Exécution Solana Core</a></li>
<li><a id="openDemoExecutionSplLink" class="dropdown-item" href="#">Exécution SPL</a></li>
<li><hr class="dropdown-divider"></li>
<li><a id="openDemoSqlDiagLink" class="dropdown-item" href="#">SQL diagnostics</a></li>
<li><a id="openDemoSqlPgRawLink" class="dropdown-item" href="#">SQL PostgreSQL raw</a></li>

View File

@@ -0,0 +1,31 @@
// file: kb-app-demo-desktop/frontend/ts/demo_execution_solana_core.ts
// version: 6
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { frontendDebug, frontendError, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
import type { DemoExecutionSolanaCoreGeneratedRecipientPayload } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreGeneratedRecipientPayload.ts";
import type { DemoExecutionSolanaCoreOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreOptionsPayload.ts";
import type { DemoExecutionSolanaCoreProfileOption } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreProfileOption.ts";
import type { DemoExecutionSolanaCoreProgressPayload } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreProgressPayload.ts";
import type { DemoExecutionSolanaCoreRequest } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreRequest.ts";
import type { DemoExecutionSolanaCoreSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreSummaryPayload.ts";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
const logLines: string[] = [];
let running = false;
let profileOptions: DemoExecutionSolanaCoreProfileOption[] = [];
function element<T extends HTMLElement>(selector: string): T { const value=document.querySelector<T>(selector); if(!value){throw new Error(`Missing UI element: ${selector}`);} return value; }
function integerValue(selector: string): number { const value=Number.parseInt(element<HTMLInputElement>(selector).value,10); if(!Number.isFinite(value)||value<0){throw new Error(`Valeur numérique invalide pour ${selector}`);} return value; }
function selectedProfile(): DemoExecutionSolanaCoreProfileOption|null { const name=element<HTMLSelectElement>("#executionProfileSelect").value; return profileOptions.find(profile=>profile.name===name)??null; }
function appendLog(payload: DemoExecutionSolanaCoreProgressPayload|{timestamp:string;level:string;stage:string;message:string;signature?:string|null}):void { const signature=payload.signature?` signature=${payload.signature}`:""; logLines.push(`${payload.timestamp} ${payload.level.toUpperCase()} [${payload.stage}] ${payload.message}${signature}`); const output=element<HTMLTextAreaElement>("#executionLogOutput"); output.value=logLines.join("\n"); output.scrollTop=output.scrollHeight; }
function setRunning(value:boolean):void { running=value; const profile=selectedProfile(); element<HTMLButtonElement>("#simulateExecutionButton").disabled=value||!profile; element<HTMLButtonElement>("#submitExecutionButton").disabled=value||!profile||!profile.sendEnabled; element<HTMLButtonElement>("#generateRecipientButton").disabled=value; element<HTMLButtonElement>("#cancelExecutionButton").disabled=!value; const badge=element<HTMLElement>("#executionStatusBadge"); badge.textContent=value?"Exécution en cours":"Prêt"; badge.className=value?"badge text-bg-warning":"badge text-bg-success"; }
function updateProfileHelp():void { const profile=selectedProfile(); const help=element<HTMLElement>("#executionProfileHelp"); if(!profile){help.textContent="Aucun profil Devnet compatible.";return;} help.textContent=`wallet=${profile.walletAlias}, spendMax=${profile.maxSpendLamports}, airdropMax=${profile.maxAirdropLamports}, send=${profile.sendEnabled?"enabled":"disabled"}`; element<HTMLInputElement>("#executionAirdropInput").max=String(profile.maxAirdropLamports); element<HTMLInputElement>("#executionLamportsInput").max=String(profile.maxSpendLamports); setRunning(running); }
function buildRequest(submit:boolean):DemoExecutionSolanaCoreRequest { const recipient=element<HTMLInputElement>("#executionRecipientInput").value.trim(); if(recipient.length===0){throw new Error("Le destinataire est obligatoire.");} return {profileName:element<HTMLSelectElement>("#executionProfileSelect").value,recipient,lamports:integerValue("#executionLamportsInput"),airdropLamports:integerValue("#executionAirdropInput"),submit,operatorConfirmed:element<HTMLInputElement>("#executionOperatorConfirmedInput").checked,forcePostValidationReplay:element<HTMLInputElement>("#executionForceReplayInput").checked,materializeAfterDecode:element<HTMLInputElement>("#executionMaterializeInput").checked}; }
function displaySummary(summary:DemoExecutionSolanaCoreSummaryPayload):void { element<HTMLTextAreaElement>("#executionSummaryOutput").value=JSON.stringify(summary,null,2); element<HTMLTextAreaElement>("#executionPlanOutput").value=summary.planJson; element<HTMLTextAreaElement>("#executionSimulationOutput").value=summary.simulationJson; element<HTMLTextAreaElement>("#executionDiagnosticsOutput").value=summary.diagnosticsJson; }
async function execute(submit:boolean):Promise<void>{if(running){return;}try{const request=buildRequest(submit);if(submit&&!request.operatorConfirmed){frontendWarn("kb-app-demo-desktop.frontend.demo_execution_solana_core","Submission blocked: operator confirmation missing");return;}setRunning(true);const summary=await invoke<DemoExecutionSolanaCoreSummaryPayload>("demo_execution_solana_core_execute",{request});displaySummary(summary);}catch(caughtError){const message=caughtError instanceof Error?caughtError.message:String(caughtError);frontendError("kb-app-demo-desktop.frontend.demo_execution_solana_core",`Execution failed: ${message}`);}finally{setRunning(false);}}
async function loadOptions():Promise<void>{const options=await invoke<DemoExecutionSolanaCoreOptionsPayload>("demo_execution_solana_core_options");profileOptions=options.profiles;const select=element<HTMLSelectElement>("#executionProfileSelect");select.replaceChildren();for(const profile of profileOptions){const option=document.createElement("option");option.value=profile.name;option.textContent=profile.name;option.selected=profile.name===options.defaultProfileName;select.append(option);}element<HTMLInputElement>("#executionLamportsInput").value=String(options.defaultTransferLamports);updateProfileHelp();setRunning(options.running);}
document.addEventListener("DOMContentLoaded",async()=>{installFrontendConsoleBridge("kb-app-demo-desktop.frontend.demo_execution_solana_core");frontendDebug("kb-app-demo-desktop.frontend.demo_execution_solana_core","Solana Core execution window loaded");await listen<DemoExecutionSolanaCoreProgressPayload>("demo-execution-solana-core-progress",event=>appendLog(event.payload));element<HTMLSelectElement>("#executionProfileSelect").addEventListener("change",updateProfileHelp);element<HTMLButtonElement>("#generateRecipientButton").addEventListener("click",async()=>{const payload=await invoke<DemoExecutionSolanaCoreGeneratedRecipientPayload>("demo_execution_solana_core_generate_recipient");element<HTMLInputElement>("#executionRecipientInput").value=payload.publicKey;});element<HTMLButtonElement>("#simulateExecutionButton").addEventListener("click",()=>void execute(false));element<HTMLButtonElement>("#submitExecutionButton").addEventListener("click",()=>void execute(true));element<HTMLButtonElement>("#cancelExecutionButton").addEventListener("click",()=>void invoke("demo_execution_solana_core_cancel"));element<HTMLButtonElement>("#clearExecutionLogButton").addEventListener("click",()=>{logLines.length=0;element<HTMLTextAreaElement>("#executionLogOutput").value="";});try{await loadOptions();}catch(caughtError){const message=caughtError instanceof Error?caughtError.message:String(caughtError);frontendError("kb-app-demo-desktop.frontend.demo_execution_solana_core",`Options loading failed: ${message}`);}});

View File

@@ -0,0 +1,881 @@
// file: kb-app-demo-desktop/frontend/ts/demo_execution_spl.ts
// version: 2
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { frontendDebug, frontendError, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
import type { DemoExecutionSolanaCoreGeneratedRecipientPayload } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreGeneratedRecipientPayload.ts";
import type { DemoExecutionSolanaCoreOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreOptionsPayload.ts";
import type { DemoExecutionSolanaCoreProfileOption } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreProfileOption.ts";
import type { DemoExecutionSolanaCoreProgressPayload } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreProgressPayload.ts";
import type { DemoExecutionSolanaCoreRequest } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreRequest.ts";
import type { DemoExecutionSolanaCoreSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreSummaryPayload.ts";
import type { DemoExecutionMemoRequest } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionMemoRequest.ts";
import type { DemoExecutionMemoSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionMemoSummaryPayload.ts";
import type { DemoExecutionSplTokenRequest } from "./bindings/kb_app_demo_desktop/demo_spl_token/DemoExecutionSplTokenRequest.ts";
import type { DemoExecutionSplTokenSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_spl_token/DemoExecutionSplTokenSummaryPayload.ts";
import type { DemoSplTokenJournalRequest } from "./bindings/kb_app_demo_desktop/demo_spl_token/DemoSplTokenJournalRequest.ts";
import type { DemoSplTokenJournalRow } from "./bindings/kb_app_demo_desktop/demo_spl_token/DemoSplTokenJournalRow.ts";
import type { DemoExecutionSplAtaRequest } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoExecutionSplAtaRequest.ts";
import type { DemoExecutionSplAtaSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoExecutionSplAtaSummaryPayload.ts";
import type { DemoSplAtaDerivationPayload } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaDerivationPayload.ts";
import type { DemoSplAtaDerivationRequest } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaDerivationRequest.ts";
import type { DemoSplAtaJournalRequest } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaJournalRequest.ts";
import type { DemoSplAtaJournalRow } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaJournalRow.ts";
import type { DemoExecutionSplToken2022Request } from "./bindings/kb_app_demo_desktop/demo_spl_token2022/DemoExecutionSplToken2022Request.ts";
import type { DemoExecutionSplToken2022SummaryPayload } from "./bindings/kb_app_demo_desktop/demo_spl_token2022/DemoExecutionSplToken2022SummaryPayload.ts";
import type { DemoSplToken2022FixturePayload } from "./bindings/kb_app_demo_desktop/demo_spl_token2022/DemoSplToken2022FixturePayload.ts";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
const logLines: string[] = [];
const maximumLogLines = 1200;
let running = false;
let profileOptions: DemoExecutionSolanaCoreProfileOption[] = [];
let token2022Fixture: DemoSplToken2022FixturePayload | null = null;
function element<T extends HTMLElement>(selector: string): T {
const value = document.querySelector<T>(selector);
if (!value) {
throw new Error(`Missing UI element: ${selector}`);
}
return value;
}
function integerValue(selector: string): number {
const value = Number.parseInt(element<HTMLInputElement>(selector).value, 10);
if (!Number.isFinite(value) || value < 0) {
throw new Error(`Valeur numérique invalide pour ${selector}`);
}
return value;
}
function appendLog(payload: DemoExecutionSolanaCoreProgressPayload | { timestamp: string; level: string; stage: string; message: string; signature?: string | null }): void {
const signature = payload.signature ? ` signature=${payload.signature}` : "";
logLines.push(`${payload.timestamp} ${payload.level.toUpperCase()} [${payload.stage}] ${payload.message}${signature}`);
while (logLines.length > maximumLogLines) {
logLines.shift();
}
const output = element<HTMLTextAreaElement>("#executionLogOutput");
output.value = logLines.join("\n");
output.scrollTop = output.scrollHeight;
}
function setRunning(value: boolean): void {
running = value;
const profile = selectedProfile();
element<HTMLButtonElement>("#simulateExecutionButton").disabled = value || !profile;
element<HTMLButtonElement>("#submitExecutionButton").disabled = value || !profile || !profile.sendEnabled;
element<HTMLButtonElement>("#simulateMemoButton").disabled = value || !profile;
element<HTMLButtonElement>("#submitMemoButton").disabled = value || !profile || !profile.sendEnabled;
element<HTMLButtonElement>("#simulateTokenButton").disabled = value || !profile;
element<HTMLButtonElement>("#submitTokenButton").disabled = value || !profile || !profile.sendEnabled;
element<HTMLButtonElement>("#deriveAtaButton").disabled = value || !profile;
element<HTMLButtonElement>("#simulateAtaButton").disabled = value || !profile;
element<HTMLButtonElement>("#submitAtaButton").disabled = value || !profile || !profile.sendEnabled;
element<HTMLButtonElement>("#simulateToken2022Button").disabled = value || !profile;
element<HTMLButtonElement>("#submitToken2022Button").disabled = value || !profile || !profile.sendEnabled;
element<HTMLButtonElement>("#loadToken2022FixtureButton").disabled = value || !profile;
element<HTMLButtonElement>("#generateRecipientButton").disabled = value;
element<HTMLButtonElement>("#cancelExecutionButton").disabled = !value;
const badge = element<HTMLElement>("#executionStatusBadge");
badge.textContent = value ? "Exécution en cours" : "Prêt";
badge.className = value ? "badge text-bg-warning" : "badge text-bg-success";
}
function selectedProfile(): DemoExecutionSolanaCoreProfileOption | null {
const name = element<HTMLSelectElement>("#executionProfileSelect").value;
return profileOptions.find(profile => profile.name === name) ?? null;
}
function updateProfileHelp(): void {
const profile = selectedProfile();
const help = element<HTMLElement>("#executionProfileHelp");
if (!profile) {
help.textContent = "Aucun profil Devnet compatible.";
return;
}
help.textContent = `wallet=${profile.walletAlias}, spendMax=${profile.maxSpendLamports}, airdropMax=${profile.maxAirdropLamports}, send=${profile.sendEnabled ? "enabled" : "disabled"}`;
element<HTMLInputElement>("#executionAirdropInput").max = String(profile.maxAirdropLamports);
element<HTMLInputElement>("#executionLamportsInput").max = String(profile.maxSpendLamports);
setRunning(running);
}
function buildRequest(submit: boolean): DemoExecutionSolanaCoreRequest {
const recipient = element<HTMLInputElement>("#executionRecipientInput").value.trim();
if (recipient.length === 0) {
throw new Error("Le destinataire est obligatoire.");
}
return {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
recipient,
lamports: integerValue("#executionLamportsInput"),
airdropLamports: integerValue("#executionAirdropInput"),
submit,
operatorConfirmed: element<HTMLInputElement>("#executionOperatorConfirmedInput").checked,
forcePostValidationReplay: element<HTMLInputElement>("#executionForceReplayInput").checked,
materializeAfterDecode: element<HTMLInputElement>("#executionMaterializeInput").checked,
};
}
function displaySummary(summary: DemoExecutionSolanaCoreSummaryPayload): void {
const concise = {
profileName: summary.profileName,
cluster: summary.cluster,
genesisHash: summary.genesisHash,
walletPublicKey: summary.walletPublicKey,
recipient: summary.recipient,
recipientExistedBefore: summary.recipientExistedBefore,
recipientBalanceBeforeLamports: summary.recipientBalanceBeforeLamports,
recipientMinimumBalanceLamports: summary.recipientMinimumBalanceLamports,
balanceBeforeLamports: summary.balanceBeforeLamports,
balanceAfterFundingLamports: summary.balanceAfterFundingLamports,
feeLamports: summary.feeLamports,
simulationSuccess: summary.simulationSuccess,
simulationError: summary.simulationError,
airdropSignature: summary.airdropSignature,
transactionSignature: summary.transactionSignature,
confirmationStatus: summary.confirmationStatus,
canonicalInserted: summary.canonicalInserted,
coreExtracted: summary.coreExtracted,
decodeCompleted: summary.decodeCompleted,
decodeFailedInputs: summary.decodeFailedInputs,
};
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify(concise, null, 2);
element<HTMLTextAreaElement>("#executionPlanOutput").value = summary.planJson;
element<HTMLTextAreaElement>("#executionSimulationOutput").value = summary.simulationJson;
element<HTMLTextAreaElement>("#executionDiagnosticsOutput").value = summary.diagnosticsJson;
}
function buildMemoRequest(submit: boolean): DemoExecutionMemoRequest {
return {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
message: element<HTMLTextAreaElement>("#memoMessageInput").value,
includeWalletAsMemoSigner: element<HTMLInputElement>("#memoWalletSignerInput").checked,
submit,
operatorConfirmed: element<HTMLInputElement>("#executionOperatorConfirmedInput").checked,
forcePostValidationReplay: element<HTMLInputElement>("#executionForceReplayInput").checked,
};
}
function displayMemoSummary(summary: DemoExecutionMemoSummaryPayload): void {
const concise = {
operation: "spl_memo_v4.add_memo",
profileName: summary.profileName,
cluster: summary.cluster,
genesisHash: summary.genesisHash,
walletPublicKey: summary.walletPublicKey,
balanceLamports: summary.balanceLamports,
feeLamports: summary.feeLamports,
simulationSuccess: summary.simulationSuccess,
simulationError: summary.simulationError,
transactionSignature: summary.transactionSignature,
confirmationStatus: summary.confirmationStatus,
canonicalInserted: summary.canonicalInserted,
coreExtracted: summary.coreExtracted,
decodeReplayed: summary.decodeReplayed,
materialized: summary.materialized,
annotationCount: summary.annotationCount,
idempotenceValidated: summary.idempotenceValidated,
};
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify(concise, null, 2);
element<HTMLTextAreaElement>("#executionPlanOutput").value = summary.planJson;
element<HTMLTextAreaElement>("#executionSimulationOutput").value = summary.simulationJson;
element<HTMLTextAreaElement>("#executionDiagnosticsOutput").value = summary.diagnosticsJson;
}
function requiredText(selector: string, label: string): string {
const value = element<HTMLInputElement>(selector).value.trim();
if (value.length === 0) {
throw new Error(`${label} est obligatoire.`);
}
return value;
}
function optionalText(selector: string): string | null {
const value = element<HTMLInputElement>(selector).value.trim();
return value.length === 0 ? null : value;
}
function buildTokenRequest(submit: boolean): DemoExecutionSplTokenRequest {
const amountRaw = requiredText("#tokenAmountRawInput", "Le montant brut");
if (!/^[0-9]+$/.test(amountRaw)) {
throw new Error("Le montant brut doit être un entier décimal non signé.");
}
const decimals = integerValue("#tokenDecimalsInput");
if (decimals > 255) {
throw new Error("Les decimals doivent être compris entre 0 et 255.");
}
return {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
source: requiredText("#tokenSourceInput", "Le compte source"),
mint: requiredText("#tokenMintInput", "Le mint"),
destination: requiredText("#tokenDestinationInput", "Le compte destination"),
authority: requiredText("#tokenAuthorityInput", "Lautorité"),
amountRaw,
decimals,
submit,
operatorConfirmed: element<HTMLInputElement>("#executionOperatorConfirmedInput").checked,
forcePostValidationReplay: element<HTMLInputElement>("#executionForceReplayInput").checked,
};
}
function displayTokenSummary(summary: DemoExecutionSplTokenSummaryPayload): void {
const concise = {
operation: summary.operation,
profileName: summary.profileName,
cluster: summary.cluster,
genesisHash: summary.genesisHash,
walletPublicKey: summary.walletPublicKey,
balanceLamports: summary.balanceLamports,
feeLamports: summary.feeLamports,
amountRaw: summary.amountRaw,
decimals: summary.decimals,
readinessStatus: summary.readinessStatus,
simulationSuccess: summary.simulationSuccess,
simulationError: summary.simulationError,
transactionSignature: summary.transactionSignature,
confirmationStatus: summary.confirmationStatus,
canonicalInserted: summary.canonicalInserted,
coreExtracted: summary.coreExtracted,
decodeReplayed: summary.decodeReplayed,
materializationCount: summary.materializationCount,
idempotenceValidated: summary.idempotenceValidated,
};
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify(concise, null, 2);
element<HTMLTextAreaElement>("#executionPlanOutput").value = `PRÉFLIGHT\n${summary.readinessJson}\n\nPLAN\n${summary.planJson}`;
element<HTMLTextAreaElement>("#executionSimulationOutput").value = summary.simulationJson;
element<HTMLTextAreaElement>("#executionDiagnosticsOutput").value = summary.diagnosticsJson;
}
function ataDerivationRequest(): DemoSplAtaDerivationRequest {
return {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
mint: requiredText("#ataMintInput", "Le mint ATA"),
tokenProgram: element<HTMLSelectElement>("#ataTokenProgramSelect").value,
};
}
async function deriveAta(): Promise<DemoSplAtaDerivationPayload> {
const payload = await invoke<DemoSplAtaDerivationPayload>("demo_spl_ata_derive", {
request: ataDerivationRequest(),
});
element<HTMLInputElement>("#ataPayerInput").value = payload.payer;
element<HTMLInputElement>("#ataWalletOwnerInput").value = payload.walletOwner;
element<HTMLInputElement>("#ataDerivedInput").value = payload.associatedTokenAccount;
return payload;
}
function displayAtaSummary(summary: DemoExecutionSplAtaSummaryPayload): void {
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify({
operation: summary.operation,
profileName: summary.profileName,
walletPublicKey: summary.walletPublicKey,
tokenProgramId: summary.tokenProgramId,
associatedTokenAccount: summary.associatedTokenAccount,
balanceLamports: summary.balanceLamports,
feeLamports: summary.feeLamports,
readinessStatus: summary.readinessStatus,
simulationSuccess: summary.simulationSuccess,
simulationError: summary.simulationError,
transactionSignature: summary.transactionSignature,
confirmationStatus: summary.confirmationStatus,
materializationCount: summary.materializationCount,
idempotenceValidated: summary.idempotenceValidated,
}, null, 2);
element<HTMLTextAreaElement>("#executionPlanOutput").value = `PRÉFLIGHT\n${summary.readinessJson}\n\nPLAN\n${summary.planJson}`;
element<HTMLTextAreaElement>("#executionSimulationOutput").value = summary.simulationJson;
element<HTMLTextAreaElement>("#executionDiagnosticsOutput").value = summary.diagnosticsJson;
}
async function executeAta(submit: boolean): Promise<void> {
if (running) {
return;
}
try {
if (submit && !element<HTMLInputElement>("#executionOperatorConfirmedInput").checked) {
throw new Error("La confirmation opérateur est obligatoire avant signature ATA.");
}
const derivation = await deriveAta();
const request: DemoExecutionSplAtaRequest = {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
walletOwner: derivation.walletOwner,
mint: requiredText("#ataMintInput", "Le mint ATA"),
tokenProgram: element<HTMLSelectElement>("#ataTokenProgramSelect").value,
mode: element<HTMLSelectElement>("#ataModeSelect").value,
submit,
operatorConfirmed: element<HTMLInputElement>("#executionOperatorConfirmedInput").checked,
forcePostValidationReplay: element<HTMLInputElement>("#executionForceReplayInput").checked,
};
setRunning(true);
appendLog({
timestamp: new Date().toISOString(),
level: "info",
stage: submit ? "ata_submit" : "ata_simulation",
message: `${request.mode} ${request.tokenProgram}, ATA=${derivation.associatedTokenAccount}`,
});
const summary = await invoke<DemoExecutionSplAtaSummaryPayload>("demo_execution_spl_ata_execute", { request });
displayAtaSummary(summary);
appendLog({
timestamp: new Date().toISOString(),
level: summary.simulationSuccess && (!submit || summary.idempotenceValidated) ? "info" : "error",
stage: "ata_completed",
message: `preflight=${summary.readinessStatus}, simulation=${summary.simulationSuccess}, confirmation=${summary.confirmationStatus ?? "none"}, materializations=${summary.materializationCount}, idempotence=${summary.idempotenceValidated}`,
signature: summary.transactionSignature,
});
if (summary.transactionSignature) {
element<HTMLInputElement>("#ataJournalSignatureInput").value = summary.transactionSignature;
element<HTMLInputElement>("#ataJournalMintInput").value = request.mint;
element<HTMLInputElement>("#ataJournalAddressInput").value = summary.associatedTokenAccount;
await loadAtaJournal();
}
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "ata_failed", message });
frontendError("kb-app-demo-desktop.frontend.demo_execution_spl", `ATA execution failed: ${message}`);
} finally {
setRunning(false);
}
}
function ataJournalRequest(): DemoSplAtaJournalRequest {
return {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
signatureContains: optionalText("#ataJournalSignatureInput"),
mint: optionalText("#ataJournalMintInput"),
associatedTokenAccount: optionalText("#ataJournalAddressInput"),
operation: optionalText("#ataJournalOperationInput"),
limit: integerValue("#ataJournalLimitInput"),
};
}
async function loadAtaJournal(): Promise<void> {
const button = element<HTMLButtonElement>("#refreshAtaJournalButton");
button.disabled = true;
try {
const rows = await invoke<DemoSplAtaJournalRow[]>("demo_spl_ata_journal", { request: ataJournalRequest() });
element<HTMLTextAreaElement>("#ataJournalOutput").value = JSON.stringify(rows.map(row => ({
...row,
payloadJson: JSON.parse(row.payloadJson) as unknown,
})), null, 2);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#ataJournalOutput").value = JSON.stringify({ error: message }, null, 2);
} finally {
button.disabled = false;
}
}
async function execute(submit: boolean): Promise<void> {
if (running) {
return;
}
try {
const request = buildRequest(submit);
if (submit && !request.operatorConfirmed) {
const message = "La confirmation opérateur doit être cochée avant signature et envoi.";
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify({ warning: message }, null, 2);
appendLog({
timestamp: new Date().toISOString(),
level: "warn",
stage: "policy",
message,
});
frontendWarn("kb-app-demo-desktop.frontend.demo_execution_spl", `Execution blocked by policy: ${message}`);
return;
}
setRunning(true);
element<HTMLTextAreaElement>("#executionSummaryOutput").value = "Exécution en cours...";
appendLog({
timestamp: new Date().toISOString(),
level: "info",
stage: submit ? "submit" : "simulation",
message: submit ? "Démarrage du parcours Devnet complet." : "Démarrage de la simulation exacte.",
});
const summary = await invoke<DemoExecutionSolanaCoreSummaryPayload>("demo_execution_solana_core_execute", { request });
displaySummary(summary);
appendLog({
timestamp: new Date().toISOString(),
level: summary.simulationSuccess ? "info" : "error",
stage: "completed",
message: `simulation=${summary.simulationSuccess}, confirmation=${summary.confirmationStatus ?? "none"}, canonical=${summary.canonicalInserted ?? 0}, core=${summary.coreExtracted ?? 0}, decode=${summary.decodeCompleted ?? 0}`,
signature: summary.transactionSignature,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify({ error: message }, null, 2);
appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "failed", message });
frontendError("kb-app-demo-desktop.frontend.demo_execution_spl", `Execution failed: ${message}`);
} finally {
setRunning(false);
}
}
async function executeMemo(submit: boolean): Promise<void> {
if (running) {
return;
}
try {
const request = buildMemoRequest(submit);
if (submit && !request.operatorConfirmed) {
const message = "La confirmation opérateur doit être cochée avant signature et envoi du Memo.";
appendLog({ timestamp: new Date().toISOString(), level: "warn", stage: "memo_policy", message });
frontendWarn("kb-app-demo-desktop.frontend.demo_execution_spl", `Memo execution blocked by policy: ${message}`);
return;
}
setRunning(true);
element<HTMLTextAreaElement>("#executionSummaryOutput").value = "Exécution Memo en cours...";
appendLog({
timestamp: new Date().toISOString(),
level: "info",
stage: submit ? "memo_submit" : "memo_simulation",
message: submit ? "Démarrage du parcours Memo v4 Devnet complet." : "Démarrage de la simulation Memo v4 exacte.",
});
const summary = await invoke<DemoExecutionMemoSummaryPayload>("demo_execution_spl_memo_execute", { request });
displayMemoSummary(summary);
appendLog({
timestamp: new Date().toISOString(),
level: summary.simulationSuccess && (!submit || summary.materialized) ? "info" : "error",
stage: "memo_completed",
message: `simulation=${summary.simulationSuccess}, confirmation=${summary.confirmationStatus ?? "none"}, annotation=${summary.annotationCount}, idempotence=${summary.idempotenceValidated}`,
signature: summary.transactionSignature,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify({ error: message }, null, 2);
appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "memo_failed", message });
frontendError("kb-app-demo-desktop.frontend.demo_execution_spl", `Memo execution failed: ${message}`);
} finally {
setRunning(false);
}
}
async function executeToken(submit: boolean): Promise<void> {
if (running) {
return;
}
try {
const request = buildTokenRequest(submit);
if (submit && !request.operatorConfirmed) {
const message = "La confirmation opérateur doit être cochée avant signature et envoi Token.";
appendLog({ timestamp: new Date().toISOString(), level: "warn", stage: "spl_token_policy", message });
frontendWarn("kb-app-demo-desktop.frontend.demo_execution_spl", `Token execution blocked by policy: ${message}`);
return;
}
setRunning(true);
element<HTMLTextAreaElement>("#executionSummaryOutput").value = "Préflight et exécution SPL Token en cours...";
appendLog({
timestamp: new Date().toISOString(),
level: "info",
stage: submit ? "spl_token_submit" : "spl_token_simulation",
message: submit ? "Démarrage du TransferChecked Devnet complet." : "Démarrage du préflight et de la simulation TransferChecked.",
});
const summary = await invoke<DemoExecutionSplTokenSummaryPayload>("demo_execution_spl_token_execute", { request });
displayTokenSummary(summary);
appendLog({
timestamp: new Date().toISOString(),
level: summary.simulationSuccess && (!submit || summary.idempotenceValidated) ? "info" : "error",
stage: "spl_token_completed",
message: `preflight=${summary.readinessStatus}, simulation=${summary.simulationSuccess}, confirmation=${summary.confirmationStatus ?? "none"}, materializations=${summary.materializationCount}, idempotence=${summary.idempotenceValidated}`,
signature: summary.transactionSignature,
});
if (summary.transactionSignature) {
element<HTMLInputElement>("#tokenJournalSignatureInput").value = summary.transactionSignature;
await loadTokenJournal();
}
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify({ error: message }, null, 2);
appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "spl_token_failed", message });
frontendError("kb-app-demo-desktop.frontend.demo_execution_spl", `Token execution failed: ${message}`);
} finally {
setRunning(false);
}
}
function tokenJournalRequest(): DemoSplTokenJournalRequest {
return {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
signatureContains: optionalText("#tokenJournalSignatureInput"),
mint: optionalText("#tokenJournalMintInput"),
account: optionalText("#tokenJournalAccountInput"),
family: element<HTMLSelectElement>("#tokenJournalFamilySelect").value || null,
operation: optionalText("#tokenJournalOperationInput"),
limit: integerValue("#tokenJournalLimitInput"),
};
}
function renderTokenJournal(rows: DemoSplTokenJournalRow[]): void {
const body = element<HTMLTableSectionElement>("#tokenJournalRows");
body.replaceChildren();
if (rows.length === 0) {
const row = document.createElement("tr");
const cell = document.createElement("td");
cell.colSpan = 5;
cell.className = "text-body-secondary";
cell.textContent = "Aucun événement correspondant.";
row.append(cell);
body.append(row);
element<HTMLTextAreaElement>("#tokenJournalDetailOutput").value = "Aucun événement sélectionné.";
return;
}
for (const item of rows) {
const row = document.createElement("tr");
row.tabIndex = 0;
row.setAttribute("role", "button");
const values = [
item.slot,
item.operation ?? "—",
item.family,
item.amountRaw ?? "—",
item.signature.length > 20 ? `${item.signature.slice(0, 10)}${item.signature.slice(-8)}` : item.signature,
];
for (const value of values) {
const cell = document.createElement("td");
cell.textContent = value;
row.append(cell);
}
row.title = item.signature;
const select = (): void => {
element<HTMLTextAreaElement>("#tokenJournalDetailOutput").value = item.payloadJson;
};
row.addEventListener("click", select);
row.addEventListener("keydown", event => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
select();
}
});
body.append(row);
}
element<HTMLTextAreaElement>("#tokenJournalDetailOutput").value = rows[0].payloadJson;
}
function token2022ScenarioAmount(fixture: DemoSplToken2022FixturePayload, scenarioId: string): string {
if (scenarioId === "token2022_mint_to_checked") {
return "1";
}
if (scenarioId === "token2022_approve_checked") {
return "2";
}
if (scenarioId === "token2022_burn_checked") {
return "1";
}
return fixture.defaultAmountRaw;
}
function applyToken2022Fixture(): void {
if (!token2022Fixture) {
return;
}
const scenarioId = element<HTMLSelectElement>("#token2022ScenarioSelect").value;
element<HTMLInputElement>("#token2022SourceInput").value = scenarioId === "token2022_close_destination"
? token2022Fixture.closeAccount
: token2022Fixture.source;
element<HTMLInputElement>("#token2022MintInput").value = token2022Fixture.mint;
element<HTMLInputElement>("#token2022DestinationInput").value = scenarioId === "token2022_close_destination"
? token2022Fixture.authority
: token2022Fixture.destination;
element<HTMLInputElement>("#token2022DelegateInput").value = token2022Fixture.delegate;
element<HTMLInputElement>("#token2022AuthorityInput").value = token2022Fixture.authority;
element<HTMLInputElement>("#token2022FreezeAuthorityInput").value = token2022Fixture.freezeAuthority;
element<HTMLInputElement>("#token2022AmountInput").value = token2022ScenarioAmount(token2022Fixture, scenarioId);
element<HTMLInputElement>("#token2022DecimalsInput").value = String(token2022Fixture.decimals);
}
async function loadToken2022Fixture(): Promise<void> {
try {
token2022Fixture = await invoke<DemoSplToken2022FixturePayload>("demo_spl_token2022_fixture", {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
});
applyToken2022Fixture();
element<HTMLElement>("#token2022FixtureHelp").textContent = `Fixture: ${token2022Fixture.fixturePath} — program=${token2022Fixture.programId}`;
appendLog({
timestamp: new Date().toISOString(),
level: "info",
stage: "token2022_fixture",
message: `fixture chargée: mint=${token2022Fixture.mint}, source=${token2022Fixture.source}, destination=${token2022Fixture.destination}`,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
token2022Fixture = null;
element<HTMLElement>("#token2022FixtureHelp").textContent = message;
appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "token2022_fixture", message });
}
}
function buildToken2022Request(submit: boolean): DemoExecutionSplToken2022Request {
const scenarioId = element<HTMLSelectElement>("#token2022ScenarioSelect").value;
const source = element<HTMLInputElement>("#token2022SourceInput").value.trim();
const mint = element<HTMLInputElement>("#token2022MintInput").value.trim();
const destination = element<HTMLInputElement>("#token2022DestinationInput").value.trim();
const delegate = element<HTMLInputElement>("#token2022DelegateInput").value.trim();
const authority = element<HTMLInputElement>("#token2022AuthorityInput").value.trim();
const freezeAuthority = element<HTMLInputElement>("#token2022FreezeAuthorityInput").value.trim();
if (source.length === 0) {
throw new Error("Le compte source ou cible Token-2022 est obligatoire.");
}
if (scenarioId !== "token2022_revoke" && scenarioId !== "token2022_close_destination" && mint.length === 0) {
throw new Error("Le mint Token-2022 est obligatoire pour ce scénario.");
}
if (scenarioId === "token2022_transfer_checked" && destination.length === 0) {
throw new Error("Le compte destination Token-2022 est obligatoire.");
}
if (scenarioId === "token2022_close_destination" && destination.length === 0) {
throw new Error("La destination lamports est obligatoire pour CloseAccount.");
}
if (scenarioId === "token2022_approve_checked" && delegate.length === 0) {
throw new Error("Le delegate est obligatoire pour ApproveChecked.");
}
if (["token2022_freeze_account", "token2022_thaw_account"].includes(scenarioId) && freezeAuthority.length === 0) {
throw new Error("La freeze authority est obligatoire pour FreezeAccount et ThawAccount.");
}
if (!["token2022_freeze_account", "token2022_thaw_account"].includes(scenarioId) && authority.length === 0) {
throw new Error("Lautorité Token-2022 est obligatoire.");
}
return {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
scenarioId,
source,
mint,
destination,
delegate,
authority,
freezeAuthority,
amountRaw: element<HTMLInputElement>("#token2022AmountInput").value.trim(),
decimals: integerValue("#token2022DecimalsInput"),
submit,
operatorConfirmed: element<HTMLInputElement>("#executionOperatorConfirmedInput").checked,
forcePostValidationReplay: element<HTMLInputElement>("#executionForceReplayInput").checked,
};
}
function displayToken2022Summary(summary: DemoExecutionSplToken2022SummaryPayload): void {
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify({
scenarioId: summary.scenarioId,
operation: summary.operation,
profileName: summary.profileName,
cluster: summary.cluster,
genesisHash: summary.genesisHash,
walletPublicKey: summary.walletPublicKey,
balanceLamports: summary.balanceLamports,
feeLamports: summary.feeLamports,
simulationSuccess: summary.simulationSuccess,
simulationError: summary.simulationError,
transactionSignature: summary.transactionSignature,
confirmationStatus: summary.confirmationStatus,
canonicalInserted: summary.canonicalInserted,
coreExtracted: summary.coreExtracted,
decodeReplayed: summary.decodeReplayed,
materializationCount: summary.materializationCount,
idempotenceValidated: summary.idempotenceValidated,
}, null, 2);
element<HTMLTextAreaElement>("#executionPlanOutput").value = `PRÉFLIGHT\n${summary.preflightJson}\n\nPLAN\n${summary.planJson}`;
element<HTMLTextAreaElement>("#executionSimulationOutput").value = summary.simulationJson;
element<HTMLTextAreaElement>("#executionDiagnosticsOutput").value = summary.diagnosticsJson;
}
async function executeToken2022(submit: boolean): Promise<void> {
if (running) {
return;
}
let request: DemoExecutionSplToken2022Request;
try {
request = buildToken2022Request(submit);
if (submit && !request.operatorConfirmed) {
const message = "La confirmation opérateur est obligatoire avant signature Token-2022.";
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify({ warning: message }, null, 2);
appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "token2022_failed", message });
return;
}
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "token2022_failed", message });
return;
}
setRunning(true);
element<HTMLTextAreaElement>("#executionSummaryOutput").value = "Préflight et exécution Token-2022 en cours...";
appendLog({
timestamp: new Date().toISOString(),
level: "info",
stage: submit ? "token2022_submit" : "token2022_simulation",
message: `${request.scenarioId}, source=${request.source}`,
});
try {
const summary = await invoke<DemoExecutionSplToken2022SummaryPayload>("demo_execution_spl_token2022_execute", { request });
displayToken2022Summary(summary);
appendLog({
timestamp: new Date().toISOString(),
level: summary.simulationSuccess ? "info" : "error",
stage: "token2022_completed",
message: `scenario=${summary.scenarioId}, simulation=${summary.simulationSuccess}, confirmation=${summary.confirmationStatus ?? "none"}, materializations=${summary.materializationCount}, idempotence=${summary.idempotenceValidated}`,
signature: summary.transactionSignature,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify({ error: message }, null, 2);
appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "token2022_failed", message });
frontendError("kb-app-demo-desktop.frontend.demo_execution_spl", `Token-2022 execution failed: ${message}`);
} finally {
setRunning(false);
}
}
async function loadTokenJournal(): Promise<void> {
const button = element<HTMLButtonElement>("#refreshTokenJournalButton");
button.disabled = true;
try {
const rows = await invoke<DemoSplTokenJournalRow[]>("demo_spl_token_journal", { request: tokenJournalRequest() });
renderTokenJournal(rows);
appendLog({
timestamp: new Date().toISOString(),
level: "info",
stage: "spl_token_journal",
message: `${rows.length} événement(s) matérialisé(s) chargé(s).`,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#tokenJournalDetailOutput").value = JSON.stringify({ error: message }, null, 2);
frontendError("kb-app-demo-desktop.frontend.demo_execution_spl", `Token journal loading failed: ${message}`);
} finally {
button.disabled = false;
}
}
async function generateRecipient(): Promise<void> {
try {
const payload = await invoke<DemoExecutionSolanaCoreGeneratedRecipientPayload>("demo_execution_solana_core_generate_recipient");
element<HTMLInputElement>("#executionRecipientInput").value = payload.publicKey;
appendLog({
timestamp: new Date().toISOString(),
level: "info",
stage: "recipient",
message: `Adresse destinataire jetable générée : ${payload.publicKey}`,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb-app-demo-desktop.frontend.demo_execution_spl", `Recipient generation failed: ${message}`);
}
}
async function cancelExecution(): Promise<void> {
try {
const accepted = await invoke<boolean>("demo_execution_solana_core_cancel");
appendLog({
timestamp: new Date().toISOString(),
level: accepted ? "warn" : "info",
stage: "cancel",
message: accepted ? "Demande dannulation enregistrée." : "Aucune exécution active.",
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb-app-demo-desktop.frontend.demo_execution_spl", `Cancellation failed: ${message}`);
}
}
async function loadOptions(): Promise<void> {
const options = await invoke<DemoExecutionSolanaCoreOptionsPayload>("demo_execution_solana_core_options");
profileOptions = options.profiles;
const select = element<HTMLSelectElement>("#executionProfileSelect");
select.replaceChildren();
for (const profile of profileOptions) {
const option = document.createElement("option");
option.value = profile.name;
option.textContent = profile.name;
option.selected = profile.name === options.defaultProfileName;
select.append(option);
}
element<HTMLInputElement>("#executionLamportsInput").value = String(options.defaultTransferLamports);
updateProfileHelp();
setRunning(options.running);
if (profileOptions.length === 0) {
element<HTMLButtonElement>("#simulateExecutionButton").disabled = true;
element<HTMLButtonElement>("#submitExecutionButton").disabled = true;
element<HTMLButtonElement>("#simulateMemoButton").disabled = true;
element<HTMLButtonElement>("#submitMemoButton").disabled = true;
element<HTMLButtonElement>("#simulateTokenButton").disabled = true;
element<HTMLButtonElement>("#submitTokenButton").disabled = true;
element<HTMLButtonElement>("#deriveAtaButton").disabled = true;
element<HTMLButtonElement>("#simulateAtaButton").disabled = true;
element<HTMLButtonElement>("#submitAtaButton").disabled = true;
element<HTMLButtonElement>("#simulateToken2022Button").disabled = true;
element<HTMLButtonElement>("#submitToken2022Button").disabled = true;
element<HTMLButtonElement>("#loadToken2022FixtureButton").disabled = true;
}
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb-app-demo-desktop.frontend.demo_execution_spl");
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", "execution demo window loaded");
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(item => new bootstrap.Tooltip(item));
element<HTMLSelectElement>("#executionProfileSelect").addEventListener("change", updateProfileHelp);
element<HTMLButtonElement>("#generateRecipientButton").addEventListener("click", () => {
void generateRecipient();
});
element<HTMLButtonElement>("#simulateExecutionButton").addEventListener("click", () => {
void execute(false);
});
element<HTMLButtonElement>("#submitExecutionButton").addEventListener("click", () => {
void execute(true);
});
element<HTMLButtonElement>("#simulateMemoButton").addEventListener("click", () => {
void executeMemo(false);
});
element<HTMLButtonElement>("#submitMemoButton").addEventListener("click", () => {
void executeMemo(true);
});
element<HTMLButtonElement>("#simulateTokenButton").addEventListener("click", () => {
void executeToken(false);
});
element<HTMLButtonElement>("#submitTokenButton").addEventListener("click", () => {
void executeToken(true);
});
element<HTMLButtonElement>("#loadToken2022FixtureButton").addEventListener("click", () => {
void loadToken2022Fixture();
});
element<HTMLSelectElement>("#token2022ScenarioSelect").addEventListener("change", applyToken2022Fixture);
element<HTMLButtonElement>("#simulateToken2022Button").addEventListener("click", () => {
void executeToken2022(false);
});
element<HTMLButtonElement>("#submitToken2022Button").addEventListener("click", () => {
void executeToken2022(true);
});
element<HTMLButtonElement>("#refreshTokenJournalButton").addEventListener("click", () => {
void loadTokenJournal();
});
element<HTMLButtonElement>("#deriveAtaButton").addEventListener("click", () => {
void deriveAta();
});
element<HTMLButtonElement>("#simulateAtaButton").addEventListener("click", () => {
void executeAta(false);
});
element<HTMLButtonElement>("#submitAtaButton").addEventListener("click", () => {
void executeAta(true);
});
element<HTMLButtonElement>("#refreshAtaJournalButton").addEventListener("click", () => {
void loadAtaJournal();
});
element<HTMLButtonElement>("#cancelExecutionButton").addEventListener("click", () => {
void cancelExecution();
});
element<HTMLButtonElement>("#clearExecutionLogButton").addEventListener("click", () => {
logLines.length = 0;
element<HTMLTextAreaElement>("#executionLogOutput").value = "";
});
void listen<DemoExecutionSolanaCoreProgressPayload>("demo-execution-solana-core-progress", event => {
appendLog(event.payload);
});
void loadOptions().catch(caughtError => {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "options", message });
frontendError("kb-app-demo-desktop.frontend.demo_execution_spl", `Options loading failed: ${message}`);
});
});

View File

@@ -78,6 +78,8 @@ if (openDemoBackfillLink) {
const pipelineDemoLinks: ReadonlyArray<readonly [string, string]> = [
["openDemoCoreExtractionLink", "open_demo_core_extraction_window"],
["openDemoDecodeReplayLink", "open_demo_decode_replay_window"],
["openDemoExecutionSolanaCoreLink", "open_demo_execution_solana_core_window"],
["openDemoExecutionSplLink", "open_demo_execution_spl_window"],
];
for (const [elementId, command] of pipelineDemoLinks) {
const link = document.querySelector<HTMLAnchorElement>(`#${elementId}`);