v0.1.0-pre.053
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# file: kb-app-demo-desktop/Cargo.toml
|
# file: kb-app-demo-desktop/Cargo.toml
|
||||||
# version: 3
|
# version: 4
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "kb-app-demo-desktop"
|
name = "kb-app-demo-desktop"
|
||||||
@@ -25,15 +25,18 @@ chrono.workspace = true
|
|||||||
fs2.workspace = true
|
fs2.workspace = true
|
||||||
kb-config = { path = "../kb-config" }
|
kb-config = { path = "../kb-config" }
|
||||||
kb-core = { path = "../kb-core" }
|
kb-core = { path = "../kb-core" }
|
||||||
kb-lib = { path = "../kb-lib" }
|
|
||||||
kb-logging = { path = "../kb-logging" }
|
kb-logging = { path = "../kb-logging" }
|
||||||
|
kb-lib = { path = "../kb-lib" }
|
||||||
kb-program-ids = { path = "../kb-program-ids" }
|
kb-program-ids = { path = "../kb-program-ids" }
|
||||||
kb-store = { path = "../kb-store" }
|
kb-store = { path = "../kb-store" }
|
||||||
|
kb-wallet = { path = "../kb-wallet" }
|
||||||
kb-pipeline = { path = "../kb-pipeline" }
|
kb-pipeline = { path = "../kb-pipeline" }
|
||||||
kb-pipeline-demo-scenarios = { path = "../kb-pipeline-demo-scenarios" }
|
kb-pipeline-demo-scenarios = { path = "../kb-pipeline-demo-scenarios" }
|
||||||
kb-onchain-transport = { path = "../kb-onchain-transport" }
|
kb-onchain-transport = { path = "../kb-onchain-transport" }
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
spl-associated-token-account-interface.workspace = true
|
||||||
|
solana-pubkey.workspace = true
|
||||||
rustls.workspace = true
|
rustls.workspace = true
|
||||||
tauri.workspace = true
|
tauri.workspace = true
|
||||||
tauri-plugin-tracing.workspace = true
|
tauri-plugin-tracing.workspace = true
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
"demo_sql_replay_candidates",
|
"demo_sql_replay_candidates",
|
||||||
"demo_config",
|
"demo_config",
|
||||||
"demo_core_extraction",
|
"demo_core_extraction",
|
||||||
"demo_decode_replay"
|
"demo_decode_replay",
|
||||||
|
"demo_execution_solana_core",
|
||||||
|
"demo_execution_spl"
|
||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"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">
|
<header class="app-header">
|
||||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||||
<div class="container my-0">
|
<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" />
|
<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 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">
|
<div class="ms-auto d-flex align-items-center gap-2">
|
||||||
<span class="badge text-bg-primary">SQL diagnostics</span>
|
<span class="badge text-bg-primary">SQL diagnostics</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||||
<div class="container py-4">
|
<div class="container py-4">
|
||||||
<div class="row g-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 shadow-sm border-0">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
|
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12 col-xl-6">
|
||||||
<div class="card shadow-sm border-0">
|
<div class="card shadow-sm border-0">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="h5 card-title mb-3">Tables raw/core connues</h2>
|
<h2 class="h5 card-title mb-3">Tables raw/core connues</h2>
|
||||||
|
|||||||
@@ -14,10 +14,10 @@
|
|||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||||
<div class="container my-0">
|
<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" />
|
<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 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">
|
<div class="ms-auto d-flex align-items-center gap-2">
|
||||||
<span class="badge text-bg-primary">PostgreSQL core</span>
|
<span class="badge text-bg-primary">PostgreSQL core</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||||
<div class="container py-4">
|
<div class="container py-4">
|
||||||
<div class="row g-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 shadow-sm border-0">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
|
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12 col-xl-6">
|
||||||
<div class="card shadow-sm border-0">
|
<div class="card shadow-sm border-0">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="h5 card-title mb-3">Tables core</h2>
|
<h2 class="h5 card-title mb-3">Tables core</h2>
|
||||||
|
|||||||
@@ -14,10 +14,10 @@
|
|||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||||
<div class="container my-0">
|
<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" />
|
<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 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">
|
<div class="ms-auto d-flex align-items-center gap-2">
|
||||||
<span class="badge text-bg-primary">PostgreSQL acquisition</span>
|
<span class="badge text-bg-primary">PostgreSQL acquisition</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||||
<div class="container py-4">
|
<div class="container py-4">
|
||||||
<div class="row g-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 shadow-sm border-0">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
|
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12 col-xl-6">
|
||||||
<div class="card shadow-sm border-0">
|
<div class="card shadow-sm border-0">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="h5 card-title mb-3">Transactions canoniques et observations</h2>
|
<h2 class="h5 card-title mb-3">Transactions canoniques et observations</h2>
|
||||||
|
|||||||
@@ -14,10 +14,10 @@
|
|||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||||
<div class="container-fluid px-4 my-0">
|
<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" />
|
<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 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">
|
<div class="ms-auto d-flex align-items-center gap-2">
|
||||||
<span id="replayCandidateProfileBadge" class="badge text-bg-info">Profil —</span>
|
<span id="replayCandidateProfileBadge" class="badge text-bg-info">Profil —</span>
|
||||||
<span id="replayCandidateStatusBadge" class="badge text-bg-secondary">Initialisation</span>
|
<span id="replayCandidateStatusBadge" class="badge text-bg-secondary">Initialisation</span>
|
||||||
|
|||||||
@@ -30,6 +30,8 @@
|
|||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li><a id="openDemoCoreExtractionLink" class="dropdown-item" href="#">Extraction core</a></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="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><hr class="dropdown-divider"></li>
|
||||||
<li><a id="openDemoSqlDiagLink" class="dropdown-item" href="#">SQL diagnostics</a></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>
|
<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]> = [
|
const pipelineDemoLinks: ReadonlyArray<readonly [string, string]> = [
|
||||||
["openDemoCoreExtractionLink", "open_demo_core_extraction_window"],
|
["openDemoCoreExtractionLink", "open_demo_core_extraction_window"],
|
||||||
["openDemoDecodeReplayLink", "open_demo_decode_replay_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) {
|
for (const [elementId, command] of pipelineDemoLinks) {
|
||||||
const link = document.querySelector<HTMLAnchorElement>(`#${elementId}`);
|
const link = document.querySelector<HTMLAnchorElement>(`#${elementId}`);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/app_state.rs
|
// file: kb-app-demo-desktop/src/app_state.rs
|
||||||
// version: 5
|
// version: 6
|
||||||
|
|
||||||
//! Shared Tauri application state and startup initialization.
|
//! 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_running: std::sync::atomic::AtomicBool,
|
||||||
demo_decode_replay_cancel_requested: 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_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 {
|
impl crate::AppState {
|
||||||
@@ -60,6 +62,8 @@ impl crate::AppState {
|
|||||||
demo_decode_replay_running: std::sync::atomic::AtomicBool::new(false),
|
demo_decode_replay_running: std::sync::atomic::AtomicBool::new(false),
|
||||||
demo_decode_replay_cancel_requested: 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_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;
|
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.
|
/// Returns the number of logging routes held by the logging guard.
|
||||||
pub(crate) fn logging_route_count(&self) -> usize {
|
pub(crate) fn logging_route_count(&self) -> usize {
|
||||||
let lock_result = self.logging_guard.lock();
|
let lock_result = self.logging_guard.lock();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/demo_decode_replay.rs
|
// file: kb-app-demo-desktop/src/demo_decode_replay.rs
|
||||||
// version: 24
|
// version: 25
|
||||||
|
|
||||||
//! Tauri commands and UI payloads for contextual instruction decode replay.
|
//! 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>> {
|
-> std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> {
|
||||||
return std::vec![
|
return std::vec![
|
||||||
std::sync::Arc::new(kb_lib::DcSolanaCoreDecoder),
|
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::DcSplAssociatedTokenAccountDecoder,),
|
||||||
std::sync::Arc::new(kb_lib::DcSplElgamalRegistryDecoder),
|
std::sync::Arc::new(kb_lib::DcSplElgamalRegistryDecoder),
|
||||||
std::sync::Arc::new(kb_lib::DcSplMemoDecoder),
|
std::sync::Arc::new(kb_lib::DcSplMemoDecoder),
|
||||||
@@ -716,9 +717,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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();
|
let decoders = crate::available_decoders();
|
||||||
assert_eq!(decoders.len(), 6);
|
assert_eq!(decoders.len(), 7);
|
||||||
let names = decoders
|
let names = decoders
|
||||||
.iter()
|
.iter()
|
||||||
.map(|decoder| return decoder.identity().name)
|
.map(|decoder| return decoder.identity().name)
|
||||||
@@ -726,12 +727,13 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
names,
|
names,
|
||||||
std::vec![
|
std::vec![
|
||||||
|
"kb_decoder_metadata_metaplex_token_metadata".to_string(),
|
||||||
"solana_native_classifier".to_string(),
|
"solana_native_classifier".to_string(),
|
||||||
"spl_associated_token_account".to_string(),
|
"spl_associated_token_account".to_string(),
|
||||||
"spl_elgamal_registry".to_string(),
|
"spl_elgamal_registry".to_string(),
|
||||||
"spl_memo".to_string(),
|
"spl_memo".to_string(),
|
||||||
"spl_token".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
|
// file: kb-app-demo-desktop/src/lib.rs
|
||||||
// version: 7
|
// version: 9
|
||||||
|
|
||||||
//! Tauri desktop demo application for `khadhroony-bot3`.
|
//! Tauri desktop demo application for `khadhroony-bot3`.
|
||||||
|
|
||||||
@@ -13,7 +13,12 @@ mod demo_backfill;
|
|||||||
mod demo_config;
|
mod demo_config;
|
||||||
mod demo_core_extraction;
|
mod demo_core_extraction;
|
||||||
mod demo_decode_replay;
|
mod demo_decode_replay;
|
||||||
|
mod demo_execution_solana_core;
|
||||||
|
mod demo_execution_spl;
|
||||||
mod demo_http;
|
mod demo_http;
|
||||||
|
mod demo_spl_ata;
|
||||||
|
mod demo_spl_token;
|
||||||
|
mod demo_spl_token2022;
|
||||||
mod demo_sql_common;
|
mod demo_sql_common;
|
||||||
mod demo_sql_diag;
|
mod demo_sql_diag;
|
||||||
mod demo_sql_pg_core;
|
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;
|
pub(crate) use self::demo_config::DemoConfigPayload;
|
||||||
/// Builds the configuration payload from shared application state.
|
/// Builds the configuration payload from shared application state.
|
||||||
pub(crate) use self::demo_config::demo_config_payload;
|
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.
|
/// HTTP demo response payload.
|
||||||
pub(crate) use self::demo_http::DemoHttpExecutionPayload;
|
pub(crate) use self::demo_http::DemoHttpExecutionPayload;
|
||||||
/// One selectable HTTP JSON-RPC method.
|
/// 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;
|
pub(crate) use self::demo_http::build_http_role_options;
|
||||||
/// Executes one raw HTTP JSON-RPC request.
|
/// Executes one raw HTTP JSON-RPC request.
|
||||||
pub(crate) use self::demo_http::demo_http_execute_request_inner;
|
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.
|
/// UI-safe SQL table diagnostic snapshot.
|
||||||
pub(crate) use self::demo_sql_common::DemoSqlTableSnapshot;
|
pub(crate) use self::demo_sql_common::DemoSqlTableSnapshot;
|
||||||
/// Connects to the configured PostgreSQL store.
|
/// Connects to the configured PostgreSQL store.
|
||||||
pub(crate) use self::demo_sql_common::connect_postgres_store;
|
pub(crate) use self::demo_sql_common::connect_postgres_store;
|
||||||
/// Formats diagnostic statuses.
|
/// Formats diagnostic statuses.
|
||||||
pub(crate) use self::demo_sql_common::debug_status;
|
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.
|
/// Opens or focuses one SQL demo window.
|
||||||
pub(crate) use self::demo_sql_common::open_sql_demo_window;
|
pub(crate) use self::demo_sql_common::open_sql_demo_window;
|
||||||
/// Builds PostgreSQL options from the active profile.
|
/// 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;
|
pub(crate) use self::demo_ws::demo_ws_status_inner;
|
||||||
/// Unsubscribes one active WebSocket subscription.
|
/// Unsubscribes one active WebSocket subscription.
|
||||||
pub(crate) use self::demo_ws::demo_ws_unsubscribe_inner;
|
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.
|
/// Frontend logging payload.
|
||||||
pub(crate) use self::frontend_log::FrontendLogPayload;
|
pub(crate) use self::frontend_log::FrontendLogPayload;
|
||||||
/// Emits one normalized frontend log event.
|
/// Emits one normalized frontend log event.
|
||||||
@@ -175,64 +330,6 @@ pub(crate) use self::splash::emit_splash_order;
|
|||||||
/// Waits for the minimum splash duration.
|
/// Waits for the minimum splash duration.
|
||||||
pub(crate) use self::splash::wait_until_minimum;
|
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.
|
// Keep the canonical tracing target as the final facade export.
|
||||||
/// Canonical tracing target for the desktop demo application.
|
/// Canonical tracing target for the desktop demo application.
|
||||||
pub(crate) use self::constants::TRACING_TARGET;
|
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_programs,
|
||||||
load_demo_sql_replay_entities,
|
load_demo_sql_replay_entities,
|
||||||
export_demo_sql_replay_csv,
|
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,
|
open_demo_core_extraction_window,
|
||||||
demo_core_extraction_options,
|
demo_core_extraction_options,
|
||||||
demo_core_extraction_execute,
|
demo_core_extraction_execute,
|
||||||
@@ -1307,3 +1322,867 @@ async fn load_demo_sql_replay_entities(
|
|||||||
}
|
}
|
||||||
return std::result::Result::Ok(output);
|
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
|
// file: kb-app-demo-desktop/vite.config.ts
|
||||||
// version: 5
|
// version: 6
|
||||||
|
|
||||||
import { defineConfig, normalizePath } from "vite";
|
import { defineConfig, normalizePath } from "vite";
|
||||||
import { NodePackageImporter } from "sass-embedded";
|
import { NodePackageImporter } from "sass-embedded";
|
||||||
@@ -33,6 +33,8 @@ export default defineConfig(() => ({
|
|||||||
"demo_config": normalizePath(resolve(__dirname, 'frontend/demo_config.html')),
|
"demo_config": normalizePath(resolve(__dirname, 'frontend/demo_config.html')),
|
||||||
"demo_core_extraction": normalizePath(resolve(__dirname, 'frontend/demo_core_extraction.html')),
|
"demo_core_extraction": normalizePath(resolve(__dirname, 'frontend/demo_core_extraction.html')),
|
||||||
"demo_decode_replay": normalizePath(resolve(__dirname, 'frontend/demo_decode_replay.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: {
|
output: {
|
||||||
entryFileNames: 'js/[name]-[hash].js',
|
entryFileNames: 'js/[name]-[hash].js',
|
||||||
|
|||||||
Reference in New Issue
Block a user