314 lines
15 KiB
Rust
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;
|