Files
khadhroony-bobobot/kb_demo_app/frontend/ts/demo3.ts
2026-05-20 23:57:15 +02:00

366 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// file: kb_demo_app/frontend/ts/demo3.ts
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { debug, takeoverConsole } from "@fltsci/tauri-plugin-tracing";
import type { Demo3LocalDexCorpusSearchRequest } from "./bindings/Demo3LocalDexCorpusSearchRequest.ts";
import type { Demo3LocalDexCorpusSearchPayload } from "./bindings/Demo3LocalDexCorpusSearchPayload.ts";
import type { Demo3OnchainDexDiscoveryRequest } from "./bindings/Demo3OnchainDexDiscoveryRequest.ts";
import type { Demo3OnchainDexDiscoveryResult } from "./bindings/Demo3OnchainDexDiscoveryResult.ts";
import type { Demo3OnchainDexPairCandidate } from "./bindings/Demo3OnchainDexPairCandidate.ts";
import type { Demo3LocalDexCorpusSearchResult } from "./bindings/Demo3LocalDexCorpusSearchResult.ts";
import type { Demo3OnchainDexDiscoveryPayload } from "./bindings/Demo3OnchainDexDiscoveryPayload.ts";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
interface Demo3Preset {
label: string;
dexCode: string;
programId: string;
description: string;
}
const presets: Demo3Preset[] = [
{ label: "PumpSwap", dexCode: "pump_swap", programId: "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", description: "DEX effectif PumpSwap." },
{ label: "Raydium CPMM", dexCode: "raydium_cpmm", programId: "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C", description: "Raydium CPMM." },
{ label: "Raydium CLMM", dexCode: "raydium_clmm", programId: "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK", description: "Raydium CLMM." },
{ label: "Raydium AMM v4", dexCode: "raydium_amm_v4", programId: "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8", description: "Raydium AMM v4 legacy. À prouver par corpus avant décodage swap." },
{ label: "Raydium Stable Swap", dexCode: "raydium_stable_swap", programId: "5quBtoiQqxF9Jv6KYKctB59NT3gtJD2Y65kdnB1Uev3h", description: "Stable Swap Raydium à vérifier par corpus." },
{ label: "Meteora DLMM", dexCode: "meteora_dlmm", programId: "LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo", description: "Meteora DLMM." },
{ label: "Meteora DAMM v1", dexCode: "meteora_damm_v1", programId: "Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB", description: "Meteora DAMM v1." },
{ label: "Meteora DAMM v2", dexCode: "meteora_damm_v2", programId: "cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG", description: "Meteora DAMM v2." },
{ label: "Meteora DBC", dexCode: "meteora_dbc", programId: "dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN", description: "Meteora DBC." },
{ label: "Orca Whirlpools", dexCode: "orca_whirlpools", programId: "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc", description: "Orca Whirlpools CLMM." },
{ label: "FluxBeam", dexCode: "fluxbeam", programId: "FLUXubRmkEi2q6K3Y9kBPg9248ggaZVsoSFhtJHSrm1X", description: "FluxBeam." },
{ label: "DexLab", dexCode: "dexlab", programId: "DSwpgjMvXhtGn6BsbqmacdBZyfLj6jSWf3HJpdJtmg6N", description: "DexLab Swap/Pool." },
{ label: "metaDAO", dexCode: "metadao", programId: "", description: "DEX à vérifier. Aucun program id n'est inventé." },
{ label: "Printr", dexCode: "printr", programId: "", description: "DEX à vérifier. Aucun program id n'est inventé." },
];
let lastResultJson = "";
function byId<T extends HTMLElement>(id: string): T {
const element = document.getElementById(id);
if (element === null) {
throw new Error(`missing element #${id}`);
}
return element as T;
}
function valueOrNull(value: string): string | null {
const trimmed = value.trim();
return trimmed === "" ? null : trimmed;
}
function numberValueOrNull(value: string): number | null {
const trimmed = value.trim();
if (trimmed === "") {
return null;
}
const parsed = Number.parseInt(trimmed, 10);
return Number.isFinite(parsed) ? parsed : null;
}
function intValue(id: string, fallback: number): number {
const parsed = Number.parseInt(byId<HTMLInputElement>(id).value, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function escapeHtml(value: string): string {
return value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function shortText(value: string | null, maxLength: number): string {
if (value === null) {
return "-";
}
if (value.length <= maxLength) {
return value;
}
return `${value.slice(0, maxLength)}`;
}
function shortList(values: string[], maxItems: number, itemLength: number): string {
if (values.length === 0) {
return "-";
}
return values.slice(0, maxItems).map((value) => shortText(value, itemLength)).join(", ");
}
function candidateAccountList(accounts: Demo3OnchainDexPairCandidate["candidatePoolAccounts"], maxItems: number): string {
if (accounts.length === 0) {
return "-";
}
return accounts.slice(0, maxItems).map((account) => shortText(account.address, 14)).join(", ");
}
function tokenDeltaList(candidate: Demo3OnchainDexPairCandidate): string {
if (candidate.tokenBalanceDeltas.length === 0) {
return "-";
}
return candidate.tokenBalanceDeltas.slice(0, 4).map((delta) => {
const amount = delta.deltaRaw ?? "?";
return `${shortText(delta.mint, 10)}:${amount}`;
}).join(", ");
}
function appendLogLine(line: string): void {
const textarea = byId<HTMLTextAreaElement>("demo3LogTextarea");
const timestamp = new Date().toLocaleTimeString("fr-CH", { hour12: false });
const lines = textarea.value === "" ? [] : textarea.value.split("\n");
lines.push(`[${timestamp}] ${line}`);
textarea.value = lines.slice(-400).join("\n");
textarea.scrollTop = textarea.scrollHeight;
}
function setStatus(label: string, cssClass: string): void {
const badge = byId<HTMLElement>("demo3StatusBadge");
badge.className = `badge ${cssClass}`;
badge.textContent = label;
}
function populatePresetSelect(): void {
const select = byId<HTMLSelectElement>("demo3PresetSelect");
select.innerHTML = '<option value="">Custom / empty</option>';
presets.forEach((preset, index) => {
const option = document.createElement("option");
option.value = String(index);
option.textContent = preset.label;
select.appendChild(option);
});
}
function applyPreset(indexText: string): void {
if (indexText === "") {
return;
}
const index = Number.parseInt(indexText, 10);
if (!Number.isFinite(index) || index < 0 || index >= presets.length) {
return;
}
const preset = presets[index];
byId<HTMLInputElement>("demo3DexCodeInput").value = preset.dexCode;
byId<HTMLInputElement>("demo3ProgramIdInput").value = preset.programId;
byId<HTMLElement>("demo3PresetHelp").textContent = preset.description;
}
function readOnchainRequest(): Demo3OnchainDexDiscoveryRequest {
return {
dexCode: valueOrNull(byId<HTMLInputElement>("demo3DexCodeInput").value),
programId: valueOrNull(byId<HTMLInputElement>("demo3ProgramIdInput").value),
httpRole: byId<HTMLInputElement>("demo3HttpRoleInput").value.trim() || "history_backfill",
signatureLimit: intValue("demo3SignatureLimitInput", 50),
transactionLimit: intValue("demo3TransactionLimitInput", 25),
candidateLimit: intValue("demo3CandidateLimitInput", 25),
};
}
function readLocalRequest(): Demo3LocalDexCorpusSearchRequest {
return {
dexCode: valueOrNull(byId<HTMLInputElement>("demo3DexCodeInput").value),
programId: valueOrNull(byId<HTMLInputElement>("demo3ProgramIdInput").value),
pairId: numberValueOrNull(byId<HTMLInputElement>("demo3PairIdInput").value),
poolAddress: valueOrNull(byId<HTMLInputElement>("demo3PoolAddressInput").value),
tokenMint: valueOrNull(byId<HTMLInputElement>("demo3TokenMintInput").value),
signature: valueOrNull(byId<HTMLInputElement>("demo3SignatureInput").value),
limit: intValue("demo3CandidateLimitInput", 25),
};
}
function clearFilters(): void {
byId<HTMLInputElement>("demo3DexCodeInput").value = "";
byId<HTMLInputElement>("demo3ProgramIdInput").value = "";
byId<HTMLInputElement>("demo3PairIdInput").value = "";
byId<HTMLInputElement>("demo3PoolAddressInput").value = "";
byId<HTMLInputElement>("demo3TokenMintInput").value = "";
byId<HTMLInputElement>("demo3SignatureInput").value = "";
byId<HTMLSelectElement>("demo3PresetSelect").value = "";
byId<HTMLElement>("demo3PresetHelp").textContent = "Choisis un DEX ou saisis un program id manuellement.";
}
function renderOnchainResult(result: Demo3OnchainDexDiscoveryResult): void {
byId<HTMLElement>("demo3SummarySignatureCount").textContent = String(result.fetchedSignatureCount);
byId<HTMLElement>("demo3SummaryFetchedTxCount").textContent = String(result.fetchedTransactionCount);
byId<HTMLElement>("demo3SummaryMissingTxCount").textContent = String(result.missingTransactionCount);
byId<HTMLElement>("demo3SummaryFailedTxCount").textContent = String(result.failedTransactionCount);
byId<HTMLElement>("demo3SummaryCandidateCount").textContent = String(result.candidateCount);
byId<HTMLElement>("demo3TargetText").textContent = `${result.resolvedDexCode ?? "custom"} / ${result.resolvedProgramId}`;
renderOnchainCandidates(result.candidates);
}
function renderOnchainCandidates(candidates: Demo3OnchainDexPairCandidate[]): void {
const body = byId<HTMLTableSectionElement>("demo3OnchainCandidateTableBody");
if (candidates.length === 0) {
body.innerHTML = '<tr><td colspan="11" class="text-body-secondary">No on-chain candidate.</td></tr>';
return;
}
body.innerHTML = candidates.map((candidate) => {
const verifiedPool = candidate.verifiedPoolAddress;
const firstCandidatePool = candidate.candidatePoolAccounts.length > 0 ? candidate.candidatePoolAccounts[0].address : null;
const poolForFilter = verifiedPool ?? candidate.poolAddress ?? firstCandidatePool;
if (poolForFilter !== null) {
byId<HTMLInputElement>("demo3PoolAddressInput").value = poolForFilter;
}
if (candidate.tokenAMint !== null && byId<HTMLInputElement>("demo3TokenMintInput").value.trim() === "") {
byId<HTMLInputElement>("demo3TokenMintInput").value = candidate.tokenAMint;
}
if (candidate.tokenAMint === null && candidate.observedTokenMints.length > 0 && byId<HTMLInputElement>("demo3TokenMintInput").value.trim() === "") {
byId<HTMLInputElement>("demo3TokenMintInput").value = candidate.observedTokenMints[0];
}
byId<HTMLInputElement>("demo3SignatureInput").value = candidate.signature;
const accountTitle = candidate.candidatePoolAccounts.map((account) => `${account.address} (${account.reason})`).join("\n");
const vaultTitle = candidate.candidateTokenVaultAccounts.map((account) => `${account.address} (${account.reason})`).join("\n");
return `
<tr>
<td class="font-monospace" title="${escapeHtml(candidate.signature)}">${escapeHtml(shortText(candidate.signature, 18))}</td>
<td>${candidate.slot ?? "-"}</td>
<td><span class="badge text-bg-info">${escapeHtml(candidate.candidateKind)}</span></td>
<td><span class="badge text-bg-${candidate.confidence === "high" ? "success" : candidate.confidence === "medium" ? "warning" : "secondary"}">${escapeHtml(candidate.confidence)}</span></td>
<td class="font-monospace" title="${escapeHtml(verifiedPool ?? "")}">${escapeHtml(shortText(verifiedPool, 14))}</td>
<td class="font-monospace" title="${escapeHtml(candidate.tokenAMint ?? "")}">${escapeHtml(shortText(candidate.tokenAMint, 14))}</td>
<td class="font-monospace" title="${escapeHtml(candidate.tokenBMint ?? "")}">${escapeHtml(shortText(candidate.tokenBMint, 14))}</td>
<td class="font-monospace" title="${escapeHtml(candidate.observedTokenMints.join("\n"))}">${escapeHtml(shortList(candidate.observedTokenMints, 3, 10))}</td>
<td class="font-monospace" title="${escapeHtml(tokenDeltaList(candidate))}">${escapeHtml(tokenDeltaList(candidate))}</td>
<td class="font-monospace" title="${escapeHtml(accountTitle)}">${escapeHtml(candidateAccountList(candidate.candidatePoolAccounts, 3))}<br /><span class="text-body-secondary">vaults: ${escapeHtml(candidateAccountList(candidate.candidateTokenVaultAccounts, 2))}</span></td>
<td class="small" title="${escapeHtml(vaultTitle)}">${escapeHtml(candidate.backfillHint)}</td>
</tr>`;
}).join("");
}
function renderLocalResult(result: Demo3LocalDexCorpusSearchResult): void {
byId<HTMLElement>("demo3SummaryLocalPairCount").textContent = String(result.summary.pairCount);
const body = byId<HTMLTableSectionElement>("demo3LocalPoolPairTableBody");
if (result.poolPairSamples.length === 0) {
body.innerHTML = '<tr><td colspan="8" class="text-body-secondary">No local pool/pair sample.</td></tr>';
return;
}
body.innerHTML = result.poolPairSamples.map((sample) => `
<tr>
<td>${escapeHtml(sample.dexCode ?? "-")}</td>
<td class="font-monospace" title="${escapeHtml(sample.poolAddress ?? "")}">${escapeHtml(shortText(sample.poolAddress, 16))}</td>
<td>${sample.pairId ?? "-"}</td>
<td>${escapeHtml(sample.pairSymbol ?? "-")}</td>
<td class="font-monospace" title="${escapeHtml(sample.baseMint ?? "")}">${escapeHtml(sample.baseSymbol ?? shortText(sample.baseMint, 12))}</td>
<td class="font-monospace" title="${escapeHtml(sample.quoteMint ?? "")}">${escapeHtml(sample.quoteSymbol ?? shortText(sample.quoteMint, 12))}</td>
<td>${sample.tradeEventCount}</td>
<td>${sample.pairCandleCount}</td>
</tr>`).join("");
}
async function discoverOnchain(): Promise<void> {
const request = readOnchainRequest();
setStatus("running", "text-bg-warning");
appendLogLine(`on-chain discovery dex='${request.dexCode ?? ""}' program='${request.programId ?? ""}' role='${request.httpRole}'`);
try {
const payload = await invoke<Demo3OnchainDexDiscoveryPayload>("demo3_discover_onchain_dex_pairs", { request });
lastResultJson = payload.resultJson;
byId<HTMLTextAreaElement>("demo3JsonTextarea").value = payload.resultJson;
renderOnchainResult(payload.result);
setStatus("ok", "text-bg-success");
appendLogLine(`on-chain discovery completed: candidates='${payload.result.candidateCount}' signatures='${payload.result.fetchedSignatureCount}'`);
} catch (error) {
setStatus("error", "text-bg-danger");
appendLogLine(`on-chain discovery failed: ${String(error)}`);
}
}
async function searchLocalDb(): Promise<void> {
const request = readLocalRequest();
setStatus("running", "text-bg-warning");
appendLogLine("local DB search started");
try {
const payload = await invoke<Demo3LocalDexCorpusSearchPayload>("demo3_search_local_dex_corpus", { request });
lastResultJson = payload.resultJson;
byId<HTMLTextAreaElement>("demo3JsonTextarea").value = payload.resultJson;
renderLocalResult(payload.result);
setStatus("ok", "text-bg-success");
appendLogLine(`local DB search completed: pairs='${payload.result.summary.pairCount}' tx='${payload.result.summary.transactionCount}'`);
} catch (error) {
setStatus("error", "text-bg-danger");
appendLogLine(`local DB search failed: ${String(error)}`);
}
}
async function copyJson(): Promise<void> {
if (lastResultJson === "") {
appendLogLine("nothing to copy");
return;
}
try {
await navigator.clipboard.writeText(lastResultJson);
appendLogLine("JSON copied to clipboard");
} catch (error) {
appendLogLine(`copy failed: ${String(error)}`);
}
}
function init(): void {
takeoverConsole();
void debug("demo3 on-chain discovery initialized");
debug("demo3 window loaded");
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
if (sidebarToggle) {
// restaurer létat depuis localStorage
if (localStorage.getItem('sidebar-toggle') === 'true') {
document.body.classList.add('sidenav-toggled');
}
sidebarToggle.addEventListener('click', (event) => {
event.preventDefault();
document.body.classList.toggle('sidenav-toggled');
localStorage.setItem('sidebar-toggle', document.body.classList.contains('sidenav-toggled') ? 'true' : 'false');
});
}
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
const toastElList = document.querySelectorAll('.toast');
Array.from(toastElList).map(toastEl => new bootstrap.Toast(toastEl));
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
Array.from(popoverTriggerList).map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl));
const gobackto = location.pathname + location.search;
document.querySelectorAll<HTMLAnchorElement>('a[data-setlang]').forEach((a) => {
const href = a.getAttribute("href");
if (!href) return; // pas de href => on ignore
const url = new URL(href, location.origin);
url.searchParams.set("gobackto", gobackto);
// conserve une URL relative (path + query)
a.setAttribute("href", url.pathname + "?" + url.searchParams.toString());
});
populatePresetSelect();
byId<HTMLSelectElement>("demo3PresetSelect").addEventListener("change", (event) => {
applyPreset((event.target as HTMLSelectElement).value);
});
byId<HTMLButtonElement>("demo3DiscoverButton").addEventListener("click", () => {
void discoverOnchain();
});
byId<HTMLButtonElement>("demo3LocalSearchButton").addEventListener("click", () => {
void searchLocalDb();
});
byId<HTMLButtonElement>("demo3ClearFiltersButton").addEventListener("click", clearFilters);
byId<HTMLButtonElement>("demo3CopyJsonButton").addEventListener("click", () => {
void copyJson();
});
byId<HTMLButtonElement>("demo3ClearLogButton").addEventListener("click", () => {
byId<HTMLTextAreaElement>("demo3LogTextarea").value = "";
});
appendLogLine("ready");
}
document.addEventListener("DOMContentLoaded", init);