922 lines
45 KiB
Rust
922 lines
45 KiB
Rust
// file: crates/ksp-app-store-desk/src/store_runtime.rs
|
|
// version: 5
|
|
|
|
//! Composite-selected Store readiness, status and bounded shutdown lifecycle owned by Store Desk.
|
|
|
|
/// Startup result for Store, including safe diagnostics when the desktop shell remains available without an opened runtime.
|
|
pub(crate) struct StoreStartup {
|
|
backend_kind: std::option::Option<String>,
|
|
diagnostic: std::option::Option<crate::CommandErrorDto>,
|
|
network: std::option::Option<String>,
|
|
profile_id: std::option::Option<String>,
|
|
runtime: std::option::Option<crate::StoreRuntime>,
|
|
}
|
|
|
|
impl crate::StoreStartup {
|
|
/// Builds a fresh backend-neutral runtime/health projection for the Overview screen.
|
|
pub(crate) async fn status(&self) -> crate::StoreRuntimeStatusDto {
|
|
let runtime = self.runtime.as_ref();
|
|
return match runtime {
|
|
std::option::Option::Some(value) => value.status().await,
|
|
std::option::Option::None => crate::StoreRuntimeStatusDto {
|
|
backend_kind: self.backend_kind.clone(),
|
|
diagnostic: self.diagnostic.clone(),
|
|
health_state: "unavailable".to_owned(),
|
|
migration_version_decimal: std::option::Option::None,
|
|
network: self.network.clone(),
|
|
pending_migration_count: 0,
|
|
pool_available: 0,
|
|
pool_capacity: 0,
|
|
pool_size: 0,
|
|
pool_waiting: 0,
|
|
profile_id: self.profile_id.clone(),
|
|
store_open: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
/// Executes one bounded server-side RAW account-state inspection query.
|
|
pub(crate) async fn query_accounts(&self, request: crate::StoreAccountQueryRequestDto) -> ksp_core_lib::Result<crate::StoreAccountQueryResponseDto> {
|
|
let runtime = self.runtime.as_ref();
|
|
return match runtime {
|
|
std::option::Option::Some(value) => value.query_accounts(request).await,
|
|
std::option::Option::None => std::result::Result::Err(store_runtime_unavailable()),
|
|
};
|
|
}
|
|
|
|
/// Loads one explicit bounded RAW account-state detail projection.
|
|
pub(crate) async fn account_detail(&self, request: crate::StoreAccountDetailRequestDto) -> ksp_core_lib::Result<crate::StoreAccountDetailDto> {
|
|
let runtime = self.runtime.as_ref();
|
|
return match runtime {
|
|
std::option::Option::Some(value) => value.account_detail(request).await,
|
|
std::option::Option::None => std::result::Result::Err(store_runtime_unavailable()),
|
|
};
|
|
}
|
|
|
|
/// Executes one bounded metadata-only observation inspection for an explicit RAW account state.
|
|
pub(crate) async fn query_account_observations(
|
|
&self,
|
|
request: crate::StoreAccountObservationQueryRequestDto,
|
|
) -> ksp_core_lib::Result<crate::StoreAccountObservationQueryResponseDto> {
|
|
let runtime = self.runtime.as_ref();
|
|
return match runtime {
|
|
std::option::Option::Some(value) => value.query_account_observations(request).await,
|
|
std::option::Option::None => std::result::Result::Err(store_runtime_unavailable()),
|
|
};
|
|
}
|
|
|
|
/// Executes one bounded server-side RAW transaction inspection query.
|
|
pub(crate) async fn query_transactions(
|
|
&self,
|
|
request: crate::StoreTransactionQueryRequestDto,
|
|
) -> ksp_core_lib::Result<crate::StoreTransactionQueryResponseDto> {
|
|
let runtime = self.runtime.as_ref();
|
|
return match runtime {
|
|
std::option::Option::Some(value) => value.query_transactions(request).await,
|
|
std::option::Option::None => std::result::Result::Err(store_runtime_unavailable()),
|
|
};
|
|
}
|
|
|
|
/// Loads one explicit bounded RAW transaction detail projection.
|
|
pub(crate) async fn transaction_detail(&self, request: crate::StoreTransactionDetailRequestDto) -> ksp_core_lib::Result<crate::StoreTransactionDetailDto> {
|
|
let runtime = self.runtime.as_ref();
|
|
return match runtime {
|
|
std::option::Option::Some(value) => value.transaction_detail(request).await,
|
|
std::option::Option::None => std::result::Result::Err(store_runtime_unavailable()),
|
|
};
|
|
}
|
|
|
|
/// Executes one bounded metadata-only observation inspection for an explicit RAW transaction.
|
|
pub(crate) async fn query_transaction_observations(
|
|
&self,
|
|
request: crate::StoreTransactionObservationQueryRequestDto,
|
|
) -> ksp_core_lib::Result<crate::StoreTransactionObservationQueryResponseDto> {
|
|
let runtime = self.runtime.as_ref();
|
|
return match runtime {
|
|
std::option::Option::Some(value) => value.query_transaction_observations(request).await,
|
|
std::option::Option::None => std::result::Result::Err(store_runtime_unavailable()),
|
|
};
|
|
}
|
|
|
|
/// Closes the retained Store runtime if startup reached the physical Store-open phase.
|
|
pub(crate) async fn close(&self) -> ksp_core_lib::Result<()> {
|
|
let runtime = self.runtime.as_ref();
|
|
return match runtime {
|
|
std::option::Option::Some(value) => value.close().await,
|
|
std::option::Option::None => std::result::Result::Ok(()),
|
|
};
|
|
}
|
|
}
|
|
|
|
/// Executable Store runtime retained behind an async read/write lock so read-only Desk queries may coexist while shutdown remains exclusive.
|
|
pub(crate) struct StoreRuntime {
|
|
profile_id: String,
|
|
store: tokio::sync::RwLock<std::option::Option<ksp_store_lib::Store>>,
|
|
}
|
|
|
|
impl crate::StoreRuntime {
|
|
/// Builds one fresh safe Store status from the retained facade without exposing backend connection details.
|
|
pub(crate) async fn status(&self) -> crate::StoreRuntimeStatusDto {
|
|
let locked = self.store.read().await;
|
|
let store = locked.as_ref();
|
|
let store = match store {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return crate::StoreRuntimeStatusDto {
|
|
backend_kind: std::option::Option::None,
|
|
diagnostic: std::option::Option::None,
|
|
health_state: "closed".to_owned(),
|
|
migration_version_decimal: std::option::Option::None,
|
|
network: std::option::Option::None,
|
|
pending_migration_count: 0,
|
|
pool_available: 0,
|
|
pool_capacity: 0,
|
|
pool_size: 0,
|
|
pool_waiting: 0,
|
|
profile_id: std::option::Option::Some(self.profile_id.clone()),
|
|
store_open: false,
|
|
};
|
|
},
|
|
};
|
|
let runtime = store.runtime_snapshot();
|
|
let health = store.health().await;
|
|
let health_state = store_health_state_code(health.state()).to_owned();
|
|
let diagnostic = non_ready_health_diagnostic(&health);
|
|
return crate::StoreRuntimeStatusDto {
|
|
backend_kind: std::option::Option::Some(runtime.backend_kind().code().to_owned()),
|
|
diagnostic,
|
|
health_state,
|
|
migration_version_decimal: health.migration_version().map(|value| return value.to_string()),
|
|
network: std::option::Option::Some(runtime.network().as_str().to_owned()),
|
|
pending_migration_count: health.pending_migration_count(),
|
|
pool_available: runtime.pool_available(),
|
|
pool_capacity: runtime.pool_capacity(),
|
|
pool_size: runtime.pool_size(),
|
|
pool_waiting: runtime.pool_waiting(),
|
|
profile_id: std::option::Option::Some(self.profile_id.clone()),
|
|
store_open: true,
|
|
};
|
|
}
|
|
|
|
/// Executes one validated random-access account-state inspection query while retaining the Store read guard.
|
|
pub(crate) async fn query_accounts(&self, request: crate::StoreAccountQueryRequestDto) -> ksp_core_lib::Result<crate::StoreAccountQueryResponseDto> {
|
|
let locked = self.store.read().await;
|
|
let store = locked.as_ref();
|
|
let store = match store {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::result::Result::Err(store_runtime_unavailable()),
|
|
};
|
|
let query = account_query_from_request(store, &request);
|
|
let query = match query {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let page = ksp_store_lib::RawAccountStateInspectionRead::inspect_raw_account_states(store, &query).await;
|
|
let page = match page {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let records_total_decimal = page.total_items().to_string();
|
|
let records_filtered_decimal = page.filtered_items().to_string();
|
|
let items = page.into_items();
|
|
let mut rows = std::vec::Vec::with_capacity(items.len());
|
|
for item in items {
|
|
rows.push(account_row_dto(&item));
|
|
}
|
|
return std::result::Result::Ok(crate::StoreAccountQueryResponseDto { records_filtered_decimal, records_total_decimal, rows });
|
|
}
|
|
|
|
/// Loads one account-state detail by application-owned row identity without exposing Store network selection to the frontend.
|
|
pub(crate) async fn account_detail(&self, request: crate::StoreAccountDetailRequestDto) -> ksp_core_lib::Result<crate::StoreAccountDetailDto> {
|
|
let locked = self.store.read().await;
|
|
let store = locked.as_ref();
|
|
let store = match store {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::result::Result::Err(store_runtime_unavailable()),
|
|
};
|
|
let reference = account_reference_from_request(store, &request);
|
|
let reference = match reference {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let state = ksp_store_lib::RawAccountStateRead::get_raw_account_state(store, &reference).await;
|
|
let state = match state {
|
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
|
std::result::Result::Ok(std::option::Option::None) => return std::result::Result::Err(store_account_not_found()),
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return std::result::Result::Ok(account_detail_dto(&state));
|
|
}
|
|
|
|
/// Executes one validated metadata-only observation query for one exact account-state identity.
|
|
pub(crate) async fn query_account_observations(
|
|
&self,
|
|
request: crate::StoreAccountObservationQueryRequestDto,
|
|
) -> ksp_core_lib::Result<crate::StoreAccountObservationQueryResponseDto> {
|
|
let locked = self.store.read().await;
|
|
let store = locked.as_ref();
|
|
let store = match store {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::result::Result::Err(store_runtime_unavailable()),
|
|
};
|
|
let query = account_observation_query_from_request(store, &request);
|
|
let query = match query {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let page = ksp_store_lib::RawAccountObservationInspectionRead::inspect_raw_account_observations(store, &query).await;
|
|
let page = match page {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let records_total_decimal = page.total_items().to_string();
|
|
let records_filtered_decimal = page.filtered_items().to_string();
|
|
let items = page.into_items();
|
|
let mut rows = std::vec::Vec::with_capacity(items.len());
|
|
for item in items {
|
|
rows.push(account_observation_row_dto(&item));
|
|
}
|
|
return std::result::Result::Ok(crate::StoreAccountObservationQueryResponseDto { records_filtered_decimal, records_total_decimal, rows });
|
|
}
|
|
|
|
/// Executes one validated random-access transaction inspection query while retaining the Store read guard.
|
|
pub(crate) async fn query_transactions(
|
|
&self,
|
|
request: crate::StoreTransactionQueryRequestDto,
|
|
) -> ksp_core_lib::Result<crate::StoreTransactionQueryResponseDto> {
|
|
let locked = self.store.read().await;
|
|
let store = locked.as_ref();
|
|
let store = match store {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::result::Result::Err(store_runtime_unavailable()),
|
|
};
|
|
let query = transaction_query_from_request(store, &request);
|
|
let query = match query {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let page = ksp_store_lib::RawTransactionInspectionRead::inspect_raw_transactions(store, &query).await;
|
|
let page = match page {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let records_total_decimal = page.total_items().to_string();
|
|
let records_filtered_decimal = page.filtered_items().to_string();
|
|
let items = page.into_items();
|
|
let mut rows = std::vec::Vec::with_capacity(items.len());
|
|
for item in items {
|
|
rows.push(transaction_row_dto(&item));
|
|
}
|
|
return std::result::Result::Ok(crate::StoreTransactionQueryResponseDto { records_filtered_decimal, records_total_decimal, rows });
|
|
}
|
|
|
|
/// Loads one transaction detail by application-owned row identity without exposing Store network selection to the frontend.
|
|
pub(crate) async fn transaction_detail(&self, request: crate::StoreTransactionDetailRequestDto) -> ksp_core_lib::Result<crate::StoreTransactionDetailDto> {
|
|
let locked = self.store.read().await;
|
|
let store = locked.as_ref();
|
|
let store = match store {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::result::Result::Err(store_runtime_unavailable()),
|
|
};
|
|
let reference = transaction_reference_from_request(store, request.signature.as_str());
|
|
let reference = match reference {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let retention = ksp_store_lib::RawTransactionRetentionRead::get_raw_transaction_retention_state(store, &reference).await;
|
|
let retention = match retention {
|
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
|
std::result::Result::Ok(std::option::Option::None) => return std::result::Result::Err(store_transaction_not_found()),
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return match retention {
|
|
ksp_store_lib::RawRetentionState::Full | ksp_store_lib::RawRetentionState::Archived => {
|
|
transaction_detail_with_payload(store, &reference, retention).await
|
|
},
|
|
ksp_store_lib::RawRetentionState::Purged => transaction_detail_from_tombstone(store, &reference).await,
|
|
_ => std::result::Result::Err(ksp_core_lib::Error::new(
|
|
crate::ERROR_CODE_APP_STATE_INVALID,
|
|
"Store Desk cannot project an unsupported transaction retention state",
|
|
)),
|
|
};
|
|
}
|
|
|
|
/// Executes one validated metadata-only observation query for one exact transaction identity.
|
|
pub(crate) async fn query_transaction_observations(
|
|
&self,
|
|
request: crate::StoreTransactionObservationQueryRequestDto,
|
|
) -> ksp_core_lib::Result<crate::StoreTransactionObservationQueryResponseDto> {
|
|
let locked = self.store.read().await;
|
|
let store = locked.as_ref();
|
|
let store = match store {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::result::Result::Err(store_runtime_unavailable()),
|
|
};
|
|
let query = transaction_observation_query_from_request(store, &request);
|
|
let query = match query {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let page = ksp_store_lib::RawTransactionObservationInspectionRead::inspect_raw_transaction_observations(store, &query).await;
|
|
let page = match page {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let records_total_decimal = page.total_items().to_string();
|
|
let records_filtered_decimal = page.filtered_items().to_string();
|
|
let items = page.into_items();
|
|
let mut rows = std::vec::Vec::with_capacity(items.len());
|
|
for item in items {
|
|
rows.push(transaction_observation_row_dto(&item));
|
|
}
|
|
return std::result::Result::Ok(crate::StoreTransactionObservationQueryResponseDto { records_filtered_decimal, records_total_decimal, rows });
|
|
}
|
|
|
|
/// Explicitly closes the Store exactly once through its backend-neutral facade after all read guards have drained.
|
|
pub(crate) async fn close(&self) -> ksp_core_lib::Result<()> {
|
|
let mut locked = self.store.write().await;
|
|
let store = locked.take();
|
|
drop(locked);
|
|
let store = match store {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::result::Result::Ok(()),
|
|
};
|
|
let snapshot = store.runtime_snapshot();
|
|
let backend_kind = snapshot.backend_kind().code().to_owned();
|
|
let network = snapshot.network().as_str().to_owned();
|
|
let closed = store.close().await;
|
|
return match closed {
|
|
std::result::Result::Ok(()) => {
|
|
ksp_logging_lib::debug!(
|
|
target: crate::TRACING_TARGET,
|
|
domain = crate::TRACING_DOMAIN_STORE,
|
|
store_profile = self.profile_id.as_str(),
|
|
backend = backend_kind.as_str(),
|
|
network = network.as_str(),
|
|
"closed Store Desk Store runtime"
|
|
);
|
|
std::result::Result::Ok(())
|
|
},
|
|
std::result::Result::Err(error) => std::result::Result::Err(
|
|
ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_SHUTDOWN_FAILED, "Store Desk Store shutdown failed").with_source(error),
|
|
),
|
|
};
|
|
}
|
|
}
|
|
|
|
const ACCOUNT_DETAIL_PREVIEW_BYTES: usize = 512;
|
|
const TRANSACTION_DETAIL_PREVIEW_BYTES: usize = 512;
|
|
|
|
fn inspection_page_from_request(offset_text: &str, limit_value: u32) -> ksp_core_lib::Result<ksp_store_lib::RawInspectionPageRequest> {
|
|
let limit = match limit_value {
|
|
25 | 50 | 100 => ksp_store_lib::RawPageLimit::new(u64::from(limit_value)),
|
|
_ => return std::result::Result::Err(store_query_invalid()),
|
|
};
|
|
let limit = match limit {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
|
};
|
|
let offset = offset_text.parse::<u64>();
|
|
let offset = match offset {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
|
};
|
|
return std::result::Result::Ok(ksp_store_lib::RawInspectionPageRequest::new(offset, limit));
|
|
}
|
|
|
|
fn account_observation_query_from_request(
|
|
store: &ksp_store_lib::Store,
|
|
request: &crate::StoreAccountObservationQueryRequestDto,
|
|
) -> ksp_core_lib::Result<ksp_store_lib::RawAccountObservationInspectionQuery> {
|
|
let page = inspection_page_from_request(request.offset.as_str(), request.limit);
|
|
let page = match page {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let detail_request =
|
|
crate::StoreAccountDetailRequestDto { pubkey: request.pubkey.clone(), slot: request.slot.clone(), state_hash: request.state_hash.clone() };
|
|
let reference = account_reference_from_request(store, &detail_request);
|
|
let reference = match reference {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let network = store.runtime_snapshot().network().clone();
|
|
let query = ksp_store_lib::RawAccountObservationInspectionQuery::try_new(
|
|
network,
|
|
std::option::Option::Some(reference),
|
|
ksp_store_lib::RawSortDirection::Descending,
|
|
page,
|
|
);
|
|
return match query {
|
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
|
std::result::Result::Err(_) => std::result::Result::Err(store_query_invalid()),
|
|
};
|
|
}
|
|
|
|
fn transaction_observation_query_from_request(
|
|
store: &ksp_store_lib::Store,
|
|
request: &crate::StoreTransactionObservationQueryRequestDto,
|
|
) -> ksp_core_lib::Result<ksp_store_lib::RawTransactionObservationInspectionQuery> {
|
|
let page = inspection_page_from_request(request.offset.as_str(), request.limit);
|
|
let page = match page {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let reference = transaction_reference_from_request(store, request.signature.as_str());
|
|
let reference = match reference {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let network = store.runtime_snapshot().network().clone();
|
|
let query = ksp_store_lib::RawTransactionObservationInspectionQuery::try_new(
|
|
network,
|
|
std::option::Option::Some(reference),
|
|
ksp_store_lib::RawSortDirection::Descending,
|
|
page,
|
|
);
|
|
return match query {
|
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
|
std::result::Result::Err(_) => std::result::Result::Err(store_query_invalid()),
|
|
};
|
|
}
|
|
|
|
fn account_query_from_request(
|
|
store: &ksp_store_lib::Store,
|
|
request: &crate::StoreAccountQueryRequestDto,
|
|
) -> ksp_core_lib::Result<ksp_store_lib::RawAccountStateInspectionQuery> {
|
|
let page = inspection_page_from_request(request.offset.as_str(), request.limit);
|
|
let page = match page {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let pubkey = parse_optional_pubkey(request.pubkey.as_deref());
|
|
let pubkey = match pubkey {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let slot_min = parse_optional_decimal_u64(request.slot_min.as_deref());
|
|
let slot_min = match slot_min {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let slot_max = parse_optional_decimal_u64(request.slot_max.as_deref());
|
|
let slot_max = match slot_max {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let slots = ksp_store_lib::RawSlotRange::new(slot_min, slot_max);
|
|
let slots = match slots {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
|
};
|
|
let direction = match request.direction.as_str() {
|
|
"ascending" => ksp_store_lib::RawSortDirection::Ascending,
|
|
"descending" => ksp_store_lib::RawSortDirection::Descending,
|
|
_ => return std::result::Result::Err(store_query_invalid()),
|
|
};
|
|
let network = store.runtime_snapshot().network().clone();
|
|
return std::result::Result::Ok(ksp_store_lib::RawAccountStateInspectionQuery::new(network, pubkey, slots, direction, page));
|
|
}
|
|
|
|
fn account_reference_from_request(
|
|
store: &ksp_store_lib::Store,
|
|
request: &crate::StoreAccountDetailRequestDto,
|
|
) -> ksp_core_lib::Result<ksp_store_lib::RawAccountStateReference> {
|
|
let pubkey = request.pubkey.parse::<ksp_store_lib::Pubkey>();
|
|
let pubkey = match pubkey {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
|
};
|
|
let slot = request.slot.parse::<u64>();
|
|
let slot = match slot {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
|
};
|
|
let state_hash = decode_hex_32(request.state_hash.as_str());
|
|
let state_hash = match state_hash {
|
|
std::result::Result::Ok(value) => ksp_store_lib::RawContentHash::new(value),
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let network = store.runtime_snapshot().network().clone();
|
|
return std::result::Result::Ok(ksp_store_lib::RawAccountStateReference::new(network, pubkey, slot, state_hash));
|
|
}
|
|
|
|
fn account_detail_dto(state: &ksp_store_lib::RawAccountState) -> crate::StoreAccountDetailDto {
|
|
let data = state.data();
|
|
let preview_len = std::cmp::min(data.len(), ACCOUNT_DETAIL_PREVIEW_BYTES);
|
|
let state_hash = state.reference().state_hash();
|
|
return crate::StoreAccountDetailDto {
|
|
data_length_decimal: state.data_len().to_string(),
|
|
data_preview_hex: encode_hex(&data[..preview_len]),
|
|
data_preview_truncated: data.len() > preview_len,
|
|
executable: state.executable(),
|
|
lamports_decimal: state.lamports().to_string(),
|
|
owner: state.owner().to_string(),
|
|
pubkey: state.reference().pubkey().to_string(),
|
|
rent_epoch_decimal: state.rent_epoch().to_string(),
|
|
slot_decimal: state.reference().slot().to_string(),
|
|
state_hash: encode_hex(state_hash.as_bytes()),
|
|
};
|
|
}
|
|
|
|
fn account_row_dto(summary: &ksp_store_lib::RawAccountStateSummary) -> crate::StoreAccountRowDto {
|
|
let state_hash = summary.reference().state_hash();
|
|
return crate::StoreAccountRowDto {
|
|
data_length_decimal: summary.data_length_bytes().to_string(),
|
|
executable: summary.executable(),
|
|
lamports_decimal: summary.lamports().to_string(),
|
|
owner: summary.owner().to_string(),
|
|
pubkey: summary.reference().pubkey().to_string(),
|
|
rent_epoch_decimal: summary.rent_epoch().to_string(),
|
|
slot_decimal: summary.reference().slot().to_string(),
|
|
state_hash: encode_hex(state_hash.as_bytes()),
|
|
};
|
|
}
|
|
|
|
fn observation_provenance_dto(provenance: &ksp_store_lib::RawAcquisitionProvenance) -> crate::StoreObservationProvenanceDto {
|
|
return crate::StoreObservationProvenanceDto {
|
|
acquisition_method: provenance.acquisition_method().as_str().to_owned(),
|
|
capture_session_id: provenance.capture_session_id().map(|value| return value.as_str().to_owned()),
|
|
commitment: provenance.commitment().map(|value| return value.as_str().to_owned()),
|
|
endpoint_id: provenance.endpoint_id().map(|value| return value.as_str().to_owned()),
|
|
filter_id: provenance.filter_id().map(|value| return value.as_str().to_owned()),
|
|
observed_at_unix_millis_decimal: provenance.observed_at().map(|value| return value.unix_millis().to_string()),
|
|
origin: acquisition_origin_code(provenance.origin()).to_owned(),
|
|
protocol: provenance.protocol().as_str().to_owned(),
|
|
provider: provenance.provider().as_str().to_owned(),
|
|
received_at_unix_millis_decimal: provenance.received_at().unix_millis().to_string(),
|
|
source_payload_hash: provenance.source_payload_hash().map(|value| return encode_hex(value.as_bytes())),
|
|
source_payload_size_decimal: provenance.source_payload_size_bytes().map(|value| return value.to_string()),
|
|
};
|
|
}
|
|
|
|
fn account_observation_row_dto(summary: &ksp_store_lib::RawAccountObservationSummary) -> crate::StoreAccountObservationRowDto {
|
|
return crate::StoreAccountObservationRowDto {
|
|
is_startup: summary.is_startup(),
|
|
observation_key: encode_hex(summary.observation_key().as_bytes()),
|
|
provenance: observation_provenance_dto(summary.provenance()),
|
|
transaction_signature: summary.transaction_signature().map(|value| return encode_hex(value.as_bytes())),
|
|
write_version_decimal: summary.write_version().map(|value| return value.to_string()),
|
|
};
|
|
}
|
|
|
|
fn transaction_observation_row_dto(summary: &ksp_store_lib::RawTransactionObservationSummary) -> crate::StoreTransactionObservationRowDto {
|
|
return crate::StoreTransactionObservationRowDto {
|
|
observation_key: encode_hex(summary.observation_key().as_bytes()),
|
|
provenance: observation_provenance_dto(summary.provenance()),
|
|
};
|
|
}
|
|
|
|
fn acquisition_origin_code(origin: ksp_store_lib::RawAcquisitionOrigin) -> &'static str {
|
|
return match origin {
|
|
ksp_store_lib::RawAcquisitionOrigin::Backfill => "backfill",
|
|
ksp_store_lib::RawAcquisitionOrigin::Import => "import",
|
|
ksp_store_lib::RawAcquisitionOrigin::Live => "live",
|
|
ksp_store_lib::RawAcquisitionOrigin::Repair => "repair",
|
|
ksp_store_lib::RawAcquisitionOrigin::Replay => "replay",
|
|
_ => "unknown",
|
|
};
|
|
}
|
|
|
|
fn transaction_query_from_request(
|
|
store: &ksp_store_lib::Store,
|
|
request: &crate::StoreTransactionQueryRequestDto,
|
|
) -> ksp_core_lib::Result<ksp_store_lib::RawTransactionInspectionQuery> {
|
|
let page = inspection_page_from_request(request.offset.as_str(), request.limit);
|
|
let page = match page {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let slot_min = parse_optional_decimal_u64(request.slot_min.as_deref());
|
|
let slot_min = match slot_min {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let slot_max = parse_optional_decimal_u64(request.slot_max.as_deref());
|
|
let slot_max = match slot_max {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let slots = ksp_store_lib::RawSlotRange::new(slot_min, slot_max);
|
|
let slots = match slots {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
|
};
|
|
let direction = match request.direction.as_str() {
|
|
"ascending" => ksp_store_lib::RawSortDirection::Ascending,
|
|
"descending" => ksp_store_lib::RawSortDirection::Descending,
|
|
_ => return std::result::Result::Err(store_query_invalid()),
|
|
};
|
|
let network = store.runtime_snapshot().network().clone();
|
|
return std::result::Result::Ok(ksp_store_lib::RawTransactionInspectionQuery::new(network, slots, direction, page));
|
|
}
|
|
|
|
fn transaction_reference_from_request(store: &ksp_store_lib::Store, signature_text: &str) -> ksp_core_lib::Result<ksp_store_lib::RawTransactionReference> {
|
|
let signature = decode_hex_64(signature_text);
|
|
let signature = match signature {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let network = store.runtime_snapshot().network().clone();
|
|
return std::result::Result::Ok(ksp_store_lib::RawTransactionReference::new(network, ksp_store_lib::RawTransactionSignature::new(signature)));
|
|
}
|
|
|
|
async fn transaction_detail_with_payload(
|
|
store: &ksp_store_lib::Store,
|
|
reference: &ksp_store_lib::RawTransactionReference,
|
|
retention: ksp_store_lib::RawRetentionState,
|
|
) -> ksp_core_lib::Result<crate::StoreTransactionDetailDto> {
|
|
let transaction = ksp_store_lib::RawTransactionRead::get_raw_transaction(store, reference).await;
|
|
let transaction = match transaction {
|
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
|
std::result::Result::Ok(std::option::Option::None) => {
|
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
|
crate::ERROR_CODE_APP_STATE_INVALID,
|
|
"Store Desk transaction retention state and payload availability disagree",
|
|
));
|
|
},
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let payload = transaction.payload();
|
|
let payload_bytes = payload.bytes();
|
|
let preview_len = std::cmp::min(payload_bytes.len(), TRANSACTION_DETAIL_PREVIEW_BYTES);
|
|
let preview = encode_hex(&payload_bytes[..preview_len]);
|
|
return std::result::Result::Ok(crate::StoreTransactionDetailDto {
|
|
block_time_unix_millis_decimal: transaction.block_time().map(|value| return value.unix_millis().to_string()),
|
|
content_hash: encode_hex(payload.content_hash().as_bytes()),
|
|
format_id: payload.format_id().as_str().to_owned(),
|
|
format_version: payload.format_version(),
|
|
payload_preview_hex: std::option::Option::Some(preview),
|
|
payload_preview_truncated: payload_bytes.len() > preview_len,
|
|
payload_size_decimal: std::option::Option::Some(payload.byte_len().to_string()),
|
|
retention_state: retention_state_code(retention).to_owned(),
|
|
signature: encode_hex(reference.signature().as_bytes()),
|
|
slot_decimal: transaction.slot().to_string(),
|
|
});
|
|
}
|
|
|
|
async fn transaction_detail_from_tombstone(
|
|
store: &ksp_store_lib::Store,
|
|
reference: &ksp_store_lib::RawTransactionReference,
|
|
) -> ksp_core_lib::Result<crate::StoreTransactionDetailDto> {
|
|
let tombstone = ksp_store_lib::RawTransactionRetentionRead::get_raw_transaction_tombstone(store, reference).await;
|
|
let tombstone = match tombstone {
|
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
|
std::result::Result::Ok(std::option::Option::None) => {
|
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
|
crate::ERROR_CODE_APP_STATE_INVALID,
|
|
"Store Desk purged transaction is missing its durable tombstone",
|
|
));
|
|
},
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return std::result::Result::Ok(crate::StoreTransactionDetailDto {
|
|
block_time_unix_millis_decimal: std::option::Option::None,
|
|
content_hash: encode_hex(tombstone.content_hash().as_bytes()),
|
|
format_id: tombstone.format_id().as_str().to_owned(),
|
|
format_version: tombstone.format_version(),
|
|
payload_preview_hex: std::option::Option::None,
|
|
payload_preview_truncated: false,
|
|
payload_size_decimal: std::option::Option::None,
|
|
retention_state: retention_state_code(ksp_store_lib::RawRetentionState::Purged).to_owned(),
|
|
signature: encode_hex(reference.signature().as_bytes()),
|
|
slot_decimal: tombstone.slot().to_string(),
|
|
});
|
|
}
|
|
|
|
fn transaction_row_dto(summary: &ksp_store_lib::RawTransactionSummary) -> crate::StoreTransactionRowDto {
|
|
return crate::StoreTransactionRowDto {
|
|
block_time_unix_millis_decimal: summary.block_time().map(|value| return value.unix_millis().to_string()),
|
|
content_hash: encode_hex(summary.content_hash().as_bytes()),
|
|
format_id: summary.format_id().as_str().to_owned(),
|
|
format_version: summary.format_version(),
|
|
payload_size_decimal: summary.payload_size_bytes().map(|value| return value.to_string()),
|
|
retention_state: retention_state_code(summary.retention_state()).to_owned(),
|
|
signature: encode_hex(summary.reference().signature().as_bytes()),
|
|
slot_decimal: summary.slot().to_string(),
|
|
};
|
|
}
|
|
|
|
fn retention_state_code(state: ksp_store_lib::RawRetentionState) -> &'static str {
|
|
return match state {
|
|
ksp_store_lib::RawRetentionState::Full => "full",
|
|
ksp_store_lib::RawRetentionState::Compacted => "compacted",
|
|
ksp_store_lib::RawRetentionState::Archived => "archived",
|
|
ksp_store_lib::RawRetentionState::Purged => "purged",
|
|
_ => "unknown",
|
|
};
|
|
}
|
|
|
|
fn parse_optional_pubkey(value: std::option::Option<&str>) -> ksp_core_lib::Result<std::option::Option<ksp_store_lib::Pubkey>> {
|
|
return match value {
|
|
std::option::Option::Some(text) => match text.parse::<ksp_store_lib::Pubkey>() {
|
|
std::result::Result::Ok(parsed) => std::result::Result::Ok(std::option::Option::Some(parsed)),
|
|
std::result::Result::Err(_) => std::result::Result::Err(store_query_invalid()),
|
|
},
|
|
std::option::Option::None => std::result::Result::Ok(std::option::Option::None),
|
|
};
|
|
}
|
|
|
|
fn parse_optional_decimal_u64(value: std::option::Option<&str>) -> ksp_core_lib::Result<std::option::Option<u64>> {
|
|
return match value {
|
|
std::option::Option::Some(text) => match text.parse::<u64>() {
|
|
std::result::Result::Ok(parsed) => std::result::Result::Ok(std::option::Option::Some(parsed)),
|
|
std::result::Result::Err(_) => std::result::Result::Err(store_query_invalid()),
|
|
},
|
|
std::option::Option::None => std::result::Result::Ok(std::option::Option::None),
|
|
};
|
|
}
|
|
|
|
fn decode_hex_32(value: &str) -> ksp_core_lib::Result<[u8; 32]> {
|
|
if value.len() != 64 || !value.is_ascii() {
|
|
return std::result::Result::Err(store_query_invalid());
|
|
}
|
|
let source = value.as_bytes();
|
|
let mut decoded = [0_u8; 32];
|
|
for index in 0..32 {
|
|
let high = decode_hex_nibble(source[index * 2]);
|
|
let low = decode_hex_nibble(source[index * 2 + 1]);
|
|
let (high, low) = match (high, low) {
|
|
(std::option::Option::Some(high), std::option::Option::Some(low)) => (high, low),
|
|
_ => return std::result::Result::Err(store_query_invalid()),
|
|
};
|
|
decoded[index] = (high << 4) | low;
|
|
}
|
|
return std::result::Result::Ok(decoded);
|
|
}
|
|
|
|
fn decode_hex_64(value: &str) -> ksp_core_lib::Result<[u8; 64]> {
|
|
if value.len() != 128 || !value.is_ascii() {
|
|
return std::result::Result::Err(store_query_invalid());
|
|
}
|
|
let source = value.as_bytes();
|
|
let mut decoded = [0_u8; 64];
|
|
for index in 0..64 {
|
|
let high = decode_hex_nibble(source[index * 2]);
|
|
let low = decode_hex_nibble(source[index * 2 + 1]);
|
|
let (high, low) = match (high, low) {
|
|
(std::option::Option::Some(high), std::option::Option::Some(low)) => (high, low),
|
|
_ => return std::result::Result::Err(store_query_invalid()),
|
|
};
|
|
decoded[index] = (high << 4) | low;
|
|
}
|
|
return std::result::Result::Ok(decoded);
|
|
}
|
|
|
|
fn decode_hex_nibble(value: u8) -> std::option::Option<u8> {
|
|
return match value {
|
|
b'0'..=b'9' => std::option::Option::Some(value - b'0'),
|
|
b'a'..=b'f' => std::option::Option::Some(value - b'a' + 10),
|
|
b'A'..=b'F' => std::option::Option::Some(value - b'A' + 10),
|
|
_ => std::option::Option::None,
|
|
};
|
|
}
|
|
|
|
fn encode_hex(bytes: &[u8]) -> String {
|
|
const HEX: &[u8; 16] = b"0123456789abcdef";
|
|
let mut encoded = String::with_capacity(bytes.len() * 2);
|
|
for byte in bytes {
|
|
let value = *byte;
|
|
encoded.push(char::from(HEX[usize::from(value >> 4)]));
|
|
encoded.push(char::from(HEX[usize::from(value & 0x0f)]));
|
|
}
|
|
return encoded;
|
|
}
|
|
|
|
fn store_account_not_found() -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_ACCOUNT_NOT_FOUND, "Store Desk account state was not found");
|
|
}
|
|
|
|
fn store_query_invalid() -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_QUERY_INVALID, "Store Desk RAW inspection/detail request is invalid");
|
|
}
|
|
|
|
fn store_runtime_unavailable() -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_RUNTIME_UNAVAILABLE, "Store Desk Store runtime is unavailable");
|
|
}
|
|
|
|
fn store_transaction_not_found() -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_TRANSACTION_NOT_FOUND, "Store Desk transaction was not found");
|
|
}
|
|
|
|
/// Resolves the composite Store target, opens Store through the facade and captures one initial readiness probe.
|
|
pub(crate) async fn initialize_store(management: &ksp_config_lib::ConfigManagement) -> crate::StoreStartup {
|
|
let environment = ksp_config_lib::ConfigEnvironment::load();
|
|
let environment = match environment {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return unavailable_startup(std::option::Option::None, std::option::Option::None, std::option::Option::None, error),
|
|
};
|
|
let composite = crate::load_store_desk_composite(management);
|
|
let composite = match composite {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return unavailable_startup(std::option::Option::None, std::option::Option::None, std::option::Option::None, error),
|
|
};
|
|
let profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_STORE, ksp_config_lib::FILE_ID_STD_STORE);
|
|
let profile = match profile {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return unavailable_startup(
|
|
std::option::Option::Some(composite.profile_id().to_owned()),
|
|
std::option::Option::None,
|
|
std::option::Option::None,
|
|
error,
|
|
);
|
|
},
|
|
};
|
|
let resolved = management.engine().resolve_store_config_profile(&profile, &environment);
|
|
let resolved = match resolved {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return unavailable_startup(
|
|
std::option::Option::Some(profile.profile_id().to_owned()),
|
|
std::option::Option::None,
|
|
std::option::Option::None,
|
|
error,
|
|
);
|
|
},
|
|
};
|
|
let profile_id = resolved.profile_id().to_owned();
|
|
let backend_kind = resolved.settings().backend_kind().code().to_owned();
|
|
let network = resolved.settings().network().as_str().to_owned();
|
|
let store = ksp_store_lib::Store::open(resolved.into_settings()).await;
|
|
let store = match store {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return unavailable_startup(
|
|
std::option::Option::Some(profile_id),
|
|
std::option::Option::Some(backend_kind),
|
|
std::option::Option::Some(network),
|
|
error,
|
|
);
|
|
},
|
|
};
|
|
let health = store.health().await;
|
|
ksp_logging_lib::debug!(
|
|
target: crate::TRACING_TARGET,
|
|
domain = crate::TRACING_DOMAIN_STORE,
|
|
store_profile = profile_id.as_str(),
|
|
backend = backend_kind.as_str(),
|
|
network = network.as_str(),
|
|
health_state = store_health_state_code(health.state()),
|
|
pending_migration_count = health.pending_migration_count(),
|
|
"initialized Store Desk Store from composite-managed configuration"
|
|
);
|
|
return crate::StoreStartup {
|
|
backend_kind: std::option::Option::Some(backend_kind),
|
|
diagnostic: non_ready_health_diagnostic(&health),
|
|
network: std::option::Option::Some(network),
|
|
profile_id: std::option::Option::Some(profile_id.clone()),
|
|
runtime: std::option::Option::Some(crate::StoreRuntime { profile_id, store: tokio::sync::RwLock::new(std::option::Option::Some(store)) }),
|
|
};
|
|
}
|
|
|
|
fn non_ready_health_diagnostic(health: &ksp_store_lib::StoreHealthSnapshot) -> std::option::Option<crate::CommandErrorDto> {
|
|
if matches!(health.state(), ksp_store_lib::StoreHealthState::Ready) {
|
|
return std::option::Option::None;
|
|
}
|
|
let code = health.last_error_code();
|
|
return match code {
|
|
std::option::Option::Some(value) => std::option::Option::Some(crate::CommandErrorDto {
|
|
code: value.code().to_owned(),
|
|
domain: value.domain().to_owned(),
|
|
message: "Store health probe did not prove readiness".to_owned(),
|
|
}),
|
|
std::option::Option::None => std::option::Option::Some(crate::CommandErrorDto {
|
|
code: "store_not_ready".to_owned(),
|
|
domain: "store_desk".to_owned(),
|
|
message: "Store health probe did not prove readiness".to_owned(),
|
|
}),
|
|
};
|
|
}
|
|
|
|
fn store_health_state_code(state: ksp_store_lib::StoreHealthState) -> &'static str {
|
|
return match state {
|
|
ksp_store_lib::StoreHealthState::Ready => "ready",
|
|
ksp_store_lib::StoreHealthState::NotReady => "not_ready",
|
|
_ => "unknown",
|
|
};
|
|
}
|
|
|
|
fn unavailable_startup(
|
|
profile_id: std::option::Option<String>,
|
|
backend_kind: std::option::Option<String>,
|
|
network: std::option::Option<String>,
|
|
error: ksp_core_lib::Error,
|
|
) -> crate::StoreStartup {
|
|
let diagnostic = crate::CommandErrorDto::from_error(&error);
|
|
ksp_logging_lib::warn!(
|
|
target: crate::TRACING_TARGET,
|
|
domain = crate::TRACING_DOMAIN_STORE,
|
|
error_domain = diagnostic.domain.as_str(),
|
|
error_code = diagnostic.code.as_str(),
|
|
"Store Desk Store readiness is unavailable; keeping desktop shell available"
|
|
);
|
|
return crate::StoreStartup {
|
|
backend_kind,
|
|
diagnostic: std::option::Option::Some(diagnostic),
|
|
network,
|
|
profile_id,
|
|
runtime: std::option::Option::None,
|
|
};
|
|
}
|