1087 lines
40 KiB
TypeScript
1087 lines
40 KiB
TypeScript
// file: kb_demo_app/frontend/ts/demo_pipeline.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 { DemoPipelineInspectRequest } from './bindings/DemoPipelineInspectRequest.ts';
|
||
import { DemoPipelineInspectPayload } from './bindings/DemoPipelineInspectPayload.ts';
|
||
import { DemoPipelineInspectTokenRequest } from './bindings/DemoPipelineInspectTokenRequest.ts';
|
||
import { DemoPipelineInspectPairRequest } from './bindings/DemoPipelineInspectPairRequest.ts';
|
||
import { DemoPipelineInspectPoolRequest } from './bindings/DemoPipelineInspectPoolRequest.ts';
|
||
import { DemoPipelineBackfillTokenRequest } from './bindings/DemoPipelineBackfillTokenRequest.ts';
|
||
import { DemoPipelineBackfillTokenPayload } from './bindings/DemoPipelineBackfillTokenPayload.ts';
|
||
import { DemoPipelineBackfillPoolRequest } from './bindings/DemoPipelineBackfillPoolRequest.ts';
|
||
import { DemoPipelineBackfillPoolPayload } from './bindings/DemoPipelineBackfillPoolPayload.ts';
|
||
import * as echarts from "echarts";
|
||
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||
|
||
|
||
interface DemoPipelinePairCandle {
|
||
id: number | null;
|
||
pair_id: number;
|
||
timeframe_seconds: number;
|
||
bucket_start_unix: number;
|
||
bucket_end_unix: number;
|
||
open_price_quote_per_base: number;
|
||
high_price_quote_per_base: number;
|
||
low_price_quote_per_base: number;
|
||
close_price_quote_per_base: number;
|
||
trade_count: number;
|
||
buy_count: number;
|
||
sell_count: number;
|
||
base_volume_raw: string | null;
|
||
quote_volume_raw: string | null;
|
||
first_trade_signature: string | null;
|
||
last_trade_signature: string | null;
|
||
created_at: string;
|
||
updated_at: string;
|
||
}
|
||
|
||
interface DemoPipelinePairCandleGroup {
|
||
pairId: number;
|
||
timeframeSeconds: number;
|
||
candles: DemoPipelinePairCandle[];
|
||
}
|
||
|
||
function parsePairCandleGroups(rawJson: string): DemoPipelinePairCandleGroup[] {
|
||
if (rawJson.trim() === "") {
|
||
return [];
|
||
}
|
||
|
||
try {
|
||
const parsed = JSON.parse(rawJson) as unknown;
|
||
if (!Array.isArray(parsed)) {
|
||
return [];
|
||
}
|
||
|
||
const groups: DemoPipelinePairCandleGroup[] = [];
|
||
|
||
for (const value of parsed) {
|
||
if (typeof value !== "object" || value === null) {
|
||
continue;
|
||
}
|
||
|
||
const maybeGroup = value as {
|
||
pairId?: unknown;
|
||
timeframeSeconds?: unknown;
|
||
candles?: unknown;
|
||
};
|
||
|
||
if (
|
||
typeof maybeGroup.pairId !== "number" ||
|
||
typeof maybeGroup.timeframeSeconds !== "number" ||
|
||
!Array.isArray(maybeGroup.candles)
|
||
) {
|
||
continue;
|
||
}
|
||
|
||
groups.push({
|
||
pairId: maybeGroup.pairId,
|
||
timeframeSeconds: maybeGroup.timeframeSeconds,
|
||
candles: maybeGroup.candles as DemoPipelinePairCandle[],
|
||
});
|
||
}
|
||
|
||
return groups;
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function formatTimeframeLabel(timeframeSeconds: number): string {
|
||
if (timeframeSeconds % 3600 === 0) {
|
||
return `${timeframeSeconds / 3600}h`;
|
||
}
|
||
if (timeframeSeconds % 60 === 0) {
|
||
return `${timeframeSeconds / 60}m`;
|
||
}
|
||
return `${timeframeSeconds}s`;
|
||
}
|
||
|
||
function parseRawVolume(text: string | null, fallback: number): number {
|
||
if (text === null || text.trim() === "") {
|
||
return fallback;
|
||
}
|
||
|
||
const parsed = Number.parseFloat(text);
|
||
if (Number.isNaN(parsed)) {
|
||
return fallback;
|
||
}
|
||
|
||
return parsed;
|
||
}
|
||
|
||
function setEmptyCandlesChart(
|
||
chart: echarts.ECharts,
|
||
chartMeta: HTMLElement,
|
||
message: string,
|
||
): void {
|
||
chartMeta.textContent = message;
|
||
|
||
chart.setOption({
|
||
animation: false,
|
||
title: {
|
||
text: message,
|
||
left: "center",
|
||
top: "middle",
|
||
textStyle: {
|
||
fontSize: 14,
|
||
fontWeight: "normal",
|
||
},
|
||
},
|
||
tooltip: {},
|
||
xAxis: { show: false, type: "category", data: [] },
|
||
yAxis: { show: false, type: "value" },
|
||
series: [],
|
||
}, true);
|
||
}
|
||
|
||
function refreshCandlesSelectors(
|
||
groups: DemoPipelinePairCandleGroup[],
|
||
pairSelect: HTMLSelectElement,
|
||
timeframeSelect: HTMLSelectElement,
|
||
): void {
|
||
const currentPairValue = pairSelect.value;
|
||
const currentTimeframeValue = timeframeSelect.value;
|
||
|
||
const uniquePairs = Array.from(new Set(groups.map((group) => group.pairId))).sort((left, right) => left - right);
|
||
|
||
pairSelect.innerHTML = "";
|
||
if (uniquePairs.length === 0) {
|
||
const option = document.createElement("option");
|
||
option.value = "";
|
||
option.textContent = "Aucune";
|
||
pairSelect.appendChild(option);
|
||
|
||
timeframeSelect.innerHTML = "";
|
||
const tfOption = document.createElement("option");
|
||
tfOption.value = "";
|
||
tfOption.textContent = "Aucun";
|
||
timeframeSelect.appendChild(tfOption);
|
||
return;
|
||
}
|
||
|
||
for (const pairId of uniquePairs) {
|
||
const option = document.createElement("option");
|
||
option.value = String(pairId);
|
||
option.textContent = `Pair #${pairId}`;
|
||
if (option.value === currentPairValue) {
|
||
option.selected = true;
|
||
}
|
||
pairSelect.appendChild(option);
|
||
}
|
||
|
||
if (pairSelect.value === "" && uniquePairs.length > 0) {
|
||
pairSelect.value = String(uniquePairs[0]);
|
||
}
|
||
|
||
const selectedPairId = Number.parseInt(pairSelect.value, 10);
|
||
const pairGroups = groups
|
||
.filter((group) => group.pairId === selectedPairId)
|
||
.sort((left, right) => left.timeframeSeconds - right.timeframeSeconds);
|
||
|
||
timeframeSelect.innerHTML = "";
|
||
|
||
if (pairGroups.length === 0) {
|
||
const option = document.createElement("option");
|
||
option.value = "";
|
||
option.textContent = "Aucun";
|
||
timeframeSelect.appendChild(option);
|
||
return;
|
||
}
|
||
|
||
for (const group of pairGroups) {
|
||
const option = document.createElement("option");
|
||
option.value = String(group.timeframeSeconds);
|
||
option.textContent = formatTimeframeLabel(group.timeframeSeconds);
|
||
if (option.value === currentTimeframeValue) {
|
||
option.selected = true;
|
||
}
|
||
timeframeSelect.appendChild(option);
|
||
}
|
||
|
||
if (timeframeSelect.value === "" && pairGroups.length > 0) {
|
||
timeframeSelect.value = String(pairGroups[0].timeframeSeconds);
|
||
}
|
||
}
|
||
|
||
function renderSelectedCandlesChart(
|
||
chart: echarts.ECharts,
|
||
chartMeta: HTMLElement,
|
||
groups: DemoPipelinePairCandleGroup[],
|
||
pairSelect: HTMLSelectElement,
|
||
timeframeSelect: HTMLSelectElement,
|
||
): void {
|
||
if (groups.length === 0) {
|
||
setEmptyCandlesChart(chart, chartMeta, "Aucune candle disponible.");
|
||
return;
|
||
}
|
||
|
||
const selectedPairId = Number.parseInt(pairSelect.value, 10);
|
||
const selectedTimeframe = Number.parseInt(timeframeSelect.value, 10);
|
||
|
||
if (Number.isNaN(selectedPairId) || Number.isNaN(selectedTimeframe)) {
|
||
setEmptyCandlesChart(chart, chartMeta, "Sélection de paire/timeframe invalide.");
|
||
return;
|
||
}
|
||
|
||
const group = groups.find(
|
||
(value) =>
|
||
value.pairId === selectedPairId &&
|
||
value.timeframeSeconds === selectedTimeframe,
|
||
);
|
||
|
||
if (!group || group.candles.length === 0) {
|
||
setEmptyCandlesChart(
|
||
chart,
|
||
chartMeta,
|
||
`Aucune candle pour la pair #${selectedPairId} en ${formatTimeframeLabel(selectedTimeframe)}.`,
|
||
);
|
||
return;
|
||
}
|
||
|
||
const candles = [...group.candles].sort(
|
||
(left, right) => left.bucket_start_unix - right.bucket_start_unix,
|
||
);
|
||
|
||
const categoryData = candles.map((candle) =>
|
||
new Date(candle.bucket_start_unix * 1000).toLocaleString("fr-CH", {
|
||
hour12: false,
|
||
year: "2-digit",
|
||
month: "2-digit",
|
||
day: "2-digit",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
}),
|
||
);
|
||
|
||
const ohlcData = candles.map((candle) => [
|
||
candle.open_price_quote_per_base,
|
||
candle.close_price_quote_per_base,
|
||
candle.low_price_quote_per_base,
|
||
candle.high_price_quote_per_base,
|
||
]);
|
||
|
||
const volumeData = candles.map((candle) =>
|
||
parseRawVolume(candle.quote_volume_raw, candle.trade_count),
|
||
);
|
||
|
||
chartMeta.textContent =
|
||
`Pair #${selectedPairId} • ${formatTimeframeLabel(selectedTimeframe)} • ${candles.length} candles`;
|
||
|
||
chart.setOption(
|
||
{
|
||
animation: false,
|
||
legend: {
|
||
data: ["OHLC", "Volume"],
|
||
top: 0,
|
||
},
|
||
tooltip: {
|
||
trigger: "axis",
|
||
axisPointer: {
|
||
type: "cross",
|
||
},
|
||
},
|
||
axisPointer: {
|
||
link: [{ xAxisIndex: "all" }],
|
||
},
|
||
grid: [
|
||
{ left: 60, right: 24, top: 40, height: "58%" },
|
||
{ left: 60, right: 24, top: "74%", height: "16%" },
|
||
],
|
||
xAxis: [
|
||
{
|
||
type: "category",
|
||
data: categoryData,
|
||
boundaryGap: true,
|
||
axisLine: { onZero: false },
|
||
splitLine: { show: false },
|
||
min: "dataMin",
|
||
max: "dataMax",
|
||
},
|
||
{
|
||
type: "category",
|
||
gridIndex: 1,
|
||
data: categoryData,
|
||
boundaryGap: true,
|
||
axisLine: { onZero: false },
|
||
axisTick: { show: false },
|
||
splitLine: { show: false },
|
||
axisLabel: { show: false },
|
||
min: "dataMin",
|
||
max: "dataMax",
|
||
},
|
||
],
|
||
yAxis: [
|
||
{
|
||
scale: true,
|
||
splitArea: { show: false },
|
||
},
|
||
{
|
||
gridIndex: 1,
|
||
scale: true,
|
||
splitNumber: 2,
|
||
},
|
||
],
|
||
dataZoom: [
|
||
{
|
||
type: "inside",
|
||
xAxisIndex: [0, 1],
|
||
start: 0,
|
||
end: 100,
|
||
},
|
||
{
|
||
show: true,
|
||
type: "slider",
|
||
xAxisIndex: [0, 1],
|
||
bottom: 6,
|
||
start: 0,
|
||
end: 100,
|
||
},
|
||
],
|
||
series: [
|
||
{
|
||
name: "OHLC",
|
||
type: "candlestick",
|
||
data: ohlcData,
|
||
},
|
||
{
|
||
name: "Volume",
|
||
type: "bar",
|
||
xAxisIndex: 1,
|
||
yAxisIndex: 1,
|
||
data: volumeData,
|
||
},
|
||
],
|
||
},
|
||
true,
|
||
);
|
||
}
|
||
|
||
function applyInspectionPayload(
|
||
payload: DemoPipelineInspectPayload,
|
||
summaryTextarea: HTMLTextAreaElement,
|
||
transactionTextarea: HTMLTextAreaElement,
|
||
decodedEventsTextarea: HTMLTextAreaElement,
|
||
poolsTextarea: HTMLTextAreaElement,
|
||
pairsTextarea: HTMLTextAreaElement,
|
||
launchAttributionsTextarea: HTMLTextAreaElement,
|
||
poolOriginsTextarea: HTMLTextAreaElement,
|
||
walletsTextarea: HTMLTextAreaElement,
|
||
tradeEventsTextarea: HTMLTextAreaElement,
|
||
pairMetricsTextarea: HTMLTextAreaElement,
|
||
pairCandlesTextarea: HTMLTextAreaElement,
|
||
pairAnalyticSignalsTextarea: HTMLTextAreaElement,
|
||
chart: echarts.ECharts,
|
||
chartMeta: HTMLElement,
|
||
pairSelect: HTMLSelectElement,
|
||
timeframeSelect: HTMLSelectElement,
|
||
): void {
|
||
summaryTextarea.value = payload.summaryJson;
|
||
transactionTextarea.value = payload.transactionJson;
|
||
decodedEventsTextarea.value = payload.decodedEventsJson;
|
||
poolsTextarea.value = payload.poolsJson;
|
||
pairsTextarea.value = payload.pairsJson;
|
||
launchAttributionsTextarea.value = payload.launchAttributionsJson;
|
||
poolOriginsTextarea.value = payload.poolOriginsJson;
|
||
walletsTextarea.value = payload.walletsJson;
|
||
tradeEventsTextarea.value = payload.tradeEventsJson;
|
||
pairMetricsTextarea.value = payload.pairMetricsJson;
|
||
pairCandlesTextarea.value = payload.pairCandlesJson;
|
||
pairAnalyticSignalsTextarea.value = payload.pairAnalyticSignalsJson;
|
||
|
||
const groups = parsePairCandleGroups(payload.pairCandlesJson);
|
||
refreshCandlesSelectors(groups, pairSelect, timeframeSelect);
|
||
renderSelectedCandlesChart(chart, chartMeta, groups, pairSelect, timeframeSelect);
|
||
}
|
||
|
||
function appendLogLine(textarea: HTMLTextAreaElement, line: string): void {
|
||
const now = new Date();
|
||
const timestamp = now.toLocaleTimeString("fr-CH", { hour12: false });
|
||
|
||
const lines = textarea.value === "" ? [] : textarea.value.split("\n");
|
||
lines.push(`[${timestamp}] ${line}`);
|
||
|
||
const maxLines = 300;
|
||
textarea.value = lines.slice(-maxLines).join("\n");
|
||
textarea.scrollTop = textarea.scrollHeight;
|
||
}
|
||
|
||
function clearInspection(
|
||
backfillTextarea: HTMLTextAreaElement,
|
||
summaryTextarea: HTMLTextAreaElement,
|
||
transactionTextarea: HTMLTextAreaElement,
|
||
decodedEventsTextarea: HTMLTextAreaElement,
|
||
poolsTextarea: HTMLTextAreaElement,
|
||
pairsTextarea: HTMLTextAreaElement,
|
||
launchAttributionsTextarea: HTMLTextAreaElement,
|
||
poolOriginsTextarea: HTMLTextAreaElement,
|
||
walletsTextarea: HTMLTextAreaElement,
|
||
tradeEventsTextarea: HTMLTextAreaElement,
|
||
pairMetricsTextarea: HTMLTextAreaElement,
|
||
pairCandlesTextarea: HTMLTextAreaElement,
|
||
pairAnalyticSignalsTextarea: HTMLTextAreaElement,
|
||
): void {
|
||
backfillTextarea.value = "";
|
||
summaryTextarea.value = "";
|
||
transactionTextarea.value = "";
|
||
decodedEventsTextarea.value = "";
|
||
poolsTextarea.value = "";
|
||
pairsTextarea.value = "";
|
||
launchAttributionsTextarea.value = "";
|
||
poolOriginsTextarea.value = "";
|
||
walletsTextarea.value = "";
|
||
tradeEventsTextarea.value = "";
|
||
pairMetricsTextarea.value = "";
|
||
pairCandlesTextarea.value = "";
|
||
pairAnalyticSignalsTextarea.value = "";
|
||
}
|
||
|
||
function readCustomTimeframeSeconds(
|
||
input: HTMLInputElement,
|
||
logTextarea: HTMLTextAreaElement,
|
||
): bigint | null | undefined {
|
||
const customTimeframeText = input.value.trim();
|
||
if (customTimeframeText === "") {
|
||
return null;
|
||
}
|
||
|
||
const parsed = Number.parseInt(customTimeframeText, 10);
|
||
if (Number.isNaN(parsed) || parsed <= 0) {
|
||
appendLogLine(logTextarea, `[ui] invalid custom timeframe '${customTimeframeText}'`);
|
||
return undefined;
|
||
}
|
||
|
||
return BigInt(parsed);
|
||
}
|
||
|
||
function readPositiveIntegerInput(
|
||
input: HTMLInputElement,
|
||
logTextarea: HTMLTextAreaElement,
|
||
label: string,
|
||
): number | undefined {
|
||
const text = input.value.trim();
|
||
if (text === "") {
|
||
appendLogLine(logTextarea, `[ui] ${label} is required`);
|
||
return undefined;
|
||
}
|
||
|
||
const parsed = Number.parseInt(text, 10);
|
||
if (Number.isNaN(parsed) || parsed <= 0) {
|
||
appendLogLine(logTextarea, `[ui] invalid ${label} '${text}'`);
|
||
return undefined;
|
||
}
|
||
|
||
return parsed;
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", async () => {
|
||
void takeoverConsole();
|
||
debug("demo_pipeline 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());
|
||
});
|
||
|
||
const signatureInput = document.querySelector<HTMLInputElement>("#demoPipelineSignatureInput");
|
||
const customTimeframeInput = document.querySelector<HTMLInputElement>("#demoPipelineCustomTimeframeInput");
|
||
const inspectButton = document.querySelector<HTMLButtonElement>("#demoPipelineInspectButton");
|
||
const clearButton = document.querySelector<HTMLButtonElement>("#demoPipelineClearButton");
|
||
const clearLogButton = document.querySelector<HTMLButtonElement>("#demoPipelineClearLogButton");
|
||
|
||
const summaryTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelineSummaryTextarea");
|
||
const transactionTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelineTransactionTextarea");
|
||
const decodedEventsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelineDecodedEventsTextarea");
|
||
const poolsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelinePoolsTextarea");
|
||
const pairsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelinePairsTextarea");
|
||
const launchAttributionsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelineLaunchAttributionsTextarea");
|
||
const poolOriginsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelinePoolOriginsTextarea");
|
||
const walletsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelineWalletsTextarea");
|
||
const tradeEventsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelineTradeEventsTextarea");
|
||
const pairMetricsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelinePairMetricsTextarea");
|
||
const pairCandlesTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelinePairCandlesTextarea");
|
||
const pairAnalyticSignalsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelinePairAnalyticSignalsTextarea");
|
||
const logTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelineLogTextarea");
|
||
const tokenMintInput = document.querySelector<HTMLInputElement>("#demoPipelineTokenMintInput");
|
||
const inspectTokenButton = document.querySelector<HTMLButtonElement>("#demoPipelineInspectTokenButton");
|
||
const pairIdInput = document.querySelector<HTMLInputElement>("#demoPipelinePairIdInput");
|
||
const inspectPairButton = document.querySelector<HTMLButtonElement>("#demoPipelineInspectPairButton");
|
||
const poolAddressInput = document.querySelector<HTMLInputElement>("#demoPipelinePoolAddressInput");
|
||
const inspectPoolButton = document.querySelector<HTMLButtonElement>("#demoPipelineInspectPoolButton");
|
||
const backfillTokenMintInput = document.querySelector<HTMLInputElement>("#demoPipelineBackfillTokenMintInput");
|
||
const backfillHttpRoleInput = document.querySelector<HTMLInputElement>("#demoPipelineBackfillHttpRoleInput");
|
||
const backfillMintLimitInput = document.querySelector<HTMLInputElement>("#demoPipelineBackfillMintLimitInput");
|
||
const backfillPoolLimitInput = document.querySelector<HTMLInputElement>("#demoPipelineBackfillPoolLimitInput");
|
||
const backfillTokenButton = document.querySelector<HTMLButtonElement>("#demoPipelineBackfillTokenButton");
|
||
const backfillTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelineBackfillTextarea");
|
||
|
||
const chartPairSelect = document.querySelector<HTMLSelectElement>("#demoPipelineChartPairSelect");
|
||
const chartTimeframeSelect = document.querySelector<HTMLSelectElement>("#demoPipelineChartTimeframeSelect");
|
||
const candlesChartElement = document.querySelector<HTMLDivElement>("#demoPipelineCandlesChart");
|
||
const candlesChartMeta = document.querySelector<HTMLDivElement>("#demoPipelineCandlesChartMeta");
|
||
|
||
const backfillPoolAddressInput = document.querySelector<HTMLInputElement>("#demoPipelineBackfillPoolAddressInput");
|
||
const backfillPoolOnlyLimitInput = document.querySelector<HTMLInputElement>("#demoPipelineBackfillPoolOnlyLimitInput");
|
||
const backfillPoolButton = document.querySelector<HTMLButtonElement>("#demoPipelineBackfillPoolButton");
|
||
|
||
|
||
if (
|
||
!chartPairSelect ||
|
||
!chartTimeframeSelect ||
|
||
!candlesChartElement ||
|
||
!candlesChartMeta ||
|
||
!backfillTokenMintInput ||
|
||
!backfillHttpRoleInput ||
|
||
!backfillMintLimitInput ||
|
||
!backfillPoolLimitInput ||
|
||
!backfillTokenButton ||
|
||
!backfillTextarea ||
|
||
!backfillPoolAddressInput ||
|
||
!backfillPoolOnlyLimitInput ||
|
||
!backfillPoolButton ||
|
||
!pairIdInput ||
|
||
!inspectPairButton ||
|
||
!poolAddressInput ||
|
||
!inspectPoolButton ||
|
||
!tokenMintInput ||
|
||
!inspectTokenButton ||
|
||
!signatureInput ||
|
||
!customTimeframeInput ||
|
||
!inspectButton ||
|
||
!clearButton ||
|
||
!clearLogButton ||
|
||
!summaryTextarea ||
|
||
!transactionTextarea ||
|
||
!decodedEventsTextarea ||
|
||
!poolsTextarea ||
|
||
!pairsTextarea ||
|
||
!launchAttributionsTextarea ||
|
||
!poolOriginsTextarea ||
|
||
!walletsTextarea ||
|
||
!tradeEventsTextarea ||
|
||
!pairMetricsTextarea ||
|
||
!pairCandlesTextarea ||
|
||
!pairAnalyticSignalsTextarea ||
|
||
!logTextarea
|
||
) {
|
||
console.error("demo_pipeline DOM is incomplete");
|
||
return;
|
||
}
|
||
|
||
const candlesChart = echarts.init(candlesChartElement);
|
||
setEmptyCandlesChart(candlesChart, candlesChartMeta, "Aucune candle disponible.");
|
||
|
||
window.addEventListener("resize", () => {
|
||
candlesChart.resize();
|
||
});
|
||
|
||
chartPairSelect.addEventListener("change", () => {
|
||
const groups = parsePairCandleGroups(pairCandlesTextarea.value);
|
||
refreshCandlesSelectors(groups, chartPairSelect, chartTimeframeSelect);
|
||
renderSelectedCandlesChart(
|
||
candlesChart,
|
||
candlesChartMeta,
|
||
groups,
|
||
chartPairSelect,
|
||
chartTimeframeSelect,
|
||
);
|
||
});
|
||
|
||
chartTimeframeSelect.addEventListener("change", () => {
|
||
const groups = parsePairCandleGroups(pairCandlesTextarea.value);
|
||
renderSelectedCandlesChart(
|
||
candlesChart,
|
||
candlesChartMeta,
|
||
groups,
|
||
chartPairSelect,
|
||
chartTimeframeSelect,
|
||
);
|
||
});
|
||
|
||
clearButton.addEventListener("click", () => {
|
||
clearInspection(
|
||
backfillTextarea,
|
||
summaryTextarea,
|
||
transactionTextarea,
|
||
decodedEventsTextarea,
|
||
poolsTextarea,
|
||
pairsTextarea,
|
||
launchAttributionsTextarea,
|
||
poolOriginsTextarea,
|
||
walletsTextarea,
|
||
tradeEventsTextarea,
|
||
pairMetricsTextarea,
|
||
pairCandlesTextarea,
|
||
pairAnalyticSignalsTextarea,
|
||
);
|
||
backfillTokenMintInput.value = "";
|
||
backfillHttpRoleInput.value = "history_backfill";
|
||
backfillMintLimitInput.value = "50";
|
||
backfillPoolLimitInput.value = "50";
|
||
signatureInput.value = "";
|
||
customTimeframeInput.value = "";
|
||
tokenMintInput.value = "";
|
||
pairIdInput.value = "";
|
||
poolAddressInput.value = "";
|
||
backfillPoolAddressInput.value = "";
|
||
backfillPoolOnlyLimitInput.value = "50";
|
||
chartPairSelect.innerHTML = `<option value="">Aucune</option>`;
|
||
chartTimeframeSelect.innerHTML = `<option value="">Aucun</option>`;
|
||
setEmptyCandlesChart(candlesChart, candlesChartMeta, "Aucune candle disponible.");
|
||
appendLogLine(logTextarea, "[ui] inspection state cleared");
|
||
});
|
||
|
||
backfillPoolButton.addEventListener("click", async () => {
|
||
const poolAddress = backfillPoolAddressInput.value.trim();
|
||
if (poolAddress === "") {
|
||
appendLogLine(logTextarea, "[ui] backfill pool address is required");
|
||
return;
|
||
}
|
||
|
||
const poolSignatureLimit = readPositiveIntegerInput(
|
||
backfillPoolOnlyLimitInput,
|
||
logTextarea,
|
||
"poolSignatureLimit",
|
||
);
|
||
if (poolSignatureLimit === undefined) {
|
||
return;
|
||
}
|
||
|
||
const httpRoleText = backfillHttpRoleInput.value.trim();
|
||
const httpRole = httpRoleText === "" ? null : httpRoleText;
|
||
|
||
appendLogLine(
|
||
logTextarea,
|
||
`[ui] launching pool backfill for '${poolAddress}' with role '${httpRole ?? "history_backfill"}' (pool=${poolSignatureLimit})`,
|
||
);
|
||
|
||
const request: DemoPipelineBackfillPoolRequest = {
|
||
poolAddress,
|
||
httpRole,
|
||
poolSignatureLimit,
|
||
};
|
||
|
||
try {
|
||
const payload = await invoke<DemoPipelineBackfillPoolPayload>(
|
||
"demo_pipeline_backfill_pool_address",
|
||
{ request },
|
||
);
|
||
|
||
backfillTextarea.value = payload.backfillJson;
|
||
appendLogLine(
|
||
logTextarea,
|
||
`[ui] pool backfill completed for '${payload.poolAddress}' with role '${payload.httpRole}'`,
|
||
);
|
||
|
||
if (!payload.poolPersistedAfterBackfill) {
|
||
appendLogLine(
|
||
logTextarea,
|
||
`[ui] backfill completed but pool '${payload.poolAddress}' is still absent from persisted pool objects; automatic pool inspection skipped`,
|
||
);
|
||
return;
|
||
}
|
||
|
||
const inspectRequest: DemoPipelineInspectPoolRequest = {
|
||
poolAddress: payload.poolAddress,
|
||
customTimeframeSeconds: null,
|
||
};
|
||
|
||
try {
|
||
const inspectPayload = await invoke<DemoPipelineInspectPayload>(
|
||
"demo_pipeline_inspect_pool_address",
|
||
{ request: inspectRequest },
|
||
);
|
||
|
||
applyInspectionPayload(
|
||
inspectPayload,
|
||
summaryTextarea,
|
||
transactionTextarea,
|
||
decodedEventsTextarea,
|
||
poolsTextarea,
|
||
pairsTextarea,
|
||
launchAttributionsTextarea,
|
||
poolOriginsTextarea,
|
||
walletsTextarea,
|
||
tradeEventsTextarea,
|
||
pairMetricsTextarea,
|
||
pairCandlesTextarea,
|
||
pairAnalyticSignalsTextarea,
|
||
candlesChart,
|
||
candlesChartMeta,
|
||
chartPairSelect,
|
||
chartTimeframeSelect,
|
||
);
|
||
|
||
appendLogLine(
|
||
logTextarea,
|
||
`[ui] pool inspection refreshed after backfill for '${payload.poolAddress}'`,
|
||
);
|
||
} catch (error) {
|
||
appendLogLine(
|
||
logTextarea,
|
||
`[ui] backfill completed but automatic pool inspection failed for '${payload.poolAddress}': ${String(error)}`,
|
||
);
|
||
}
|
||
} catch (error) {
|
||
appendLogLine(logTextarea, `[ui] pool backfill error: ${String(error)}`);
|
||
}
|
||
});
|
||
|
||
clearLogButton.addEventListener("click", () => {
|
||
logTextarea.value = "";
|
||
});
|
||
|
||
inspectButton.addEventListener("click", async () => {
|
||
const signature = signatureInput.value.trim();
|
||
if (signature === "") {
|
||
appendLogLine(logTextarea, "[ui] signature is required");
|
||
return;
|
||
}
|
||
|
||
let customTimeframeSeconds: bigint | null = null;
|
||
const customTimeframeText = customTimeframeInput.value.trim();
|
||
if (customTimeframeText !== "") {
|
||
const parsed = Number.parseInt(customTimeframeText, 10);
|
||
if (Number.isNaN(parsed) || parsed <= 0) {
|
||
appendLogLine(logTextarea, `[ui] invalid custom timeframe '${customTimeframeText}'`);
|
||
return;
|
||
}
|
||
customTimeframeSeconds = BigInt(parsed);
|
||
}
|
||
|
||
appendLogLine(
|
||
logTextarea,
|
||
`[ui] inspecting signature '${signature}'${customTimeframeSeconds === null ? "" : ` with custom timeframe ${customTimeframeSeconds}s`}`,
|
||
);
|
||
|
||
const request: DemoPipelineInspectRequest = {
|
||
signature,
|
||
customTimeframeSeconds,
|
||
};
|
||
|
||
try {
|
||
const payload = await invoke<DemoPipelineInspectPayload>("demo_pipeline_inspect_signature", { request });
|
||
|
||
applyInspectionPayload(
|
||
payload,
|
||
summaryTextarea,
|
||
transactionTextarea,
|
||
decodedEventsTextarea,
|
||
poolsTextarea,
|
||
pairsTextarea,
|
||
launchAttributionsTextarea,
|
||
poolOriginsTextarea,
|
||
walletsTextarea,
|
||
tradeEventsTextarea,
|
||
pairMetricsTextarea,
|
||
pairCandlesTextarea,
|
||
pairAnalyticSignalsTextarea,
|
||
candlesChart,
|
||
candlesChartMeta,
|
||
chartPairSelect,
|
||
chartTimeframeSelect,
|
||
);
|
||
|
||
appendLogLine(logTextarea, `[ui] inspection completed for '${payload.signature}'`);
|
||
} catch (error) {
|
||
appendLogLine(logTextarea, `[ui] inspect error: ${String(error)}`);
|
||
}
|
||
});
|
||
|
||
inspectTokenButton.addEventListener("click", async () => {
|
||
const tokenMint = tokenMintInput.value.trim();
|
||
if (tokenMint === "") {
|
||
appendLogLine(logTextarea, "[ui] token mint is required");
|
||
return;
|
||
}
|
||
|
||
let customTimeframeSeconds: bigint | null = null;
|
||
const customTimeframeText = customTimeframeInput.value.trim();
|
||
if (customTimeframeText !== "") {
|
||
const parsed = Number.parseInt(customTimeframeText, 10);
|
||
if (Number.isNaN(parsed) || parsed <= 0) {
|
||
appendLogLine(logTextarea, `[ui] invalid custom timeframe '${customTimeframeText}'`);
|
||
return;
|
||
}
|
||
customTimeframeSeconds = BigInt(parsed);
|
||
}
|
||
|
||
appendLogLine(
|
||
logTextarea,
|
||
`[ui] inspecting token mint '${tokenMint}'${customTimeframeSeconds === null ? "" : ` with custom timeframe ${customTimeframeSeconds}s`}`,
|
||
);
|
||
|
||
const request: DemoPipelineInspectTokenRequest = {
|
||
tokenMint,
|
||
customTimeframeSeconds,
|
||
};
|
||
|
||
try {
|
||
const payload = await invoke<DemoPipelineInspectPayload>("demo_pipeline_inspect_token_mint", { request });
|
||
|
||
applyInspectionPayload(
|
||
payload,
|
||
summaryTextarea,
|
||
transactionTextarea,
|
||
decodedEventsTextarea,
|
||
poolsTextarea,
|
||
pairsTextarea,
|
||
launchAttributionsTextarea,
|
||
poolOriginsTextarea,
|
||
walletsTextarea,
|
||
tradeEventsTextarea,
|
||
pairMetricsTextarea,
|
||
pairCandlesTextarea,
|
||
pairAnalyticSignalsTextarea,
|
||
candlesChart,
|
||
candlesChartMeta,
|
||
chartPairSelect,
|
||
chartTimeframeSelect,
|
||
);
|
||
|
||
appendLogLine(logTextarea, `[ui] token inspection completed for '${payload.signature}'`);
|
||
} catch (error) {
|
||
appendLogLine(logTextarea, `[ui] token inspect error: ${String(error)}`);
|
||
}
|
||
});
|
||
|
||
inspectPairButton.addEventListener("click", async () => {
|
||
const pairIdText = pairIdInput.value.trim();
|
||
if (pairIdText === "") {
|
||
appendLogLine(logTextarea, "[ui] pair id is required");
|
||
return;
|
||
}
|
||
|
||
const parsedPairId = Number.parseInt(pairIdText, 10);
|
||
if (Number.isNaN(parsedPairId) || parsedPairId <= 0) {
|
||
appendLogLine(logTextarea, `[ui] invalid pair id '${pairIdText}'`);
|
||
return;
|
||
}
|
||
|
||
const customTimeframeSeconds = readCustomTimeframeSeconds(customTimeframeInput, logTextarea);
|
||
if (customTimeframeSeconds === undefined) {
|
||
return;
|
||
}
|
||
|
||
appendLogLine(
|
||
logTextarea,
|
||
`[ui] inspecting pair id '${parsedPairId}'${customTimeframeSeconds === null ? "" : ` with custom timeframe ${customTimeframeSeconds}s`}`,
|
||
);
|
||
|
||
const request: DemoPipelineInspectPairRequest = {
|
||
pairId: BigInt(parsedPairId),
|
||
customTimeframeSeconds,
|
||
};
|
||
|
||
try {
|
||
const payload = await invoke<DemoPipelineInspectPayload>("demo_pipeline_inspect_pair_id", { request });
|
||
|
||
applyInspectionPayload(
|
||
payload,
|
||
summaryTextarea,
|
||
transactionTextarea,
|
||
decodedEventsTextarea,
|
||
poolsTextarea,
|
||
pairsTextarea,
|
||
launchAttributionsTextarea,
|
||
poolOriginsTextarea,
|
||
walletsTextarea,
|
||
tradeEventsTextarea,
|
||
pairMetricsTextarea,
|
||
pairCandlesTextarea,
|
||
pairAnalyticSignalsTextarea,
|
||
candlesChart,
|
||
candlesChartMeta,
|
||
chartPairSelect,
|
||
chartTimeframeSelect,
|
||
);
|
||
|
||
appendLogLine(logTextarea, `[ui] pair inspection completed for '${payload.signature}'`);
|
||
} catch (error) {
|
||
appendLogLine(logTextarea, `[ui] pair inspect error: ${String(error)}`);
|
||
}
|
||
});
|
||
|
||
inspectPoolButton.addEventListener("click", async () => {
|
||
const poolAddress = poolAddressInput.value.trim();
|
||
if (poolAddress === "") {
|
||
appendLogLine(logTextarea, "[ui] pool address is required");
|
||
return;
|
||
}
|
||
|
||
const customTimeframeSeconds = readCustomTimeframeSeconds(customTimeframeInput, logTextarea);
|
||
if (customTimeframeSeconds === undefined) {
|
||
return;
|
||
}
|
||
|
||
appendLogLine(
|
||
logTextarea,
|
||
`[ui] inspecting pool '${poolAddress}'${customTimeframeSeconds === null ? "" : ` with custom timeframe ${customTimeframeSeconds}s`}`,
|
||
);
|
||
|
||
const request: DemoPipelineInspectPoolRequest = {
|
||
poolAddress,
|
||
customTimeframeSeconds,
|
||
};
|
||
|
||
try {
|
||
const payload = await invoke<DemoPipelineInspectPayload>("demo_pipeline_inspect_pool_address", { request });
|
||
|
||
applyInspectionPayload(
|
||
payload,
|
||
summaryTextarea,
|
||
transactionTextarea,
|
||
decodedEventsTextarea,
|
||
poolsTextarea,
|
||
pairsTextarea,
|
||
launchAttributionsTextarea,
|
||
poolOriginsTextarea,
|
||
walletsTextarea,
|
||
tradeEventsTextarea,
|
||
pairMetricsTextarea,
|
||
pairCandlesTextarea,
|
||
pairAnalyticSignalsTextarea,
|
||
candlesChart,
|
||
candlesChartMeta,
|
||
chartPairSelect,
|
||
chartTimeframeSelect,
|
||
);
|
||
|
||
appendLogLine(logTextarea, `[ui] pool inspection completed for '${payload.signature}'`);
|
||
} catch (error) {
|
||
appendLogLine(logTextarea, `[ui] pool inspect error: ${String(error)}`);
|
||
}
|
||
});
|
||
|
||
backfillTokenButton.addEventListener("click", async () => {
|
||
const tokenMint = backfillTokenMintInput.value.trim();
|
||
if (tokenMint === "") {
|
||
appendLogLine(logTextarea, "[ui] backfill token mint is required");
|
||
return;
|
||
}
|
||
|
||
const mintSignatureLimit = readPositiveIntegerInput(
|
||
backfillMintLimitInput,
|
||
logTextarea,
|
||
"mintSignatureLimit",
|
||
);
|
||
if (mintSignatureLimit === undefined) {
|
||
return;
|
||
}
|
||
|
||
const poolSignatureLimit = readPositiveIntegerInput(
|
||
backfillPoolLimitInput,
|
||
logTextarea,
|
||
"poolSignatureLimit",
|
||
);
|
||
if (poolSignatureLimit === undefined) {
|
||
return;
|
||
}
|
||
|
||
const httpRoleText = backfillHttpRoleInput.value.trim();
|
||
const httpRole = httpRoleText === "" ? null : httpRoleText;
|
||
|
||
appendLogLine(
|
||
logTextarea,
|
||
`[ui] launching token backfill for '${tokenMint}' with role '${httpRole ?? "history_backfill"}' (mint=${mintSignatureLimit}, pool=${poolSignatureLimit})`,
|
||
);
|
||
|
||
const request: DemoPipelineBackfillTokenRequest = {
|
||
tokenMint,
|
||
httpRole,
|
||
mintSignatureLimit,
|
||
poolSignatureLimit,
|
||
};
|
||
|
||
try {
|
||
const payload = await invoke<DemoPipelineBackfillTokenPayload>(
|
||
"demo_pipeline_backfill_token_mint",
|
||
{ request },
|
||
);
|
||
|
||
backfillTextarea.value = payload.backfillJson;
|
||
appendLogLine(
|
||
logTextarea,
|
||
`[ui] token backfill completed for '${payload.tokenMint}' with role '${payload.httpRole}'`,
|
||
);
|
||
|
||
if (!payload.tokenPersistedAfterBackfill) {
|
||
appendLogLine(
|
||
logTextarea,
|
||
`[ui] backfill completed but token '${payload.tokenMint}' is still absent from persisted token objects; automatic token inspection skipped`,
|
||
);
|
||
return;
|
||
}
|
||
|
||
const inspectRequest: DemoPipelineInspectTokenRequest = {
|
||
tokenMint: payload.tokenMint,
|
||
customTimeframeSeconds: null,
|
||
};
|
||
|
||
try {
|
||
const inspectPayload = await invoke<DemoPipelineInspectPayload>(
|
||
"demo_pipeline_inspect_token_mint",
|
||
{ request: inspectRequest },
|
||
);
|
||
|
||
summaryTextarea.value = inspectPayload.summaryJson;
|
||
transactionTextarea.value = inspectPayload.transactionJson;
|
||
decodedEventsTextarea.value = inspectPayload.decodedEventsJson;
|
||
poolsTextarea.value = inspectPayload.poolsJson;
|
||
pairsTextarea.value = inspectPayload.pairsJson;
|
||
launchAttributionsTextarea.value = inspectPayload.launchAttributionsJson;
|
||
poolOriginsTextarea.value = inspectPayload.poolOriginsJson;
|
||
walletsTextarea.value = inspectPayload.walletsJson;
|
||
tradeEventsTextarea.value = inspectPayload.tradeEventsJson;
|
||
pairMetricsTextarea.value = inspectPayload.pairMetricsJson;
|
||
pairCandlesTextarea.value = inspectPayload.pairCandlesJson;
|
||
pairAnalyticSignalsTextarea.value = inspectPayload.pairAnalyticSignalsJson;
|
||
|
||
appendLogLine(
|
||
logTextarea,
|
||
`[ui] token inspection refreshed after backfill for '${payload.tokenMint}'`,
|
||
);
|
||
} catch (error) {
|
||
appendLogLine(
|
||
logTextarea,
|
||
`[ui] backfill completed but automatic token inspection failed for '${payload.tokenMint}': ${String(error)}`,
|
||
);
|
||
}
|
||
} catch (error) {
|
||
appendLogLine(logTextarea, `[ui] token backfill error: ${String(error)}`);
|
||
}
|
||
});
|
||
}); |