v0.1.0-pre.053
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: kb-app-demo-desktop/Cargo.toml
|
||||
# version: 3
|
||||
# version: 4
|
||||
|
||||
[package]
|
||||
name = "kb-app-demo-desktop"
|
||||
@@ -25,15 +25,18 @@ chrono.workspace = true
|
||||
fs2.workspace = true
|
||||
kb-config = { path = "../kb-config" }
|
||||
kb-core = { path = "../kb-core" }
|
||||
kb-lib = { path = "../kb-lib" }
|
||||
kb-logging = { path = "../kb-logging" }
|
||||
kb-lib = { path = "../kb-lib" }
|
||||
kb-program-ids = { path = "../kb-program-ids" }
|
||||
kb-store = { path = "../kb-store" }
|
||||
kb-wallet = { path = "../kb-wallet" }
|
||||
kb-pipeline = { path = "../kb-pipeline" }
|
||||
kb-pipeline-demo-scenarios = { path = "../kb-pipeline-demo-scenarios" }
|
||||
kb-onchain-transport = { path = "../kb-onchain-transport" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
spl-associated-token-account-interface.workspace = true
|
||||
solana-pubkey.workspace = true
|
||||
rustls.workspace = true
|
||||
tauri.workspace = true
|
||||
tauri-plugin-tracing.workspace = true
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
"demo_sql_replay_candidates",
|
||||
"demo_config",
|
||||
"demo_core_extraction",
|
||||
"demo_decode_replay"
|
||||
"demo_decode_replay",
|
||||
"demo_execution_solana_core",
|
||||
"demo_execution_spl"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
|
||||
73
kb-app-demo-desktop/frontend/demo_execution_solana_core.html
Normal file
73
kb-app-demo-desktop/frontend/demo_execution_solana_core.html
Normal 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>
|
||||
433
kb-app-demo-desktop/frontend/demo_execution_spl.html
Normal file
433
kb-app-demo-desktop/frontend/demo_execution_spl.html
Normal 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 n’est 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 n’est 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 d’idempotence 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 l’option 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 l’autorité avant toute simulation. L’envoi 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`);}});
|
||||
881
kb-app-demo-desktop/frontend/ts/demo_execution_spl.ts
Normal file
881
kb-app-demo-desktop/frontend/ts/demo_execution_spl.ts
Normal 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", "L’autorité"),
|
||||
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("L’autorité 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 d’annulation 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}`);
|
||||
});
|
||||
});
|
||||
@@ -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}`);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/app_state.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Shared Tauri application state and startup initialization.
|
||||
|
||||
@@ -21,6 +21,8 @@ pub(crate) struct AppState {
|
||||
demo_decode_replay_running: std::sync::atomic::AtomicBool,
|
||||
demo_decode_replay_cancel_requested: std::sync::atomic::AtomicBool,
|
||||
demo_decode_replay_campaign_id: std::sync::Mutex<std::option::Option<std::string::String>>,
|
||||
demo_execution_solana_core_running: std::sync::atomic::AtomicBool,
|
||||
demo_execution_solana_core_cancel_requested: std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl crate::AppState {
|
||||
@@ -60,6 +62,8 @@ impl crate::AppState {
|
||||
demo_decode_replay_running: std::sync::atomic::AtomicBool::new(false),
|
||||
demo_decode_replay_cancel_requested: std::sync::atomic::AtomicBool::new(false),
|
||||
demo_decode_replay_campaign_id: std::sync::Mutex::new(std::option::Option::None),
|
||||
demo_execution_solana_core_running: std::sync::atomic::AtomicBool::new(false),
|
||||
demo_execution_solana_core_cancel_requested: std::sync::atomic::AtomicBool::new(false),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -152,6 +156,18 @@ impl crate::AppState {
|
||||
return &self.demo_decode_replay_campaign_id;
|
||||
}
|
||||
|
||||
/// Returns the shared execution running flag used by Solana and SPL windows.
|
||||
pub(crate) fn demo_execution_solana_core_running(&self) -> &std::sync::atomic::AtomicBool {
|
||||
return &self.demo_execution_solana_core_running;
|
||||
}
|
||||
|
||||
/// Returns the shared execution cancellation flag used by Solana and SPL windows.
|
||||
pub(crate) fn demo_execution_solana_core_cancel_requested(
|
||||
&self,
|
||||
) -> &std::sync::atomic::AtomicBool {
|
||||
return &self.demo_execution_solana_core_cancel_requested;
|
||||
}
|
||||
|
||||
/// Returns the number of logging routes held by the logging guard.
|
||||
pub(crate) fn logging_route_count(&self) -> usize {
|
||||
let lock_result = self.logging_guard.lock();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/demo_decode_replay.rs
|
||||
// version: 24
|
||||
// version: 25
|
||||
|
||||
//! Tauri commands and UI payloads for contextual instruction decode replay.
|
||||
|
||||
@@ -370,6 +370,7 @@ pub(crate) fn available_decoders()
|
||||
-> std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> {
|
||||
return std::vec![
|
||||
std::sync::Arc::new(kb_lib::DcSolanaCoreDecoder),
|
||||
std::sync::Arc::new(kb_lib::DcMetadataMetaplexTokenMetadataDecoder),
|
||||
std::sync::Arc::new(kb_lib::DcSplAssociatedTokenAccountDecoder,),
|
||||
std::sync::Arc::new(kb_lib::DcSplElgamalRegistryDecoder),
|
||||
std::sync::Arc::new(kb_lib::DcSplMemoDecoder),
|
||||
@@ -716,9 +717,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_memo_token_2022_elgamal_classic_token_and_ata_decoders_are_registered() {
|
||||
fn native_memo_token2022_elgamal_classic_token_and_ata_decoders_are_registered() {
|
||||
let decoders = crate::available_decoders();
|
||||
assert_eq!(decoders.len(), 6);
|
||||
assert_eq!(decoders.len(), 7);
|
||||
let names = decoders
|
||||
.iter()
|
||||
.map(|decoder| return decoder.identity().name)
|
||||
@@ -726,12 +727,13 @@ mod tests {
|
||||
assert_eq!(
|
||||
names,
|
||||
std::vec![
|
||||
"kb_decoder_metadata_metaplex_token_metadata".to_string(),
|
||||
"solana_native_classifier".to_string(),
|
||||
"spl_associated_token_account".to_string(),
|
||||
"spl_elgamal_registry".to_string(),
|
||||
"spl_memo".to_string(),
|
||||
"spl_token".to_string(),
|
||||
"spl_token_2022".to_string(),
|
||||
"spl_token2022".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
669
kb-app-demo-desktop/src/demo_execution_solana_core.rs
Normal file
669
kb-app-demo-desktop/src/demo_execution_solana_core.rs
Normal file
@@ -0,0 +1,669 @@
|
||||
// file: kb-app-demo-desktop/src/demo_execution_solana_core.rs
|
||||
// version: 5
|
||||
|
||||
//! Tauri adapter for bounded Solana Core execution on Devnet.
|
||||
|
||||
use tauri::Emitter; // rust-rules: trait-import
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// One Devnet execution profile exposed to the demo window.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreProfileOption.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSolanaCoreProfileOption {
|
||||
/// Stable profile name.
|
||||
pub(crate) name: std::string::String,
|
||||
/// Non-secret temporary wallet alias.
|
||||
pub(crate) wallet_alias: std::string::String,
|
||||
/// Maximum Devnet spend allowed by the profile.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) max_spend_lamports: u64,
|
||||
/// Maximum faucet request allowed by the profile.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) max_airdrop_lamports: u64,
|
||||
/// Whether signed Devnet submission is enabled.
|
||||
pub(crate) send_enabled: bool,
|
||||
/// Whether explicit operator confirmation is required.
|
||||
pub(crate) require_operator_confirmation: bool,
|
||||
}
|
||||
|
||||
/// Initial options shown by the Solana Core execution demo.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreOptionsPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSolanaCoreOptionsPayload {
|
||||
/// Devnet profiles compatible with the execution laboratory.
|
||||
pub(crate) profiles: std::vec::Vec<crate::DemoExecutionSolanaCoreProfileOption>,
|
||||
/// Suggested profile name.
|
||||
pub(crate) default_profile_name: std::option::Option<std::string::String>,
|
||||
/// Default transfer amount suitable for a new zero-data account on Devnet.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) default_transfer_lamports: u64,
|
||||
/// Whether one execution is currently running.
|
||||
pub(crate) running: bool,
|
||||
}
|
||||
|
||||
/// Request sent by the execution demo.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSolanaCoreRequest {
|
||||
/// Selected Devnet profile.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Recipient public key.
|
||||
pub(crate) recipient: std::string::String,
|
||||
/// Lamports transferred by the System instruction.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) lamports: u64,
|
||||
/// Optional bounded faucet request when the source wallet is underfunded.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) airdrop_lamports: u64,
|
||||
/// Whether the transaction should be signed and submitted after simulation.
|
||||
pub(crate) submit: bool,
|
||||
/// Explicit operator confirmation for signed submission.
|
||||
pub(crate) operator_confirmed: bool,
|
||||
/// Whether post-execution core/decode outputs should be force-replayed.
|
||||
pub(crate) force_post_validation_replay: bool,
|
||||
/// Whether compatible materializers should run after decode replay.
|
||||
pub(crate) materialize_after_decode: bool,
|
||||
}
|
||||
|
||||
/// Request sent by the Memo v4 Devnet execution panel.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionMemoRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMemoRequest {
|
||||
/// Selected Devnet profile.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Exact UTF-8 Memo payload.
|
||||
pub(crate) message: std::string::String,
|
||||
/// Whether the wallet is supplied as a readonly Memo signer account.
|
||||
pub(crate) include_wallet_as_memo_signer: bool,
|
||||
/// Whether the transaction should be signed and submitted after simulation.
|
||||
pub(crate) submit: bool,
|
||||
/// Explicit operator confirmation for signed submission.
|
||||
pub(crate) operator_confirmed: bool,
|
||||
/// Whether post-execution core and first decode outputs should be force-replayed.
|
||||
pub(crate) force_post_validation_replay: bool,
|
||||
}
|
||||
|
||||
/// Public key generated for a disposable Devnet recipient.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreGeneratedRecipientPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSolanaCoreGeneratedRecipientPayload {
|
||||
/// Base58 public key. The private key is not persisted or exposed.
|
||||
pub(crate) public_key: std::string::String,
|
||||
}
|
||||
|
||||
/// Progress event emitted during execution and post-validation.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreProgressPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSolanaCoreProgressPayload {
|
||||
/// RFC 3339 timestamp.
|
||||
pub(crate) timestamp: std::string::String,
|
||||
/// Stable severity code.
|
||||
pub(crate) level: std::string::String,
|
||||
/// Stable stage code.
|
||||
pub(crate) stage: std::string::String,
|
||||
/// Human-readable message.
|
||||
pub(crate) message: std::string::String,
|
||||
/// Transaction signature when already available.
|
||||
pub(crate) signature: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
/// UI-safe result of one execution orchestration.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreSummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSolanaCoreSummaryPayload {
|
||||
/// Profile used by the execution.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Classified cluster code.
|
||||
pub(crate) cluster: std::string::String,
|
||||
/// Genesis hash returned by the endpoint.
|
||||
pub(crate) genesis_hash: std::string::String,
|
||||
/// Source wallet public key.
|
||||
pub(crate) wallet_public_key: std::string::String,
|
||||
/// Recipient public key.
|
||||
pub(crate) recipient: std::string::String,
|
||||
/// Whether the recipient account existed before execution.
|
||||
pub(crate) recipient_existed_before: bool,
|
||||
/// Recipient balance before execution when present.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) recipient_balance_before_lamports: std::option::Option<u64>,
|
||||
/// Rent-exempt minimum required for a new zero-data account.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) recipient_minimum_balance_lamports: u64,
|
||||
/// Source balance before optional funding.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) balance_before_lamports: u64,
|
||||
/// Source balance after optional funding.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) balance_after_funding_lamports: u64,
|
||||
/// Estimated transaction fee.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) fee_lamports: std::option::Option<u64>,
|
||||
/// Whether exact simulation succeeded.
|
||||
pub(crate) simulation_success: bool,
|
||||
/// Runtime simulation error when present.
|
||||
pub(crate) simulation_error: std::option::Option<std::string::String>,
|
||||
/// Runtime simulation logs.
|
||||
pub(crate) simulation_logs: std::vec::Vec<std::string::String>,
|
||||
/// Faucet signature when funding was requested.
|
||||
pub(crate) airdrop_signature: std::option::Option<std::string::String>,
|
||||
/// Submitted transaction signature.
|
||||
pub(crate) transaction_signature: std::option::Option<std::string::String>,
|
||||
/// Terminal confirmation status.
|
||||
pub(crate) confirmation_status: std::option::Option<std::string::String>,
|
||||
/// Canonical rows inserted after submission.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) canonical_inserted: std::option::Option<u64>,
|
||||
/// Core transactions extracted after submission.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) core_extracted: std::option::Option<u64>,
|
||||
/// Contextual decode inputs completed after submission.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) decode_completed: std::option::Option<u64>,
|
||||
/// Contextual decode input failures after submission.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) decode_failed_inputs: std::option::Option<u64>,
|
||||
/// Pretty JSON representation of the exact execution plan.
|
||||
pub(crate) plan_json: std::string::String,
|
||||
/// Pretty JSON representation of the simulation result.
|
||||
pub(crate) simulation_json: std::string::String,
|
||||
/// Pretty JSON representation of confirmation and post-validation diagnostics.
|
||||
pub(crate) diagnostics_json: std::string::String,
|
||||
}
|
||||
|
||||
/// UI-safe result of one Memo v4 Devnet execution orchestration.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionMemoSummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMemoSummaryPayload {
|
||||
/// Profile used by the execution.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Classified cluster code.
|
||||
pub(crate) cluster: std::string::String,
|
||||
/// Genesis hash returned by the endpoint.
|
||||
pub(crate) genesis_hash: std::string::String,
|
||||
/// Source wallet public key.
|
||||
pub(crate) wallet_public_key: std::string::String,
|
||||
/// Exact wallet balance rendered without JSON integer precision loss.
|
||||
pub(crate) balance_lamports: std::string::String,
|
||||
/// Estimated transaction fee rendered without JSON integer precision loss.
|
||||
pub(crate) fee_lamports: std::option::Option<std::string::String>,
|
||||
/// Whether exact simulation succeeded.
|
||||
pub(crate) simulation_success: bool,
|
||||
/// Runtime simulation error when present.
|
||||
pub(crate) simulation_error: std::option::Option<std::string::String>,
|
||||
/// Submitted transaction signature.
|
||||
pub(crate) transaction_signature: std::option::Option<std::string::String>,
|
||||
/// Terminal confirmation status.
|
||||
pub(crate) confirmation_status: std::option::Option<std::string::String>,
|
||||
/// Whether canonical hydration was validated.
|
||||
pub(crate) canonical_inserted: bool,
|
||||
/// Whether core extraction was validated.
|
||||
pub(crate) core_extracted: bool,
|
||||
/// Whether Memo decode replay was validated.
|
||||
pub(crate) decode_replayed: bool,
|
||||
/// Whether the committed transaction annotation was validated.
|
||||
pub(crate) materialized: bool,
|
||||
/// Number of exact annotation rows returned by the bounded query.
|
||||
pub(crate) annotation_count: u32,
|
||||
/// Whether the second replay produced no failures or new materialized output.
|
||||
pub(crate) idempotence_validated: bool,
|
||||
/// Pretty JSON representation of the exact execution plan.
|
||||
pub(crate) plan_json: std::string::String,
|
||||
/// Pretty JSON representation of the simulation result.
|
||||
pub(crate) simulation_json: std::string::String,
|
||||
/// Pretty JSON representation of annotations and post-validation diagnostics.
|
||||
pub(crate) diagnostics_json: std::string::String,
|
||||
}
|
||||
|
||||
pub(crate) struct DemoExecutionSolanaCoreObserver<'a> {
|
||||
pub(crate) app_handle: tauri::AppHandle,
|
||||
pub(crate) cancel_requested: &'a std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl crate::DemoExecutionSolanaCoreObserver<'_> {
|
||||
fn emit(
|
||||
&self,
|
||||
timestamp: std::string::String,
|
||||
level: &str,
|
||||
stage: &str,
|
||||
message: std::string::String,
|
||||
signature: std::option::Option<std::string::String>,
|
||||
) {
|
||||
let payload = crate::DemoExecutionSolanaCoreProgressPayload {
|
||||
timestamp,
|
||||
level: level.to_string(),
|
||||
stage: stage.to_string(),
|
||||
message,
|
||||
signature,
|
||||
};
|
||||
let emit_result = self.app_handle.emit_to(
|
||||
"demo_execution_solana_core",
|
||||
"demo-execution-solana-core-progress",
|
||||
payload,
|
||||
);
|
||||
if let std::result::Result::Err(error) = emit_result {
|
||||
tracing::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "emit_execution_progress",
|
||||
error = %error,
|
||||
"cannot emit Solana Core execution progress"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn cancelled(&self) -> bool {
|
||||
return self.cancel_requested.load(std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
impl kb_pipeline::BackfillObserver for crate::DemoExecutionSolanaCoreObserver<'_> {
|
||||
fn on_progress(&self, event: &kb_pipeline::BackfillProgressEvent) {
|
||||
self.emit(
|
||||
event.timestamp.clone(),
|
||||
event.level.code(),
|
||||
"canonical_insert",
|
||||
event.message.clone(),
|
||||
std::option::Option::None,
|
||||
);
|
||||
}
|
||||
|
||||
fn is_cancelled(&self) -> bool {
|
||||
return self.cancelled();
|
||||
}
|
||||
}
|
||||
|
||||
impl kb_pipeline::CoreExtractionObserver for crate::DemoExecutionSolanaCoreObserver<'_> {
|
||||
fn on_progress(&self, event: &kb_pipeline::CoreExtractionProgressEvent) {
|
||||
self.emit(
|
||||
event.timestamp.clone(),
|
||||
event.level.code(),
|
||||
"core_extraction",
|
||||
event.message.clone(),
|
||||
std::option::Option::None,
|
||||
);
|
||||
}
|
||||
|
||||
fn is_cancelled(&self) -> bool {
|
||||
return self.cancelled();
|
||||
}
|
||||
}
|
||||
|
||||
impl kb_pipeline::DecodeReplayObserver for crate::DemoExecutionSolanaCoreObserver<'_> {
|
||||
fn on_progress(&self, event: &kb_pipeline::DecodeReplayProgressEvent) {
|
||||
self.emit(
|
||||
event.timestamp.clone(),
|
||||
event.level.code(),
|
||||
"decode_replay",
|
||||
event.message.clone(),
|
||||
std::option::Option::None,
|
||||
);
|
||||
}
|
||||
|
||||
fn is_cancelled(&self) -> bool {
|
||||
return self.cancelled();
|
||||
}
|
||||
}
|
||||
|
||||
impl kb_pipeline_demo_scenarios::SolanaExecutionObserver
|
||||
for crate::DemoExecutionSolanaCoreObserver<'_>
|
||||
{
|
||||
fn on_execution_progress(
|
||||
&self,
|
||||
event: &kb_pipeline_demo_scenarios::SolanaExecutionProgressEvent,
|
||||
) {
|
||||
self.emit(
|
||||
event.timestamp.clone(),
|
||||
event.level.code(),
|
||||
event.stage.as_str(),
|
||||
event.message.clone(),
|
||||
event.signature.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
fn is_execution_cancelled(&self) -> bool {
|
||||
return self.cancelled();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DemoExecutionSolanaCoreRunGuard<'a> {
|
||||
pub(crate) running: &'a std::sync::atomic::AtomicBool,
|
||||
}
|
||||
impl std::ops::Drop for crate::DemoExecutionSolanaCoreRunGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.running.store(false, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn devnet_profile_options(
|
||||
config: &kb_config::AppConfig,
|
||||
) -> std::vec::Vec<crate::DemoExecutionSolanaCoreProfileOption> {
|
||||
let mut output = std::vec::Vec::new();
|
||||
for profile in &config.profiles {
|
||||
if profile.wallet.cluster != "devnet"
|
||||
|| !profile.wallet.temporary_wallet_enabled
|
||||
|| !profile.wallet.temporary_wallet_persist
|
||||
{
|
||||
continue;
|
||||
}
|
||||
output.push(crate::DemoExecutionSolanaCoreProfileOption {
|
||||
name: profile.name.clone(),
|
||||
wallet_alias: profile.wallet.temporary_wallet_alias.clone(),
|
||||
max_spend_lamports: profile.execution.devnet_max_spend_lamports,
|
||||
max_airdrop_lamports: profile.execution.devnet_airdrop_max_lamports,
|
||||
send_enabled: profile.wallet.devnet_send_enabled,
|
||||
require_operator_confirmation: profile.execution.require_operator_confirmation,
|
||||
});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
pub(crate) fn select_devnet_profile(
|
||||
config: &kb_config::AppConfig,
|
||||
profile_name: &str,
|
||||
) -> std::result::Result<kb_config::ProfileConfig, std::string::String> {
|
||||
for profile in &config.profiles {
|
||||
if profile.name == profile_name
|
||||
&& profile.wallet.cluster == "devnet"
|
||||
&& profile.wallet.temporary_wallet_enabled
|
||||
&& profile.wallet.temporary_wallet_persist
|
||||
{
|
||||
return std::result::Result::Ok(profile.clone());
|
||||
}
|
||||
}
|
||||
return std::result::Result::Err(format!(
|
||||
"Devnet execution profile '{profile_name}' is unavailable"
|
||||
));
|
||||
}
|
||||
|
||||
pub(crate) fn demo_execution_solana_core_summary_payload(
|
||||
summary: kb_pipeline_demo_scenarios::DevnetSystemTransferSummary,
|
||||
) -> crate::DemoExecutionSolanaCoreSummaryPayload {
|
||||
let plan_json = crate::pretty_json(&summary.plan);
|
||||
let simulation_json = crate::pretty_json(&summary.simulation);
|
||||
let transaction_signature =
|
||||
summary.send_result.as_ref().map(|value| return value.signature.0.clone());
|
||||
let confirmation_status = summary.confirmation.as_ref().map(|value| {
|
||||
return crate::confirmation_status_code(value.status);
|
||||
});
|
||||
let canonical_inserted = summary.backfill.as_ref().map(|value| return value.canonical_inserted);
|
||||
let core_extracted = summary.core_extraction.as_ref().map(|value| return value.extracted);
|
||||
let decode_completed = summary.decode_replay.as_ref().map(|value| return value.completed);
|
||||
let decode_failed_inputs =
|
||||
summary.decode_replay.as_ref().map(|value| return value.failed_inputs);
|
||||
let diagnostics_value = serde_json::json!({
|
||||
"airdropConfirmation": summary.airdrop_confirmation.as_ref(),
|
||||
"confirmation": summary.confirmation.as_ref(),
|
||||
"postExecution": summary.post_execution.as_ref(),
|
||||
"backfill": summary.backfill.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"canonicalInserted": value.canonical_inserted,
|
||||
"canonicalSkipped": value.canonical_skipped,
|
||||
"existingSkipped": value.existing_skipped,
|
||||
"missing": value.missing,
|
||||
"failed": value.failed
|
||||
});
|
||||
}),
|
||||
"coreExtraction": summary.core_extraction.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"selected": value.selected,
|
||||
"extracted": value.extracted,
|
||||
"skipped": value.skipped,
|
||||
"failed": value.failed
|
||||
});
|
||||
}),
|
||||
"decodeReplay": summary.decode_replay.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"campaignId": value.campaign_id,
|
||||
"selected": value.selected,
|
||||
"completed": value.completed,
|
||||
"unmatched": value.unmatched,
|
||||
"failedInputs": value.failed_inputs,
|
||||
"cancelled": value.cancelled
|
||||
});
|
||||
})
|
||||
});
|
||||
let diagnostics_json = crate::pretty_json(&diagnostics_value);
|
||||
let cluster = crate::execution_cluster_code(summary.cluster);
|
||||
return crate::DemoExecutionSolanaCoreSummaryPayload {
|
||||
profile_name: summary.profile_name,
|
||||
cluster,
|
||||
genesis_hash: summary.genesis_hash,
|
||||
wallet_public_key: summary.wallet.public_key,
|
||||
recipient: summary.recipient.0,
|
||||
recipient_existed_before: summary.recipient_existed_before,
|
||||
recipient_balance_before_lamports: summary.recipient_balance_before_lamports,
|
||||
recipient_minimum_balance_lamports: summary.recipient_minimum_balance_lamports,
|
||||
balance_before_lamports: summary.balance_before_lamports,
|
||||
balance_after_funding_lamports: summary.balance_after_funding_lamports,
|
||||
fee_lamports: summary.fee.fee_lamports,
|
||||
simulation_success: summary.simulation.success,
|
||||
simulation_error: summary.simulation.error,
|
||||
simulation_logs: summary.simulation.logs,
|
||||
airdrop_signature: summary.airdrop_signature.map(|value| return value.0),
|
||||
transaction_signature,
|
||||
confirmation_status,
|
||||
canonical_inserted,
|
||||
core_extracted,
|
||||
decode_completed,
|
||||
decode_failed_inputs,
|
||||
plan_json,
|
||||
simulation_json,
|
||||
diagnostics_json,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn demo_execution_solana_core_memo_summary_payload(
|
||||
summary: kb_pipeline_demo_scenarios::DevnetMemoExecutionSummary,
|
||||
) -> crate::DemoExecutionMemoSummaryPayload {
|
||||
let plan_json = crate::pretty_json(&summary.plan);
|
||||
let simulation_json = crate::pretty_json(&summary.simulation);
|
||||
let transaction_signature =
|
||||
summary.send_result.as_ref().map(|value| return value.signature.0.clone());
|
||||
let confirmation_status = summary.confirmation.as_ref().map(|value| {
|
||||
return crate::confirmation_status_code(value.status);
|
||||
});
|
||||
let canonical_inserted = summary
|
||||
.post_execution
|
||||
.as_ref()
|
||||
.is_some_and(|value| return value.canonical_inserted);
|
||||
let core_extracted =
|
||||
summary.post_execution.as_ref().is_some_and(|value| return value.core_extracted);
|
||||
let decode_replayed = summary
|
||||
.post_execution
|
||||
.as_ref()
|
||||
.is_some_and(|value| return value.decode_replayed);
|
||||
let materialized =
|
||||
summary.post_execution.as_ref().is_some_and(|value| return value.materialized);
|
||||
let idempotence_validated = summary.idempotence_replay.as_ref().is_some_and(|replay| {
|
||||
return replay.failed_inputs == 0
|
||||
&& !replay.cancelled
|
||||
&& replay.processors.iter().all(|processor| {
|
||||
return processor.failed == 0
|
||||
&& processor.materialized_outputs == 0
|
||||
&& processor.materialization_refused == 0;
|
||||
})
|
||||
&& replay.processors.iter().map(|processor| return processor.skipped).sum::<u64>()
|
||||
>= 1;
|
||||
});
|
||||
let annotation_count = match u32::try_from(summary.annotations.len()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => u32::MAX,
|
||||
};
|
||||
let diagnostics_value = serde_json::json!({
|
||||
"confirmation": summary.confirmation.as_ref(),
|
||||
"postExecution": summary.post_execution.as_ref(),
|
||||
"backfill": summary.backfill.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"canonicalInserted": value.canonical_inserted,
|
||||
"canonicalSkipped": value.canonical_skipped,
|
||||
"existingSkipped": value.existing_skipped,
|
||||
"missing": value.missing,
|
||||
"failed": value.failed
|
||||
});
|
||||
}),
|
||||
"coreExtraction": summary.core_extraction.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"selected": value.selected,
|
||||
"extracted": value.extracted,
|
||||
"skipped": value.skipped,
|
||||
"failed": value.failed
|
||||
});
|
||||
}),
|
||||
"decodeReplay": summary.decode_replay.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"campaignId": value.campaign_id,
|
||||
"selected": value.selected,
|
||||
"completed": value.completed,
|
||||
"failedInputs": value.failed_inputs,
|
||||
"processors": value.processors.iter().map(|processor| {
|
||||
return serde_json::json!({
|
||||
"name": processor.processor_name,
|
||||
"version": processor.processor_version,
|
||||
"decoded": processor.decoded,
|
||||
"skipped": processor.skipped,
|
||||
"failed": processor.failed,
|
||||
"materializedOutputs": processor.materialized_outputs,
|
||||
"materializationRefused": processor.materialization_refused
|
||||
});
|
||||
}).collect::<std::vec::Vec<_>>()
|
||||
});
|
||||
}),
|
||||
"idempotenceReplay": summary.idempotence_replay.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"campaignId": value.campaign_id,
|
||||
"selected": value.selected,
|
||||
"completed": value.completed,
|
||||
"failedInputs": value.failed_inputs,
|
||||
"processors": value.processors.iter().map(|processor| {
|
||||
return serde_json::json!({
|
||||
"name": processor.processor_name,
|
||||
"version": processor.processor_version,
|
||||
"decoded": processor.decoded,
|
||||
"skipped": processor.skipped,
|
||||
"failed": processor.failed,
|
||||
"materializedOutputs": processor.materialized_outputs,
|
||||
"materializationRefused": processor.materialization_refused
|
||||
});
|
||||
}).collect::<std::vec::Vec<_>>()
|
||||
});
|
||||
}),
|
||||
"annotations": summary.annotations
|
||||
});
|
||||
let diagnostics_json = crate::pretty_json(&diagnostics_value);
|
||||
return crate::DemoExecutionMemoSummaryPayload {
|
||||
profile_name: summary.profile_name,
|
||||
cluster: crate::execution_cluster_code(summary.cluster),
|
||||
genesis_hash: summary.genesis_hash,
|
||||
wallet_public_key: summary.wallet.public_key,
|
||||
balance_lamports: summary.balance_lamports.to_string(),
|
||||
fee_lamports: summary.fee.fee_lamports.map(|value| return value.to_string()),
|
||||
simulation_success: summary.simulation.success,
|
||||
simulation_error: summary.simulation.error,
|
||||
transaction_signature,
|
||||
confirmation_status,
|
||||
canonical_inserted,
|
||||
core_extracted,
|
||||
decode_replayed,
|
||||
materialized,
|
||||
annotation_count,
|
||||
idempotence_validated,
|
||||
plan_json,
|
||||
simulation_json,
|
||||
diagnostics_json,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn execution_cluster_code(
|
||||
cluster: kb_lib::ExApiExecutionCluster,
|
||||
) -> std::string::String {
|
||||
return match cluster {
|
||||
kb_lib::ExApiExecutionCluster::Localnet => "localnet".to_string(),
|
||||
kb_lib::ExApiExecutionCluster::Devnet => "devnet".to_string(),
|
||||
kb_lib::ExApiExecutionCluster::Testnet => "testnet".to_string(),
|
||||
kb_lib::ExApiExecutionCluster::Mainnet => "mainnet".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn confirmation_status_code(
|
||||
status: kb_lib::ExApiExecutionConfirmationStatus,
|
||||
) -> std::string::String {
|
||||
return match status {
|
||||
kb_lib::ExApiExecutionConfirmationStatus::Processed => "processed".to_string(),
|
||||
kb_lib::ExApiExecutionConfirmationStatus::Confirmed => "confirmed".to_string(),
|
||||
kb_lib::ExApiExecutionConfirmationStatus::Finalized => "finalized".to_string(),
|
||||
kb_lib::ExApiExecutionConfirmationStatus::Failed => "failed".to_string(),
|
||||
kb_lib::ExApiExecutionConfirmationStatus::Expired => "expired".to_string(),
|
||||
kb_lib::ExApiExecutionConfirmationStatus::TimedOut => "timed_out".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn pretty_json<T>(value: &T) -> std::string::String
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
return match serde_json::to_string_pretty(value) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
serde_json::json!({"serializationError": error.to_string()}).to_string()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn example_config_exposes_one_devnet_execution_profile() {
|
||||
let config =
|
||||
match kb_config::parse_config_json(include_str!("../../config/example.config.json")) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
||||
};
|
||||
let profiles = crate::devnet_profile_options(&config);
|
||||
assert_eq!(profiles.len(), 1);
|
||||
assert_eq!(profiles[0].name, "local_devnet");
|
||||
assert!(profiles[0].send_enabled);
|
||||
assert!(profiles[0].max_spend_lamports >= 1_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_selection_rejects_mainnet_profiles() {
|
||||
let config =
|
||||
match kb_config::parse_config_json(include_str!("../../config/example.config.json")) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
||||
};
|
||||
assert!(crate::select_devnet_profile(&config, "local_devnet").is_ok());
|
||||
assert!(crate::select_devnet_profile(&config, "mainnet").is_err());
|
||||
}
|
||||
}
|
||||
71
kb-app-demo-desktop/src/demo_execution_spl.rs
Normal file
71
kb-app-demo-desktop/src/demo_execution_spl.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
// file: kb-app-demo-desktop/src/demo_execution_spl.rs
|
||||
// version: 3
|
||||
|
||||
//! Dedicated Tauri window for bounded SPL execution on Devnet.
|
||||
|
||||
/// Frontend payload for one independent Devnet SPL validation scenario.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_spl/DevnetSplValidationScenarioPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DevnetSplValidationScenarioPayload {
|
||||
/// Stable scenario identifier.
|
||||
pub id: std::string::String,
|
||||
/// Operator-visible label.
|
||||
pub label: std::string::String,
|
||||
/// Stable family code.
|
||||
pub family: std::string::String,
|
||||
/// Stable executor operation code.
|
||||
pub operation_code: std::string::String,
|
||||
/// Current implementation status.
|
||||
pub implementation_status: std::string::String,
|
||||
/// Whether the scenario requires proof material.
|
||||
pub proof_required: bool,
|
||||
/// Ordered required fixture variables.
|
||||
pub required_fixture_variables: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
#[test]
|
||||
fn memo_summary_keeps_lamports_as_json_strings() {
|
||||
let payload = crate::DemoExecutionMemoSummaryPayload {
|
||||
profile_name: "local_devnet".to_string(),
|
||||
cluster: "devnet".to_string(),
|
||||
genesis_hash: "EtWTRABZaYq6iMfeYKouRu166VU2xqa1".to_string(),
|
||||
wallet_public_key: kb_program_ids::SYSTEM_PROGRAM_ID.to_string(),
|
||||
balance_lamports: u64::MAX.to_string(),
|
||||
fee_lamports: std::option::Option::Some(u64::MAX.to_string()),
|
||||
simulation_success: true,
|
||||
simulation_error: std::option::Option::None,
|
||||
transaction_signature: std::option::Option::None,
|
||||
confirmation_status: std::option::Option::None,
|
||||
canonical_inserted: false,
|
||||
core_extracted: false,
|
||||
decode_replayed: false,
|
||||
materialized: false,
|
||||
annotation_count: 0,
|
||||
idempotence_validated: false,
|
||||
plan_json: "{}".to_string(),
|
||||
simulation_json: "{}".to_string(),
|
||||
diagnostics_json: "{}".to_string(),
|
||||
};
|
||||
let json = match serde_json::to_value(payload) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
assert_eq!(
|
||||
std::option::Option::Some(error.to_string()),
|
||||
std::option::Option::None,
|
||||
"Memo summary serialization failed"
|
||||
);
|
||||
return;
|
||||
},
|
||||
};
|
||||
assert!(json["balanceLamports"].is_string());
|
||||
assert!(json["feeLamports"].is_string());
|
||||
assert_eq!(json["balanceLamports"], u64::MAX.to_string());
|
||||
}
|
||||
}
|
||||
438
kb-app-demo-desktop/src/demo_spl_ata.rs
Normal file
438
kb-app-demo-desktop/src/demo_spl_ata.rs
Normal file
@@ -0,0 +1,438 @@
|
||||
// file: kb-app-demo-desktop/src/demo_spl_ata.rs
|
||||
// version: 4
|
||||
|
||||
//! Thin Tauri adapter for ATA execution, derivation and lifecycle journal reads.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// Request for one representative ATA creation on Devnet.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_ata/DemoExecutionSplAtaRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSplAtaRequest {
|
||||
/// Selected Devnet profile.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Wallet owner used by canonical derivation.
|
||||
pub(crate) wallet_owner: std::string::String,
|
||||
/// Mint account.
|
||||
pub(crate) mint: std::string::String,
|
||||
/// `classic` or `token2022`.
|
||||
pub(crate) token_program: std::string::String,
|
||||
/// `create` or `create_idempotent`.
|
||||
pub(crate) mode: std::string::String,
|
||||
/// Whether to sign and submit after simulation.
|
||||
pub(crate) submit: bool,
|
||||
/// Explicit operator confirmation.
|
||||
pub(crate) operator_confirmed: bool,
|
||||
/// Whether post-execution outputs should be force-replayed.
|
||||
pub(crate) force_post_validation_replay: bool,
|
||||
}
|
||||
|
||||
/// Request used to derive the representative wallet ATA before execution.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaDerivationRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSplAtaDerivationRequest {
|
||||
/// Selected Devnet profile.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Mint account.
|
||||
pub(crate) mint: std::string::String,
|
||||
/// `classic` or `token2022`.
|
||||
pub(crate) token_program: std::string::String,
|
||||
}
|
||||
|
||||
/// Readonly payer, wallet, Token Program and derived ATA values.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaDerivationPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSplAtaDerivationPayload {
|
||||
/// Profile wallet used as payer.
|
||||
pub(crate) payer: std::string::String,
|
||||
/// Wallet owner selected by the representative panel.
|
||||
pub(crate) wallet_owner: std::string::String,
|
||||
/// Exact Token Program ID.
|
||||
pub(crate) token_program_id: std::string::String,
|
||||
/// Canonically derived ATA.
|
||||
pub(crate) associated_token_account: std::string::String,
|
||||
}
|
||||
|
||||
/// UI-safe ATA execution result.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_ata/DemoExecutionSplAtaSummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSplAtaSummaryPayload {
|
||||
/// Stable operation code.
|
||||
pub(crate) operation: std::string::String,
|
||||
/// Profile name.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Profile wallet public key.
|
||||
pub(crate) wallet_public_key: std::string::String,
|
||||
/// Target Token Program ID.
|
||||
pub(crate) token_program_id: std::string::String,
|
||||
/// Derived ATA from the exact plan.
|
||||
pub(crate) associated_token_account: std::string::String,
|
||||
/// Wallet balance as lossless JSON text.
|
||||
pub(crate) balance_lamports: std::string::String,
|
||||
/// Fee estimate as lossless JSON text.
|
||||
pub(crate) fee_lamports: std::option::Option<std::string::String>,
|
||||
/// Aggregate stateful readiness.
|
||||
pub(crate) readiness_status: std::string::String,
|
||||
/// Exact simulation outcome.
|
||||
pub(crate) simulation_success: bool,
|
||||
/// Runtime simulation error.
|
||||
pub(crate) simulation_error: std::option::Option<std::string::String>,
|
||||
/// Submitted signature.
|
||||
pub(crate) transaction_signature: std::option::Option<std::string::String>,
|
||||
/// Terminal confirmation status.
|
||||
pub(crate) confirmation_status: std::option::Option<std::string::String>,
|
||||
/// Number of ATA-owned materialized facts.
|
||||
pub(crate) materialization_count: u32,
|
||||
/// Whether the second replay created no output.
|
||||
pub(crate) idempotence_validated: bool,
|
||||
/// Exact plan JSON.
|
||||
pub(crate) plan_json: std::string::String,
|
||||
/// Stateful readiness JSON.
|
||||
pub(crate) readiness_json: std::string::String,
|
||||
/// Simulation JSON.
|
||||
pub(crate) simulation_json: std::string::String,
|
||||
/// Confirmation and replay diagnostics JSON.
|
||||
pub(crate) diagnostics_json: std::string::String,
|
||||
}
|
||||
|
||||
/// Bounded ATA lifecycle journal request.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaJournalRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSplAtaJournalRequest {
|
||||
/// Selected Devnet profile.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Optional partial signature.
|
||||
pub(crate) signature_contains: std::option::Option<std::string::String>,
|
||||
/// Optional exact mint.
|
||||
pub(crate) mint: std::option::Option<std::string::String>,
|
||||
/// Optional exact ATA.
|
||||
pub(crate) associated_token_account: std::option::Option<std::string::String>,
|
||||
/// Optional exact materialized operation.
|
||||
pub(crate) operation: std::option::Option<std::string::String>,
|
||||
/// Maximum returned rows.
|
||||
pub(crate) limit: u32,
|
||||
}
|
||||
|
||||
/// One UI-safe ATA lifecycle journal row.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaJournalRow.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSplAtaJournalRow {
|
||||
/// Transaction signature.
|
||||
pub(crate) signature: std::string::String,
|
||||
/// Slot as lossless JSON text.
|
||||
pub(crate) slot: std::string::String,
|
||||
/// Materialized family.
|
||||
pub(crate) family: std::string::String,
|
||||
/// Lifecycle operation.
|
||||
pub(crate) operation: std::option::Option<std::string::String>,
|
||||
/// Exact mint when present.
|
||||
pub(crate) mint: std::option::Option<std::string::String>,
|
||||
/// Exact ATA when present.
|
||||
pub(crate) associated_token_account: std::option::Option<std::string::String>,
|
||||
/// Full bounded payload JSON.
|
||||
pub(crate) payload_json: std::string::String,
|
||||
}
|
||||
|
||||
pub(crate) async fn load_profile_wallet(
|
||||
profile: &kb_config::ProfileConfig,
|
||||
) -> std::result::Result<kb_wallet::TemporaryWallet, std::string::String> {
|
||||
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
|
||||
let directory = if configured.is_absolute() {
|
||||
configured
|
||||
} else {
|
||||
crate::workspace_root_dir().join(configured)
|
||||
};
|
||||
let store = match kb_wallet::TemporaryWalletStore::new(directory) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let alias = match kb_wallet::WalletAlias::parse(profile.wallet.temporary_wallet_alias.clone()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
return match store.load_or_create(alias).await {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn parse_token_program(
|
||||
value: &str,
|
||||
) -> std::result::Result<kb_lib::ExSplAssociatedTokenProgram, std::string::String> {
|
||||
return match value.trim() {
|
||||
"classic" => std::result::Result::Ok(kb_lib::ExSplAssociatedTokenProgram::Classic),
|
||||
"token2022" => std::result::Result::Ok(kb_lib::ExSplAssociatedTokenProgram::Token2022),
|
||||
_ => std::result::Result::Err("Token Program must be classic or token2022".to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn derive_ata(
|
||||
wallet: &str,
|
||||
mint: &str,
|
||||
token_program: &str,
|
||||
) -> std::result::Result<std::string::String, std::string::String> {
|
||||
let wallet: solana_pubkey::Pubkey = match wallet.parse() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!("invalid wallet: {error}"));
|
||||
},
|
||||
};
|
||||
let mint: solana_pubkey::Pubkey = match mint.parse() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!("invalid mint: {error}"));
|
||||
},
|
||||
};
|
||||
let token_program: solana_pubkey::Pubkey = match token_program.parse() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!("invalid Token Program: {error}"));
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok(
|
||||
spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(
|
||||
&wallet,
|
||||
&mint,
|
||||
&token_program,
|
||||
)
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn demo_spl_ata_summary_payload(
|
||||
summary: kb_pipeline_demo_scenarios::DevnetSplAssociatedTokenAccountExecutionSummary,
|
||||
) -> crate::DemoExecutionSplAtaSummaryPayload {
|
||||
let (associated_token_account, token_program_id) = match summary.plan.instructions.first() {
|
||||
std::option::Option::Some(instruction) => {
|
||||
let ata = match instruction.accounts.get(1) {
|
||||
std::option::Option::Some(account) => account.pubkey.0.clone(),
|
||||
std::option::Option::None => std::string::String::new(),
|
||||
};
|
||||
let token_program = match instruction.accounts.last() {
|
||||
std::option::Option::Some(account) => account.pubkey.0.clone(),
|
||||
std::option::Option::None => std::string::String::new(),
|
||||
};
|
||||
(ata, token_program)
|
||||
},
|
||||
std::option::Option::None => (std::string::String::new(), std::string::String::new()),
|
||||
};
|
||||
let transaction_signature =
|
||||
summary.send_result.as_ref().map(|value| return value.signature.0.clone());
|
||||
let confirmation_status = summary.confirmation.as_ref().map(|value| {
|
||||
return crate::confirmation_status_code(value.status);
|
||||
});
|
||||
let idempotence_validated = summary.idempotence_replay.as_ref().is_some_and(|replay| {
|
||||
return replay.failed_inputs == 0
|
||||
&& replay.processors.iter().all(|processor| {
|
||||
return processor.failed == 0
|
||||
&& processor.materialized_outputs == 0
|
||||
&& processor.materialization_refused == 0;
|
||||
});
|
||||
});
|
||||
let materialization_count = match u32::try_from(summary.materializations.len()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => u32::MAX,
|
||||
};
|
||||
let diagnostics = serde_json::json!({
|
||||
"confirmation": summary.confirmation.as_ref(),
|
||||
"postStateValidation": summary.post_state_validation.as_ref(),
|
||||
"postExecution": summary.post_execution.as_ref(),
|
||||
"backfill": summary.backfill.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"candidatesCompleted": value.candidates_completed,
|
||||
"candidatesCancelled": value.candidates_cancelled,
|
||||
"candidatesNotStarted": value.candidates_not_started,
|
||||
"canonicalInserted": value.canonical_inserted,
|
||||
"canonicalSkipped": value.canonical_skipped,
|
||||
"existingSkipped": value.existing_skipped,
|
||||
"missing": value.missing,
|
||||
"failed": value.failed
|
||||
});
|
||||
}),
|
||||
"coreExtraction": summary.core_extraction.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"selected": value.selected,
|
||||
"extracted": value.extracted,
|
||||
"skipped": value.skipped,
|
||||
"failed": value.failed,
|
||||
"cancelled": value.cancelled
|
||||
});
|
||||
}),
|
||||
"decodeReplay": summary.decode_replay.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"campaignId": value.campaign_id,
|
||||
"selected": value.selected,
|
||||
"completed": value.completed,
|
||||
"unmatched": value.unmatched,
|
||||
"failedInputs": value.failed_inputs,
|
||||
"cancelled": value.cancelled
|
||||
});
|
||||
}),
|
||||
"idempotenceReplay": summary.idempotence_replay.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"campaignId": value.campaign_id,
|
||||
"selected": value.selected,
|
||||
"completed": value.completed,
|
||||
"unmatched": value.unmatched,
|
||||
"failedInputs": value.failed_inputs,
|
||||
"cancelled": value.cancelled,
|
||||
"materializedOutputs": value.processors.iter().map(|processor| {
|
||||
return processor.materialized_outputs;
|
||||
}).sum::<u64>(),
|
||||
"materializationRefused": value.processors.iter().map(|processor| {
|
||||
return processor.materialization_refused;
|
||||
}).sum::<u64>()
|
||||
});
|
||||
}),
|
||||
"materializations": summary.materializations.iter().map(|value| {
|
||||
return serde_json::json!({
|
||||
"processorName": value.processor_name,
|
||||
"outputKey": value.output_key,
|
||||
"family": value.materialized_family,
|
||||
"signature": value.signature,
|
||||
"slot": value.slot.to_string(),
|
||||
"payload": value.payload_json
|
||||
});
|
||||
}).collect::<std::vec::Vec<_>>()
|
||||
});
|
||||
let plan_json = crate::pretty_json(&summary.plan);
|
||||
let readiness_json = crate::pretty_json(&summary.stateful_readiness);
|
||||
let simulation_json = crate::pretty_json(&summary.simulation);
|
||||
let diagnostics_json = crate::pretty_json(&diagnostics);
|
||||
return crate::DemoExecutionSplAtaSummaryPayload {
|
||||
operation: summary.stateful_readiness.operation_code,
|
||||
profile_name: summary.profile_name,
|
||||
wallet_public_key: summary.wallet.public_key,
|
||||
token_program_id,
|
||||
associated_token_account,
|
||||
balance_lamports: summary.balance_lamports.to_string(),
|
||||
fee_lamports: summary.fee.fee_lamports.map(|value| return value.to_string()),
|
||||
readiness_status: match summary.stateful_readiness.status {
|
||||
kb_pipeline::SplAssociatedTokenAccountStatefulReadinessStatus::Ready => {
|
||||
"ready".to_string()
|
||||
},
|
||||
kb_pipeline::SplAssociatedTokenAccountStatefulReadinessStatus::Blocked => {
|
||||
"blocked".to_string()
|
||||
},
|
||||
},
|
||||
simulation_success: summary.simulation.success,
|
||||
simulation_error: summary.simulation.error,
|
||||
transaction_signature,
|
||||
confirmation_status,
|
||||
materialization_count,
|
||||
idempotence_validated,
|
||||
plan_json,
|
||||
readiness_json,
|
||||
simulation_json,
|
||||
diagnostics_json,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn journal_matches(
|
||||
payload: &serde_json::Value,
|
||||
request: &crate::DemoSplAtaJournalRequest,
|
||||
) -> bool {
|
||||
for (expected, key) in [
|
||||
(&request.operation, "operation"),
|
||||
(&request.mint, "mint"),
|
||||
(&request.associated_token_account, "associatedTokenAccount"),
|
||||
] {
|
||||
if expected.as_ref().is_some_and(|value| {
|
||||
return payload.get(key).and_then(serde_json::Value::as_str)
|
||||
!= std::option::Option::Some(value.as_str());
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
pub(crate) fn journal_row(row: kb_store::MaterializedEventQueryRow) -> crate::DemoSplAtaJournalRow {
|
||||
let text = |key: &str| {
|
||||
return row
|
||||
.payload_json
|
||||
.get(key)
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(std::string::ToString::to_string);
|
||||
};
|
||||
let operation = text("operation");
|
||||
let mint = text("mint");
|
||||
let associated_token_account = text("associatedTokenAccount");
|
||||
let payload_json = crate::pretty_json(&row.payload_json);
|
||||
return crate::DemoSplAtaJournalRow {
|
||||
signature: row.signature,
|
||||
slot: row.slot.to_string(),
|
||||
family: row.materialized_family,
|
||||
operation,
|
||||
mint,
|
||||
associated_token_account,
|
||||
payload_json,
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn classic_and_token2022_derivations_are_distinct_and_canonical() {
|
||||
let wallet = kb_program_ids::SYSTEM_PROGRAM_ID;
|
||||
let mint = "So11111111111111111111111111111111111111112";
|
||||
let classic = crate::derive_ata(wallet, mint, kb_program_ids::SPL_TOKEN_PROGRAM_ID)
|
||||
.unwrap_or_else(|error| panic!("classic derivation failed: {error}"));
|
||||
let token2022 = crate::derive_ata(wallet, mint, kb_program_ids::SPL_TOKEN2022_PROGRAM_ID)
|
||||
.unwrap_or_else(|error| panic!("Token-2022 derivation failed: {error}"));
|
||||
assert_eq!(classic, "aqxoAhCwpy3oB1BpNw9hL1HdLYLgPpbPjzxDrrQj3Fs");
|
||||
assert_eq!(token2022, "2sZUUBGq1i6aE47ZoxCaCW89jmYm2EXLPPmNMgMDXHMS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lifecycle_journal_filters_are_exact() {
|
||||
let request = crate::DemoSplAtaJournalRequest {
|
||||
profile_name: "local_devnet".to_string(),
|
||||
signature_contains: std::option::Option::None,
|
||||
mint: std::option::Option::Some("mint111".to_string()),
|
||||
associated_token_account: std::option::Option::Some("ata111".to_string()),
|
||||
operation: std::option::Option::Some("create_idempotent".to_string()),
|
||||
limit: 100,
|
||||
};
|
||||
assert!(crate::journal_matches(
|
||||
&serde_json::json!({
|
||||
"operation": "create_idempotent",
|
||||
"mint": "mint111",
|
||||
"associatedTokenAccount": "ata111"
|
||||
}),
|
||||
&request,
|
||||
));
|
||||
assert!(!crate::journal_matches(
|
||||
&serde_json::json!({
|
||||
"operation": "create",
|
||||
"mint": "mint111",
|
||||
"associatedTokenAccount": "ata111"
|
||||
}),
|
||||
&request,
|
||||
));
|
||||
}
|
||||
}
|
||||
493
kb-app-demo-desktop/src/demo_spl_token.rs
Normal file
493
kb-app-demo-desktop/src/demo_spl_token.rs
Normal file
@@ -0,0 +1,493 @@
|
||||
// file: kb-app-demo-desktop/src/demo_spl_token.rs
|
||||
// version: 2
|
||||
|
||||
//! Thin Tauri adapter for classic SPL Token execution and materialized journals.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// Request for one checked classic SPL Token transfer on Devnet.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_token/DemoExecutionSplTokenRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSplTokenRequest {
|
||||
/// Selected Devnet profile.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Source token account.
|
||||
pub(crate) source: std::string::String,
|
||||
/// Exact mint account carried by `TransferChecked`.
|
||||
pub(crate) mint: std::string::String,
|
||||
/// Destination token account.
|
||||
pub(crate) destination: std::string::String,
|
||||
/// Simple authority resolved by the selected profile wallet.
|
||||
pub(crate) authority: std::string::String,
|
||||
/// Exact raw amount represented as a decimal string.
|
||||
pub(crate) amount_raw: std::string::String,
|
||||
/// Expected mint decimals carried by the wire.
|
||||
pub(crate) decimals: u8,
|
||||
/// Whether the transaction should be signed and submitted after simulation.
|
||||
pub(crate) submit: bool,
|
||||
/// Explicit operator confirmation for signed submission.
|
||||
pub(crate) operator_confirmed: bool,
|
||||
/// Whether post-execution core and decode outputs should be force-replayed.
|
||||
pub(crate) force_post_validation_replay: bool,
|
||||
}
|
||||
|
||||
/// UI-safe result of one checked SPL Token transfer orchestration.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_token/DemoExecutionSplTokenSummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSplTokenSummaryPayload {
|
||||
/// Stable operation code.
|
||||
pub(crate) operation: std::string::String,
|
||||
/// Profile used by the orchestration.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Classified cluster code.
|
||||
pub(crate) cluster: std::string::String,
|
||||
/// Genesis hash returned by the endpoint.
|
||||
pub(crate) genesis_hash: std::string::String,
|
||||
/// Source wallet public key.
|
||||
pub(crate) wallet_public_key: std::string::String,
|
||||
/// Exact wallet balance rendered without JSON integer precision loss.
|
||||
pub(crate) balance_lamports: std::string::String,
|
||||
/// Estimated fee rendered without JSON integer precision loss.
|
||||
pub(crate) fee_lamports: std::option::Option<std::string::String>,
|
||||
/// Exact raw token amount.
|
||||
pub(crate) amount_raw: std::string::String,
|
||||
/// Expected mint decimals.
|
||||
pub(crate) decimals: u8,
|
||||
/// Aggregate stateful preflight status.
|
||||
pub(crate) readiness_status: std::string::String,
|
||||
/// Whether exact simulation succeeded.
|
||||
pub(crate) simulation_success: bool,
|
||||
/// Runtime simulation error when present.
|
||||
pub(crate) simulation_error: std::option::Option<std::string::String>,
|
||||
/// Submitted transaction signature.
|
||||
pub(crate) transaction_signature: std::option::Option<std::string::String>,
|
||||
/// Terminal confirmation status.
|
||||
pub(crate) confirmation_status: std::option::Option<std::string::String>,
|
||||
/// Whether canonical hydration was validated.
|
||||
pub(crate) canonical_inserted: bool,
|
||||
/// Whether core extraction was validated.
|
||||
pub(crate) core_extracted: bool,
|
||||
/// Whether SPL Token decode replay was validated.
|
||||
pub(crate) decode_replayed: bool,
|
||||
/// Number of materialized rows for the submitted instruction.
|
||||
pub(crate) materialization_count: u32,
|
||||
/// Whether the second replay produced no failure or new output.
|
||||
pub(crate) idempotence_validated: bool,
|
||||
/// Pretty JSON representation of the exact execution plan.
|
||||
pub(crate) plan_json: std::string::String,
|
||||
/// Pretty JSON representation of stateful readiness.
|
||||
pub(crate) readiness_json: std::string::String,
|
||||
/// Pretty JSON representation of the exact simulation result.
|
||||
pub(crate) simulation_json: std::string::String,
|
||||
/// Pretty JSON representation of confirmation and replay diagnostics.
|
||||
pub(crate) diagnostics_json: std::string::String,
|
||||
}
|
||||
|
||||
/// Bounded exact filters for the classic SPL Token materialized journal.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_token/DemoSplTokenJournalRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSplTokenJournalRequest {
|
||||
/// Devnet profile whose PostgreSQL store is queried.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Optional partial signature handled by the bounded store query.
|
||||
pub(crate) signature_contains: std::option::Option<std::string::String>,
|
||||
/// Optional exact mint account.
|
||||
pub(crate) mint: std::option::Option<std::string::String>,
|
||||
/// Optional exact account occurring in the ordered account list.
|
||||
pub(crate) account: std::option::Option<std::string::String>,
|
||||
/// Optional exact materialized family.
|
||||
pub(crate) family: std::option::Option<std::string::String>,
|
||||
/// Optional exact operation code without the `spl_token.` prefix.
|
||||
pub(crate) operation: std::option::Option<std::string::String>,
|
||||
/// Maximum number of rows returned after typed filtering.
|
||||
pub(crate) limit: u32,
|
||||
}
|
||||
|
||||
/// UI-safe materialized classic SPL Token journal row.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_token/DemoSplTokenJournalRow.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSplTokenJournalRow {
|
||||
/// Materializer processor name.
|
||||
pub(crate) processor_name: std::string::String,
|
||||
/// Materializer processor version.
|
||||
pub(crate) processor_version: std::string::String,
|
||||
/// Stable processor-owned output key.
|
||||
pub(crate) output_key: std::string::String,
|
||||
/// Transaction signature.
|
||||
pub(crate) signature: std::string::String,
|
||||
/// Decimal slot rendered as text to preserve JSON precision.
|
||||
pub(crate) slot: std::string::String,
|
||||
/// Materialized family code.
|
||||
pub(crate) family: std::string::String,
|
||||
/// Exact operation when present.
|
||||
pub(crate) operation: std::option::Option<std::string::String>,
|
||||
/// Exact mint account when explicitly available.
|
||||
pub(crate) mint: std::option::Option<std::string::String>,
|
||||
/// Exact raw amount when carried by the materialized fact.
|
||||
pub(crate) amount_raw: std::option::Option<std::string::String>,
|
||||
/// Stable outer or inner instruction path when present.
|
||||
pub(crate) instruction_path: std::option::Option<std::string::String>,
|
||||
/// Ordered account keys preserved by the decoder.
|
||||
pub(crate) account_keys: std::vec::Vec<std::string::String>,
|
||||
/// Complete bounded typed payload rendered as JSON text.
|
||||
pub(crate) payload_json: std::string::String,
|
||||
/// Database creation timestamp.
|
||||
pub(crate) created_at: std::string::String,
|
||||
/// Database replacement timestamp.
|
||||
pub(crate) updated_at: std::string::String,
|
||||
}
|
||||
|
||||
pub(crate) fn demo_spl_token_summary_payload(
|
||||
summary: kb_pipeline_demo_scenarios::DevnetSplTokenExecutionSummary,
|
||||
amount_raw: std::string::String,
|
||||
decimals: u8,
|
||||
) -> crate::DemoExecutionSplTokenSummaryPayload {
|
||||
let plan_json = crate::pretty_json(&summary.plan);
|
||||
let readiness_json = crate::pretty_json(&summary.stateful_readiness);
|
||||
let simulation_json = crate::pretty_json(&summary.simulation);
|
||||
let transaction_signature =
|
||||
summary.send_result.as_ref().map(|value| return value.signature.0.clone());
|
||||
let confirmation_status = summary.confirmation.as_ref().map(|value| {
|
||||
return crate::confirmation_status_code(value.status);
|
||||
});
|
||||
let post_execution = summary.post_execution.as_ref();
|
||||
let canonical_inserted = post_execution.is_some_and(|value| return value.canonical_inserted);
|
||||
let core_extracted = post_execution.is_some_and(|value| return value.core_extracted);
|
||||
let decode_replayed = post_execution.is_some_and(|value| return value.decode_replayed);
|
||||
let idempotence_validated = summary.idempotence_replay.as_ref().is_some_and(|replay| {
|
||||
return replay.failed_inputs == 0
|
||||
&& !replay.cancelled
|
||||
&& replay.processors.iter().all(|processor| {
|
||||
return processor.failed == 0
|
||||
&& processor.materialized_outputs == 0
|
||||
&& processor.materialization_refused == 0;
|
||||
});
|
||||
});
|
||||
let materialization_count = match u32::try_from(summary.materializations.len()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => u32::MAX,
|
||||
};
|
||||
let diagnostics = serde_json::json!({
|
||||
"confirmation": summary.confirmation,
|
||||
"postExecution": summary.post_execution,
|
||||
"backfill": summary.backfill.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"canonicalInserted": value.canonical_inserted,
|
||||
"canonicalSkipped": value.canonical_skipped,
|
||||
"existingSkipped": value.existing_skipped,
|
||||
"missing": value.missing,
|
||||
"failed": value.failed
|
||||
});
|
||||
}),
|
||||
"coreExtraction": summary.core_extraction.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"selected": value.selected,
|
||||
"extracted": value.extracted,
|
||||
"skipped": value.skipped,
|
||||
"failed": value.failed,
|
||||
"cancelled": value.cancelled
|
||||
});
|
||||
}),
|
||||
"decodeReplay": summary.decode_replay.as_ref().map(|value| {
|
||||
return replay_diagnostics(value);
|
||||
}),
|
||||
"idempotenceReplay": summary.idempotence_replay.as_ref().map(|value| {
|
||||
return replay_diagnostics(value);
|
||||
}),
|
||||
"materializations": summary.materializations
|
||||
});
|
||||
return crate::DemoExecutionSplTokenSummaryPayload {
|
||||
operation: summary.stateful_readiness.operation_code,
|
||||
profile_name: summary.profile_name,
|
||||
cluster: crate::execution_cluster_code(summary.cluster),
|
||||
genesis_hash: summary.genesis_hash,
|
||||
wallet_public_key: summary.wallet.public_key,
|
||||
balance_lamports: summary.balance_lamports.to_string(),
|
||||
fee_lamports: summary.fee.fee_lamports.map(|value| return value.to_string()),
|
||||
amount_raw,
|
||||
decimals,
|
||||
readiness_status: match summary.stateful_readiness.status {
|
||||
kb_pipeline::SplTokenStatefulReadinessStatus::Ready => "ready".to_string(),
|
||||
kb_pipeline::SplTokenStatefulReadinessStatus::Blocked => "blocked".to_string(),
|
||||
},
|
||||
simulation_success: summary.simulation.success,
|
||||
simulation_error: summary.simulation.error,
|
||||
transaction_signature,
|
||||
confirmation_status,
|
||||
canonical_inserted,
|
||||
core_extracted,
|
||||
decode_replayed,
|
||||
materialization_count,
|
||||
idempotence_validated,
|
||||
plan_json,
|
||||
readiness_json,
|
||||
simulation_json,
|
||||
diagnostics_json: crate::pretty_json(&diagnostics),
|
||||
};
|
||||
}
|
||||
|
||||
fn replay_diagnostics(summary: &kb_pipeline::DecodeReplaySummary) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"campaignId": summary.campaign_id,
|
||||
"selected": summary.selected,
|
||||
"completed": summary.completed,
|
||||
"failedInputs": summary.failed_inputs,
|
||||
"cancelled": summary.cancelled,
|
||||
"processors": summary.processors.iter().map(|processor| {
|
||||
return serde_json::json!({
|
||||
"name": processor.processor_name,
|
||||
"version": processor.processor_version,
|
||||
"decoded": processor.decoded,
|
||||
"skipped": processor.skipped,
|
||||
"failed": processor.failed,
|
||||
"materializedOutputs": processor.materialized_outputs,
|
||||
"materializationRefused": processor.materialization_refused
|
||||
});
|
||||
}).collect::<std::vec::Vec<_>>()
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn validate_journal_request(
|
||||
request: crate::DemoSplTokenJournalRequest,
|
||||
) -> std::result::Result<crate::DemoSplTokenJournalRequest, std::string::String> {
|
||||
if request.limit == 0 || request.limit > kb_store::MAX_MATERIALIZED_EVENT_QUERY_ROWS {
|
||||
return std::result::Result::Err(format!(
|
||||
"journal limit must be between 1 and {}",
|
||||
kb_store::MAX_MATERIALIZED_EVENT_QUERY_ROWS
|
||||
));
|
||||
}
|
||||
if request.profile_name.trim().is_empty() {
|
||||
return std::result::Result::Err("journal profile name must not be empty".to_string());
|
||||
}
|
||||
let signature_contains =
|
||||
match bounded_optional(request.signature_contains, "signature filter", 128) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mint = match bounded_optional(request.mint, "mint filter", 64) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let account = match bounded_optional(request.account, "account filter", 64) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let operation = match bounded_optional(request.operation, "operation filter", 64) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let family = match bounded_optional(request.family, "family filter", 32) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if family.as_ref().is_some_and(|value| {
|
||||
return !matches!(value.as_str(), "token_account" | "admin" | "risk");
|
||||
}) {
|
||||
return std::result::Result::Err(
|
||||
"journal family must be token_account, admin or risk".to_string(),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(crate::DemoSplTokenJournalRequest {
|
||||
profile_name: request.profile_name.trim().to_string(),
|
||||
signature_contains,
|
||||
mint,
|
||||
account,
|
||||
family,
|
||||
operation,
|
||||
limit: request.limit,
|
||||
});
|
||||
}
|
||||
|
||||
fn bounded_optional(
|
||||
value: std::option::Option<std::string::String>,
|
||||
label: &str,
|
||||
maximum_length: usize,
|
||||
) -> std::result::Result<std::option::Option<std::string::String>, std::string::String> {
|
||||
let trimmed = value.map(|text| return text.trim().to_string());
|
||||
let trimmed = trimmed.filter(|text| return !text.is_empty());
|
||||
if trimmed.as_ref().is_some_and(|text| return text.len() > maximum_length) {
|
||||
return std::result::Result::Err(format!("{label} must not exceed {maximum_length} bytes"));
|
||||
}
|
||||
return std::result::Result::Ok(trimmed);
|
||||
}
|
||||
|
||||
pub(crate) fn payload_matches(
|
||||
payload: &serde_json::Value,
|
||||
request: &crate::DemoSplTokenJournalRequest,
|
||||
) -> bool {
|
||||
if request.operation.as_ref().is_some_and(|expected| {
|
||||
return payload.get("operation").and_then(serde_json::Value::as_str)
|
||||
!= std::option::Option::Some(expected.as_str());
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
if request.mint.as_ref().is_some_and(|expected| {
|
||||
return payload_mint(payload).as_deref() != std::option::Option::Some(expected.as_str());
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
if request.account.as_ref().is_some_and(|expected| {
|
||||
return !payload_account_keys(payload).iter().any(|value| return value == expected);
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
pub(crate) fn demo_spl_token_journal_row(
|
||||
row: kb_store::MaterializedEventQueryRow,
|
||||
) -> DemoSplTokenJournalRow {
|
||||
let operation = row
|
||||
.payload_json
|
||||
.get("operation")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(std::string::ToString::to_string);
|
||||
let amount_raw = row
|
||||
.payload_json
|
||||
.get("amountRaw")
|
||||
.or_else(|| {
|
||||
return row
|
||||
.payload_json
|
||||
.get("parameters")
|
||||
.and_then(|value| return value.get("amountRaw"));
|
||||
})
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(std::string::ToString::to_string);
|
||||
let instruction_path = row
|
||||
.payload_json
|
||||
.get("instructionPath")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(std::string::ToString::to_string);
|
||||
let payload_json = crate::pretty_json(&row.payload_json);
|
||||
return DemoSplTokenJournalRow {
|
||||
processor_name: row.processor_name,
|
||||
processor_version: row.processor_version,
|
||||
output_key: row.output_key,
|
||||
signature: row.signature,
|
||||
slot: row.slot.to_string(),
|
||||
family: row.materialized_family,
|
||||
operation,
|
||||
mint: payload_mint(&row.payload_json),
|
||||
amount_raw,
|
||||
instruction_path,
|
||||
account_keys: payload_account_keys(&row.payload_json),
|
||||
payload_json,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
fn payload_mint(payload: &serde_json::Value) -> std::option::Option<std::string::String> {
|
||||
let direct = payload.get("mint").and_then(serde_json::Value::as_str);
|
||||
if let std::option::Option::Some(value) = direct {
|
||||
return std::option::Option::Some(value.to_string());
|
||||
}
|
||||
return payload.get("accounts").and_then(serde_json::Value::as_array).and_then(|rows| {
|
||||
return rows.iter().find_map(|row| {
|
||||
if row.get("role").and_then(serde_json::Value::as_str)
|
||||
!= std::option::Option::Some("mint")
|
||||
{
|
||||
return std::option::Option::None;
|
||||
}
|
||||
return row
|
||||
.get("accountKey")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(std::string::ToString::to_string);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn payload_account_keys(payload: &serde_json::Value) -> std::vec::Vec<std::string::String> {
|
||||
let values = payload.get("accounts").and_then(serde_json::Value::as_array).map(|rows| {
|
||||
return rows
|
||||
.iter()
|
||||
.filter_map(|row| {
|
||||
return row
|
||||
.get("accountKey")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(std::string::ToString::to_string);
|
||||
})
|
||||
.collect();
|
||||
});
|
||||
return match values {
|
||||
std::option::Option::Some(values) => values,
|
||||
std::option::Option::None => std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn journal_filters_are_exact_bounded_and_preserve_raw_amounts() {
|
||||
let request = crate::DemoSplTokenJournalRequest {
|
||||
profile_name: "local_devnet".to_string(),
|
||||
signature_contains: std::option::Option::None,
|
||||
mint: std::option::Option::Some("mint111".to_string()),
|
||||
account: std::option::Option::Some("source111".to_string()),
|
||||
family: std::option::Option::Some("token_account".to_string()),
|
||||
operation: std::option::Option::Some("transfer_checked".to_string()),
|
||||
limit: 25,
|
||||
};
|
||||
let payload = serde_json::json!({
|
||||
"operation": "transfer_checked",
|
||||
"mint": "mint111",
|
||||
"amountRaw": "18446744073709551615",
|
||||
"accounts": [
|
||||
{"role": "source", "accountKey": "source111"},
|
||||
{"role": "mint", "accountKey": "mint111"}
|
||||
]
|
||||
});
|
||||
assert!(crate::payload_matches(&payload, &request));
|
||||
assert_eq!(
|
||||
payload.get("amountRaw").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("18446744073709551615")
|
||||
);
|
||||
let row = crate::demo_spl_token_journal_row(kb_store::MaterializedEventQueryRow {
|
||||
processor_name: "spl_token_accounts".to_string(),
|
||||
processor_version: "0.4.4".to_string(),
|
||||
input_key: "input".to_string(),
|
||||
output_key: "output".to_string(),
|
||||
source_event_key: "event".to_string(),
|
||||
source_decoder_name: "spl_token".to_string(),
|
||||
source_decoder_version: "0.4.4".to_string(),
|
||||
signature: "signature".to_string(),
|
||||
slot: u64::MAX,
|
||||
materialized_family: "token_account".to_string(),
|
||||
payload_json: payload,
|
||||
created_at: "created".to_string(),
|
||||
updated_at: "updated".to_string(),
|
||||
});
|
||||
assert_eq!(row.slot, u64::MAX.to_string());
|
||||
assert_eq!(row.amount_raw, std::option::Option::Some(u64::MAX.to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn journal_rejects_unbounded_or_unknown_family_requests() {
|
||||
let request = crate::DemoSplTokenJournalRequest {
|
||||
profile_name: "local_devnet".to_string(),
|
||||
signature_contains: std::option::Option::None,
|
||||
mint: std::option::Option::None,
|
||||
account: std::option::Option::None,
|
||||
family: std::option::Option::Some("fee".to_string()),
|
||||
operation: std::option::Option::None,
|
||||
limit: 501,
|
||||
};
|
||||
assert!(crate::validate_journal_request(request).is_err());
|
||||
}
|
||||
}
|
||||
399
kb-app-demo-desktop/src/demo_spl_token2022.rs
Normal file
399
kb-app-demo-desktop/src/demo_spl_token2022.rs
Normal file
@@ -0,0 +1,399 @@
|
||||
// file: kb-app-demo-desktop/src/demo_spl_token2022.rs
|
||||
// version: 4
|
||||
|
||||
//! Thin Tauri adapter for independent public Token-2022 Devnet validation scenarios.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// Public values loaded from one persisted Token-2022 validation fixture.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_token2022/DemoSplToken2022FixturePayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSplToken2022FixturePayload {
|
||||
/// Fixture file used by the application.
|
||||
pub(crate) fixture_path: std::string::String,
|
||||
/// Token-2022 program ID.
|
||||
pub(crate) program_id: std::string::String,
|
||||
/// Mint account.
|
||||
pub(crate) mint: std::string::String,
|
||||
/// Source token account.
|
||||
pub(crate) source: std::string::String,
|
||||
/// Destination token account.
|
||||
pub(crate) destination: std::string::String,
|
||||
/// Dedicated empty account reserved for CloseAccount validation.
|
||||
pub(crate) close_account: std::string::String,
|
||||
/// Delegate account.
|
||||
pub(crate) delegate: std::string::String,
|
||||
/// Profile-wallet authority.
|
||||
pub(crate) authority: std::string::String,
|
||||
/// Freeze authority when configured on the mint.
|
||||
pub(crate) freeze_authority: std::string::String,
|
||||
/// Mint decimals.
|
||||
pub(crate) decimals: u8,
|
||||
/// Default raw amount for the selected scenario.
|
||||
pub(crate) default_amount_raw: std::string::String,
|
||||
}
|
||||
|
||||
/// Request for one independent public Token-2022 Devnet scenario.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_token2022/DemoExecutionSplToken2022Request.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSplToken2022Request {
|
||||
/// Selected Devnet profile.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Stable scenario identifier.
|
||||
pub(crate) scenario_id: std::string::String,
|
||||
/// Source or target token account depending on the selected operation.
|
||||
pub(crate) source: std::string::String,
|
||||
/// Exact mint account.
|
||||
pub(crate) mint: std::string::String,
|
||||
/// Destination token account or close-account lamport destination.
|
||||
pub(crate) destination: std::string::String,
|
||||
/// Delegate account used by `ApproveChecked`.
|
||||
pub(crate) delegate: std::string::String,
|
||||
/// Owner or mint authority resolved by the selected profile wallet.
|
||||
pub(crate) authority: std::string::String,
|
||||
/// Freeze authority used by freeze and thaw scenarios.
|
||||
pub(crate) freeze_authority: std::string::String,
|
||||
/// Exact raw amount represented as a decimal string.
|
||||
pub(crate) amount_raw: std::string::String,
|
||||
/// Expected mint decimals carried by checked instructions.
|
||||
pub(crate) decimals: u8,
|
||||
/// Whether the transaction should be signed and submitted after simulation.
|
||||
pub(crate) submit: bool,
|
||||
/// Explicit operator confirmation for signed submission.
|
||||
pub(crate) operator_confirmed: bool,
|
||||
/// Whether post-execution core and decode outputs should be force-replayed.
|
||||
pub(crate) force_post_validation_replay: bool,
|
||||
}
|
||||
|
||||
/// UI-safe result of one public Token-2022 Devnet orchestration.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_token2022/DemoExecutionSplToken2022SummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSplToken2022SummaryPayload {
|
||||
/// Stable scenario identifier.
|
||||
pub(crate) scenario_id: std::string::String,
|
||||
/// Stable operation code.
|
||||
pub(crate) operation: std::string::String,
|
||||
/// Profile used by the orchestration.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Classified cluster code.
|
||||
pub(crate) cluster: std::string::String,
|
||||
/// Genesis hash returned by the endpoint.
|
||||
pub(crate) genesis_hash: std::string::String,
|
||||
/// Source wallet public key.
|
||||
pub(crate) wallet_public_key: std::string::String,
|
||||
/// Exact wallet balance rendered without JSON integer precision loss.
|
||||
pub(crate) balance_lamports: std::string::String,
|
||||
/// Estimated fee rendered without JSON integer precision loss.
|
||||
pub(crate) fee_lamports: std::option::Option<std::string::String>,
|
||||
/// Whether exact simulation succeeded.
|
||||
pub(crate) simulation_success: bool,
|
||||
/// Runtime simulation error when present.
|
||||
pub(crate) simulation_error: std::option::Option<std::string::String>,
|
||||
/// Submitted transaction signature.
|
||||
pub(crate) transaction_signature: std::option::Option<std::string::String>,
|
||||
/// Terminal confirmation status.
|
||||
pub(crate) confirmation_status: std::option::Option<std::string::String>,
|
||||
/// Whether canonical hydration was validated.
|
||||
pub(crate) canonical_inserted: bool,
|
||||
/// Whether core extraction was validated.
|
||||
pub(crate) core_extracted: bool,
|
||||
/// Whether Token-2022 decode replay was validated.
|
||||
pub(crate) decode_replayed: bool,
|
||||
/// Number of materialized rows for the submitted instruction.
|
||||
pub(crate) materialization_count: u32,
|
||||
/// Whether the second replay produced no failure or new output.
|
||||
pub(crate) idempotence_validated: bool,
|
||||
/// Pretty JSON representation of the exact execution plan.
|
||||
pub(crate) plan_json: std::string::String,
|
||||
/// Pretty JSON representation of stateful preflight.
|
||||
pub(crate) preflight_json: std::string::String,
|
||||
/// Pretty JSON representation of the exact simulation result.
|
||||
pub(crate) simulation_json: std::string::String,
|
||||
/// Pretty JSON representation of confirmation and replay diagnostics.
|
||||
pub(crate) diagnostics_json: std::string::String,
|
||||
}
|
||||
|
||||
pub(crate) fn operation_from_request(
|
||||
request: &crate::DemoExecutionSplToken2022Request,
|
||||
) -> std::result::Result<kb_lib::ExSplToken2022Operation, std::string::String> {
|
||||
let authority = kb_lib::ExSplTokenAuthority {
|
||||
authority: kb_lib::MdPubkey(request.authority.trim().to_string()),
|
||||
multisig_signers: std::vec::Vec::new(),
|
||||
};
|
||||
let freeze_authority = kb_lib::ExSplTokenAuthority {
|
||||
authority: kb_lib::MdPubkey(request.freeze_authority.trim().to_string()),
|
||||
multisig_signers: std::vec::Vec::new(),
|
||||
};
|
||||
let value = match request.scenario_id.trim() {
|
||||
"token2022_mint_to_checked" => kb_lib::ExSplTokenSingleOperation::MintToChecked {
|
||||
mint: kb_lib::MdPubkey(request.mint.trim().to_string()),
|
||||
destination: kb_lib::MdPubkey(request.source.trim().to_string()),
|
||||
authority,
|
||||
amount: kb_lib::ExSplTokenAmount(request.amount_raw.trim().to_string()),
|
||||
decimals: request.decimals,
|
||||
},
|
||||
"token2022_transfer_checked" => kb_lib::ExSplTokenSingleOperation::TransferChecked {
|
||||
source: kb_lib::MdPubkey(request.source.trim().to_string()),
|
||||
mint: kb_lib::MdPubkey(request.mint.trim().to_string()),
|
||||
destination: kb_lib::MdPubkey(request.destination.trim().to_string()),
|
||||
authority,
|
||||
amount: kb_lib::ExSplTokenAmount(request.amount_raw.trim().to_string()),
|
||||
decimals: request.decimals,
|
||||
},
|
||||
"token2022_approve_checked" => kb_lib::ExSplTokenSingleOperation::ApproveChecked {
|
||||
source: kb_lib::MdPubkey(request.source.trim().to_string()),
|
||||
mint: kb_lib::MdPubkey(request.mint.trim().to_string()),
|
||||
delegate: kb_lib::MdPubkey(request.delegate.trim().to_string()),
|
||||
authority,
|
||||
amount: kb_lib::ExSplTokenAmount(request.amount_raw.trim().to_string()),
|
||||
decimals: request.decimals,
|
||||
},
|
||||
"token2022_revoke" => kb_lib::ExSplTokenSingleOperation::Revoke {
|
||||
source: kb_lib::MdPubkey(request.source.trim().to_string()),
|
||||
authority,
|
||||
},
|
||||
"token2022_burn_checked" => kb_lib::ExSplTokenSingleOperation::BurnChecked {
|
||||
source: kb_lib::MdPubkey(request.source.trim().to_string()),
|
||||
mint: kb_lib::MdPubkey(request.mint.trim().to_string()),
|
||||
authority,
|
||||
amount: kb_lib::ExSplTokenAmount(request.amount_raw.trim().to_string()),
|
||||
decimals: request.decimals,
|
||||
},
|
||||
"token2022_freeze_account" => kb_lib::ExSplTokenSingleOperation::FreezeAccount {
|
||||
account: kb_lib::MdPubkey(request.source.trim().to_string()),
|
||||
mint: kb_lib::MdPubkey(request.mint.trim().to_string()),
|
||||
authority: freeze_authority,
|
||||
},
|
||||
"token2022_thaw_account" => kb_lib::ExSplTokenSingleOperation::ThawAccount {
|
||||
account: kb_lib::MdPubkey(request.source.trim().to_string()),
|
||||
mint: kb_lib::MdPubkey(request.mint.trim().to_string()),
|
||||
authority: freeze_authority,
|
||||
},
|
||||
"token2022_close_destination" => kb_lib::ExSplTokenSingleOperation::CloseAccount {
|
||||
account: kb_lib::MdPubkey(request.source.trim().to_string()),
|
||||
destination: kb_lib::MdPubkey(request.destination.trim().to_string()),
|
||||
authority,
|
||||
},
|
||||
other => {
|
||||
return std::result::Result::Err(format!(
|
||||
"unsupported public Token-2022 Devnet scenario {other}"
|
||||
));
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok(kb_lib::ExSplToken2022Operation::Instruction {
|
||||
value: std::boxed::Box::new(value),
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn demo_spl_token2022_summary_payload(
|
||||
scenario_id: std::string::String,
|
||||
operation: std::string::String,
|
||||
summary: kb_pipeline_demo_scenarios::DevnetSplToken2022ExecutionSummary,
|
||||
) -> crate::DemoExecutionSplToken2022SummaryPayload {
|
||||
let plan_json = crate::pretty_json(&summary.plan);
|
||||
let preflight_json = crate::pretty_json(&summary.stateful_preflight);
|
||||
let simulation_json = crate::pretty_json(&summary.simulation);
|
||||
let transaction_signature =
|
||||
summary.send_result.as_ref().map(|value| return value.signature.0.clone());
|
||||
let confirmation_status = summary.confirmation.as_ref().map(|value| {
|
||||
return crate::confirmation_status_code(value.status);
|
||||
});
|
||||
let post_execution = summary.post_execution.as_ref();
|
||||
let canonical_inserted = post_execution.is_some_and(|value| return value.canonical_inserted);
|
||||
let core_extracted = post_execution.is_some_and(|value| return value.core_extracted);
|
||||
let decode_replayed = post_execution.is_some_and(|value| return value.decode_replayed);
|
||||
let idempotence_validated = summary.idempotence_replay.as_ref().is_some_and(|replay| {
|
||||
return replay.failed_inputs == 0
|
||||
&& !replay.cancelled
|
||||
&& replay.processors.iter().all(|processor| {
|
||||
return processor.failed == 0
|
||||
&& processor.materialized_outputs == 0
|
||||
&& processor.materialization_refused == 0;
|
||||
});
|
||||
});
|
||||
let materialization_count = match u32::try_from(summary.materializations.len()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => u32::MAX,
|
||||
};
|
||||
let diagnostics = serde_json::json!({
|
||||
"confirmation": summary.confirmation,
|
||||
"postExecution": summary.post_execution,
|
||||
"backfill": summary.backfill.as_ref().map(backfill_summary_json),
|
||||
"coreExtraction": summary.core_extraction.as_ref().map(core_extraction_summary_json),
|
||||
"decodeReplay": summary.decode_replay.as_ref().map(decode_replay_summary_json),
|
||||
"idempotenceReplay": summary.idempotence_replay.as_ref().map(decode_replay_summary_json),
|
||||
"materializations": summary.materializations
|
||||
});
|
||||
return crate::DemoExecutionSplToken2022SummaryPayload {
|
||||
scenario_id,
|
||||
operation,
|
||||
profile_name: summary.profile_name,
|
||||
cluster: crate::execution_cluster_code(summary.cluster),
|
||||
genesis_hash: summary.genesis_hash,
|
||||
wallet_public_key: summary.wallet.public_key,
|
||||
balance_lamports: summary.balance_lamports.to_string(),
|
||||
fee_lamports: summary.fee.fee_lamports.map(|value| return value.to_string()),
|
||||
simulation_success: summary.simulation.success,
|
||||
simulation_error: summary.simulation.error,
|
||||
transaction_signature,
|
||||
confirmation_status,
|
||||
canonical_inserted,
|
||||
core_extracted,
|
||||
decode_replayed,
|
||||
materialization_count,
|
||||
idempotence_validated,
|
||||
plan_json,
|
||||
preflight_json,
|
||||
simulation_json,
|
||||
diagnostics_json: crate::pretty_json(&diagnostics),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn parse_fixture(
|
||||
contents: &str,
|
||||
) -> std::collections::BTreeMap<std::string::String, std::string::String> {
|
||||
let mut values = std::collections::BTreeMap::new();
|
||||
for line in contents.lines() {
|
||||
let trimmed = line.trim();
|
||||
let assignment = match trimmed.strip_prefix("export ") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
let (name, raw_value) = match assignment.split_once('=') {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
let value = raw_value.trim().trim_matches('\'').trim_matches('"').to_string();
|
||||
values.insert(name.trim().to_string(), value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
fn backfill_summary_json(summary: &kb_pipeline::BackfillSummary) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"captureSessionId": summary.capture_session_id,
|
||||
"filterCode": summary.filter_code,
|
||||
"role": summary.role,
|
||||
"provider": summary.provider,
|
||||
"endpointCode": summary.endpoint_code,
|
||||
"pagesFetched": summary.pages_fetched,
|
||||
"candidatesSelected": summary.candidates_selected,
|
||||
"candidatesStarted": summary.candidates_started,
|
||||
"candidatesCompleted": summary.candidates_completed,
|
||||
"candidatesCancelled": summary.candidates_cancelled,
|
||||
"candidatesNotStarted": summary.candidates_not_started,
|
||||
"transactionsReceived": summary.transactions_received,
|
||||
"canonicalInserted": summary.canonical_inserted,
|
||||
"canonicalSkipped": summary.canonical_skipped,
|
||||
"existingSkipped": summary.existing_skipped,
|
||||
"missing": summary.missing,
|
||||
"failed": summary.failed,
|
||||
"observationsInserted": summary.observations_inserted,
|
||||
"attempts": summary.attempts,
|
||||
"cancelled": summary.cancelled,
|
||||
"resumeBeforeSignature": summary.resume_before_signature,
|
||||
"startedAt": summary.started_at,
|
||||
"finishedAt": summary.finished_at
|
||||
});
|
||||
}
|
||||
|
||||
fn core_extraction_summary_json(summary: &kb_pipeline::CoreExtractionSummary) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"processorVersion": summary.processor_version,
|
||||
"selected": summary.selected,
|
||||
"started": summary.started,
|
||||
"completed": summary.completed,
|
||||
"skipped": summary.skipped,
|
||||
"extracted": summary.extracted,
|
||||
"failed": summary.failed,
|
||||
"cancelledCandidates": summary.cancelled_candidates,
|
||||
"notStarted": summary.not_started,
|
||||
"cancelled": summary.cancelled,
|
||||
"startedAt": summary.started_at,
|
||||
"finishedAt": summary.finished_at
|
||||
});
|
||||
}
|
||||
|
||||
fn decode_replay_summary_json(summary: &kb_pipeline::DecodeReplaySummary) -> serde_json::Value {
|
||||
let processors = summary
|
||||
.processors
|
||||
.iter()
|
||||
.map(|processor| {
|
||||
return serde_json::json!({
|
||||
"name": processor.processor_name,
|
||||
"version": processor.processor_version,
|
||||
"dispatched": processor.dispatched,
|
||||
"skipped": processor.skipped,
|
||||
"decoded": processor.decoded,
|
||||
"ignored": processor.ignored,
|
||||
"unsupported": processor.unsupported,
|
||||
"failed": processor.failed,
|
||||
"materializedOutputs": processor.materialized_outputs,
|
||||
"materializationRefused": processor.materialization_refused
|
||||
});
|
||||
})
|
||||
.collect::<std::vec::Vec<serde_json::Value>>();
|
||||
return serde_json::json!({
|
||||
"campaignId": summary.campaign_id,
|
||||
"pipelineVersion": summary.pipeline_version,
|
||||
"selected": summary.selected,
|
||||
"started": summary.started,
|
||||
"completed": summary.completed,
|
||||
"unmatched": summary.unmatched,
|
||||
"notStarted": summary.not_started,
|
||||
"failedInputs": summary.failed_inputs,
|
||||
"cancelled": summary.cancelled,
|
||||
"processors": processors,
|
||||
"startedAt": summary.started_at,
|
||||
"finishedAt": summary.finished_at
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn request(scenario_id: &str) -> crate::DemoExecutionSplToken2022Request {
|
||||
return crate::DemoExecutionSplToken2022Request {
|
||||
profile_name: "local_devnet".to_string(),
|
||||
scenario_id: scenario_id.to_string(),
|
||||
source: "source".to_string(),
|
||||
mint: "mint".to_string(),
|
||||
destination: "destination".to_string(),
|
||||
delegate: "delegate".to_string(),
|
||||
authority: "authority".to_string(),
|
||||
freeze_authority: "freeze".to_string(),
|
||||
amount_raw: "1".to_string(),
|
||||
decimals: 9,
|
||||
submit: false,
|
||||
operator_confirmed: false,
|
||||
force_post_validation_replay: true,
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_public_scenario_builds_one_typed_operation() {
|
||||
for scenario in [
|
||||
"token2022_mint_to_checked",
|
||||
"token2022_transfer_checked",
|
||||
"token2022_approve_checked",
|
||||
"token2022_revoke",
|
||||
"token2022_burn_checked",
|
||||
"token2022_freeze_account",
|
||||
"token2022_thaw_account",
|
||||
"token2022_close_destination",
|
||||
] {
|
||||
assert!(crate::operation_from_request(&request(scenario)).is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/lib.rs
|
||||
// version: 7
|
||||
// version: 9
|
||||
|
||||
//! Tauri desktop demo application for `khadhroony-bot3`.
|
||||
|
||||
@@ -13,7 +13,12 @@ mod demo_backfill;
|
||||
mod demo_config;
|
||||
mod demo_core_extraction;
|
||||
mod demo_decode_replay;
|
||||
mod demo_execution_solana_core;
|
||||
mod demo_execution_spl;
|
||||
mod demo_http;
|
||||
mod demo_spl_ata;
|
||||
mod demo_spl_token;
|
||||
mod demo_spl_token2022;
|
||||
mod demo_sql_common;
|
||||
mod demo_sql_diag;
|
||||
mod demo_sql_pg_core;
|
||||
@@ -54,6 +59,100 @@ pub(crate) use self::demo_backfill::demo_backfill_summary_payload;
|
||||
pub(crate) use self::demo_config::DemoConfigPayload;
|
||||
/// Builds the configuration payload from shared application state.
|
||||
pub(crate) use self::demo_config::demo_config_payload;
|
||||
/// Internal demo core extraction item.
|
||||
pub(crate) use self::demo_core_extraction::DemoCoreExtractionObserver;
|
||||
/// Internal demo core extraction item.
|
||||
pub(crate) use self::demo_core_extraction::DemoCoreExtractionOptionsPayload;
|
||||
/// Internal demo core extraction item.
|
||||
pub(crate) use self::demo_core_extraction::DemoCoreExtractionProgressPayload;
|
||||
/// Internal demo core extraction item.
|
||||
pub(crate) use self::demo_core_extraction::DemoCoreExtractionRequest;
|
||||
/// Internal demo core extraction item.
|
||||
pub(crate) use self::demo_core_extraction::DemoCoreExtractionRunGuard;
|
||||
/// Internal demo core extraction item.
|
||||
pub(crate) use self::demo_core_extraction::DemoCoreExtractionSummaryPayload;
|
||||
/// Internal demo core extraction item.
|
||||
pub(crate) use self::demo_core_extraction::build_demo_core_extraction_pipeline_request;
|
||||
/// Internal demo core extraction item.
|
||||
pub(crate) use self::demo_core_extraction::demo_core_extraction_summary_payload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeCoverageSummaryPayload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeDiagnosticsPayload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeProcessorSummaryPayload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeReplayDecoderOption;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeReplayObserver;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeReplayOptionsPayload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeReplayProgressPayload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeReplayRequest;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeReplayRunGuard;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeReplaySummaryPayload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoTransactionAnnotationRequest;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoTransactionAnnotationRow;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::annotation_payload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::available_decoders;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::available_materializers;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::build_demo_decode_replay_pipeline_request;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::coverage_payload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::demo_decode_replay_summary_payload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::optional_line_count;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::register_active_campaign;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::text_sample;
|
||||
/// Request sent by the Memo v4 Devnet execution panel.
|
||||
pub(crate) use self::demo_execution_solana_core::DemoExecutionMemoRequest;
|
||||
/// UI-safe result of one Memo v4 Devnet execution orchestration.
|
||||
pub(crate) use self::demo_execution_solana_core::DemoExecutionMemoSummaryPayload;
|
||||
/// Public key generated for a disposable Devnet recipient.
|
||||
pub(crate) use self::demo_execution_solana_core::DemoExecutionSolanaCoreGeneratedRecipientPayload;
|
||||
/// ????
|
||||
pub(crate) use self::demo_execution_solana_core::DemoExecutionSolanaCoreObserver;
|
||||
/// Initial options shown by the Solana Core execution demo.
|
||||
pub(crate) use self::demo_execution_solana_core::DemoExecutionSolanaCoreOptionsPayload;
|
||||
/// One Devnet execution profile exposed to the demo window.
|
||||
pub(crate) use self::demo_execution_solana_core::DemoExecutionSolanaCoreProfileOption;
|
||||
/// Progress event emitted during execution and post-validation.
|
||||
pub(crate) use self::demo_execution_solana_core::DemoExecutionSolanaCoreProgressPayload;
|
||||
/// Request sent by the execution demo.
|
||||
pub(crate) use self::demo_execution_solana_core::DemoExecutionSolanaCoreRequest;
|
||||
/// ????
|
||||
pub(crate) use self::demo_execution_solana_core::DemoExecutionSolanaCoreRunGuard;
|
||||
/// UI-safe result of one execution orchestration.
|
||||
pub(crate) use self::demo_execution_solana_core::DemoExecutionSolanaCoreSummaryPayload;
|
||||
/// ????
|
||||
pub(crate) use self::demo_execution_solana_core::confirmation_status_code;
|
||||
/// ????
|
||||
pub(crate) use self::demo_execution_solana_core::demo_execution_solana_core_memo_summary_payload;
|
||||
/// ????
|
||||
pub(crate) use self::demo_execution_solana_core::demo_execution_solana_core_summary_payload;
|
||||
/// ????
|
||||
pub(crate) use self::demo_execution_solana_core::devnet_profile_options;
|
||||
/// ????
|
||||
pub(crate) use self::demo_execution_solana_core::execution_cluster_code;
|
||||
/// ????
|
||||
pub(crate) use self::demo_execution_solana_core::pretty_json;
|
||||
/// ????
|
||||
pub(crate) use self::demo_execution_solana_core::select_devnet_profile;
|
||||
/// Frontend payload for one independent Devnet SPL validation scenario.
|
||||
pub(crate) use self::demo_execution_spl::DevnetSplValidationScenarioPayload;
|
||||
/// HTTP demo response payload.
|
||||
pub(crate) use self::demo_http::DemoHttpExecutionPayload;
|
||||
/// One selectable HTTP JSON-RPC method.
|
||||
@@ -70,12 +169,66 @@ pub(crate) use self::demo_http::build_http_method_options;
|
||||
pub(crate) use self::demo_http::build_http_role_options;
|
||||
/// Executes one raw HTTP JSON-RPC request.
|
||||
pub(crate) use self::demo_http::demo_http_execute_request_inner;
|
||||
/// Request for one representative ATA creation on Devnet.
|
||||
pub(crate) use self::demo_spl_ata::DemoExecutionSplAtaRequest;
|
||||
/// UI-safe ATA execution result.
|
||||
pub(crate) use self::demo_spl_ata::DemoExecutionSplAtaSummaryPayload;
|
||||
/// Readonly payer, wallet, Token Program and derived ATA values.
|
||||
pub(crate) use self::demo_spl_ata::DemoSplAtaDerivationPayload;
|
||||
/// Request used to derive the representative wallet ATA before execution.
|
||||
pub(crate) use self::demo_spl_ata::DemoSplAtaDerivationRequest;
|
||||
/// Bounded ATA lifecycle journal request.
|
||||
pub(crate) use self::demo_spl_ata::DemoSplAtaJournalRequest;
|
||||
/// One UI-safe ATA lifecycle journal row.
|
||||
pub(crate) use self::demo_spl_ata::DemoSplAtaJournalRow;
|
||||
/// ????
|
||||
pub(crate) use self::demo_spl_ata::demo_spl_ata_summary_payload;
|
||||
/// ????
|
||||
pub(crate) use self::demo_spl_ata::derive_ata;
|
||||
/// ????
|
||||
pub(crate) use self::demo_spl_ata::journal_matches;
|
||||
/// ????
|
||||
pub(crate) use self::demo_spl_ata::journal_row;
|
||||
/// ????
|
||||
pub(crate) use self::demo_spl_ata::load_profile_wallet;
|
||||
/// ????
|
||||
pub(crate) use self::demo_spl_ata::parse_token_program;
|
||||
/// Request for one checked classic SPL Token transfer on Devnet.
|
||||
pub(crate) use self::demo_spl_token::DemoExecutionSplTokenRequest;
|
||||
/// UI-safe result of one checked SPL Token transfer orchestration.
|
||||
pub(crate) use self::demo_spl_token::DemoExecutionSplTokenSummaryPayload;
|
||||
/// Bounded exact filters for the classic SPL Token materialized journal.
|
||||
pub(crate) use self::demo_spl_token::DemoSplTokenJournalRequest;
|
||||
/// UI-safe materialized classic SPL Token journal row.
|
||||
pub(crate) use self::demo_spl_token::DemoSplTokenJournalRow;
|
||||
/// ????
|
||||
pub(crate) use self::demo_spl_token::demo_spl_token_journal_row;
|
||||
/// Executes one checked SPL Token simulation or explicitly authorized Devnet submission.
|
||||
pub(crate) use self::demo_spl_token::demo_spl_token_summary_payload;
|
||||
/// ????
|
||||
pub(crate) use self::demo_spl_token::payload_matches;
|
||||
/// Loads a bounded, typed journal of committed classic SPL Token projections.
|
||||
pub(crate) use self::demo_spl_token::validate_journal_request;
|
||||
/// Request for one independent public Token-2022 Devnet scenario.
|
||||
pub(crate) use self::demo_spl_token2022::DemoExecutionSplToken2022Request;
|
||||
/// UI-safe result of one public Token-2022 Devnet orchestration.
|
||||
pub(crate) use self::demo_spl_token2022::DemoExecutionSplToken2022SummaryPayload;
|
||||
/// Public values loaded from one persisted Token-2022 validation fixture.
|
||||
pub(crate) use self::demo_spl_token2022::DemoSplToken2022FixturePayload;
|
||||
/// ????
|
||||
pub(crate) use self::demo_spl_token2022::demo_spl_token2022_summary_payload;
|
||||
/// ????
|
||||
pub(crate) use self::demo_spl_token2022::operation_from_request;
|
||||
/// ????
|
||||
pub(crate) use self::demo_spl_token2022::parse_fixture;
|
||||
/// UI-safe SQL table diagnostic snapshot.
|
||||
pub(crate) use self::demo_sql_common::DemoSqlTableSnapshot;
|
||||
/// Connects to the configured PostgreSQL store.
|
||||
pub(crate) use self::demo_sql_common::connect_postgres_store;
|
||||
/// Formats diagnostic statuses.
|
||||
pub(crate) use self::demo_sql_common::debug_status;
|
||||
/// Initializes the PostgreSQL schema during startup.
|
||||
pub(crate) use self::demo_sql_common::initialize_postgres_schema_for_startup;
|
||||
/// Opens or focuses one SQL demo window.
|
||||
pub(crate) use self::demo_sql_common::open_sql_demo_window;
|
||||
/// Builds PostgreSQL options from the active profile.
|
||||
@@ -154,6 +307,8 @@ pub(crate) use self::demo_ws::demo_ws_disconnect_inner;
|
||||
pub(crate) use self::demo_ws::demo_ws_status_inner;
|
||||
/// Unsubscribes one active WebSocket subscription.
|
||||
pub(crate) use self::demo_ws::demo_ws_unsubscribe_inner;
|
||||
/// Disconnects the persistent WebSocket session during application shutdown.
|
||||
pub(crate) use self::demo_ws::disconnect_demo_ws_app_state;
|
||||
/// Frontend logging payload.
|
||||
pub(crate) use self::frontend_log::FrontendLogPayload;
|
||||
/// Emits one normalized frontend log event.
|
||||
@@ -175,64 +330,6 @@ pub(crate) use self::splash::emit_splash_order;
|
||||
/// Waits for the minimum splash duration.
|
||||
pub(crate) use self::splash::wait_until_minimum;
|
||||
|
||||
/// Internal demo core extraction item.
|
||||
pub(crate) use self::demo_core_extraction::DemoCoreExtractionObserver;
|
||||
/// Internal demo core extraction item.
|
||||
pub(crate) use self::demo_core_extraction::DemoCoreExtractionOptionsPayload;
|
||||
/// Internal demo core extraction item.
|
||||
pub(crate) use self::demo_core_extraction::DemoCoreExtractionProgressPayload;
|
||||
/// Internal demo core extraction item.
|
||||
pub(crate) use self::demo_core_extraction::DemoCoreExtractionRequest;
|
||||
/// Internal demo core extraction item.
|
||||
pub(crate) use self::demo_core_extraction::DemoCoreExtractionRunGuard;
|
||||
/// Internal demo core extraction item.
|
||||
pub(crate) use self::demo_core_extraction::DemoCoreExtractionSummaryPayload;
|
||||
/// Internal demo core extraction item.
|
||||
pub(crate) use self::demo_core_extraction::build_demo_core_extraction_pipeline_request;
|
||||
/// Internal demo core extraction item.
|
||||
pub(crate) use self::demo_core_extraction::demo_core_extraction_summary_payload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeCoverageSummaryPayload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeDiagnosticsPayload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeProcessorSummaryPayload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeReplayDecoderOption;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeReplayObserver;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeReplayOptionsPayload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeReplayProgressPayload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeReplayRequest;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeReplayRunGuard;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoDecodeReplaySummaryPayload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoTransactionAnnotationRequest;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::DemoTransactionAnnotationRow;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::annotation_payload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::available_decoders;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::available_materializers;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::build_demo_decode_replay_pipeline_request;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::coverage_payload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::demo_decode_replay_summary_payload;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::optional_line_count;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::register_active_campaign;
|
||||
/// Internal demo decode replay item.
|
||||
pub(crate) use self::demo_decode_replay::text_sample;
|
||||
// Keep the canonical tracing target as the final facade export.
|
||||
/// Canonical tracing target for the desktop demo application.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
|
||||
@@ -64,6 +64,21 @@ pub fn run() -> kb_core::Result<()> {
|
||||
load_demo_sql_replay_programs,
|
||||
load_demo_sql_replay_entities,
|
||||
export_demo_sql_replay_csv,
|
||||
demo_execution_solana_core_options,
|
||||
demo_execution_solana_core_generate_recipient,
|
||||
demo_execution_solana_core_execute,
|
||||
open_demo_execution_solana_core_window,
|
||||
demo_execution_spl_memo_execute,
|
||||
demo_execution_solana_core_cancel,
|
||||
open_demo_execution_spl_window,
|
||||
demo_execution_spl_validation_scenarios,
|
||||
demo_execution_spl_token_execute,
|
||||
demo_spl_token_journal,
|
||||
demo_execution_spl_token2022_execute,
|
||||
demo_spl_token2022_fixture,
|
||||
demo_spl_ata_derive,
|
||||
demo_execution_spl_ata_execute,
|
||||
demo_spl_ata_journal,
|
||||
open_demo_core_extraction_window,
|
||||
demo_core_extraction_options,
|
||||
demo_core_extraction_execute,
|
||||
@@ -1307,3 +1322,867 @@ async fn load_demo_sql_replay_entities(
|
||||
}
|
||||
return std::result::Result::Ok(output);
|
||||
}
|
||||
|
||||
/// Opens or focuses the Solana Core execution demo window.
|
||||
#[tauri::command]
|
||||
fn open_demo_execution_solana_core_window(
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> std::result::Result<(), std::string::String> {
|
||||
let existing_window = app_handle.get_webview_window("demo_execution_solana_core");
|
||||
if let std::option::Option::Some(window) = existing_window {
|
||||
let show_result = window.show();
|
||||
if let std::result::Result::Err(error) = show_result {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
let focus_result = window.set_focus();
|
||||
if let std::result::Result::Err(error) = focus_result {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
let build_result = tauri::WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
"demo_execution_solana_core",
|
||||
tauri::WebviewUrl::App("demo_execution_solana_core.html".into()),
|
||||
)
|
||||
.title("Khadhroony Bot3 - Exécution Solana Devnet")
|
||||
.inner_size(1360.0, 920.0)
|
||||
.min_inner_size(1040.0, 700.0)
|
||||
.resizable(true)
|
||||
.visible(true)
|
||||
.build();
|
||||
return match build_result {
|
||||
std::result::Result::Ok(window) => {
|
||||
let focus_result = window.set_focus();
|
||||
if let std::result::Result::Err(error) = focus_result {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns compatible Devnet profiles and conservative defaults.
|
||||
#[tauri::command]
|
||||
fn demo_execution_solana_core_options(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> crate::DemoExecutionSolanaCoreOptionsPayload {
|
||||
let profiles = crate::devnet_profile_options(state.app_config());
|
||||
let default_profile_name = profiles.first().map(|profile| return profile.name.clone());
|
||||
return crate::DemoExecutionSolanaCoreOptionsPayload {
|
||||
profiles,
|
||||
default_profile_name,
|
||||
default_transfer_lamports: 1_000_000,
|
||||
running: state
|
||||
.demo_execution_solana_core_running()
|
||||
.load(std::sync::atomic::Ordering::Acquire),
|
||||
};
|
||||
}
|
||||
|
||||
/// Generates a disposable recipient public key without exposing its private key.
|
||||
#[tauri::command]
|
||||
fn demo_execution_solana_core_generate_recipient()
|
||||
-> std::result::Result<crate::DemoExecutionSolanaCoreGeneratedRecipientPayload, std::string::String>
|
||||
{
|
||||
let alias = match kb_wallet::WalletAlias::parse("demo-devnet-recipient") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let wallet = kb_wallet::TemporaryWallet::generate(alias);
|
||||
return std::result::Result::Ok(crate::DemoExecutionSolanaCoreGeneratedRecipientPayload {
|
||||
public_key: wallet.public_key(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Executes one bounded System transfer simulation or Devnet submission.
|
||||
#[tauri::command]
|
||||
#[allow(clippy::question_mark_used)]
|
||||
async fn demo_execution_solana_core_execute(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoExecutionSolanaCoreRequest,
|
||||
) -> std::result::Result<crate::DemoExecutionSolanaCoreSummaryPayload, std::string::String> {
|
||||
let acquire_result = state.demo_execution_solana_core_running().compare_exchange(
|
||||
false,
|
||||
true,
|
||||
std::sync::atomic::Ordering::AcqRel,
|
||||
std::sync::atomic::Ordering::Acquire,
|
||||
);
|
||||
if acquire_result.is_err() {
|
||||
return std::result::Result::Err("a Solana Core execution is already running".to_string());
|
||||
}
|
||||
let _run_guard = crate::DemoExecutionSolanaCoreRunGuard {
|
||||
running: state.demo_execution_solana_core_running(),
|
||||
};
|
||||
state
|
||||
.demo_execution_solana_core_cancel_requested()
|
||||
.store(false, std::sync::atomic::Ordering::Release);
|
||||
let profile =
|
||||
match crate::select_devnet_profile(state.app_config(), request.profile_name.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let http_pool = match kb_onchain_transport::HttpEndpointPool::from_profile(&profile) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let store = match crate::connect_postgres_store(&profile).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let initialize_result = store.initialize_store_schema().await;
|
||||
if let std::result::Result::Err(error) = initialize_result {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
let mut pipeline_request = kb_pipeline_demo_scenarios::DevnetSystemTransferRequest::new(
|
||||
format!("demo-execution-{}", chrono::Utc::now().timestamp_micros()),
|
||||
kb_lib::MdPubkey(request.recipient.trim().to_string()),
|
||||
request.lamports,
|
||||
);
|
||||
pipeline_request.airdrop_lamports = request.airdrop_lamports;
|
||||
pipeline_request.submit = request.submit;
|
||||
pipeline_request.operator_confirmed = request.operator_confirmed;
|
||||
pipeline_request.post_validation_max_retries = 20;
|
||||
pipeline_request.force_post_validation_replay = request.force_post_validation_replay;
|
||||
pipeline_request.materialize_after_decode = request.materialize_after_decode;
|
||||
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
|
||||
std::vec![std::sync::Arc::new(kb_lib::DcSolanaCoreDecoder)];
|
||||
let materializers: std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>> =
|
||||
if request.materialize_after_decode {
|
||||
std::vec![
|
||||
std::sync::Arc::new(kb_lib::MtAdminMaterializer),
|
||||
std::sync::Arc::new(kb_lib::MtComplianceAuditMaterializer,),
|
||||
std::sync::Arc::new(kb_lib::MtLifecycleMaterializer),
|
||||
std::sync::Arc::new(kb_lib::MtStakingMaterializer),
|
||||
]
|
||||
} else {
|
||||
std::vec::Vec::new()
|
||||
};
|
||||
let observer = crate::DemoExecutionSolanaCoreObserver {
|
||||
app_handle,
|
||||
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
||||
};
|
||||
let workspace_root = crate::workspace_root_dir();
|
||||
let summary = match kb_pipeline_demo_scenarios::execute_devnet_system_transfer(
|
||||
&http_pool,
|
||||
&store,
|
||||
&profile,
|
||||
workspace_root.as_path(),
|
||||
&pipeline_request,
|
||||
decoders.as_slice(),
|
||||
materializers.as_slice(),
|
||||
&observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
return std::result::Result::Ok(crate::demo_execution_solana_core_summary_payload(summary));
|
||||
}
|
||||
|
||||
/// Executes one Memo v4 simulation or explicitly authorized Devnet submission.
|
||||
#[tauri::command]
|
||||
#[allow(clippy::question_mark_used)]
|
||||
async fn demo_execution_spl_memo_execute(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoExecutionMemoRequest,
|
||||
) -> std::result::Result<crate::DemoExecutionMemoSummaryPayload, std::string::String> {
|
||||
let acquire_result = state.demo_execution_solana_core_running().compare_exchange(
|
||||
false,
|
||||
true,
|
||||
std::sync::atomic::Ordering::AcqRel,
|
||||
std::sync::atomic::Ordering::Acquire,
|
||||
);
|
||||
if acquire_result.is_err() {
|
||||
return std::result::Result::Err("a Devnet execution is already running".to_string());
|
||||
}
|
||||
let _run_guard = crate::DemoExecutionSolanaCoreRunGuard {
|
||||
running: state.demo_execution_solana_core_running(),
|
||||
};
|
||||
state
|
||||
.demo_execution_solana_core_cancel_requested()
|
||||
.store(false, std::sync::atomic::Ordering::Release);
|
||||
let profile =
|
||||
match crate::select_devnet_profile(state.app_config(), request.profile_name.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let http_pool = match kb_onchain_transport::HttpEndpointPool::from_profile(&profile) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let store = match crate::connect_postgres_store(&profile).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
let mut pipeline_request = kb_pipeline_demo_scenarios::DevnetMemoExecutionRequest::new(
|
||||
format!("demo-memo-execution-{}", chrono::Utc::now().timestamp_micros()),
|
||||
request.message,
|
||||
);
|
||||
pipeline_request.include_wallet_as_memo_signer = request.include_wallet_as_memo_signer;
|
||||
pipeline_request.submit = request.submit;
|
||||
pipeline_request.operator_confirmed = request.operator_confirmed;
|
||||
pipeline_request.post_validation_max_retries = 20;
|
||||
pipeline_request.force_post_validation_replay = request.force_post_validation_replay;
|
||||
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
|
||||
std::vec![std::sync::Arc::new(kb_lib::DcSplMemoDecoder)];
|
||||
let materializers: std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>> =
|
||||
std::vec![std::sync::Arc::new(kb_lib::MtTransactionAnnotationMaterializer,)];
|
||||
let observer = crate::DemoExecutionSolanaCoreObserver {
|
||||
app_handle,
|
||||
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
||||
};
|
||||
let workspace_root = crate::workspace_root_dir();
|
||||
let summary = match kb_pipeline_demo_scenarios::execute_devnet_memo(
|
||||
&http_pool,
|
||||
&store,
|
||||
&profile,
|
||||
workspace_root.as_path(),
|
||||
&pipeline_request,
|
||||
decoders.as_slice(),
|
||||
materializers.as_slice(),
|
||||
&observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
return std::result::Result::Ok(crate::demo_execution_solana_core_memo_summary_payload(
|
||||
summary,
|
||||
));
|
||||
}
|
||||
|
||||
/// Requests cooperative cancellation before the next execution stage.
|
||||
#[tauri::command]
|
||||
fn demo_execution_solana_core_cancel(state: tauri::State<'_, crate::AppState>) -> bool {
|
||||
let running = state
|
||||
.demo_execution_solana_core_running()
|
||||
.load(std::sync::atomic::Ordering::Acquire);
|
||||
if !running {
|
||||
return false;
|
||||
}
|
||||
state
|
||||
.demo_execution_solana_core_cancel_requested()
|
||||
.store(true, std::sync::atomic::Ordering::Release);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Returns the complete ordered Devnet SPL validation inventory for milestone 0.4.6.
|
||||
#[tauri::command]
|
||||
fn demo_execution_spl_validation_scenarios()
|
||||
-> std::vec::Vec<crate::DevnetSplValidationScenarioPayload> {
|
||||
return kb_pipeline_demo_scenarios::devnet_spl_validation_scenarios()
|
||||
.into_iter()
|
||||
.map(|scenario| {
|
||||
return crate::DevnetSplValidationScenarioPayload {
|
||||
id: scenario.id,
|
||||
label: scenario.label,
|
||||
family: match scenario.family {
|
||||
kb_pipeline_demo_scenarios::DevnetSplValidationFamily::Token2022Public => {
|
||||
"token2022_public".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::DevnetSplValidationFamily::ElGamalRegistry => {
|
||||
"elgamal_registry".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::DevnetSplValidationFamily::Token2022Confidential => {
|
||||
"token2022_confidential".to_string()
|
||||
},
|
||||
},
|
||||
operation_code: scenario.operation_code,
|
||||
implementation_status: match scenario.implementation_status {
|
||||
kb_pipeline_demo_scenarios::DevnetSplValidationImplementationStatus::Executable => {
|
||||
"executable".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::DevnetSplValidationImplementationStatus::BackendReady => {
|
||||
"backend_ready".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::DevnetSplValidationImplementationStatus::ProofFixtureRequired => {
|
||||
"proof_fixture_required".to_string()
|
||||
},
|
||||
},
|
||||
proof_required: scenario.proof_required,
|
||||
required_fixture_variables: scenario.required_fixture_variables,
|
||||
};
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// Opens or focuses the dedicated SPL execution window.
|
||||
#[tauri::command]
|
||||
fn open_demo_execution_spl_window(
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> std::result::Result<(), std::string::String> {
|
||||
tracing::info!(target: crate::TRACING_TARGET, "open SPL Devnet execution window");
|
||||
let existing_window = app_handle.get_webview_window("demo_execution_spl");
|
||||
if let std::option::Option::Some(window) = existing_window {
|
||||
let show_result = window.show();
|
||||
if let std::result::Result::Err(error) = show_result {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
let focus_result = window.set_focus();
|
||||
if let std::result::Result::Err(error) = focus_result {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
let build_result = tauri::WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
"demo_execution_spl",
|
||||
tauri::WebviewUrl::App("demo_execution_spl.html".into()),
|
||||
)
|
||||
.title("Khadhroony Bot3 - Exécution SPL Devnet")
|
||||
.inner_size(1500.0, 960.0)
|
||||
.min_inner_size(1120.0, 720.0)
|
||||
.resizable(true)
|
||||
.visible(true)
|
||||
.build();
|
||||
return match build_result {
|
||||
std::result::Result::Ok(window) => {
|
||||
let focus_result = window.set_focus();
|
||||
if let std::result::Result::Err(error) = focus_result {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes one checked SPL Token simulation or explicitly authorized Devnet submission.
|
||||
#[tauri::command]
|
||||
#[allow(clippy::question_mark_used)]
|
||||
async fn demo_execution_spl_token_execute(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoExecutionSplTokenRequest,
|
||||
) -> std::result::Result<crate::DemoExecutionSplTokenSummaryPayload, std::string::String> {
|
||||
let acquire_result = state.demo_execution_solana_core_running().compare_exchange(
|
||||
false,
|
||||
true,
|
||||
std::sync::atomic::Ordering::AcqRel,
|
||||
std::sync::atomic::Ordering::Acquire,
|
||||
);
|
||||
if acquire_result.is_err() {
|
||||
return std::result::Result::Err("a Devnet execution is already running".to_string());
|
||||
}
|
||||
let _run_guard = crate::DemoExecutionSolanaCoreRunGuard {
|
||||
running: state.demo_execution_solana_core_running(),
|
||||
};
|
||||
state
|
||||
.demo_execution_solana_core_cancel_requested()
|
||||
.store(false, std::sync::atomic::Ordering::Release);
|
||||
let profile =
|
||||
match crate::select_devnet_profile(state.app_config(), request.profile_name.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let http_pool = match kb_onchain_transport::HttpEndpointPool::from_profile(&profile) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let store = match crate::connect_postgres_store(&profile).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
let amount_raw = request.amount_raw.trim().to_string();
|
||||
let operation = kb_lib::ExSplClassicTokenOperation::Instruction {
|
||||
value: kb_lib::ExSplClassicTokenSingleOperation::TransferChecked {
|
||||
source: kb_lib::MdPubkey(request.source.trim().to_string()),
|
||||
mint: kb_lib::MdPubkey(request.mint.trim().to_string()),
|
||||
destination: kb_lib::MdPubkey(request.destination.trim().to_string()),
|
||||
authority: kb_lib::ExSplClassicTokenAuthority {
|
||||
authority: kb_lib::MdPubkey(request.authority.trim().to_string()),
|
||||
multisig_signers: std::vec::Vec::new(),
|
||||
},
|
||||
amount: kb_lib::ExSplClassicTokenAmount(amount_raw.clone()),
|
||||
decimals: request.decimals,
|
||||
},
|
||||
};
|
||||
let mut pipeline_request = kb_pipeline_demo_scenarios::DevnetSplTokenExecutionRequest::new(
|
||||
format!("demo-spl-token-execution-{}", chrono::Utc::now().timestamp_micros()),
|
||||
operation,
|
||||
);
|
||||
pipeline_request.submit = request.submit;
|
||||
pipeline_request.operator_confirmed = request.operator_confirmed;
|
||||
pipeline_request.post_validation_max_retries = 20;
|
||||
pipeline_request.force_post_validation_replay = request.force_post_validation_replay;
|
||||
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
|
||||
std::vec![std::sync::Arc::new(kb_lib::DcSplTokenDecoder)];
|
||||
let materializers: std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>> = std::vec![
|
||||
std::sync::Arc::new(kb_lib::MtTokenAccountsMaterializer),
|
||||
std::sync::Arc::new(kb_lib::MtAdminMaterializer),
|
||||
std::sync::Arc::new(kb_lib::MtRiskMaterializer),
|
||||
];
|
||||
let observer = crate::DemoExecutionSolanaCoreObserver {
|
||||
app_handle,
|
||||
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
||||
};
|
||||
let workspace_root = crate::workspace_root_dir();
|
||||
let summary = match kb_pipeline_demo_scenarios::execute_devnet_spl_token(
|
||||
&http_pool,
|
||||
&store,
|
||||
&profile,
|
||||
workspace_root.as_path(),
|
||||
&pipeline_request,
|
||||
decoders.as_slice(),
|
||||
materializers.as_slice(),
|
||||
&observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
return std::result::Result::Ok(crate::demo_spl_token_summary_payload(
|
||||
summary,
|
||||
amount_raw,
|
||||
request.decimals,
|
||||
));
|
||||
}
|
||||
|
||||
/// Loads a bounded, typed journal of committed classic SPL Token projections.
|
||||
#[tauri::command]
|
||||
#[allow(clippy::question_mark_used)]
|
||||
async fn demo_spl_token_journal(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoSplTokenJournalRequest,
|
||||
) -> std::result::Result<std::vec::Vec<crate::DemoSplTokenJournalRow>, std::string::String> {
|
||||
let validated = match crate::validate_journal_request(request) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let profile =
|
||||
match crate::select_devnet_profile(state.app_config(), validated.profile_name.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let store = match crate::connect_postgres_store(&profile).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
let needs_payload_filter =
|
||||
validated.mint.is_some() || validated.account.is_some() || validated.operation.is_some();
|
||||
let query_limit = if needs_payload_filter {
|
||||
kb_store::MAX_MATERIALIZED_EVENT_QUERY_ROWS
|
||||
} else {
|
||||
validated.limit
|
||||
};
|
||||
let filter = match kb_store::MaterializedEventFilter::new(
|
||||
std::option::Option::None,
|
||||
validated.family.clone(),
|
||||
validated.signature_contains.clone(),
|
||||
query_limit,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let rows = match kb_store::DecodePipelineStore::list_materialized_events(&store, &filter).await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let mut output = std::vec::Vec::new();
|
||||
for row in rows {
|
||||
if row.source_decoder_name != "spl_token"
|
||||
|| !crate::payload_matches(&row.payload_json, &validated)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
output.push(crate::demo_spl_token_journal_row(row));
|
||||
if output.len() >= validated.limit as usize {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(output);
|
||||
}
|
||||
|
||||
/// Derives the representative profile wallet ATA without exposing private key material.
|
||||
#[tauri::command]
|
||||
#[allow(clippy::question_mark_used)]
|
||||
async fn demo_spl_ata_derive(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoSplAtaDerivationRequest,
|
||||
) -> std::result::Result<crate::DemoSplAtaDerivationPayload, std::string::String> {
|
||||
let profile =
|
||||
match crate::select_devnet_profile(state.app_config(), request.profile_name.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let wallet = match crate::load_profile_wallet(&profile).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let wallet_owner = wallet.public_key();
|
||||
let token_program = match crate::parse_token_program(&request.token_program) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let associated_token_account = match crate::derive_ata(
|
||||
wallet_owner.as_str(),
|
||||
request.mint.trim(),
|
||||
token_program.program_id(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::DemoSplAtaDerivationPayload {
|
||||
payer: wallet_owner.clone(),
|
||||
wallet_owner,
|
||||
token_program_id: token_program.program_id().to_string(),
|
||||
associated_token_account,
|
||||
});
|
||||
}
|
||||
|
||||
/// Executes one representative Create or CreateIdempotent ATA flow.
|
||||
#[tauri::command]
|
||||
#[allow(clippy::question_mark_used)]
|
||||
async fn demo_execution_spl_ata_execute(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoExecutionSplAtaRequest,
|
||||
) -> std::result::Result<crate::DemoExecutionSplAtaSummaryPayload, std::string::String> {
|
||||
let acquired = state.demo_execution_solana_core_running().compare_exchange(
|
||||
false,
|
||||
true,
|
||||
std::sync::atomic::Ordering::AcqRel,
|
||||
std::sync::atomic::Ordering::Acquire,
|
||||
);
|
||||
if acquired.is_err() {
|
||||
return std::result::Result::Err("a Devnet execution is already running".to_string());
|
||||
}
|
||||
let _run_guard = crate::DemoExecutionSolanaCoreRunGuard {
|
||||
running: state.demo_execution_solana_core_running(),
|
||||
};
|
||||
state
|
||||
.demo_execution_solana_core_cancel_requested()
|
||||
.store(false, std::sync::atomic::Ordering::Release);
|
||||
let profile =
|
||||
match crate::select_devnet_profile(state.app_config(), request.profile_name.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let profile_wallet = match crate::load_profile_wallet(&profile).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if request.wallet_owner.trim() != profile_wallet.public_key() {
|
||||
return std::result::Result::Err(
|
||||
"the representative ATA panel requires the profile wallet as wallet owner".to_string(),
|
||||
);
|
||||
}
|
||||
let token_program = match crate::parse_token_program(&request.token_program) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let wallet_owner = kb_lib::MdPubkey(request.wallet_owner.trim().to_string());
|
||||
let mint = kb_lib::MdPubkey(request.mint.trim().to_string());
|
||||
let operation = match request.mode.as_str() {
|
||||
"create" => kb_lib::ExSplAssociatedTokenAccountOperation::Create {
|
||||
wallet_owner,
|
||||
mint,
|
||||
token_program,
|
||||
},
|
||||
"create_idempotent" => kb_lib::ExSplAssociatedTokenAccountOperation::CreateIdempotent {
|
||||
wallet_owner,
|
||||
mint,
|
||||
token_program,
|
||||
},
|
||||
_ => {
|
||||
return std::result::Result::Err(
|
||||
"ATA mode must be create or create_idempotent".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let mut pipeline_request =
|
||||
kb_pipeline_demo_scenarios::DevnetSplAssociatedTokenAccountExecutionRequest::new(
|
||||
format!("demo-spl-ata-execution-{}", chrono::Utc::now().timestamp_micros()),
|
||||
operation,
|
||||
);
|
||||
pipeline_request.submit = request.submit;
|
||||
pipeline_request.operator_confirmed = request.operator_confirmed;
|
||||
pipeline_request.post_validation_max_retries = 20;
|
||||
pipeline_request.force_post_validation_replay = request.force_post_validation_replay;
|
||||
let http_pool = match kb_onchain_transport::HttpEndpointPool::from_profile(&profile) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let store = match crate::connect_postgres_store(&profile).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> = std::vec![
|
||||
std::sync::Arc::new(kb_lib::DcSplAssociatedTokenAccountDecoder),
|
||||
std::sync::Arc::new(kb_lib::DcSplTokenDecoder),
|
||||
];
|
||||
let materializers: std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>> = std::vec![
|
||||
std::sync::Arc::new(kb_lib::MtTokenAccountsMaterializer),
|
||||
std::sync::Arc::new(kb_lib::MtRiskMaterializer),
|
||||
];
|
||||
let observer = crate::DemoExecutionSolanaCoreObserver {
|
||||
app_handle,
|
||||
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
||||
};
|
||||
let summary = match kb_pipeline_demo_scenarios::execute_devnet_spl_associated_token_account(
|
||||
&http_pool,
|
||||
&store,
|
||||
&profile,
|
||||
crate::workspace_root_dir().as_path(),
|
||||
&pipeline_request,
|
||||
decoders.as_slice(),
|
||||
materializers.as_slice(),
|
||||
&observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
return std::result::Result::Ok(crate::demo_spl_ata_summary_payload(summary));
|
||||
}
|
||||
|
||||
/// Loads a bounded journal of ATA-owned lifecycle facts.
|
||||
#[tauri::command]
|
||||
#[allow(clippy::question_mark_used)]
|
||||
async fn demo_spl_ata_journal(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoSplAtaJournalRequest,
|
||||
) -> std::result::Result<std::vec::Vec<crate::DemoSplAtaJournalRow>, std::string::String> {
|
||||
if request.limit == 0 || request.limit > 500 {
|
||||
return std::result::Result::Err("ATA journal limit must be between 1 and 500".to_string());
|
||||
}
|
||||
let profile =
|
||||
match crate::select_devnet_profile(state.app_config(), request.profile_name.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let store = match crate::connect_postgres_store(&profile).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let filter = match kb_store::MaterializedEventFilter::new(
|
||||
std::option::Option::Some("spl_token_accounts".to_string()),
|
||||
std::option::Option::None,
|
||||
request.signature_contains.clone(),
|
||||
500,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let rows = match kb_store::DecodePipelineStore::list_materialized_events(&store, &filter).await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let mut output = std::vec::Vec::new();
|
||||
for row in rows {
|
||||
if row.source_decoder_name != "spl_associated_token_account"
|
||||
|| !crate::journal_matches(&row.payload_json, &request)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
output.push(crate::journal_row(row));
|
||||
if output.len() >= request.limit as usize {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(output);
|
||||
}
|
||||
|
||||
/// Executes one public Token-2022 simulation or explicitly authorized Devnet submission.
|
||||
#[tauri::command]
|
||||
#[allow(clippy::question_mark_used)]
|
||||
async fn demo_execution_spl_token2022_execute(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoExecutionSplToken2022Request,
|
||||
) -> std::result::Result<crate::DemoExecutionSplToken2022SummaryPayload, std::string::String> {
|
||||
let acquire_result = state.demo_execution_solana_core_running().compare_exchange(
|
||||
false,
|
||||
true,
|
||||
std::sync::atomic::Ordering::AcqRel,
|
||||
std::sync::atomic::Ordering::Acquire,
|
||||
);
|
||||
if acquire_result.is_err() {
|
||||
return std::result::Result::Err("a Devnet execution is already running".to_string());
|
||||
}
|
||||
let _run_guard = crate::DemoExecutionSolanaCoreRunGuard {
|
||||
running: state.demo_execution_solana_core_running(),
|
||||
};
|
||||
state
|
||||
.demo_execution_solana_core_cancel_requested()
|
||||
.store(false, std::sync::atomic::Ordering::Release);
|
||||
let profile =
|
||||
match crate::select_devnet_profile(state.app_config(), request.profile_name.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let http_pool = match kb_onchain_transport::HttpEndpointPool::from_profile(&profile) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let store = match crate::connect_postgres_store(&profile).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
let operation = match crate::operation_from_request(&request) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let operation_code = operation.operation_code().to_string();
|
||||
let mut pipeline_request = kb_pipeline_demo_scenarios::DevnetSplToken2022ExecutionRequest::new(
|
||||
format!("demo-spl-token-2022-execution-{}", chrono::Utc::now().timestamp_micros()),
|
||||
operation,
|
||||
);
|
||||
pipeline_request.submit = request.submit;
|
||||
pipeline_request.operator_confirmed = request.operator_confirmed;
|
||||
pipeline_request.post_validation_max_retries = 20;
|
||||
pipeline_request.force_post_validation_replay = request.force_post_validation_replay;
|
||||
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
|
||||
std::vec![std::sync::Arc::new(kb_lib::DcSplToken2022Decoder)];
|
||||
let materializers: std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>> = std::vec![
|
||||
std::sync::Arc::new(kb_lib::MtTokenAccountsMaterializer),
|
||||
std::sync::Arc::new(kb_lib::MtAdminMaterializer),
|
||||
std::sync::Arc::new(kb_lib::MtRiskMaterializer),
|
||||
std::sync::Arc::new(kb_lib::MtFeesMaterializer),
|
||||
std::sync::Arc::new(kb_lib::MtComplianceAuditMaterializer),
|
||||
];
|
||||
let observer = crate::DemoExecutionSolanaCoreObserver {
|
||||
app_handle,
|
||||
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
||||
};
|
||||
let workspace_root = crate::workspace_root_dir();
|
||||
let summary = match kb_pipeline_demo_scenarios::execute_devnet_spl_token2022(
|
||||
&http_pool,
|
||||
&store,
|
||||
&profile,
|
||||
workspace_root.as_path(),
|
||||
&pipeline_request,
|
||||
decoders.as_slice(),
|
||||
materializers.as_slice(),
|
||||
&observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
return std::result::Result::Ok(crate::demo_spl_token2022_summary_payload(
|
||||
request.scenario_id,
|
||||
operation_code,
|
||||
summary,
|
||||
));
|
||||
}
|
||||
|
||||
/// Loads public Token-2022 fixture values without reading private key bytes.
|
||||
#[tauri::command]
|
||||
fn demo_spl_token2022_fixture(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
profile_name: std::string::String,
|
||||
) -> std::result::Result<crate::DemoSplToken2022FixturePayload, std::string::String> {
|
||||
let profile = match crate::select_devnet_profile(state.app_config(), profile_name.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
|
||||
let wallet_dir = if configured.is_absolute() {
|
||||
configured
|
||||
} else {
|
||||
crate::workspace_root_dir().join(configured)
|
||||
};
|
||||
let fixture_path = wallet_dir.join("spl_token2022_validation").join("fixture.env");
|
||||
let contents = match std::fs::read_to_string(&fixture_path) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!(
|
||||
"unable to read Token-2022 fixture {}: {error}",
|
||||
fixture_path.display()
|
||||
));
|
||||
},
|
||||
};
|
||||
let values = crate::parse_fixture(contents.as_str());
|
||||
let required = |name: &str| -> std::result::Result<std::string::String, std::string::String> {
|
||||
return match values.get(name) {
|
||||
std::option::Option::Some(value) if !value.trim().is_empty() => {
|
||||
std::result::Result::Ok(value.clone())
|
||||
},
|
||||
_ => std::result::Result::Err(format!("Token-2022 fixture is missing {name}")),
|
||||
};
|
||||
};
|
||||
let program_id = match required("TOKEN_2022_PROGRAM") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mint = match required("TOKEN_2022_MINT") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let source = match required("TOKEN_2022_SOURCE") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let destination = match required("TOKEN_2022_DESTINATION") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let close_account = match required("TOKEN_2022_CLOSE_ACCOUNT") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let delegate = match required("TOKEN_2022_DELEGATE") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let authority = match required("TOKEN_2022_AUTHORITY") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let freeze_authority = match required("TOKEN_2022_FREEZE_AUTHORITY") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let decimals_text = match required("TOKEN_2022_DECIMALS") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let decimals = match decimals_text.parse::<u8>() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!("invalid TOKEN_2022_DECIMALS value: {error}"));
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok(crate::DemoSplToken2022FixturePayload {
|
||||
fixture_path: fixture_path.display().to_string(),
|
||||
program_id,
|
||||
mint,
|
||||
source,
|
||||
destination,
|
||||
close_account,
|
||||
delegate,
|
||||
authority,
|
||||
freeze_authority,
|
||||
decimals,
|
||||
default_amount_raw: values
|
||||
.get("TOKEN_2022_TRANSFER_AMOUNT_RAW")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| return "1".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/vite.config.ts
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
import { defineConfig, normalizePath } from "vite";
|
||||
import { NodePackageImporter } from "sass-embedded";
|
||||
@@ -33,6 +33,8 @@ export default defineConfig(() => ({
|
||||
"demo_config": normalizePath(resolve(__dirname, 'frontend/demo_config.html')),
|
||||
"demo_core_extraction": normalizePath(resolve(__dirname, 'frontend/demo_core_extraction.html')),
|
||||
"demo_decode_replay": normalizePath(resolve(__dirname, 'frontend/demo_decode_replay.html')),
|
||||
"demo_execution_solana_core": normalizePath(resolve(__dirname, 'frontend/demo_execution_solana_core.html')),
|
||||
"demo_execution_spl": normalizePath(resolve(__dirname, 'frontend/demo_execution_spl.html')),
|
||||
},
|
||||
output: {
|
||||
entryFileNames: 'js/[name]-[hash].js',
|
||||
|
||||
Reference in New Issue
Block a user