Files
khadhroony-solana-project/crates/ksp-app-backfill-desk/src/backfill_request.rs
2026-09-02 16:07:57 +02:00

314 lines
15 KiB
Rust

// 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;