v0.3.7-pre.007

This commit is contained in:
2026-09-02 16:07:57 +02:00
parent 6fc5b5b607
commit 9c4469248a
21 changed files with 1518 additions and 67 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 424
# version: 425
[workspace]
resolver = "3"
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
[workspace.package]
version = "0.3.7-pre.6.fix.1"
version = "0.3.7-pre.7"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-app-backfill-desk/Cargo.toml
# version: 3
# version: 4
[package]
name = "ksp-app-backfill-desk"
@@ -26,6 +26,8 @@ chrono = { workspace = true, features = ["std", "now"] }
fs2.workspace = true
ksp-config-lib = { path = "../ksp-config-lib" }
ksp-core-lib = { path = "../ksp-core-lib" }
ksp-job-api = { path = "../ksp-job-api" }
ksp-job-backfill-lib = { path = "../ksp-job-backfill-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" }
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
ksp-store-lib = { path = "../ksp-store-lib" }

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-backfill-desk/frontend/main.html -->
<!-- version: 2 -->
<!-- version: 3 -->
<!DOCTYPE html>
<html lang="fr">
@@ -47,12 +47,12 @@
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-4">
<div>
<h1 class="h3 mb-1">Backfill</h1>
<p class="text-body-secondary mb-0">Scaffold desktop prêt. Les contrôles Backfill seront branchés dans les tranches runtime suivantes.</p>
<p class="text-body-secondary mb-0">Mainnet par défaut, transport HTTP sélectionnable et mapping de campagne validé côté Rust avant tout démarrage de job.</p>
</div>
<span id="appVersionBadge" class="badge text-bg-secondary"></span>
</div>
<div class="row g-4">
<div class="col-xl-7">
<div class="col-xl-4">
<div class="card shadow-sm h-100">
<div class="card-header fw-semibold">Transport HTTP</div>
<div class="card-body">
@@ -71,12 +71,77 @@
</div>
</div>
</div>
<div class="col-xl-5">
<div class="app-placeholder h-100">
<i class="fa-solid fa-clock-rotate-left text-body-secondary" aria-hidden="true"></i>
<div>
<h2 class="h5">Campagne Backfill</h2>
<p class="text-body-secondary mb-0">Les scopes, commitments et le bouton Start seront branchés dans la tranche suivante. Le transport reste HTTP uniquement en v0.3.7.</p>
<div class="col-xl-8">
<div class="card shadow-sm h-100">
<div class="card-header fw-semibold">Campagne Backfill</div>
<div class="card-body">
<form id="backfillCampaignForm" novalidate>
<div class="row g-3">
<div class="col-lg-6">
<label for="backfillScopeKind" class="form-label">Scope</label>
<select id="backfillScopeKind" class="form-select" disabled></select>
</div>
<div class="col-lg-6">
<label for="backfillCommitment" class="form-label">Commitment</label>
<select id="backfillCommitment" class="form-select" disabled></select>
</div>
<div id="backfillAddressGroup" class="col-12">
<label for="backfillAddress" class="form-label">Adresse Solana</label>
<input id="backfillAddress" class="form-control font-monospace" type="text" autocomplete="off" spellcheck="false">
</div>
<div id="backfillAnchorGroup" class="col-12" hidden>
<label for="backfillAnchorSignature" class="form-label">Signature d'ancrage exclusive</label>
<input id="backfillAnchorSignature" class="form-control font-monospace" type="text" autocomplete="off" spellcheck="false">
</div>
<div id="backfillExplicitSignaturesGroup" class="col-12" hidden>
<label for="backfillExplicitSignatures" class="form-label">Signatures explicites</label>
<textarea id="backfillExplicitSignatures" class="form-control font-monospace" rows="5" autocomplete="off" spellcheck="false"></textarea>
<div id="backfillExplicitSignaturesHelp" class="form-text">Une signature par ligne.</div>
</div>
<div class="col-md-6 col-xxl-3">
<label for="backfillPageSize" class="form-label">Page size</label>
<input id="backfillPageSize" class="form-control" type="number" step="1">
</div>
<div class="col-md-6 col-xxl-3">
<label for="backfillMaxPages" class="form-label">Max pages</label>
<input id="backfillMaxPages" class="form-control" type="number" step="1">
</div>
<div class="col-md-6 col-xxl-3">
<label for="backfillMaxCandidates" class="form-label">Max candidates</label>
<input id="backfillMaxCandidates" class="form-control" type="number" step="1">
</div>
<div class="col-md-6 col-xxl-3">
<label for="backfillHydrationConcurrency" class="form-label">Concurrence</label>
<input id="backfillHydrationConcurrency" class="form-control" type="number" step="1">
</div>
<div id="backfillMinContextSlotGroup" class="col-lg-6">
<label for="backfillMinContextSlot" class="form-label">Min context slot</label>
<input id="backfillMinContextSlot" class="form-control font-monospace" type="text" inputmode="numeric" autocomplete="off" spellcheck="false" placeholder="optionnel">
<div class="form-text">Chaîne décimale validée côté Rust pour préserver la précision u64.</div>
</div>
</div>
<div class="d-flex align-items-center gap-3 flex-wrap mt-4">
<button id="validateBackfillRequest" class="btn btn-primary" type="submit" disabled>
<i class="fa-solid fa-shield-halved me-2" aria-hidden="true"></i>Valider la requête
</button>
<span class="text-body-secondary small">Cette tranche ne démarre aucun job.</span>
</div>
</form>
<div id="backfillRequestValidation" class="alert alert-secondary mt-4 mb-0" role="status" aria-live="polite" hidden></div>
<div id="backfillRequestPreview" class="card bg-body-tertiary border-0 mt-3" hidden>
<div class="card-body">
<h2 class="h6 mb-3">Projection backend sûre</h2>
<dl class="row mb-0 app-runtime-list">
<dt class="col-sm-5">Réseau</dt><dd id="validatedNetwork" class="col-sm-7"></dd>
<dt class="col-sm-5">Route HTTP</dt><dd id="validatedHttpRole" class="col-sm-7"></dd>
<dt class="col-sm-5">Commitment</dt><dd id="validatedCommitment" class="col-sm-7"></dd>
<dt class="col-sm-5">Scope</dt><dd id="validatedScopeKind" class="col-sm-7"></dd>
<dt class="col-sm-5">Signatures explicites</dt><dd id="validatedExplicitSignatureCount" class="col-sm-7"></dd>
<dt class="col-sm-5">page/pages/candidates/concurrence</dt><dd id="validatedBounds" class="col-sm-7"></dd>
<dt class="col-sm-5">Min context slot</dt><dd id="validatedMinContextSlot" class="col-sm-7"></dd>
</dl>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,10 +1,12 @@
// file: crates/ksp-app-backfill-desk/frontend/ts/main.ts
// version: 2
// version: 3
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
import "simplebar";
import { getCurrentWindow } from "@tauri-apps/api/window";
import type { BackfillRequestPreviewDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestPreviewDto.ts";
import type { BackfillStartRequestDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartRequestDto.ts";
import type { BackfillDeskOptionsDto } from "./bindings/ksp_app_backfill_desk/dto_common/BackfillDeskOptionsDto.ts";
import type { BackfillHttpRouteOptionDto } from "./bindings/ksp_app_backfill_desk/dto_common/BackfillHttpRouteOptionDto.ts";
import type { ShellStatusDto } from "./bindings/ksp_app_backfill_desk/dto_common/ShellStatusDto.ts";
@@ -21,6 +23,19 @@ const viewTitles: Record<ViewId, string> = {
diagnostics: "Diagnostics",
};
const commitmentLabels: Record<string, string> = {
confirmed: "confirmed",
finalized: "finalized",
};
const scopeLabels: Record<string, string> = {
latest_address: "Adresse — historique le plus récent",
before_address: "Adresse — avant une signature",
after_address: "Adresse — après une signature",
explicit_signatures: "Signatures explicites",
};
function isViewId(value: string | undefined): value is ViewId {
return value === "backfill" || value === "diagnostics";
}
@@ -79,6 +94,245 @@ function bindNavigation(): void {
frontendTrace("main", "Backfill Desk navigation handlers installed");
}
function populateCodeSelect(selectId: string, values: string[], labels: Record<string, string>): void {
const select = document.querySelector<HTMLSelectElement>(`#${selectId}`);
if (!select) {
return;
}
const previous = select.value;
select.replaceChildren();
for (const value of values) {
const option = document.createElement("option");
option.value = value;
option.textContent = labels[value] ?? value;
select.append(option);
}
select.disabled = values.length === 0;
if (values.includes(previous)) {
select.value = previous;
}
}
function applyNumberInputContract(inputId: string, maximum: number, defaultValue: number): void {
const input = document.querySelector<HTMLInputElement>(`#${inputId}`);
if (!input) {
return;
}
input.min = "1";
input.max = maximum.toString();
if (input.value === "") {
input.value = defaultValue.toString();
}
}
function renderCampaignContract(options: BackfillDeskOptionsDto): void {
populateCodeSelect("backfillScopeKind", options.scopeKinds, scopeLabels);
populateCodeSelect("backfillCommitment", options.commitments, commitmentLabels);
applyNumberInputContract("backfillPageSize", options.limits.maxPageSize, options.limits.defaultPageSize);
applyNumberInputContract("backfillMaxPages", options.limits.maxPages, options.limits.defaultMaxPages);
applyNumberInputContract("backfillMaxCandidates", options.limits.maxCandidates, options.limits.defaultMaxCandidates);
applyNumberInputContract("backfillHydrationConcurrency", options.limits.maxHydrationConcurrency, options.limits.defaultHydrationConcurrency);
const signatureHelp = document.querySelector<HTMLElement>("#backfillExplicitSignaturesHelp");
if (signatureHelp) {
signatureHelp.textContent = `Une signature par ligne, ${options.limits.minSignatureTextBytes}${options.limits.maxSignatureTextBytes} octets encodés ; maximum ${options.limits.maxCandidates}.`;
}
const validateButton = document.querySelector<HTMLButtonElement>("#validateBackfillRequest");
if (validateButton) {
validateButton.disabled = !options.compositionReady || options.httpRoutes.length === 0;
}
updateCampaignScopeFields();
frontendTrace("main", "Backfill Desk campaign contract rendered", {
commitmentCount: options.commitments.length,
compositionReady: options.compositionReady,
scopeCount: options.scopeKinds.length,
});
}
function currentScopeKind(): string {
const select = document.querySelector<HTMLSelectElement>("#backfillScopeKind");
return select?.value ?? "";
}
function updateCampaignScopeFields(): void {
const scopeKind = currentScopeKind();
const addressGroup = document.querySelector<HTMLElement>("#backfillAddressGroup");
const anchorGroup = document.querySelector<HTMLElement>("#backfillAnchorGroup");
const signaturesGroup = document.querySelector<HTMLElement>("#backfillExplicitSignaturesGroup");
const minContextSlotGroup = document.querySelector<HTMLElement>("#backfillMinContextSlotGroup");
const addressScope = scopeKind === "latest_address" || scopeKind === "before_address" || scopeKind === "after_address";
const anchoredScope = scopeKind === "before_address" || scopeKind === "after_address";
const explicitScope = scopeKind === "explicit_signatures";
if (addressGroup) {
addressGroup.hidden = !addressScope;
}
if (anchorGroup) {
anchorGroup.hidden = !anchoredScope;
}
if (signaturesGroup) {
signaturesGroup.hidden = !explicitScope;
}
if (minContextSlotGroup) {
minContextSlotGroup.hidden = explicitScope;
}
frontendTrace("main", "Backfill Desk campaign scope fields updated", { scopeKind: scopeKind || "none" });
}
function invalidateCampaignValidation(source: "input" | "scope" | "route"): void {
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
if (status) {
status.hidden = true;
status.textContent = "";
status.classList.remove("alert-success", "alert-danger");
status.classList.add("alert-secondary");
}
const summary = document.querySelector<HTMLElement>("#backfillRequestPreview");
if (summary) {
summary.hidden = true;
}
frontendTrace("main", "Backfill Desk campaign validation invalidated", { source });
}
function numericInputValue(inputId: string): number {
const input = document.querySelector<HTMLInputElement>(`#${inputId}`);
if (!input || !Number.isSafeInteger(input.valueAsNumber) || input.valueAsNumber < 0) {
return 0;
}
return input.valueAsNumber;
}
function rawOptionalInput(inputId: string, enabled: boolean): string | null {
if (!enabled) {
return null;
}
const input = document.querySelector<HTMLInputElement>(`#${inputId}`);
if (!input || input.value === "") {
return null;
}
return input.value;
}
function explicitSignatureLines(): string[] {
const textarea = document.querySelector<HTMLTextAreaElement>("#backfillExplicitSignatures");
if (!textarea || currentScopeKind() !== "explicit_signatures") {
return [];
}
return textarea.value.split(/\r?\n/).filter(line => line.length > 0);
}
function buildBackfillStartRequest(): BackfillStartRequestDto {
const scopeKind = currentScopeKind();
const role = document.querySelector<HTMLSelectElement>("#backfillHttpRoute")?.value ?? "";
const commitment = document.querySelector<HTMLSelectElement>("#backfillCommitment")?.value ?? "";
const addressScope = scopeKind === "latest_address" || scopeKind === "before_address" || scopeKind === "after_address";
const anchoredScope = scopeKind === "before_address" || scopeKind === "after_address";
const minContextSlotEnabled = scopeKind !== "explicit_signatures";
return {
address: rawOptionalInput("backfillAddress", addressScope),
anchorSignature: rawOptionalInput("backfillAnchorSignature", anchoredScope),
commitment,
explicitSignatures: explicitSignatureLines(),
hydrationConcurrency: numericInputValue("backfillHydrationConcurrency"),
httpRole: role,
maxCandidates: numericInputValue("backfillMaxCandidates"),
maxPages: numericInputValue("backfillMaxPages"),
minContextSlot: rawOptionalInput("backfillMinContextSlot", minContextSlotEnabled),
pageSize: numericInputValue("backfillPageSize"),
scopeKind,
};
}
function renderRequestPreview(preview: BackfillRequestPreviewDto): void {
const values: Record<string, string> = {
validatedNetwork: preview.network,
validatedHttpRole: preview.httpRole,
validatedCommitment: preview.commitment,
validatedScopeKind: preview.scopeKind,
validatedExplicitSignatureCount: preview.explicitSignatureCount.toString(),
validatedBounds: `${preview.pageSize} / ${preview.maxPages} / ${preview.maxCandidates} / ${preview.hydrationConcurrency}`,
validatedMinContextSlot: preview.minContextSlotPresent ? "présent" : "absent",
};
for (const [id, value] of Object.entries(values)) {
const element = document.querySelector<HTMLElement>(`#${id}`);
if (element) {
element.textContent = value;
}
}
const summary = document.querySelector<HTMLElement>("#backfillRequestPreview");
if (summary) {
summary.hidden = false;
}
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
if (status) {
status.hidden = false;
status.classList.remove("alert-secondary", "alert-danger");
status.classList.add("alert-success");
status.textContent = "Requête valide. Aucun job n'a été démarré.";
}
frontendTrace("main", "Backfill Desk validated request preview rendered", {
commitment: preview.commitment,
httpRole: preview.httpRole,
scopeKind: preview.scopeKind,
});
}
function renderRequestValidationError(caughtError: unknown): void {
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
if (!status) {
return;
}
let message = "Requête refusée par le backend.";
if (typeof caughtError === "object" && caughtError !== null) {
const candidate = caughtError as { code?: unknown; domain?: unknown; message?: unknown };
if (typeof candidate.domain === "string" && typeof candidate.code === "string" && typeof candidate.message === "string") {
message = `${candidate.domain}/${candidate.code}: ${candidate.message}`;
}
}
status.hidden = false;
status.classList.remove("alert-secondary", "alert-success");
status.classList.add("alert-danger");
status.textContent = message;
}
async function validateBackfillRequest(): Promise<void> {
const request = buildBackfillStartRequest();
frontendDebug("main", "Backfill Desk campaign validation requested", {
commitment: request.commitment,
httpRole: request.httpRole,
scopeKind: request.scopeKind,
});
try {
const preview = await invokeKsp<BackfillRequestPreviewDto>("main", "backfill_validate_request", { request });
renderRequestPreview(preview);
frontendDebug("main", "Backfill Desk campaign validation completed", {
commitment: preview.commitment,
httpRole: preview.httpRole,
scopeKind: preview.scopeKind,
});
} catch (caughtError) {
renderRequestValidationError(caughtError);
frontendWarn("main", "Backfill Desk campaign validation failed");
}
}
function bindCampaignForm(): void {
const scope = document.querySelector<HTMLSelectElement>("#backfillScopeKind");
if (scope) {
scope.addEventListener("change", () => {
frontendDebug("main", "Backfill Desk campaign scope changed", { scopeKind: scope.value || "none" });
updateCampaignScopeFields();
invalidateCampaignValidation("scope");
});
}
const form = document.querySelector<HTMLFormElement>("#backfillCampaignForm");
if (form) {
form.addEventListener("input", () => invalidateCampaignValidation("input"));
form.addEventListener("submit", event => {
event.preventDefault();
void validateBackfillRequest();
});
}
frontendTrace("main", "Backfill Desk campaign form handlers installed");
}
function routeLabel(route: BackfillHttpRouteOptionDto): string {
if (route.pooled) {
@@ -88,6 +342,7 @@ function routeLabel(route: BackfillHttpRouteOptionDto): string {
}
function renderBackfillOptions(options: BackfillDeskOptionsDto): void {
renderCampaignContract(options);
const select = document.querySelector<HTMLSelectElement>("#backfillHttpRoute");
if (select) {
const previous = select.value;
@@ -143,6 +398,7 @@ function bindBackfillRouteSelection(): void {
}
select.addEventListener("change", () => {
frontendDebug("main", "Backfill Desk HTTP route selection changed", { role: select.value || "none" });
invalidateCampaignValidation("route");
});
frontendTrace("main", "Backfill Desk HTTP route selection handler installed");
}
@@ -202,6 +458,7 @@ async function initializeMain(): Promise<void> {
bindNavigation();
bindRuntimeStatusRefresh();
bindBackfillRouteSelection();
bindCampaignForm();
activateView("backfill", "startup");
try {
await Promise.all([loadRuntimeStatus("startup"), loadBackfillOptions("startup")]);

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/app_state.rs
// version: 4
// version: 5
//! Shared backend state owned by the Backfill Desk Tauri application.
@@ -121,7 +121,7 @@ impl crate::AppState {
application_version: env!("CARGO_PKG_VERSION").to_owned(),
config_document_count: document_count,
fallback_logging_active: runtime.fallback_active,
shell_phase: "pre.006-mainnet-http-routing".to_owned(),
shell_phase: "pre.007-request-mapping".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(),
});
}
@@ -131,17 +131,27 @@ impl crate::AppState {
let runtime = self.transport_runtime.as_ref();
let options = match runtime {
std::option::Option::Some(value) => value.options(),
std::option::Option::None => std::result::Result::Ok(crate::BackfillDeskOptionsDto {
http_routes: std::vec::Vec::new(),
composition_ready: false,
configured_networks: std::vec::Vec::new(),
network_coherent: false,
store_diagnostic: std::option::Option::None,
store_network: std::option::Option::None,
store_ready: false,
transport_diagnostic: self.transport_startup_diagnostic.clone(),
transport_ready: false,
}),
std::option::Option::None => {
let limits = crate::backfill_request_limits();
let limits = match limits {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
std::result::Result::Ok(crate::BackfillDeskOptionsDto {
commitments: crate::backfill_commitment_codes(),
composition_ready: false,
configured_networks: std::vec::Vec::new(),
http_routes: std::vec::Vec::new(),
limits,
network_coherent: false,
scope_kinds: crate::backfill_scope_kind_codes(),
store_diagnostic: std::option::Option::None,
store_network: std::option::Option::None,
store_ready: false,
transport_diagnostic: self.transport_startup_diagnostic.clone(),
transport_ready: false,
})
},
};
let mut options = match options {
std::result::Result::Ok(value) => value,
@@ -151,6 +161,39 @@ impl crate::AppState {
return std::result::Result::Ok(options);
}
/// Validates and maps one frontend campaign request without starting a Backfill Job.
pub(crate) fn validate_backfill_request(&self, request: crate::BackfillStartRequestDto) -> ksp_core_lib::Result<crate::BackfillRequestPreviewDto> {
let options = self.backfill_options();
let options = match options {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let job_id = ksp_job_api::JobId::new("backfill-desk-validation");
let job_id = match job_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mapped = crate::map_backfill_request(request, &options, job_id);
let mapped = match mapped {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let preview = crate::project_backfill_request(&mapped);
let preview = match preview {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_REQUEST,
commitment = preview.commitment.as_str(),
http_role = preview.http_role.as_str(),
scope_kind = preview.scope_kind.as_str(),
"validated Backfill Desk campaign request without starting a Job"
);
return std::result::Result::Ok(preview);
}
/// Marks graceful application shutdown as started and reports whether this caller won the one-shot transition.
pub(crate) fn begin_shutdown(&self) -> bool {
return self.shutdown_started.compare_exchange(false, true, std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire).is_ok();

View File

@@ -0,0 +1,313 @@
// file: crates/ksp-app-backfill-desk/src/backfill_request.rs
// version: 1
//! Strict application mapping from frontend campaign DTOs to the KSP Backfill request contract.
/// Maps one app-owned campaign request to a validated KSP Backfill request without starting a Job.
pub(crate) fn map_backfill_request(
input: crate::BackfillStartRequestDto,
options: &crate::BackfillDeskOptionsDto,
job_id: ksp_job_api::JobId,
) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillRequest> {
let composition = validate_composition(options);
if let std::result::Result::Err(error) = composition {
return std::result::Result::Err(error);
}
let network = request_network(options);
let network = match network {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let role = request_role(input.http_role.as_str(), options);
let role = match role {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let commitment = request_commitment(input.commitment.as_str());
let commitment = match commitment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let min_context_slot = parse_min_context_slot(input.min_context_slot.as_deref());
let min_context_slot = match min_context_slot {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let scope = request_scope(&input);
let scope = match scope {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let page_size = usize::try_from(input.page_size);
let page_size = match page_size {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("page_size", error)),
};
let max_pages = usize::try_from(input.max_pages);
let max_pages = match max_pages {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("max_pages", error)),
};
let max_candidates = usize::try_from(input.max_candidates);
let max_candidates = match max_candidates {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("max_candidates", error)),
};
let hydration_concurrency = usize::try_from(input.hydration_concurrency);
let hydration_concurrency = match hydration_concurrency {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("hydration_concurrency", error)),
};
return ksp_job_backfill_lib::BackfillRequest::new(
job_id,
network,
role,
commitment,
scope,
page_size,
max_pages,
max_candidates,
hydration_concurrency,
min_context_slot,
);
}
/// Builds a safe request preview from one already validated KSP Backfill request.
pub(crate) fn project_backfill_request(request: &ksp_job_backfill_lib::BackfillRequest) -> ksp_core_lib::Result<crate::BackfillRequestPreviewDto> {
let explicit_signature_count = match request.scope().signatures() {
std::option::Option::Some(signatures) => u32::try_from(signatures.len()),
std::option::Option::None => std::result::Result::Ok(0),
};
let explicit_signature_count = match explicit_signature_count {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("explicit_signature_count", error)),
};
let hydration_concurrency = u32::try_from(request.hydration_concurrency());
let hydration_concurrency = match hydration_concurrency {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("hydration_concurrency", error)),
};
let max_candidates = u32::try_from(request.max_candidates());
let max_candidates = match max_candidates {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("max_candidates", error)),
};
let max_pages = u32::try_from(request.max_pages());
let max_pages = match max_pages {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("max_pages", error)),
};
let page_size = u32::try_from(request.page_size());
let page_size = match page_size {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(request_conversion_error("page_size", error)),
};
return std::result::Result::Ok(crate::BackfillRequestPreviewDto {
address_present: request.scope().address().is_some(),
anchor_present: request.scope().anchor().is_some(),
commitment: request.commitment().code().to_owned(),
explicit_signature_count,
hydration_concurrency,
http_role: request.role().as_str().to_owned(),
max_candidates,
max_pages,
min_context_slot_present: request.min_context_slot().is_some(),
network: request.network().as_str().to_owned(),
page_size,
scope_kind: request.scope().kind().code().to_owned(),
});
}
fn parse_min_context_slot(value: std::option::Option<&str>) -> ksp_core_lib::Result<std::option::Option<u64>> {
let value = match value {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
if value.is_empty() || value.trim() != value {
return std::result::Result::Err(request_field_error("min_context_slot"));
}
let parsed = value.parse::<u64>();
return match parsed {
std::result::Result::Ok(value) => std::result::Result::Ok(std::option::Option::Some(value)),
std::result::Result::Err(error) => std::result::Result::Err(request_field_error("min_context_slot").with_source(error)),
};
}
fn parse_pubkey(value: &str, field: &'static str) -> ksp_core_lib::Result<ksp_core_lib::Pubkey> {
if value.is_empty() || value.trim() != value {
return std::result::Result::Err(request_field_error(field));
}
let parsed = value.parse::<ksp_core_lib::Pubkey>();
return match parsed {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(request_field_error(field).with_source(error)),
};
}
fn parse_signature(value: &str, field: &'static str) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillSignature> {
if value.is_empty() || value.trim() != value {
return std::result::Result::Err(request_field_error(field));
}
let parsed = ksp_job_backfill_lib::BackfillSignature::new(value.to_owned());
return match parsed {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(request_field_error(field).with_source(error)),
};
}
fn request_commitment(value: &str) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillCommitment> {
return match value {
"confirmed" => std::result::Result::Ok(ksp_job_backfill_lib::BackfillCommitment::Confirmed),
"finalized" => std::result::Result::Ok(ksp_job_backfill_lib::BackfillCommitment::Finalized),
_ => std::result::Result::Err(request_field_error("commitment")),
};
}
fn request_conversion_error(field: &'static str, source: impl std::error::Error + Send + Sync + 'static) -> ksp_core_lib::Error {
return request_field_error(field).with_source(source);
}
fn request_field_error(field: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_BACKFILL_REQUEST_INVALID, "Backfill Desk campaign request is invalid").with_context("field", field);
}
fn request_network(options: &crate::BackfillDeskOptionsDto) -> ksp_core_lib::Result<ksp_store_lib::RawNetworkId> {
let network = options.store_network.as_deref();
let network = match network {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(request_field_error("network")),
};
let parsed = ksp_store_lib::RawNetworkId::new(network.to_owned());
return match parsed {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(request_field_error("network").with_source(error)),
};
}
fn request_role(value: &str, options: &crate::BackfillDeskOptionsDto) -> ksp_core_lib::Result<ksp_onchain_transport_lib::HttpRoleName> {
let admitted = options.http_routes.iter().any(|route| return route.role == value);
if !admitted || value.is_empty() || value.trim() != value {
return std::result::Result::Err(request_field_error("http_role"));
}
return std::result::Result::Ok(ksp_onchain_transport_lib::HttpRoleName::new(value.to_owned()));
}
fn request_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillScope> {
return match input.scope_kind.as_str() {
"latest_address" => latest_address_scope(input),
"before_address" => before_address_scope(input),
"after_address" => after_address_scope(input),
"explicit_signatures" => explicit_signatures_scope(input),
_ => std::result::Result::Err(request_field_error("scope_kind")),
};
}
fn latest_address_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillScope> {
let shape = require_no_anchor_or_signatures(input);
if let std::result::Result::Err(error) = shape {
return std::result::Result::Err(error);
}
let address = required_address(input.address.as_deref());
let address = match address {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_job_backfill_lib::BackfillScope::latest_address(address));
}
fn before_address_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillScope> {
let shape = require_no_explicit_signatures(input);
if let std::result::Result::Err(error) = shape {
return std::result::Result::Err(error);
}
let address = required_address(input.address.as_deref());
let address = match address {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let anchor = required_anchor(input.anchor_signature.as_deref());
let anchor = match anchor {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_job_backfill_lib::BackfillScope::before_address(address, anchor));
}
fn after_address_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillScope> {
let shape = require_no_explicit_signatures(input);
if let std::result::Result::Err(error) = shape {
return std::result::Result::Err(error);
}
let address = required_address(input.address.as_deref());
let address = match address {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let anchor = required_anchor(input.anchor_signature.as_deref());
let anchor = match anchor {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_job_backfill_lib::BackfillScope::after_address(address, anchor));
}
fn explicit_signatures_scope(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillScope> {
if input.address.is_some() || input.anchor_signature.is_some() || input.min_context_slot.is_some() {
return std::result::Result::Err(request_field_error("scope"));
}
if input.explicit_signatures.is_empty() || input.explicit_signatures.len() > ksp_job_backfill_lib::MAX_BACKFILL_CANDIDATES {
return std::result::Result::Err(request_field_error("explicit_signatures"));
}
let mut signatures = std::vec::Vec::with_capacity(input.explicit_signatures.len());
for value in &input.explicit_signatures {
let signature = parse_signature(value.as_str(), "explicit_signatures");
let signature = match signature {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
signatures.push(signature);
}
return ksp_job_backfill_lib::BackfillScope::explicit_signatures(signatures);
}
fn require_no_anchor_or_signatures(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<()> {
if input.anchor_signature.is_some() || !input.explicit_signatures.is_empty() {
return std::result::Result::Err(request_field_error("scope"));
}
return std::result::Result::Ok(());
}
fn require_no_explicit_signatures(input: &crate::BackfillStartRequestDto) -> ksp_core_lib::Result<()> {
if !input.explicit_signatures.is_empty() {
return std::result::Result::Err(request_field_error("explicit_signatures"));
}
return std::result::Result::Ok(());
}
fn required_address(value: std::option::Option<&str>) -> ksp_core_lib::Result<ksp_core_lib::Pubkey> {
return match value {
std::option::Option::Some(value) => parse_pubkey(value, "address"),
std::option::Option::None => std::result::Result::Err(request_field_error("address")),
};
}
fn required_anchor(value: std::option::Option<&str>) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillSignature> {
return match value {
std::option::Option::Some(value) => parse_signature(value, "anchor_signature"),
std::option::Option::None => std::result::Result::Err(request_field_error("anchor_signature")),
};
}
fn validate_composition(options: &crate::BackfillDeskOptionsDto) -> ksp_core_lib::Result<()> {
if options.composition_ready && options.transport_ready && options.store_ready && options.network_coherent {
return std::result::Result::Ok(());
}
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
"Backfill Desk cannot admit a campaign before Transport and Store composition is ready",
));
}
#[cfg(test)]
#[path = "../unit_tests/backfill_request.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/constants.rs
// version: 4
// version: 5
//! Application-owned tracing targets and domains.
@@ -9,10 +9,20 @@ pub(crate) const COMPOSITE_COMPONENT_ID_LOGGING: &str = "logging";
pub(crate) const COMPOSITE_COMPONENT_ID_STORE: &str = "store";
/// Composite component identifier for Transport.
pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT: &str = "transport";
/// Initial hydration concurrency proposed by the Desk for a new campaign.
pub(crate) const DEFAULT_BACKFILL_HYDRATION_CONCURRENCY: u32 = 4;
/// Initial candidate cap proposed by the Desk for a new campaign.
pub(crate) const DEFAULT_BACKFILL_MAX_CANDIDATES: u32 = 1_000;
/// Initial discovery-page cap proposed by the Desk for a new campaign.
pub(crate) const DEFAULT_BACKFILL_MAX_PAGES: u32 = 10;
/// Initial signature page size proposed by the Desk for a new campaign.
pub(crate) const DEFAULT_BACKFILL_PAGE_SIZE: u32 = 100;
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "backfill.bootstrap";
/// Structured domain used by technical frontend events.
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
/// Structured domain used by Backfill campaign admission and request mapping.
pub(crate) const TRACING_DOMAIN_REQUEST: &str = "backfill.request";
/// Structured domain used by the Backfill Desk shell.
pub(crate) const TRACING_DOMAIN_SHELL: &str = "backfill.shell";
/// Structured domain used by Store readiness and shutdown operations.

View File

@@ -0,0 +1,196 @@
// file: crates/ksp-app-backfill-desk/src/dto_backfill.rs
// version: 1
//! Application-owned Backfill campaign DTOs and backend-derived request limits.
use ts_rs::TS; // rust-rules: trait-import
/// Backend-derived limits and application defaults rendered by the Backfill campaign form.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestLimitsDto.ts")]
pub(crate) struct BackfillRequestLimitsDto {
/// Initial hydration concurrency proposed by this Desk while remaining below the Job-owned maximum.
pub(crate) default_hydration_concurrency: u32,
/// Initial candidate cap proposed by this Desk while remaining below the Job-owned maximum.
pub(crate) default_max_candidates: u32,
/// Initial discovery-page cap proposed by this Desk while remaining below the Job-owned maximum.
pub(crate) default_max_pages: u32,
/// Initial signature page size proposed by this Desk while remaining below the Job-owned maximum.
pub(crate) default_page_size: u32,
/// Maximum hydration concurrency owned by `ksp-job-backfill-lib`.
pub(crate) max_hydration_concurrency: u32,
/// Maximum candidate count owned by `ksp-job-backfill-lib`.
pub(crate) max_candidates: u32,
/// Maximum discovery-page count owned by `ksp-job-backfill-lib`.
pub(crate) max_pages: u32,
/// Maximum signature page size owned by `ksp-job-backfill-lib`.
pub(crate) max_page_size: u32,
/// Maximum encoded transaction-signature text length owned by `ksp-job-backfill-lib`.
pub(crate) max_signature_text_bytes: u32,
/// Minimum encoded transaction-signature text length owned by `ksp-job-backfill-lib`.
pub(crate) min_signature_text_bytes: u32,
}
/// App-owned request received from the Backfill campaign form before a Job is started.
///
/// Network and physical endpoint information are intentionally absent. The backend derives the
/// network from the configured Store and maps `http_role` only after checking the current safe
/// Transport inventory.
#[derive(serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartRequestDto.ts")]
pub(crate) struct BackfillStartRequestDto {
/// Optional address text used only by address-based scopes.
pub(crate) address: std::option::Option<String>,
/// Optional exclusive anchor signature used only by Before/After scopes.
pub(crate) anchor_signature: std::option::Option<String>,
/// Stable Backfill commitment code.
pub(crate) commitment: String,
/// Explicit signature texts used only by the explicit-signatures scope.
pub(crate) explicit_signatures: std::vec::Vec<String>,
/// Maximum number of concurrent candidate hydrations.
pub(crate) hydration_concurrency: u32,
/// Logical HTTP role selected from `BackfillDeskOptionsDto::http_routes`.
pub(crate) http_role: String,
/// Maximum number of candidates admitted by the campaign.
pub(crate) max_candidates: u32,
/// Maximum number of address-discovery pages admitted by the campaign.
pub(crate) max_pages: u32,
/// Optional minimum context slot encoded as decimal text to avoid JavaScript integer precision loss.
pub(crate) min_context_slot: std::option::Option<String>,
/// Signature page size used by address discovery.
pub(crate) page_size: u32,
/// Stable Backfill scope kind code.
pub(crate) scope_kind: String,
}
/// Safe projection proving that one app request mapped to the KSP Backfill contract.
///
/// Address and signature values are intentionally reduced to presence/count metadata. This DTO is
/// suitable for request validation feedback before the runtime Start slice exists.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestPreviewDto.ts")]
pub(crate) struct BackfillRequestPreviewDto {
/// Whether an address participates in the validated scope.
pub(crate) address_present: bool,
/// Whether an exclusive anchor participates in the validated scope.
pub(crate) anchor_present: bool,
/// Stable validated commitment code.
pub(crate) commitment: String,
/// Number of explicit signatures after Backfill-owned stable deduplication.
pub(crate) explicit_signature_count: u32,
/// Validated hydration concurrency.
pub(crate) hydration_concurrency: u32,
/// Validated logical HTTP role.
pub(crate) http_role: String,
/// Validated maximum candidate count.
pub(crate) max_candidates: u32,
/// Validated maximum discovery-page count.
pub(crate) max_pages: u32,
/// Whether a minimum context slot participates in the validated request.
pub(crate) min_context_slot_present: bool,
/// Backend-derived logical Store network.
pub(crate) network: String,
/// Validated signature page size.
pub(crate) page_size: u32,
/// Stable validated scope kind code.
pub(crate) scope_kind: String,
}
/// Returns the exact commitment codes currently admitted by the Backfill runtime.
#[must_use]
pub(crate) fn backfill_commitment_codes() -> std::vec::Vec<String> {
return vec![ksp_job_backfill_lib::BackfillCommitment::Finalized.code().to_owned(), ksp_job_backfill_lib::BackfillCommitment::Confirmed.code().to_owned()];
}
/// Returns the exact HTTP scope codes currently admitted by the Backfill runtime.
#[must_use]
pub(crate) fn backfill_scope_kind_codes() -> std::vec::Vec<String> {
return vec![
ksp_job_backfill_lib::BackfillScopeKind::LatestAddress.code().to_owned(),
ksp_job_backfill_lib::BackfillScopeKind::BeforeAddress.code().to_owned(),
ksp_job_backfill_lib::BackfillScopeKind::AfterAddress.code().to_owned(),
ksp_job_backfill_lib::BackfillScopeKind::ExplicitSignatures.code().to_owned(),
];
}
/// Builds frontend-safe request limits directly from the public Backfill constants.
pub(crate) fn backfill_request_limits() -> ksp_core_lib::Result<BackfillRequestLimitsDto> {
let max_hydration_concurrency = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_HYDRATION_CONCURRENCY, "max_hydration_concurrency");
let max_hydration_concurrency = match max_hydration_concurrency {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let max_candidates = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_CANDIDATES, "max_candidates");
let max_candidates = match max_candidates {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let max_pages = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_PAGES, "max_pages");
let max_pages = match max_pages {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let max_page_size = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_PAGE_SIZE, "max_page_size");
let max_page_size = match max_page_size {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let max_signature_text_bytes = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_SIGNATURE_TEXT_BYTES, "max_signature_text_bytes");
let max_signature_text_bytes = match max_signature_text_bytes {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let min_signature_text_bytes = usize_to_u32(ksp_job_backfill_lib::MIN_BACKFILL_SIGNATURE_TEXT_BYTES, "min_signature_text_bytes");
let min_signature_text_bytes = match min_signature_text_bytes {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if crate::DEFAULT_BACKFILL_HYDRATION_CONCURRENCY == 0 || crate::DEFAULT_BACKFILL_HYDRATION_CONCURRENCY > max_hydration_concurrency {
return std::result::Result::Err(limit_contract_error("default_hydration_concurrency"));
}
if crate::DEFAULT_BACKFILL_MAX_CANDIDATES == 0 || crate::DEFAULT_BACKFILL_MAX_CANDIDATES > max_candidates {
return std::result::Result::Err(limit_contract_error("default_max_candidates"));
}
if crate::DEFAULT_BACKFILL_MAX_PAGES == 0 || crate::DEFAULT_BACKFILL_MAX_PAGES > max_pages {
return std::result::Result::Err(limit_contract_error("default_max_pages"));
}
if crate::DEFAULT_BACKFILL_PAGE_SIZE == 0 || crate::DEFAULT_BACKFILL_PAGE_SIZE > max_page_size {
return std::result::Result::Err(limit_contract_error("default_page_size"));
}
return std::result::Result::Ok(BackfillRequestLimitsDto {
default_hydration_concurrency: crate::DEFAULT_BACKFILL_HYDRATION_CONCURRENCY,
default_max_candidates: crate::DEFAULT_BACKFILL_MAX_CANDIDATES,
default_max_pages: crate::DEFAULT_BACKFILL_MAX_PAGES,
default_page_size: crate::DEFAULT_BACKFILL_PAGE_SIZE,
max_hydration_concurrency,
max_candidates,
max_pages,
max_page_size,
max_signature_text_bytes,
min_signature_text_bytes,
});
}
fn limit_contract_error(field: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Backfill Desk request-limit projection is internally inconsistent")
.with_context("field", field);
}
fn usize_to_u32(value: usize, field: &'static str) -> ksp_core_lib::Result<u32> {
let converted = u32::try_from(value);
return match converted {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Backfill Desk cannot project a Job-owned bound to the frontend")
.with_context("field", field)
.with_source(error),
),
};
}
#[cfg(test)]
#[path = "../unit_tests/dto_backfill.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/dto_common.rs
// version: 4
// version: 5
//! Common Tauri DTOs shared by the Backfill Desk shell.
@@ -21,15 +21,19 @@ pub(crate) struct BackfillHttpRouteOptionDto {
pub(crate) role: String,
}
/// Safe Transport-and-Store readiness subset of the Backfill Desk options contract.
/// Safe Backfill Desk options contract combining readiness, HTTP routes and Job-owned campaign bounds.
///
/// Campaign scopes, commitments and backend-owned bounds are added in the dedicated request/DTO slice. Endpoint URLs and credentials are intentionally absent.
/// Endpoint URLs, credentials and physical routing details are intentionally absent.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_common/BackfillDeskOptionsDto.ts")]
pub(crate) struct BackfillDeskOptionsDto {
/// Stable commitment codes currently admitted by `ksp-job-backfill-lib`.
pub(crate) commitments: std::vec::Vec<String>,
/// Selectable logical HTTP routes that support both RPC methods required by the Backfill runtime.
pub(crate) http_routes: std::vec::Vec<BackfillHttpRouteOptionDto>,
/// Backend-owned maxima plus application defaults used by the campaign form.
pub(crate) limits: crate::BackfillRequestLimitsDto,
/// Whether the Transport and Store readiness gates jointly permit later Backfill composition.
pub(crate) composition_ready: bool,
/// Distinct enabled HTTP cluster labels selected by the active Transport profile.
@@ -44,6 +48,8 @@ pub(crate) struct BackfillDeskOptionsDto {
pub(crate) store_ready: bool,
/// Safe startup diagnostic when Transport configuration could not be resolved or constructed.
pub(crate) transport_diagnostic: std::option::Option<CommandErrorDto>,
/// Stable HTTP scope codes currently admitted by `ksp-job-backfill-lib`.
pub(crate) scope_kinds: std::vec::Vec<String>,
/// Whether Transport currently has one coherent network and at least one compatible available role.
pub(crate) transport_ready: bool,
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/errors.rs
// version: 5
// version: 6
//! Application-local error codes for Backfill Desk composition and desktop runtime surfaces.
@@ -7,6 +7,11 @@
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "app_state_invalid");
/// Shared Backfill Desk runtime state cannot be locked safely.
pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "app_state_lock_failed");
/// Backfill Desk cannot admit a campaign before Transport and Store composition is ready.
pub(crate) const ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_composition_not_ready");
/// Backfill Desk received a campaign field or logical route that cannot map to the Backfill contract.
pub(crate) const ERROR_CODE_BACKFILL_REQUEST_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_request_invalid");
/// Backfill Desk composite configuration is missing or references an unexpected document.
pub(crate) const ERROR_CODE_CONFIG_COMPOSITE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "config_composite_invalid");
/// Frontend logging requested an unsupported level.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/lib.rs
// version: 5
// version: 6
//! Tauri desktop application scaffold for controlling and inspecting KSP RAW backfill jobs.
@@ -8,8 +8,10 @@
#![warn(missing_docs)]
mod app_state;
mod backfill_request;
mod bootstrap;
mod constants;
mod dto_backfill;
mod dto_common;
mod errors;
mod frontend_logging;
@@ -26,6 +28,10 @@ pub use self::tauri::run;
/// Shared Backfill Desk application state managed by Tauri.
pub(crate) use self::app_state::AppState;
/// Maps one app-owned campaign DTO to the validated KSP Backfill request contract.
pub(crate) use self::backfill_request::map_backfill_request;
/// Projects one validated Backfill request without returning address or signature values.
pub(crate) use self::backfill_request::project_backfill_request;
/// Crate-internal Logging startup state shared by the application state.
pub(crate) use self::bootstrap::LoggingStartup;
/// Builds the Config management facade from the common KSP CLI bootstrap contract.
@@ -42,10 +48,20 @@ pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_LOGGING;
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_STORE;
/// Composite component identifier for Transport.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT;
/// Initial hydration concurrency proposed by the Backfill campaign form.
pub(crate) use self::constants::DEFAULT_BACKFILL_HYDRATION_CONCURRENCY;
/// Initial candidate cap proposed by the Backfill campaign form.
pub(crate) use self::constants::DEFAULT_BACKFILL_MAX_CANDIDATES;
/// Initial discovery-page cap proposed by the Backfill campaign form.
pub(crate) use self::constants::DEFAULT_BACKFILL_MAX_PAGES;
/// Initial signature page size proposed by the Backfill campaign form.
pub(crate) use self::constants::DEFAULT_BACKFILL_PAGE_SIZE;
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
/// Structured domain used by technical frontend events.
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
/// Structured domain used by Backfill campaign admission and request mapping.
pub(crate) use self::constants::TRACING_DOMAIN_REQUEST;
/// Structured domain used by the Backfill Desk shell.
pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
/// Structured domain used by Store readiness and shutdown operations.
@@ -62,7 +78,19 @@ pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
/// Owning target for splash-window frontend events.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
/// Safe Transport-and-Store readiness subset of the Backfill Desk options contract.
/// Backend-derived request bounds and Desk defaults for the campaign form.
pub(crate) use self::dto_backfill::BackfillRequestLimitsDto;
/// Safe request preview produced after strict backend mapping.
pub(crate) use self::dto_backfill::BackfillRequestPreviewDto;
/// App-owned campaign request received from the frontend.
pub(crate) use self::dto_backfill::BackfillStartRequestDto;
/// Returns commitment codes admitted by the current Backfill contract.
pub(crate) use self::dto_backfill::backfill_commitment_codes;
/// Builds frontend-safe campaign limits from Job-owned public constants.
pub(crate) use self::dto_backfill::backfill_request_limits;
/// Returns scope kind codes admitted by the current Backfill contract.
pub(crate) use self::dto_backfill::backfill_scope_kind_codes;
/// Safe Backfill Desk options contract combining readiness and campaign metadata.
pub(crate) use self::dto_common::BackfillDeskOptionsDto;
/// Safe logical HTTP route exposed for operator selection.
pub(crate) use self::dto_common::BackfillHttpRouteOptionDto;
@@ -74,6 +102,10 @@ pub(crate) use self::dto_common::ShellStatusDto;
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
/// Shared Backfill Desk runtime state cannot be locked safely.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
/// Backfill Desk cannot admit a campaign before Transport and Store composition is ready.
pub(crate) use self::errors::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY;
/// Backfill Desk received a campaign field that cannot map to the Backfill contract.
pub(crate) use self::errors::ERROR_CODE_BACKFILL_REQUEST_INVALID;
/// Backfill Desk composite configuration is missing or references an unexpected document.
pub(crate) use self::errors::ERROR_CODE_CONFIG_COMPOSITE_INVALID;
/// Frontend logging requested an unsupported level.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/tauri.rs
// version: 4
// version: 5
//! Tauri runtime assembly for the KSP Backfill desktop application.
@@ -75,7 +75,13 @@ fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<taur
#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for async command dispatch.
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.invoke_handler(tauri::generate_handler![backfill_options, emit_frontend_log, get_runtime_status, splash_frontend_ready]);
return builder.invoke_handler(tauri::generate_handler![
backfill_options,
backfill_validate_request,
emit_frontend_log,
get_runtime_status,
splash_frontend_ready
]);
}
fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
@@ -158,6 +164,18 @@ fn backfill_options(state: tauri::State<'_, crate::AppState>) -> std::result::Re
};
}
#[tauri::command]
fn backfill_validate_request(
request: crate::BackfillStartRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::BackfillRequestPreviewDto, crate::CommandErrorDto> {
let result = state.validate_backfill_request(request);
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("backfill_validate_request", crate::TRACING_DOMAIN_REQUEST, &error)),
};
}
#[tauri::command]
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
let result = crate::emit_frontend_log_event(payload);

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/transport_runtime.rs
// version: 3
// version: 4
//! Composite-selected HTTP Transport readiness and route inventory owned by Backfill Desk.
@@ -35,12 +35,20 @@ impl TransportRuntime {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let limits = crate::backfill_request_limits();
let limits = match limits {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let transport_ready = configured_networks.len() == 1 && !http_routes.is_empty() && snapshot.available_endpoint_count() > 0;
return std::result::Result::Ok(crate::BackfillDeskOptionsDto {
commitments: crate::backfill_commitment_codes(),
composition_ready: false,
configured_networks,
http_routes,
limits,
network_coherent: false,
scope_kinds: crate::backfill_scope_kind_codes(),
store_diagnostic: std::option::Option::None,
store_network: std::option::Option::None,
store_ready: false,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs
// version: 6
// version: 7
//! Structural desktop contract checks for the Backfill Desk scaffold.
@@ -240,8 +240,6 @@ fn pre_006_mainnet_default_and_http_route_selector_are_explicit_without_start_ru
for required in ["mainnet_backfill_pool", "backfill_pool", "backfill_publicnode", "backfill_solana_public", "solana-rpc.publicnode.com"] {
assert!(transport.contains(required), "missing Backfill HTTP route marker {required}");
}
let app_state = read_text(root.join("src/app_state.rs").as_path());
assert!(app_state.contains("pre.006-mainnet-http-routing"));
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(frontend.contains("Backfill Desk HTTP route selection changed"));
assert!(frontend.contains("backfill_options"));
@@ -251,3 +249,50 @@ fn pre_006_mainnet_default_and_http_route_selector_are_explicit_without_start_ru
assert!(!frontend.contains(forbidden), "pre.006 frontend opens deferred runtime marker {forbidden}");
}
}
#[test]
fn pre_007_campaign_dto_and_backend_mapping_exist_without_start_runtime() {
let root = app_root();
let manifest = read_text(root.join("Cargo.toml").as_path());
assert!(manifest.contains("ksp-job-api = { path = \"../ksp-job-api\" }"));
assert!(manifest.contains("ksp-job-backfill-lib = { path = \"../ksp-job-backfill-lib\" }"));
let dto = read_text(root.join("src/dto_backfill.rs").as_path());
for required in [
"BackfillStartRequestDto",
"BackfillRequestPreviewDto",
"MAX_BACKFILL_CANDIDATES",
"MAX_BACKFILL_HYDRATION_CONCURRENCY",
"MAX_BACKFILL_PAGES",
"MAX_BACKFILL_PAGE_SIZE",
] {
assert!(dto.contains(required), "missing pre.007 DTO/request bound marker {required}");
}
let mapping = read_text(root.join("src/backfill_request.rs").as_path());
for required in ["BackfillRequest::new", "HttpRoleName::new", "latest_address", "before_address", "after_address", "explicit_signatures"] {
assert!(mapping.contains(required), "missing strict Backfill request mapping marker {required}");
}
let tauri = read_text(root.join("src/tauri.rs").as_path());
assert!(tauri.contains("backfill_validate_request"));
let html = read_text(root.join("frontend/main.html").as_path());
for required in [
"backfillCampaignForm",
"backfillScopeKind",
"backfillCommitment",
"backfillAddress",
"backfillAnchorSignature",
"backfillExplicitSignatures",
"backfillMinContextSlot",
"validateBackfillRequest",
"Valider la requête",
] {
assert!(html.contains(required), "missing campaign form marker {required}");
}
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(frontend.contains("backfill_validate_request"));
assert!(frontend.contains("Backfill Desk campaign validation requested"));
assert!(frontend.contains("Aucun job n'a été démarré"));
for forbidden in ["backfill_start", "BackfillJobRuntime", "BackfillJobHandle"] {
assert!(!frontend.contains(forbidden), "pre.007 frontend opens deferred Start/runtime marker {forbidden}");
assert!(!tauri.contains(forbidden), "pre.007 Tauri surface opens deferred Start/runtime marker {forbidden}");
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/tests/desktop_security.rs
// version: 6
// version: 7
//! Security and dependency-boundary checks for the Backfill Desk scaffold.
@@ -43,7 +43,7 @@ fn read_text(path: &std::path::Path) -> String {
}
#[test]
fn capability_surface_remains_core_plus_tracing_while_transport_and_store_are_backend_only() {
fn capability_surface_remains_core_plus_tracing_while_backfill_runtime_stays_backend_only() {
let root = app_root();
let capability = read_text(root.join("capabilities/default.json").as_path());
assert!(capability.contains("\"core:default\""));
@@ -53,10 +53,12 @@ fn capability_surface_remains_core_plus_tracing_while_transport_and_store_are_ba
}
let manifest = read_text(root.join("Cargo.toml").as_path());
assert!(manifest.contains("tauri-plugin-tracing.workspace = true"));
assert!(manifest.contains("ksp-job-api = { path = \"../ksp-job-api\" }"));
assert!(manifest.contains("ksp-job-backfill-lib = { path = \"../ksp-job-backfill-lib\" }"));
assert!(manifest.contains("ksp-onchain-transport-lib = { path = \"../ksp-onchain-transport-lib\" }"));
assert!(manifest.contains("ksp-store-lib = { path = \"../ksp-store-lib\" }"));
for forbidden in ["ksp-job-backfill-lib", "ksp-store-api", "ksp-store-postgres-lib", "reqwest", "tokio-postgres"] {
assert!(!manifest.contains(forbidden), "current Backfill Desk opens a forbidden dependency: {forbidden}");
for forbidden in ["ksp-store-api", "ksp-store-postgres-lib", "reqwest", "tokio-postgres", "tonic", "yellowstone-grpc"] {
assert!(!manifest.contains(forbidden), "current Backfill Desk opens a forbidden direct dependency: {forbidden}");
}
}
@@ -89,7 +91,7 @@ fn pre_002_tauri_commands_remain_centralized() {
assert_eq!(count, 0, "{} declares a Tauri command outside tauri.rs", path.display());
}
}
assert_eq!(command_count, 4);
assert_eq!(command_count, 5);
}
#[test]
@@ -109,21 +111,23 @@ fn pre_002_frontend_instrumentation_avoids_business_or_secret_payloads() {
assert!(main.contains(required));
}
}
for forbidden in ["JSON.stringify(status)", "signature", "address", "apiKey", "authorization", "database_url"] {
assert!(!main.contains(forbidden));
for forbidden in [
"JSON.stringify(status)",
"apiKey",
"authorization",
"database_url",
"campaign validation requested\", {\n address",
"campaign validation requested\", {\n anchorSignature",
"campaign validation requested\", {\n explicitSignatures",
"campaign validation requested\", {\n minContextSlot",
] {
assert!(!main.contains(forbidden), "frontend tracing includes forbidden request payload marker {forbidden}");
}
}
#[test]
fn pre_005_opens_store_facade_without_backend_or_job_dependencies() {
fn pre_005_store_runtime_remains_backend_neutral_after_job_mapping_opens() {
let root = app_root();
let manifest = read_text(root.join("Cargo.toml").as_path());
assert!(manifest.contains("ksp-config-lib = { path = \"../ksp-config-lib\" }"));
assert!(manifest.contains("ksp-onchain-transport-lib = { path = \"../ksp-onchain-transport-lib\" }"));
assert!(manifest.contains("ksp-store-lib = { path = \"../ksp-store-lib\" }"));
for forbidden in ["ksp-job-backfill-lib", "ksp-store-api", "ksp-store-postgres-lib", "reqwest", "tokio-postgres"] {
assert!(!manifest.contains(forbidden), "pre.005 opens a forbidden direct dependency: {forbidden}");
}
let bootstrap = read_text(root.join("src/bootstrap.rs").as_path());
assert!(bootstrap.contains("LogFilterLevel::Trace"));
assert!(!bootstrap.contains("HttpTransportPool"));
@@ -132,8 +136,8 @@ fn pre_005_opens_store_facade_without_backend_or_job_dependencies() {
assert!(store.contains("ksp_store_lib::Store::open"));
assert!(store.contains("store.health().await"));
assert!(store.contains("store.close().await"));
for forbidden in ["connection_uri", "postgres", "database_url", "provider()", "endpoint_url"] {
assert!(!store.contains(forbidden), "Store runtime leaks or depends on forbidden physical metadata marker {forbidden}");
for forbidden in ["ksp_job_", "BackfillRequest", "connection_uri", "postgres", "database_url", "provider()", "endpoint_url"] {
assert!(!store.contains(forbidden), "Store runtime leaks or absorbs another responsibility: {forbidden}");
}
let dto = read_text(root.join("src/dto_common.rs").as_path());
assert!(dto.contains("BackfillDeskOptionsDto"));
@@ -157,3 +161,46 @@ fn pre_006_frontend_receives_only_safe_http_route_metadata_and_no_endpoint_mater
assert!(!frontend.contains(forbidden), "frontend embeds forbidden provider material marker {forbidden}");
}
}
#[test]
fn pre_007_request_mapping_keeps_network_endpoint_and_payload_secrets_backend_owned() {
let root = app_root();
let dto = read_text(root.join("src/dto_backfill.rs").as_path());
let request_start = dto.find("pub(crate) struct BackfillStartRequestDto");
let preview_start = dto.find("pub(crate) struct BackfillRequestPreviewDto");
assert!(request_start.is_some());
assert!(preview_start.is_some());
let request_source = match (request_start, preview_start) {
(std::option::Option::Some(start), std::option::Option::Some(end)) if start < end => &dto[start..end],
_ => "",
};
for forbidden in [
"pub(crate) network:",
"pub(crate) provider",
"pub(crate) endpoint",
"pub(crate) url",
"pub(crate) job_id",
"pub(crate) credential",
"pub(crate) token",
] {
assert!(!request_source.contains(forbidden), "frontend request DTO owns forbidden field marker {forbidden}");
}
let mapping = read_text(root.join("src/backfill_request.rs").as_path());
assert!(mapping.contains("options.store_network.as_deref()"));
assert!(mapping.contains("options.http_routes.iter().any"));
assert!(mapping.contains("BackfillRequest::new"));
assert!(!mapping.contains("HttpTransportPool::new"));
assert!(!mapping.contains("Store::open"));
let state = read_text(root.join("src/app_state.rs").as_path());
assert!(state.contains("backfill-desk-validation"));
assert!(state.contains("validated Backfill Desk campaign request without starting a Job"));
for forbidden in [
"preview.address",
"preview.anchor_signature",
"preview.explicit_signatures",
"request.address.as_str()",
"request.anchor_signature.as_str()",
] {
assert!(!state.contains(forbidden), "request validation logging includes forbidden payload marker {forbidden}");
}
}

View File

@@ -0,0 +1,198 @@
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs
// version: 1
fn ready_options() -> crate::BackfillDeskOptionsDto {
let limits = crate::backfill_request_limits();
assert!(limits.is_ok());
let limits = match limits {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => crate::BackfillRequestLimitsDto {
default_hydration_concurrency: 1,
default_max_candidates: 1,
default_max_pages: 1,
default_page_size: 1,
max_hydration_concurrency: 1,
max_candidates: 1,
max_pages: 1,
max_page_size: 1,
max_signature_text_bytes: 88,
min_signature_text_bytes: 64,
},
};
return crate::BackfillDeskOptionsDto {
commitments: crate::backfill_commitment_codes(),
composition_ready: true,
configured_networks: vec!["mainnet-beta".to_owned()],
http_routes: vec![
crate::BackfillHttpRouteOptionDto {
pooled: true,
providers: vec!["publicnode".to_owned(), "solana-public".to_owned()],
role: "backfill_pool".to_owned(),
},
crate::BackfillHttpRouteOptionDto { pooled: false, providers: vec!["publicnode".to_owned()], role: "backfill_publicnode".to_owned() },
],
limits,
network_coherent: true,
scope_kinds: crate::backfill_scope_kind_codes(),
store_diagnostic: std::option::Option::None,
store_network: std::option::Option::Some("mainnet-beta".to_owned()),
store_ready: true,
transport_diagnostic: std::option::Option::None,
transport_ready: true,
};
}
fn request(scope_kind: &str) -> crate::BackfillStartRequestDto {
return crate::BackfillStartRequestDto {
address: std::option::Option::Some("11111111111111111111111111111111".to_owned()),
anchor_signature: std::option::Option::None,
commitment: "finalized".to_owned(),
explicit_signatures: std::vec::Vec::new(),
hydration_concurrency: 4,
http_role: "backfill_pool".to_owned(),
max_candidates: 100,
max_pages: 10,
min_context_slot: std::option::Option::Some("123456".to_owned()),
page_size: 100,
scope_kind: scope_kind.to_owned(),
};
}
fn job_id() -> std::option::Option<ksp_job_api::JobId> {
let job_id = ksp_job_api::JobId::new("request-mapping-test");
assert!(job_id.is_ok());
return match job_id {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(_) => std::option::Option::None,
};
}
#[test]
fn latest_address_maps_to_backend_network_role_commitment_and_bounds() {
let job_id = job_id();
let job_id = match job_id {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let mapped = crate::map_backfill_request(request("latest_address"), &ready_options(), job_id);
assert!(mapped.is_ok());
let mapped = match mapped {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(mapped.network().as_str(), "mainnet-beta");
assert_eq!(mapped.role().as_str(), "backfill_pool");
assert_eq!(mapped.commitment(), ksp_job_backfill_lib::BackfillCommitment::Finalized);
assert_eq!(mapped.scope().kind(), ksp_job_backfill_lib::BackfillScopeKind::LatestAddress);
assert_eq!(mapped.page_size(), 100);
assert_eq!(mapped.max_pages(), 10);
assert_eq!(mapped.max_candidates(), 100);
assert_eq!(mapped.hydration_concurrency(), 4);
assert_eq!(mapped.min_context_slot(), std::option::Option::Some(123456));
}
#[test]
fn before_and_after_scopes_require_an_exact_anchor_shape() {
for scope_kind in ["before_address", "after_address"] {
let job_id = job_id();
let job_id = match job_id {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let mut input = request(scope_kind);
input.anchor_signature = std::option::Option::Some("1".repeat(64));
let mapped = crate::map_backfill_request(input, &ready_options(), job_id);
assert!(mapped.is_ok(), "scope {scope_kind} should map with a bounded anchor");
}
let job_id = job_id();
let job_id = match job_id {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let missing_anchor = crate::map_backfill_request(request("before_address"), &ready_options(), job_id);
assert!(missing_anchor.is_err());
}
#[test]
fn explicit_signatures_reject_address_context_and_deduplicate_in_job_contract() {
let job_id = job_id();
let job_id = match job_id {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let mut input = request("explicit_signatures");
input.address = std::option::Option::None;
input.min_context_slot = std::option::Option::None;
input.explicit_signatures = vec!["1".repeat(64), "1".repeat(64), "2".repeat(64)];
let mapped = crate::map_backfill_request(input, &ready_options(), job_id);
assert!(mapped.is_ok());
let mapped = match mapped {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let signatures = mapped.scope().signatures();
assert!(signatures.is_some());
if let std::option::Option::Some(signatures) = signatures {
assert_eq!(signatures.len(), 2);
}
let mut hostile = request("explicit_signatures");
hostile.address = std::option::Option::None;
hostile.min_context_slot = std::option::Option::None;
hostile.explicit_signatures = vec![" 1".to_owned()];
let hostile_job_id = job_id();
let hostile_job_id = match hostile_job_id {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert!(crate::map_backfill_request(hostile, &ready_options(), hostile_job_id).is_err());
}
#[test]
fn route_inventory_and_composition_are_revalidated_before_mapping() {
let first_job_id = job_id();
let first_job_id = match first_job_id {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let mut unknown_route = request("latest_address");
unknown_route.http_role = "provider-from-frontend".to_owned();
let rejected = crate::map_backfill_request(unknown_route, &ready_options(), first_job_id);
assert!(rejected.is_err());
let mut unavailable = ready_options();
unavailable.composition_ready = false;
let second_job_id = job_id();
let second_job_id = match second_job_id {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert!(crate::map_backfill_request(request("latest_address"), &unavailable, second_job_id).is_err());
}
#[test]
fn request_preview_never_contains_address_or_signature_values() {
let job_id = job_id();
let job_id = match job_id {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let mapped = crate::map_backfill_request(request("latest_address"), &ready_options(), job_id);
assert!(mapped.is_ok());
let mapped = match mapped {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let preview = crate::project_backfill_request(&mapped);
assert!(preview.is_ok());
let preview = match preview {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let serialized = serde_json::to_string(&preview);
assert!(serialized.is_ok());
if let std::result::Result::Ok(serialized) = serialized {
assert!(!serialized.contains("11111111111111111111111111111111"));
assert!(!serialized.contains(&"1".repeat(64)));
assert!(serialized.contains("mainnet-beta"));
assert!(serialized.contains("backfill_pool"));
}
}

View File

@@ -0,0 +1,31 @@
// file: crates/ksp-app-backfill-desk/unit_tests/dto_backfill.rs
// version: 1
#[test]
fn request_limits_are_derived_from_job_owned_public_bounds() {
let limits = crate::backfill_request_limits();
assert!(limits.is_ok());
let limits = match limits {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(usize::try_from(limits.max_page_size), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_PAGE_SIZE));
assert_eq!(usize::try_from(limits.max_pages), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_PAGES));
assert_eq!(usize::try_from(limits.max_candidates), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_CANDIDATES));
assert_eq!(usize::try_from(limits.max_hydration_concurrency), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_HYDRATION_CONCURRENCY));
assert_eq!(usize::try_from(limits.min_signature_text_bytes), std::result::Result::Ok(ksp_job_backfill_lib::MIN_BACKFILL_SIGNATURE_TEXT_BYTES));
assert_eq!(usize::try_from(limits.max_signature_text_bytes), std::result::Result::Ok(ksp_job_backfill_lib::MAX_BACKFILL_SIGNATURE_TEXT_BYTES));
assert!(limits.default_page_size <= limits.max_page_size);
assert!(limits.default_max_pages <= limits.max_pages);
assert!(limits.default_max_candidates <= limits.max_candidates);
assert!(limits.default_hydration_concurrency <= limits.max_hydration_concurrency);
}
#[test]
fn request_options_expose_exact_current_commitments_and_http_scopes() {
assert_eq!(crate::backfill_commitment_codes(), vec!["finalized".to_owned(), "confirmed".to_owned()]);
assert_eq!(
crate::backfill_scope_kind_codes(),
vec!["latest_address".to_owned(), "before_address".to_owned(), "after_address".to_owned(), "explicit_signatures".to_owned()]
);
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/unit_tests/dto_common.rs
// version: 4
// version: 5
#[test]
fn command_error_projection_excludes_arbitrary_context_and_source_values() {
@@ -15,11 +15,20 @@ fn command_error_projection_excludes_arbitrary_context_and_source_values() {
#[test]
fn transport_options_projection_contains_only_safe_transport_metadata() {
let limits = crate::backfill_request_limits();
assert!(limits.is_ok());
let limits = match limits {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let dto = crate::BackfillDeskOptionsDto {
commitments: crate::backfill_commitment_codes(),
http_routes: vec![crate::BackfillHttpRouteOptionDto { pooled: false, providers: vec!["solana-public".to_owned()], role: "default".to_owned() }],
limits,
composition_ready: true,
configured_networks: vec!["devnet".to_owned()],
network_coherent: true,
scope_kinds: crate::backfill_scope_kind_codes(),
store_diagnostic: std::option::Option::None,
store_network: std::option::Option::Some("devnet".to_owned()),
store_ready: true,

138
deltas/0.3.7/pre.007.md Normal file
View File

@@ -0,0 +1,138 @@
<!-- file: deltas/0.3.7/pre.007.md -->
<!-- version: 1 -->
# Delta `0.3.7-pre.007` — DTO et mapping de requête Backfill
## 1. Base requise
```text
0.3.7-pre.006-fix.001
workspace.package.version = 0.3.7-pre.6.fix.1
```
## 2. Objectif
Ouvrir le contrat Job Backfill dans la Desk uniquement pour construire, valider et prévisualiser une requête de campagne HTTP, sans créer ni démarrer de `BackfillJobRuntime`.
## 3. Contrat applicatif
Backfill Desk dépend désormais directement de `ksp-job-api` et `ksp-job-backfill-lib`. Le frontend reçoit les quatre scopes HTTP V1, les deux commitments, les maxima publics Job et des defaults applicatifs bornés.
`BackfillStartRequestDto` reste possédé par l'application et ne contient ni réseau, provider, endpoint, URL, credential, checkpoint ou JobId. Le réseau est dérivé du Store composé ; `http_role` est revalidé contre l'inventaire sûr `BackfillDeskOptionsDto::http_routes` avant construction du contrat Job.
`min_context_slot` traverse IPC sous forme de texte décimal et est parsé en `u64` côté Rust afin d'éviter toute perte de précision JavaScript.
## 4. Mapping autoritaire
Le mapping Rust :
- valide la readiness Transport + Store et leur cohérence réseau ;
- mappe explicitement `finalized` / `confirmed` ;
- mappe explicitement `latest_address`, `before_address`, `after_address` et `explicit_signatures` ;
- parse l'adresse et les signatures dans les types propriétaires ;
- convertit les bornes `u32` vers `usize` ;
- délègue la validation finale à `ksp-job-backfill-lib::BackfillRequest::new` ;
- n'ouvre aucun runtime Job.
La commande Tauri `backfill_validate_request` utilise exactement ce mapping avec un JobId interne réservé à la validation, puis renvoie une `BackfillRequestPreviewDto` sûre. La preview expose réseau/rôle/commitment/scope/bornes, mais réduit adresse, ancre, `min_context_slot` et signatures à des booléens ou comptages.
## 5. Frontend
Le formulaire Backfill expose :
- route HTTP logique ;
- scope ;
- commitment ;
- adresse et ancre selon le scope ;
- signatures explicites ;
- page size ;
- max pages ;
- max candidates ;
- hydration concurrency ;
- `min_context_slot`.
Le bouton reste **Valider la requête**. Aucun Start, Cancel, Resume ou checkpoint n'est ouvert dans cette tranche. Les logs frontend ne contiennent aucune adresse, signature, ancre ou valeur de `min_context_slot`.
## 6. Version
```text
workspace.package.version = 0.3.7-pre.7
label = 0.3.7-pre.007
```
## 7. Fichiers ajoutés
```text
crates/ksp-app-backfill-desk/src/backfill_request.rs
crates/ksp-app-backfill-desk/src/dto_backfill.rs
crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs
crates/ksp-app-backfill-desk/unit_tests/dto_backfill.rs
deltas/0.3.7/pre.007.md
```
## 8. Fichiers modifiés
```text
Cargo.toml
crates/ksp-app-backfill-desk/Cargo.toml
crates/ksp-app-backfill-desk/frontend/main.html
crates/ksp-app-backfill-desk/frontend/ts/main.ts
crates/ksp-app-backfill-desk/src/app_state.rs
crates/ksp-app-backfill-desk/src/constants.rs
crates/ksp-app-backfill-desk/src/dto_common.rs
crates/ksp-app-backfill-desk/src/errors.rs
crates/ksp-app-backfill-desk/src/lib.rs
crates/ksp-app-backfill-desk/src/tauri.rs
crates/ksp-app-backfill-desk/src/transport_runtime.rs
crates/ksp-app-backfill-desk/tests/desktop_contract.rs
crates/ksp-app-backfill-desk/tests/desktop_security.rs
crates/ksp-app-backfill-desk/unit_tests/dto_common.rs
docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md
docs/validation/024-V0_3_7_BACKFILL_DESK.md
```
## 9. Fichiers supprimés
Aucun.
## 10. Gate précédent acquis
Le replay opérateur de `0.3.7-pre.006-fix.001` est propre : audits Rust/Markdown, `cargo check --workspace`, Clippy, `ksp-config-lib`, Backfill Desk et arbres Cargo passent sans anomalie rapportée.
## 11. Validations exécutées dans l'environnement d'assemblage
```text
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.7
```
Contrôles ciblés :
- syntaxe TypeScript par transpilation locale ;
- cohérence des headers/version des fichiers modifiés ;
- présence des quatre scopes et deux commitments à partir des contrats Job publics ;
- absence de réseau/provider/URL/endpoint/JobId dans le DTO frontend ;
- absence de `BackfillJobRuntime`, Start, Cancel, Resume et checkpoint dans le runtime de cette tranche ;
- absence de valeur d'adresse/signature/ancre/`min_context_slot` dans la preview et les logs frontend ;
- dépendances directes limitées aux façades/API prévues ;
- contrôle différentiel et reconstruction par application du delta.
## 12. Validations non exécutées dans l'environnement d'assemblage
`cargo` et `rustfmt` ne sont pas disponibles dans l'environnement d'assemblage. Gate opérateur obligatoire avant `pre.008` :
```text
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.7
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-job-backfill-lib
cargo test -p ksp-app-backfill-desk
cargo tree -p ksp-app-backfill-desk --edges normal
cargo tree -p ksp-app-backfill-desk -e features
```
## 13. Suite
`pre.008` ouvrira le runtime et Start : état single-active-run, construction `BackfillJobRuntime`, admission atomique, spawn non bloquant et handle installé avant exécution. Le choix HTTP pool/provider et le mapping `BackfillRequest` de `pre.007` restent les entrées autoritaires.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# Plan v0.3.7 — Backfill Desk
@@ -259,7 +259,9 @@ Faire de `mainnet` le profil composite par défaut, conserver `supertrace` penda
### pre.007 — DTO/request mapping
Ajouter `ksp-job-backfill-lib` et compléter les DTO TS-RS app-owned : quatre scopes HTTP existants, commitments, min context slot, bornes backend-owned, signatures explicites hostiles et rôle HTTP sélectionné. Mapper strictement vers `BackfillRequest`/`HttpRoleName` côté Rust sans Start effectif et refuser toute route qui n'appartient pas à l'inventaire courant.
Ajouter `ksp-job-api` et `ksp-job-backfill-lib` comme dépendances backend directes de la Desk, sans ouvrir encore `BackfillJobRuntime`. Les DTO TS-RS app-owned exposent les quatre scopes HTTP existants, les commitments, `min_context_slot` sous forme de texte décimal, les bornes publiques Job et des defaults applicatifs bornés (`page_size=100`, `max_pages=10`, `max_candidates=1000`, `hydration_concurrency=4`). Le request DTO ne contient ni réseau, ni provider, ni endpoint, ni URL, ni JobId.
Le mapping Rust dérive le réseau du Store configuré, revalide `http_role` contre `BackfillDeskOptionsDto::http_routes`, parse adresse/signatures et délègue l'autorité finale des bornes/cohérences à `BackfillRequest::new`. Une commande `backfill_validate_request` utilise exactement ce mapping pour produire une projection sûre réduite à présence/comptage et paramètres non sensibles ; elle ne lance aucun Job. Le frontend matérialise le formulaire de campagne et le bouton **Valider la requête** ; adresse, ancre, signatures explicites et `min_context_slot` ne sont jamais journalisés.
### pre.008 — runtime + Start

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/024-V0_3_7_BACKFILL_DESK.md -->
<!-- version: 8 -->
<!-- version: 9 -->
# Validation v0.3.7 — Backfill Desk
@@ -61,10 +61,10 @@ Le journal opérateur fourni rapporte `cargo clean`, `cargo fmt`, audits Rust/Ma
- [X] Composite dédié `config/composite.ksp-app-backfill-desk.json` prévu.
- [X] Profils `devnet`, `mainnet`, `testnet` prévus avec couples Transport/Store de même réseau.
- [X] Default prévu : `devnet`.
- [X] Default final : `mainnet` depuis `pre.006`; Devnet/Testnet restent disponibles par reconfiguration du composite.
- [X] Aucun `std.job_backfill.json` en V1.
- [X] Les rôles sont dérivés de Transport et validés pour `getSignaturesForAddress` + `getTransaction`.
- [X] Aucun rôle `history_backfill` n'est hardcodé ; la base standard actuelle expose `default`.
- [X] Aucun rôle `history_backfill` n'est hardcodé ; `pre.006` expose les rôles logiques `backfill_pool`, `backfill_solana_public` et `backfill_publicnode` depuis Transport.
- [X] Une incohérence réseau est rejetée avant Start.
- [X] Le Desk ouvre Store via `ksp-store-lib`, jamais via backend direct.
@@ -97,8 +97,8 @@ Le journal opérateur fourni rapporte `cargo clean`, `cargo fmt`, audits Rust/Ma
- [X] Splash/main + tracing conformes.
- [X] Composite Backfill Desk packagé et valide.
- [X] Transport readiness et role inventory sûrs.
- [ ] Store readiness et cohérence réseau.
- [ ] Quatre scopes + commitments + bornes mappés.
- [X] Store readiness et cohérence réseau.
- [X] Quatre scopes + commitments + bornes mappés.
- [ ] Single active run + Start.
- [ ] Snapshot latest-value bridge.
- [ ] Cancel + races terminales.
@@ -207,7 +207,7 @@ cargo test -p ksp-app-backfill-desk FAIL — canari Ca
Backfill Desk dépend désormais directement de `ksp-onchain-transport-lib` au niveau backend uniquement. `initialize_transport()` résout le profil Transport déjà sélectionné par le composite, construit `HttpTransportPool`, dérive les clusters HTTP activés et déduplique les rôles logiques à partir des snapshots sûrs. Chaque rôle candidat doit réussir `select_for_method()` pour `getSignaturesForAddress` **et** `getTransaction` avant d'être exposé comme compatible.
Le sous-contrat `BackfillDeskOptionsDto` expose uniquement : réseaux configurés, rôles compatibles, readiness Transport et diagnostic applicatif sûr. Il ne contient ni provider, URL, endpoint physique, token, checkpoint, payload RAW ou objet Transport. La commande Tauri `backfill_options` est centralisée dans `tauri.rs`. Les scopes, commitments et bornes restent hors de cette tranche et seront ajoutés en `pre.006`.
Le sous-contrat `BackfillDeskOptionsDto` expose uniquement : réseaux configurés, rôles compatibles, readiness Transport et diagnostic applicatif sûr. Il ne contient ni provider, URL, endpoint physique, token, checkpoint, payload RAW ou objet Transport. La commande Tauri `backfill_options` est centralisée dans `tauri.rs`. Les scopes, commitments et bornes restent hors de cette tranche et seront ajoutés en `pre.007` après la stabilisation du routage HTTP de `pre.006`.
Une seule network distincte et au moins un rôle compatible sont requis pour `transport_ready = true`. Une erreur Config/Transport n'empêche pas le shell de démarrer : elle produit une options projection non-ready avec `CommandErrorDto`, tandis que le pool n'est pas conservé. La cohérence avec le réseau Store reste explicitement réservée à `pre.005`.
@@ -287,7 +287,7 @@ Le support Yellowstone gRPC de `ksp-job-backfill-lib`/Backfill Desk est retiré
- [X] le frontend charge/rend le sélecteur et trace son changement ;
- [X] aucun Start ni dépendance `ksp-job-backfill-lib` n'est ouvert par la Desk ;
- [X] audits statiques Rust/Markdown, JSON Schema et syntaxe TypeScript propres dans l'environnement d'assemblage ;
- [ ] replay opérateur `cargo fmt/check/clippy/test` de `pre.006` à exécuter.
- [X] replay opérateur `cargo fmt/check/clippy/test` de `pre.006` exécuté ; un fix Clippy strictement local a été requis puis traité.
## Correctif `pre.006-fix.001`
@@ -301,4 +301,30 @@ Le correctif reste strictement dans la responsabilité de `pre.006` : il ajoute
- [X] aucun contenu fonctionnel de `pre.006` n'est modifié ;
- [X] version Cargo synchronisée en `0.3.7-pre.6.fix.1` conformément à `VER-ID-007` et `VER-ID-010` ;
- [X] audits statiques Rust/Markdown rejoués dans l'environnement d'assemblage ;
- [ ] replay opérateur `cargo fmt/check/clippy/test` du fix à exécuter.
- [X] replay opérateur `cargo fmt/check/clippy/test` du fix propre : audits, check, Clippy, Config et toutes les suites Backfill Desk passent.
## 15. `pre.007` — DTO et mapping de requête
Le replay opérateur de `pre.006-fix.001` est acquis : audits Rust/Markdown propres, `cargo check --workspace` et Clippy propres, `ksp-config-lib` intégralement vert, puis Backfill Desk vert sur ses 21 tests unitaires, 5 tests de composition, 10 desktop contract, 6 desktop security et 1 public API. Les arbres Cargo ont également été exécutés sans anomalie rapportée.
`pre.007` ouvre `ksp-job-api` et `ksp-job-backfill-lib` uniquement pour construire et valider le contrat de requête. `BackfillStartRequestDto` reste app-owned et ne transporte ni network, provider, endpoint, URL, credential, checkpoint ou JobId. Le backend dérive le réseau depuis le Store déjà composé et refuse tout `http_role` absent de l'inventaire sûr courant.
Le formulaire expose les quatre scopes HTTP V1 (`latest_address`, `before_address`, `after_address`, `explicit_signatures`), `finalized`/`confirmed`, les quatre bornes numériques et `min_context_slot`. Ce dernier traverse l'IPC comme texte décimal afin d'éviter toute perte de précision JavaScript avant parsing `u64` côté Rust. Les maxima proviennent exclusivement des constantes publiques de `ksp-job-backfill-lib`; les defaults de Desk restent strictement inférieurs ou égaux à ces maxima.
La commande `backfill_validate_request` construit un JobId interne uniquement pour satisfaire le contrat `BackfillRequest`, applique le même mapping qui sera réutilisé par Start, puis renvoie une `BackfillRequestPreviewDto` sûre. La preview contient le réseau, le rôle logique, commitment/scope, bornes et seulement présence/comptage pour les valeurs sensibles. Aucun Job n'est créé ni lancé dans cette tranche.
### Gate statique local `pre.007`
- [X] dépendances directes `ksp-job-api` et `ksp-job-backfill-lib` ajoutées à Backfill Desk ;
- [X] aucune dépendance directe backend Store, `reqwest`, `tonic` ou Yellowstone ajoutée ;
- [X] request DTO sans network/provider/endpoint/URL/JobId ;
- [X] réseau dérivé du Store configuré et rôle HTTP revalidé contre l'inventaire courant ;
- [X] quatre scopes et deux commitments mappés explicitement ;
- [X] `min_context_slot` transporté comme texte puis parsé en `u64` côté Rust ;
- [X] bornes maxima dérivées des constantes publiques Job et defaults applicatifs vérifiés ;
- [X] preview sûre sans adresse ni signature ;
- [X] formulaire frontend + validation backend disponibles, sans bouton Start ni `BackfillJobRuntime` ;
- [X] instrumentation frontend limitée aux codes/rôle/scope et métadonnées de contrôle ;
- [X] capabilities Tauri inchangées : `core:default + tracing:default` ;
- [ ] `cargo fmt/check/clippy/test` de `pre.007` à rejouer par l'opérateur ; `cargo`/`rustfmt` restent absents du sandbox.