This commit is contained in:
2026-06-01 19:05:46 +02:00
parent abb810d544
commit 27e25d5bf4
59 changed files with 5727 additions and 1706 deletions

View File

@@ -218,6 +218,16 @@
<div class="form-text">Leave all unchecked for generic discovery. Check several surfaces to scan once and keep candidates matching any selected target.</div>
<div class="form-text">Use this to find corpus signatures for non-swap decoders without promoting unverified events. Leave all unchecked to request target='any'.</div>
</div>
<div class="col-6">
<label for="demo3TargetInstructionNameInput" class="form-label">Target instruction name</label>
<input id="demo3TargetInstructionNameInput" type="text" class="form-control font-monospace" placeholder="withdraw, raydium_cpmm.deposit" />
<div class="form-text">Optional exact instruction-name filter. Accepts comma/space separated names.</div>
</div>
<div class="col-6">
<label for="demo3TargetDiscriminatorHexInput" class="form-label">Target discriminator hex</label>
<input id="demo3TargetDiscriminatorHexInput" type="text" class="form-control font-monospace" placeholder="b712469c946da122, f223c68952e1f2b6" />
<div class="form-text">Optional first 8 bytes of instruction data, matching the Solscan instruction filter.</div>
</div>
<div class="col-6">
<label for="demo3HttpRoleInput" class="form-label">HTTP role</label>
<input id="demo3HttpRoleInput" type="text" class="form-control" value="history_backfill" />
@@ -335,6 +345,7 @@
<th>Kind</th>
<th>Confidence</th>
<th>Data prefix</th>
<th>Discriminator</th>
<th>Verified pool</th>
<th>Token A</th>
<th>Token B</th>
@@ -346,7 +357,7 @@
</thead>
<tbody id="demo3OnchainCandidateTableBody">
<tr>
<td colspan="12" class="text-body-secondary">No on-chain candidate.</td>
<td colspan="13" class="text-body-secondary">No on-chain candidate.</td>
</tr>
</tbody>
</table>

View File

@@ -416,7 +416,7 @@
Aucun jeu de candles chargé.
</div>
</div>
<div id="demoPipeline2Chart" class="w-100 border rounded bg-body" style="height: 560px;"></div>
<div id="demoPipeline2Chart" class="w-100 border rounded bg-body" style="height: 680px; min-height: 680px;"></div>
</div>
</div>
</div>

View File

@@ -44,6 +44,14 @@ scanOrder: string | null,
* Optional target event family used to find non-swap signatures.
*/
targetEvent: string | null,
/**
* Optional instruction name filter, e.g. `withdraw` or `raydium_cpmm.withdraw`.
*/
targetInstructionName: string | null,
/**
* Optional instruction discriminator filter as 16-char lower hex, comma-separated when needed.
*/
targetDiscriminatorHex: string | null,
/**
* Whether transactions containing swap-like logs should be skipped.
*/

View File

@@ -54,6 +54,10 @@ instructionName: string | null,
* Prefix of the raw base58 instruction data, useful for audit grouping.
*/
instructionDataPrefix: string | null,
/**
* First eight instruction-data bytes as lower hex.
*/
instructionDiscriminatorHex: string | null,
/**
* Candidate pool address.
*/

View File

@@ -187,6 +187,20 @@ function validateOnchainRequest(request: Demo3OnchainDexDiscoveryRequest): void
}
throw new Error("Program id filter must be a valid Solana program id, or empty when using a preset that resolves it.");
}
validateDiscriminatorFilter(request.targetDiscriminatorHex);
}
function validateDiscriminatorFilter(value: string | null): void {
if (value === null || value.trim() === "") {
return;
}
const tokens = value.split(/[\s,]+/).map((token) => token.trim()).filter((token) => token !== "");
for (const token of tokens) {
const normalized = token.startsWith("0x") || token.startsWith("0X") ? token.slice(2) : token;
if (!/^[0-9a-fA-F]{16}$/.test(normalized)) {
throw new Error(`Target discriminator '${token}' must be exactly 8 bytes / 16 hex characters.`);
}
}
}
function numberValueOrNull(value: string): number | null {
@@ -320,6 +334,8 @@ function readOnchainRequest(): Demo3OnchainDexDiscoveryRequest {
maxPages: intValue("demo3MaxPagesInput", 1),
scanOrder: valueOrNull(byId<HTMLSelectElement>("demo3ScanOrderSelect").value),
targetEvent: readTargetEventFilter(),
targetInstructionName: valueOrNull(byId<HTMLInputElement>("demo3TargetInstructionNameInput").value),
targetDiscriminatorHex: valueOrNull(byId<HTMLInputElement>("demo3TargetDiscriminatorHexInput").value),
excludeSwaps: byId<HTMLInputElement>("demo3ExcludeSwapsInput").checked,
includeFailed: byId<HTMLInputElement>("demo3IncludeFailedInput").checked,
httpRole: byId<HTMLInputElement>("demo3HttpRoleInput").value.trim() || "history_backfill",
@@ -375,8 +391,10 @@ function renderOnchainResult(result: Demo3OnchainDexDiscoveryResult): void {
byId<HTMLElement>("demo3SummaryRejectedCandidateCount").textContent = String(result.targetRejectedCandidateCount);
byId<HTMLElement>("demo3SummaryCandidateCount").textContent = String(result.candidateCount);
const targetEvent = targetEventLabel(result.request.targetEvent);
const targetInstruction = result.request.targetInstructionName ?? "any";
const targetDiscriminator = result.request.targetDiscriminatorHex ?? "any";
const sourceText = result.resolvedSignatureAddresses.length === 0 ? result.resolvedSignatureAddress : result.resolvedSignatureAddresses.join(",");
byId<HTMLElement>("demo3TargetText").textContent = `${result.resolvedDexCode ?? "custom"} / program=${result.resolvedProgramId} / source=${result.resolvedSignatureSource}:${sourceText} / target=${targetEvent} / order=${result.request.scanOrder ?? "newest_first"}`;
byId<HTMLElement>("demo3TargetText").textContent = `${result.resolvedDexCode ?? "custom"} / program=${result.resolvedProgramId} / source=${result.resolvedSignatureSource}:${sourceText} / target=${targetEvent} / instr=${targetInstruction} / disc=${targetDiscriminator} / order=${result.request.scanOrder ?? "newest_first"}`;
byId<HTMLElement>("demo3UniqueSignatureText").textContent = result.uniqueBackfillSignatures.length === 0 ? "-" : result.uniqueBackfillSignatures.join(", ");
byId<HTMLElement>("demo3NextBeforeText").textContent = result.nextBeforeByAddress.length === 0 ? "-" : result.nextBeforeByAddress.map((cursor) => `${cursor.address}:${cursor.nextBeforeSignature ?? "-"}`).join(" | ");
renderRejectedSummary(result);
@@ -402,7 +420,7 @@ function renderRejectedSummary(result: Demo3OnchainDexDiscoveryResult): void {
function renderOnchainCandidates(candidates: Demo3OnchainDexPairCandidate[]): void {
const body = byId<HTMLTableSectionElement>("demo3OnchainCandidateTableBody");
if (candidates.length === 0) {
body.innerHTML = '<tr><td colspan="12" class="text-body-secondary">No on-chain candidate.</td></tr>';
body.innerHTML = '<tr><td colspan="13" class="text-body-secondary">No on-chain candidate.</td></tr>';
return;
}
body.innerHTML = candidates.map((candidate) => {
@@ -428,6 +446,7 @@ function renderOnchainCandidates(candidates: Demo3OnchainDexPairCandidate[]): vo
<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(candidate.instructionDataPrefix ?? "")}">${escapeHtml(shortText(candidate.instructionDataPrefix, 14))}</td>
<td class="font-monospace" title="${escapeHtml(candidate.instructionDiscriminatorHex ?? "")}">${escapeHtml(shortText(candidate.instructionDiscriminatorHex, 16))}</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>
@@ -469,7 +488,7 @@ async function discoverOnchain(): Promise<void> {
return;
}
setStatus("running", "text-bg-warning");
appendLogLine(`on-chain discovery dex='${request.dexCode ?? ""}' program='${request.programId ?? ""}' source='${request.signatureSource ?? "program_id"}:${request.sourceAddresses.join(",")}' target='${targetEventLabel(request.targetEvent)}' pages='${request.maxPages}' order='${request.scanOrder ?? "newest_first"}' before='${request.beforeSignature ?? ""}' until='${request.untilSignature ?? ""}' excludeSwaps='${request.excludeSwaps}' role='${request.httpRole}'`);
appendLogLine(`on-chain discovery dex='${request.dexCode ?? ""}' program='${request.programId ?? ""}' source='${request.signatureSource ?? "program_id"}:${request.sourceAddresses.join(",")}' target='${targetEventLabel(request.targetEvent)}' instruction='${request.targetInstructionName ?? ""}' discriminator='${request.targetDiscriminatorHex ?? ""}' pages='${request.maxPages}' order='${request.scanOrder ?? "newest_first"}' before='${request.beforeSignature ?? ""}' until='${request.untilSignature ?? ""}' excludeSwaps='${request.excludeSwaps}' role='${request.httpRole}'`);
try {
const payload = await invoke<Demo3OnchainDexDiscoveryPayload>("demo3_discover_onchain_dex_pairs", { request });
lastResultJson = payload.resultJson;

View File

@@ -186,6 +186,32 @@ function renderCatalogTextareas(
pairsTextarea.value = JSON.stringify(catalog.pairs, null, 2);
}
function toChartNumber(value: number | string | null | undefined): number {
if (value === null || value === undefined) {
return 0;
}
if (typeof value === "number") {
return Number.isFinite(value) ? value : 0;
}
const parsed = Number.parseFloat(value);
if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
return 0;
}
return parsed;
}
function calculateVisibleWindowStart(totalCandles: number): number {
if (totalCandles <= 90) {
return 0;
}
return Math.max(0, ((totalCandles - 90) / totalCandles) * 100);
}
function parseCandlesJson(raw: string): PairCandle[] {
if (raw.trim() === "") {
return [];
@@ -239,16 +265,18 @@ function renderCandlesChart(
);
const ohlcData = sorted.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,
toChartNumber(candle.open_price_quote_per_base),
toChartNumber(candle.close_price_quote_per_base),
toChartNumber(candle.low_price_quote_per_base),
toChartNumber(candle.high_price_quote_per_base),
]);
const volumeData = sorted.map((candle) =>
parseVolume(candle.quote_volume_raw, candle.trade_count),
);
const zoomStart = calculateVisibleWindowStart(sorted.length);
chartMeta.textContent =
`Pair #${pairId.toString()} • timeframe ${timeframeSeconds.toString()}s • ${sorted.length} candles`;
@@ -256,7 +284,8 @@ function renderCandlesChart(
animation: false,
legend: {
data: ["OHLC", "Volume"],
top: 0,
top: 4,
left: 16,
},
tooltip: {
trigger: "axis",
@@ -268,8 +297,8 @@ function renderCandlesChart(
link: [{ xAxisIndex: "all" }],
},
grid: [
{ left: 60, right: 24, top: 40, height: "58%" },
{ left: 60, right: 24, top: "74%", height: "16%" },
{ left: 76, right: 32, top: 52, height: "58%" },
{ left: 76, right: 32, top: "77%", height: "12%" },
],
xAxis: [
{
@@ -277,7 +306,9 @@ function renderCandlesChart(
data: categoryData,
boundaryGap: true,
axisLine: { onZero: false },
splitLine: { show: false },
axisTick: { alignWithLabel: true },
splitLine: { show: true },
axisLabel: { hideOverlap: true, margin: 14 },
min: "dataMin",
max: "dataMax",
},
@@ -287,9 +318,9 @@ function renderCandlesChart(
data: categoryData,
boundaryGap: true,
axisLine: { onZero: false },
axisTick: { show: false },
axisTick: { alignWithLabel: true },
splitLine: { show: false },
axisLabel: { show: false },
axisLabel: { hideOverlap: true, margin: 10 },
min: "dataMin",
max: "dataMax",
},
@@ -297,27 +328,33 @@ function renderCandlesChart(
yAxis: [
{
scale: true,
splitNumber: 5,
splitArea: { show: false },
axisLabel: { margin: 12 },
},
{
gridIndex: 1,
scale: true,
splitNumber: 2,
axisLabel: { margin: 12 },
},
],
dataZoom: [
{
type: "inside",
xAxisIndex: [0, 1],
start: 0,
filterMode: "none",
start: zoomStart,
end: 100,
},
{
show: true,
type: "slider",
xAxisIndex: [0, 1],
bottom: 6,
start: 0,
filterMode: "none",
bottom: 8,
height: 22,
start: zoomStart,
end: 100,
},
],
@@ -326,11 +363,12 @@ function renderCandlesChart(
name: "OHLC",
type: "candlestick",
data: ohlcData,
xAxisIndex: 0,
yAxisIndex: 0,
barMinWidth: 4,
barMaxWidth: 16,
itemStyle: {
color: "#16a34a",
color0: "#dc2626",
borderColor: "#15803d",
borderColor0: "#b91c1c",
borderWidth: 1.4,
},
},
{
@@ -339,9 +377,13 @@ function renderCandlesChart(
xAxisIndex: 1,
yAxisIndex: 1,
data: volumeData,
barMinWidth: 2,
barMaxWidth: 10,
},
],
}, true);
window.setTimeout(() => chart.resize(), 0);
}
document.addEventListener("DOMContentLoaded", async () => {
@@ -468,6 +510,10 @@ document.addEventListener("DOMContentLoaded", async () => {
const chart = echarts.init(safeChartElement);
setEmptyChart(chart, safeChartMeta, "Aucune candle disponible.");
window.addEventListener("resize", () => chart.resize());
const chartCollapse = document.querySelector<HTMLDivElement>("#demoPipeline2ChartCollapse");
chartCollapse?.addEventListener("shown.bs.collapse", () => {
window.setTimeout(() => chart.resize(), 0);
});
clearLogButton.addEventListener("click", () => {
logTextarea.value = "";

View File

@@ -1,7 +1,7 @@
{
"name": "kb-demo-app",
"private": true,
"version": "0.7.47",
"version": "0.7.48",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -287,7 +287,10 @@ pub(crate) async fn demo3_search_local_dex_corpus(
/// Search request for the static upstream registry exposed through Demo3.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, TS)]
#[ts(export, export_to = "../frontend/ts/bindings/Demo3UpstreamRegistrySearchRequest.ts")]
#[ts(
export,
export_to = "../frontend/ts/bindings/Demo3UpstreamRegistrySearchRequest.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Demo3UpstreamRegistrySearchRequest {
/// Optional decoder-code filter.
@@ -423,10 +426,7 @@ pub(crate) fn demo3_search_upstream_registry(
return Err(format!("cannot serialize upstream registry result: {}", error));
},
};
return Ok(Demo3UpstreamRegistryPayload {
result_json,
result: ui_result,
});
return Ok(Demo3UpstreamRegistryPayload { result_json, result: ui_result });
}
fn to_lib_upstream_registry_request(
@@ -491,8 +491,7 @@ fn from_lib_upstream_registry_summary(
account_entry_count: summary.account_entry_count,
upstream_git_unverified_count: summary.upstream_git_unverified_count,
upstream_git_mapped_unverified_count: summary.upstream_git_mapped_unverified_count,
upstream_git_local_corpus_observed_count: summary
.upstream_git_local_corpus_observed_count,
upstream_git_local_corpus_observed_count: summary.upstream_git_local_corpus_observed_count,
upstream_git_local_corpus_materialized_count: summary
.upstream_git_local_corpus_materialized_count,
upstream_git_layout_unverified_count: summary.upstream_git_layout_unverified_count,
@@ -685,6 +684,12 @@ pub(crate) struct Demo3OnchainDexDiscoveryRequest {
pub scan_order: std::option::Option<std::string::String>,
/// Optional target event family used to find non-swap signatures.
pub target_event: std::option::Option<std::string::String>,
/// Optional instruction name filter, e.g. `withdraw` or `raydium_cpmm.withdraw`.
#[serde(default)]
pub target_instruction_name: std::option::Option<std::string::String>,
/// Optional instruction discriminator filter as 16-char lower hex, comma-separated when needed.
#[serde(default)]
pub target_discriminator_hex: std::option::Option<std::string::String>,
/// Whether transactions containing swap-like logs should be skipped.
pub exclude_swaps: bool,
/// Whether failed transactions should be returned as candidates.
@@ -846,6 +851,8 @@ pub(crate) struct Demo3OnchainDexPairCandidate {
pub instruction_name: std::option::Option<std::string::String>,
/// Prefix of the raw base58 instruction data, useful for audit grouping.
pub instruction_data_prefix: std::option::Option<std::string::String>,
/// First eight instruction-data bytes as lower hex.
pub instruction_discriminator_hex: std::option::Option<std::string::String>,
/// Candidate pool address.
pub pool_address: std::option::Option<std::string::String>,
/// Candidate token A mint.
@@ -966,6 +973,8 @@ fn to_lib_onchain_request(
max_pages: request.max_pages,
scan_order: normalize_optional_text(request.scan_order.clone()),
target_event: normalize_optional_text(request.target_event.clone()),
target_instruction_name: normalize_optional_text(request.target_instruction_name.clone()),
target_discriminator_hex: normalize_optional_text(request.target_discriminator_hex.clone()),
exclude_swaps: request.exclude_swaps,
include_failed: request.include_failed,
http_role: request.http_role.trim().to_string(),
@@ -994,6 +1003,8 @@ fn from_lib_onchain_result(
max_pages: result.request.max_pages,
scan_order: result.request.scan_order,
target_event: result.request.target_event,
target_instruction_name: result.request.target_instruction_name,
target_discriminator_hex: result.request.target_discriminator_hex,
exclude_swaps: result.request.exclude_swaps,
include_failed: result.request.include_failed,
http_role: result.request.http_role,
@@ -1074,6 +1085,7 @@ fn from_lib_onchain_candidate(
inner_instruction_index: candidate.inner_instruction_index,
instruction_name: candidate.instruction_name,
instruction_data_prefix: candidate.instruction_data_prefix,
instruction_discriminator_hex: candidate.instruction_discriminator_hex,
pool_address: candidate.pool_address,
token_a_mint: candidate.token_a_mint,
token_b_mint: candidate.token_b_mint,

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "kb-demo-app",
"version": "0.7.47",
"version": "0.7.48",
"identifier": "com.sasedev.kb-demo-app",
"build": {
"beforeDevCommand": "npm run dev",