408 lines
16 KiB
Rust
408 lines
16 KiB
Rust
// file: kb-app-demo-desktop/src/demo_sql_replay_candidates.rs
|
|
// version: 7
|
|
|
|
//! Read-only SQL replay candidate browser commands.
|
|
|
|
use ts_rs::TS; // rust-rules: derive-import
|
|
|
|
/// One program identifier exposed by the runtime `kb_program_ids` registry.
|
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayKnownProgramOption.ts"
|
|
)]
|
|
pub(crate) struct DemoSqlReplayKnownProgramOption {
|
|
/// Stable lower snake case program code.
|
|
pub(crate) code: std::string::String,
|
|
/// Base58 Solana program identifier.
|
|
pub(crate) program_id: std::string::String,
|
|
}
|
|
|
|
/// Static options for the replay candidate browser.
|
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayOptionsPayload.ts"
|
|
)]
|
|
pub(crate) struct DemoSqlReplayOptionsPayload {
|
|
/// Active profile name.
|
|
pub(crate) active_profile_name: std::string::String,
|
|
/// Masked PostgreSQL DSN.
|
|
pub(crate) masked_dsn: std::string::String,
|
|
/// Maximum rows accepted by one query.
|
|
pub(crate) maximum_limit: u32,
|
|
/// Program identifiers enumerable from `kb_program_ids`.
|
|
pub(crate) known_programs: std::vec::Vec<crate::DemoSqlReplayKnownProgramOption>,
|
|
}
|
|
|
|
/// UI request for bounded transaction replay candidates.
|
|
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRequest.ts"
|
|
)]
|
|
pub(crate) struct DemoSqlReplayTransactionRequest {
|
|
/// Optional partial signature search.
|
|
pub(crate) signature_contains: std::option::Option<std::string::String>,
|
|
/// Optional inclusive minimum slot.
|
|
#[ts(type = "number | null")]
|
|
pub(crate) min_slot: std::option::Option<u64>,
|
|
/// Optional inclusive maximum slot.
|
|
#[ts(type = "number | null")]
|
|
pub(crate) max_slot: std::option::Option<u64>,
|
|
/// Optional raw processing state.
|
|
pub(crate) raw_processing_state: std::option::Option<std::string::String>,
|
|
/// Optional latest ledger status.
|
|
pub(crate) ledger_status: std::option::Option<std::string::String>,
|
|
/// Optional exact program id.
|
|
pub(crate) program_id: std::option::Option<std::string::String>,
|
|
/// Program scope code: any, outer, inner or logs.
|
|
pub(crate) program_scope: std::string::String,
|
|
/// Optional entity kind: mint, owner or account_key.
|
|
pub(crate) entity_kind: std::option::Option<std::string::String>,
|
|
/// Optional exact entity value.
|
|
pub(crate) entity_value: std::option::Option<std::string::String>,
|
|
/// Maximum returned rows.
|
|
pub(crate) limit: u32,
|
|
/// Orders newest slots first when true.
|
|
pub(crate) newest_first: bool,
|
|
}
|
|
|
|
/// One transaction row shown by the replay candidate browser.
|
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRow.ts"
|
|
)]
|
|
pub(crate) struct DemoSqlReplayTransactionRow {
|
|
/// Canonical transaction signature.
|
|
pub(crate) signature: std::string::String,
|
|
/// Transaction slot.
|
|
#[ts(type = "number")]
|
|
pub(crate) slot: i64,
|
|
/// Current raw processing state.
|
|
pub(crate) raw_processing_state: std::string::String,
|
|
/// Current raw retention state.
|
|
pub(crate) retention_state: std::string::String,
|
|
/// Whether a core transaction exists.
|
|
pub(crate) has_core_transaction: bool,
|
|
/// Core transaction failure flag when available.
|
|
pub(crate) transaction_failed: std::option::Option<bool>,
|
|
/// Latest core extraction ledger status.
|
|
pub(crate) ledger_status: std::string::String,
|
|
/// Latest processor version when available.
|
|
pub(crate) processor_version: std::option::Option<std::string::String>,
|
|
/// Latest attempt count.
|
|
pub(crate) attempt_count: i32,
|
|
/// Number of top-level instructions.
|
|
#[ts(type = "number")]
|
|
pub(crate) outer_instruction_count: i64,
|
|
/// Number of inner instructions.
|
|
#[ts(type = "number")]
|
|
pub(crate) inner_instruction_count: i64,
|
|
/// Number of distinct top-level programs.
|
|
#[ts(type = "number")]
|
|
pub(crate) outer_program_count: i64,
|
|
/// Number of distinct inner programs.
|
|
#[ts(type = "number")]
|
|
pub(crate) inner_program_count: i64,
|
|
/// Raw row update timestamp.
|
|
pub(crate) updated_at: std::string::String,
|
|
}
|
|
|
|
/// UI request for bounded program summaries.
|
|
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRequest.ts"
|
|
)]
|
|
pub(crate) struct DemoSqlReplayProgramRequest {
|
|
/// Optional partial program id search.
|
|
pub(crate) program_id_contains: std::option::Option<std::string::String>,
|
|
/// Maximum returned rows.
|
|
pub(crate) limit: u32,
|
|
}
|
|
|
|
/// Aggregated program row shown by the replay candidate browser.
|
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRow.ts"
|
|
)]
|
|
pub(crate) struct DemoSqlReplayProgramRow {
|
|
/// Optional stable code from `kb_program_ids`.
|
|
pub(crate) program_code: std::option::Option<std::string::String>,
|
|
/// Program id.
|
|
pub(crate) program_id: std::string::String,
|
|
/// Number of distinct transactions.
|
|
#[ts(type = "number")]
|
|
pub(crate) transaction_count: i64,
|
|
/// Number of top-level instruction occurrences.
|
|
#[ts(type = "number")]
|
|
pub(crate) outer_instruction_count: i64,
|
|
/// Number of inner instruction occurrences.
|
|
#[ts(type = "number")]
|
|
pub(crate) inner_instruction_count: i64,
|
|
/// Number of reliably linked log occurrences.
|
|
#[ts(type = "number")]
|
|
pub(crate) log_count: i64,
|
|
/// Lowest observed slot.
|
|
#[ts(type = "number")]
|
|
pub(crate) min_slot: i64,
|
|
/// Highest observed slot.
|
|
#[ts(type = "number")]
|
|
pub(crate) max_slot: i64,
|
|
}
|
|
|
|
/// UI request for bounded core entity summaries.
|
|
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRequest.ts"
|
|
)]
|
|
pub(crate) struct DemoSqlReplayEntityRequest {
|
|
/// Entity kind: mint, owner or account_key.
|
|
pub(crate) entity_kind: std::string::String,
|
|
/// Optional partial entity value search.
|
|
pub(crate) entity_value_contains: std::option::Option<std::string::String>,
|
|
/// Maximum returned rows.
|
|
pub(crate) limit: u32,
|
|
}
|
|
|
|
/// Aggregated core entity row shown by the replay candidate browser.
|
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRow.ts"
|
|
)]
|
|
pub(crate) struct DemoSqlReplayEntityRow {
|
|
/// Stable entity kind code.
|
|
pub(crate) entity_kind: std::string::String,
|
|
/// Mint, owner or account-key address.
|
|
pub(crate) entity_value: std::string::String,
|
|
/// Number of distinct transactions.
|
|
#[ts(type = "number")]
|
|
pub(crate) transaction_count: i64,
|
|
/// Total number of core-table occurrences.
|
|
#[ts(type = "number")]
|
|
pub(crate) occurrence_count: i64,
|
|
/// Lowest observed slot.
|
|
#[ts(type = "number")]
|
|
pub(crate) min_slot: i64,
|
|
/// Highest observed slot.
|
|
#[ts(type = "number")]
|
|
pub(crate) max_slot: i64,
|
|
}
|
|
|
|
pub(crate) fn transaction_row_from_pg(
|
|
row: kb_store::PostgresReplayTransactionCandidate,
|
|
) -> crate::DemoSqlReplayTransactionRow {
|
|
return crate::DemoSqlReplayTransactionRow {
|
|
signature: row.signature,
|
|
slot: row.slot,
|
|
raw_processing_state: row.raw_processing_state,
|
|
retention_state: row.retention_state,
|
|
has_core_transaction: row.has_core_transaction,
|
|
transaction_failed: row.transaction_failed,
|
|
ledger_status: row.ledger_status,
|
|
processor_version: row.processor_version,
|
|
attempt_count: row.attempt_count,
|
|
outer_instruction_count: row.outer_instruction_count,
|
|
inner_instruction_count: row.inner_instruction_count,
|
|
outer_program_count: row.outer_program_count,
|
|
inner_program_count: row.inner_program_count,
|
|
updated_at: row.updated_at,
|
|
};
|
|
}
|
|
|
|
pub(crate) fn program_row_from_pg(
|
|
row: kb_store::PostgresReplayProgramSummary,
|
|
) -> crate::DemoSqlReplayProgramRow {
|
|
let program_code = kb_program_ids::find_registered_program_id(&row.program_id)
|
|
.map(|entry| return entry.code().to_owned());
|
|
return crate::DemoSqlReplayProgramRow {
|
|
program_code,
|
|
program_id: row.program_id,
|
|
transaction_count: row.transaction_count,
|
|
outer_instruction_count: row.outer_instruction_count,
|
|
inner_instruction_count: row.inner_instruction_count,
|
|
log_count: row.log_count,
|
|
min_slot: row.min_slot,
|
|
max_slot: row.max_slot,
|
|
};
|
|
}
|
|
|
|
pub(crate) fn entity_row_from_pg(
|
|
row: kb_store::PostgresReplayEntitySummary,
|
|
) -> crate::DemoSqlReplayEntityRow {
|
|
return crate::DemoSqlReplayEntityRow {
|
|
entity_kind: row.entity_kind,
|
|
entity_value: row.entity_value,
|
|
transaction_count: row.transaction_count,
|
|
occurrence_count: row.occurrence_count,
|
|
min_slot: row.min_slot,
|
|
max_slot: row.max_slot,
|
|
};
|
|
}
|
|
|
|
pub(crate) fn validated_csv_file_name(
|
|
file_name: &str,
|
|
) -> std::result::Result<&'static str, std::string::String> {
|
|
return match file_name {
|
|
"replay_transactions.csv" => std::result::Result::Ok("replay_transactions.csv"),
|
|
"replay_programs.csv" => std::result::Result::Ok("replay_programs.csv"),
|
|
"replay_mints.csv" => std::result::Result::Ok("replay_mints.csv"),
|
|
"replay_owners.csv" => std::result::Result::Ok("replay_owners.csv"),
|
|
"replay_account_keys.csv" => std::result::Result::Ok("replay_account_keys.csv"),
|
|
_ => std::result::Result::Err(format!("unsupported replay CSV file name: {file_name}")),
|
|
};
|
|
}
|
|
|
|
pub(crate) fn csv_export_directory_from_current_dir(
|
|
current_dir: &std::path::Path,
|
|
) -> std::path::PathBuf {
|
|
let current_name = current_dir.file_name().and_then(std::ffi::OsStr::to_str);
|
|
if let (std::option::Option::Some("kb_app_demo_desktop"), std::option::Option::Some(parent)) =
|
|
(current_name, current_dir.parent())
|
|
{
|
|
return parent.join("data").join("exports_csv");
|
|
}
|
|
return current_dir.join("data").join("exports_csv");
|
|
}
|
|
|
|
pub(crate) async fn available_csv_export_path(
|
|
export_dir: &std::path::Path,
|
|
file_name: &str,
|
|
) -> std::result::Result<std::path::PathBuf, std::string::String> {
|
|
let direct_path = export_dir.join(file_name);
|
|
let direct_exists_result = tokio::fs::try_exists(&direct_path).await;
|
|
let direct_exists = match direct_exists_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(format!("cannot inspect CSV export path: {error}"));
|
|
},
|
|
};
|
|
if !direct_exists {
|
|
return std::result::Result::Ok(direct_path);
|
|
}
|
|
let stem = file_name.trim_end_matches(".csv");
|
|
for suffix in 1_u16..=999_u16 {
|
|
let candidate = export_dir.join(format!("{stem}_{suffix}.csv"));
|
|
let exists_result = tokio::fs::try_exists(&candidate).await;
|
|
let exists = match exists_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(format!(
|
|
"cannot inspect CSV export path: {error}"
|
|
));
|
|
},
|
|
};
|
|
if !exists {
|
|
return std::result::Result::Ok(candidate);
|
|
}
|
|
}
|
|
return std::result::Result::Err("cannot allocate a unique CSV export path".to_owned());
|
|
}
|
|
|
|
pub(crate) fn program_scope_from_code(
|
|
code: &str,
|
|
) -> std::result::Result<kb_store::PostgresReplayProgramScope, std::string::String> {
|
|
return match code {
|
|
"any" => std::result::Result::Ok(kb_store::PostgresReplayProgramScope::Any),
|
|
"outer" => std::result::Result::Ok(kb_store::PostgresReplayProgramScope::Outer),
|
|
"inner" => std::result::Result::Ok(kb_store::PostgresReplayProgramScope::Inner),
|
|
"logs" => std::result::Result::Ok(kb_store::PostgresReplayProgramScope::Logs),
|
|
_ => std::result::Result::Err(format!("unsupported replay program scope: {code}")),
|
|
};
|
|
}
|
|
|
|
pub(crate) fn optional_entity_kind_from_code(
|
|
code: std::option::Option<&str>,
|
|
) -> std::result::Result<std::option::Option<kb_store::PostgresReplayEntityKind>, std::string::String>
|
|
{
|
|
return match code {
|
|
std::option::Option::Some(value) => {
|
|
let result = entity_kind_from_code(value);
|
|
match result {
|
|
std::result::Result::Ok(kind) => {
|
|
std::result::Result::Ok(std::option::Option::Some(kind))
|
|
},
|
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
|
}
|
|
},
|
|
std::option::Option::None => std::result::Result::Ok(std::option::Option::None),
|
|
};
|
|
}
|
|
|
|
pub(crate) fn entity_kind_from_code(
|
|
code: &str,
|
|
) -> std::result::Result<kb_store::PostgresReplayEntityKind, std::string::String> {
|
|
return match code {
|
|
"mint" => std::result::Result::Ok(kb_store::PostgresReplayEntityKind::Mint),
|
|
"owner" => std::result::Result::Ok(kb_store::PostgresReplayEntityKind::Owner),
|
|
"account_key" => std::result::Result::Ok(kb_store::PostgresReplayEntityKind::AccountKey),
|
|
_ => std::result::Result::Err(format!("unsupported replay entity kind: {code}")),
|
|
};
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[test]
|
|
fn program_scope_parser_accepts_logs() {
|
|
let result = crate::program_scope_from_code("logs");
|
|
assert_eq!(result, std::result::Result::Ok(kb_store::PostgresReplayProgramScope::Logs));
|
|
}
|
|
|
|
#[test]
|
|
fn entity_kind_parser_rejects_unknown_code() {
|
|
let result = crate::entity_kind_from_code("unknown");
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn csv_file_name_rejects_unknown_name() {
|
|
let result = crate::validated_csv_file_name("arbitrary.csv");
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn csv_export_directory_uses_workspace_root_from_demo_crate() {
|
|
let current_dir = std::path::Path::new("/tmp/khadhroony-bot2/kb_app_demo_desktop");
|
|
let result = crate::csv_export_directory_from_current_dir(current_dir);
|
|
assert_eq!(result, std::path::Path::new("/tmp/khadhroony-bot2/data/exports_csv"));
|
|
}
|
|
|
|
#[test]
|
|
fn csv_file_name_accepts_split_entity_exports() {
|
|
let mint_result = crate::validated_csv_file_name("replay_mints.csv");
|
|
let owner_result = crate::validated_csv_file_name("replay_owners.csv");
|
|
let account_result = crate::validated_csv_file_name("replay_account_keys.csv");
|
|
assert!(mint_result.is_ok());
|
|
assert!(owner_result.is_ok());
|
|
assert!(account_result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn program_row_exposes_registered_code() {
|
|
let row = kb_store::PostgresReplayProgramSummary {
|
|
program_id: kb_program_ids::SYSTEM_PROGRAM_ID.to_owned(),
|
|
transaction_count: 1,
|
|
outer_instruction_count: 1,
|
|
inner_instruction_count: 0,
|
|
log_count: 1,
|
|
min_slot: 1,
|
|
max_slot: 1,
|
|
};
|
|
let converted = crate::program_row_from_pg(row);
|
|
assert_eq!(converted.program_code.as_deref(), std::option::Option::Some("system"));
|
|
}
|
|
}
|