Files
khadhroony-bot3/ks-pipeline/src/core_extraction.rs
2026-08-09 19:34:08 +02:00

1846 lines
77 KiB
Rust

// file: ks-pipeline/src/core_extraction.rs
// version: 11
//! Canonical Solana transaction to normalized core extraction pipeline.
use futures_util::FutureExt; // rust-rules: trait-import
use futures_util::StreamExt; // rust-rules: trait-import
use sha2::Digest; // rust-rules: trait-import
/// Stable processing stage used by the canonical to core extractor.
pub const CORE_EXTRACTION_STAGE: &str = "core_extraction";
/// Stable processor name used by the canonical to core extractor.
pub const CORE_EXTRACTION_PROCESSOR_NAME: &str = "canonical_to_core";
/// Current extractor implementation version.
pub const CORE_EXTRACTION_PROCESSOR_VERSION: &str = "1";
/// Source selection used by one core extraction campaign.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CoreExtractionSource {
/// Exact canonical transaction signatures.
Signatures(std::vec::Vec<std::string::String>),
/// Canonical transactions still in the received state.
Pending,
/// Canonical transactions inside an inclusive slot range.
SlotRange {
/// Inclusive minimum slot.
min_slot: u64,
/// Inclusive maximum slot.
max_slot: u64,
},
/// Transactions already containing one resolved core instruction for a program id.
ProgramId {
/// Solana program id.
program_id: std::string::String,
},
}
/// Complete bounded core extraction request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CoreExtractionRequest {
/// Source selection.
pub source: crate::CoreExtractionSource,
/// Maximum number of canonical transactions selected.
pub limit: u32,
/// Maximum concurrent extraction operations.
pub max_concurrent_extractions: u32,
/// Forces replacement even when the same processor version and hash succeeded.
pub force_replay: bool,
}
impl CoreExtractionRequest {
/// Validates campaign bounds and source values.
pub fn validate(&self) -> ks_core::Result<()> {
if self.limit == 0 {
return std::result::Result::Err(ks_core::Error::config(
"core extraction limit must be greater than zero",
));
}
if self.max_concurrent_extractions == 0 {
return std::result::Result::Err(ks_core::Error::config(
"core extraction concurrency must be greater than zero",
));
}
match &self.source {
crate::CoreExtractionSource::Signatures(signatures) => {
if signatures.is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"signature extraction requires at least one signature",
));
}
for signature in signatures {
let validation_result =
ks_onchain_transport::validate_transaction_signature_text(
signature.as_str(),
"core extraction signature",
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
}
},
crate::CoreExtractionSource::Pending => {},
crate::CoreExtractionSource::SlotRange { min_slot, max_slot } => {
if min_slot > max_slot {
return std::result::Result::Err(ks_core::Error::config(
"core extraction minimum slot must not exceed maximum slot",
));
}
},
crate::CoreExtractionSource::ProgramId { program_id } => {
let validation_result = ks_onchain_transport::validate_solana_pubkey_text(
program_id.as_str(),
"core extraction program id",
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
},
}
return std::result::Result::Ok(());
}
fn selection_filter(&self) -> ks_core::Result<ks_store::CoreExtractionSelectionFilter> {
return match &self.source {
crate::CoreExtractionSource::Signatures(signatures) => {
ks_store::CoreExtractionSelectionFilter::new(
signatures.clone(),
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
self.limit,
)
},
crate::CoreExtractionSource::Pending => {
ks_store::CoreExtractionSelectionFilter::pending(self.limit)
},
crate::CoreExtractionSource::SlotRange { min_slot, max_slot } => {
ks_store::CoreExtractionSelectionFilter::new(
std::vec::Vec::new(),
std::option::Option::Some(*min_slot),
std::option::Option::Some(*max_slot),
std::option::Option::None,
std::option::Option::None,
self.limit,
)
},
crate::CoreExtractionSource::ProgramId { program_id } => {
ks_store::CoreExtractionSelectionFilter::new(
std::vec::Vec::new(),
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(program_id.clone()),
self.limit,
)
},
};
}
}
/// Core extraction progress severity.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CoreExtractionProgressLevel {
/// Diagnostic detail.
Debug,
/// Normal campaign information.
Info,
/// Recoverable issue or cancellation.
Warning,
/// Extraction failure.
Error,
}
impl CoreExtractionProgressLevel {
/// Returns the stable lowercase level code.
pub fn code(&self) -> &'static str {
return match self {
crate::CoreExtractionProgressLevel::Debug => "debug",
crate::CoreExtractionProgressLevel::Info => "info",
crate::CoreExtractionProgressLevel::Warning => "warn",
crate::CoreExtractionProgressLevel::Error => "error",
};
}
}
/// One operator-visible extraction progress event.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CoreExtractionProgressEvent {
/// UTC timestamp rendered in RFC 3339.
pub timestamp: std::string::String,
/// Severity.
pub level: crate::CoreExtractionProgressLevel,
/// Human-readable message.
pub message: std::string::String,
/// Number of terminal candidates.
pub completed: u64,
/// Total selected candidates.
pub total: u64,
}
impl CoreExtractionProgressEvent {
fn new(
level: crate::CoreExtractionProgressLevel,
message: impl std::convert::Into<std::string::String>,
completed: u64,
total: u64,
) -> Self {
return Self {
timestamp: chrono::Utc::now().to_rfc3339(),
level,
message: message.into(),
completed,
total,
};
}
}
/// Progress and cancellation contract implemented by applications.
pub trait CoreExtractionObserver: Sync {
/// Receives one progress event.
fn on_progress(&self, event: &crate::CoreExtractionProgressEvent);
/// Returns true when cooperative cancellation was requested.
fn is_cancelled(&self) -> bool;
}
/// Final counters for one core extraction campaign.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CoreExtractionSummary {
/// Current extractor implementation version.
pub processor_version: std::string::String,
/// Number of selected canonical transactions.
pub selected: u64,
/// Number admitted to the bounded execution queue.
pub started: u64,
/// Number reaching a terminal result.
pub completed: u64,
/// Number skipped because the version and hash already succeeded.
pub skipped: u64,
/// Number extracted and committed successfully.
pub extracted: u64,
/// Number failing extraction or persistence.
pub failed: u64,
/// Number admitted but canceled before a terminal result.
pub cancelled_candidates: u64,
/// Number never admitted after cancellation.
pub not_started: u64,
/// Whether the campaign was canceled.
pub cancelled: bool,
/// Campaign start timestamp.
pub started_at: std::string::String,
/// Campaign finish timestamp.
pub finished_at: std::string::String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CoreExtractionItemStatus {
Skipped,
Extracted,
Failed,
Cancelled,
}
struct CoreExtractionItemOutcome {
index: usize,
status: CoreExtractionItemStatus,
message: std::string::String,
}
/// Executes one bounded canonical transaction to core extraction campaign.
pub async fn execute_core_extraction<S, O>(
store: &S,
request: &crate::CoreExtractionRequest,
observer: &O,
) -> ks_core::Result<crate::CoreExtractionSummary>
where
S: ks_store::CoreExtractionStore + Sync,
O: crate::CoreExtractionObserver,
{
let validation_result = request.validate();
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let filter_result = request.selection_filter();
let filter = match filter_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rows_result = store.list_raw_transactions_for_core_extraction(&filter).await;
let rows = match rows_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let started_at = chrono::Utc::now().to_rfc3339();
let total = match u64::try_from(rows.len()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::invalid_state(format!(
"core extraction selected count overflow: {error}"
)));
},
};
tracing::info!(
target: crate::TRACING_TARGET,
selected = total,
processor_version = crate::CORE_EXTRACTION_PROCESSOR_VERSION,
force_replay = request.force_replay,
"core extraction campaign started"
);
observer.on_progress(&crate::CoreExtractionProgressEvent::new(
crate::CoreExtractionProgressLevel::Info,
format!(
"core extraction started with processor version {} and {total} selected transactions",
crate::CORE_EXTRACTION_PROCESSOR_VERSION
),
0,
total,
));
let concurrency = match usize::try_from(request.max_concurrent_extractions) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::config(format!(
"core extraction concurrency conversion failed: {error}"
)));
},
};
let mut next_index = 0_usize;
let mut in_flight = futures_util::stream::FuturesUnordered::new();
let mut summary = crate::CoreExtractionSummary {
processor_version: crate::CORE_EXTRACTION_PROCESSOR_VERSION.to_string(),
selected: total,
started: 0,
completed: 0,
skipped: 0,
extracted: 0,
failed: 0,
cancelled_candidates: 0,
not_started: 0,
cancelled: false,
started_at,
finished_at: std::string::String::new(),
};
loop {
while in_flight.len() < concurrency && next_index < rows.len() && !observer.is_cancelled() {
let row = rows[next_index].clone();
let item_index = next_index;
let force_replay = request.force_replay;
in_flight.push(
async move {
return crate::core_extraction::process_one_core_extraction(
store,
row,
item_index,
force_replay,
)
.await;
}
.boxed(),
);
next_index += 1;
summary.started += 1;
}
if observer.is_cancelled() {
summary.cancelled = true;
}
let next_result = in_flight.next().await;
let outcome = match next_result {
std::option::Option::Some(value) => value,
std::option::Option::None => break,
};
match outcome.status {
CoreExtractionItemStatus::Skipped => summary.skipped += 1,
CoreExtractionItemStatus::Extracted => summary.extracted += 1,
CoreExtractionItemStatus::Failed => summary.failed += 1,
CoreExtractionItemStatus::Cancelled => summary.cancelled_candidates += 1,
}
if outcome.status != CoreExtractionItemStatus::Cancelled {
summary.completed += 1;
}
let level = match outcome.status {
CoreExtractionItemStatus::Skipped => crate::CoreExtractionProgressLevel::Debug,
CoreExtractionItemStatus::Extracted => crate::CoreExtractionProgressLevel::Info,
CoreExtractionItemStatus::Failed => crate::CoreExtractionProgressLevel::Error,
CoreExtractionItemStatus::Cancelled => crate::CoreExtractionProgressLevel::Warning,
};
observer.on_progress(&crate::CoreExtractionProgressEvent::new(
level,
format!("candidate {}: {}", outcome.index + 1, outcome.message),
summary.completed,
total,
));
}
let started_and_completed = summary.completed + summary.cancelled_candidates;
if summary.started > started_and_completed {
summary.cancelled_candidates += summary.started - started_and_completed;
}
if summary.selected > summary.started {
summary.not_started = summary.selected - summary.started;
}
summary.finished_at = chrono::Utc::now().to_rfc3339();
let final_level = if summary.failed > 0 || summary.cancelled {
crate::CoreExtractionProgressLevel::Warning
} else {
crate::CoreExtractionProgressLevel::Info
};
tracing::info!(
target: crate::TRACING_TARGET,
extracted = summary.extracted,
skipped = summary.skipped,
failed = summary.failed,
cancelled = summary.cancelled_candidates,
not_started = summary.not_started,
"core extraction campaign completed"
);
observer.on_progress(&crate::CoreExtractionProgressEvent::new(
final_level,
format!(
"core extraction completed: extracted={}, skipped={}, failed={}, cancelled={}, not_started={}",
summary.extracted,
summary.skipped,
summary.failed,
summary.cancelled_candidates,
summary.not_started
),
summary.completed,
total,
));
return std::result::Result::Ok(summary);
}
async fn process_one_core_extraction<S>(
store: &S,
row: ks_store::RawTransactionRow,
index: usize,
force_replay: bool,
) -> crate::core_extraction::CoreExtractionItemOutcome
where
S: ks_store::CoreExtractionStore + Sync,
{
let identity_result = crate::core_extraction::ledger_identity_from_raw(&row);
let identity = match identity_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return crate::core_extraction::CoreExtractionItemOutcome {
index,
status: CoreExtractionItemStatus::Failed,
message: error.to_string(),
};
},
};
if !force_replay {
let current_result = store.is_core_extraction_current(&identity).await;
match current_result {
std::result::Result::Ok(true) => {
return crate::core_extraction::CoreExtractionItemOutcome {
index,
status: CoreExtractionItemStatus::Skipped,
message: "already current for the same version and hash".to_string(),
};
},
std::result::Result::Ok(false) => {},
std::result::Result::Err(error) => {
return crate::core_extraction::CoreExtractionItemOutcome {
index,
status: CoreExtractionItemStatus::Failed,
message: error.to_string(),
};
},
}
}
tracing::debug!(
target: crate::TRACING_TARGET,
signature = row.signature.as_str(),
index,
force_replay,
"extracting canonical transaction to core"
);
let extraction_result = crate::extract_raw_transaction_to_core(&row);
let bundle = match extraction_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let failure_result = ks_store::CoreExtractionFailure::new(
row.id,
identity,
"canonical_to_core_extraction_failed",
error.to_string(),
);
if let std::result::Result::Ok(failure) = failure_result {
let _mark_result = store.mark_core_extraction_failed(&failure).await;
}
return crate::core_extraction::CoreExtractionItemOutcome {
index,
status: CoreExtractionItemStatus::Failed,
message: error.to_string(),
};
},
};
let persist_result = store.persist_core_extraction(&bundle, force_replay).await;
return match persist_result {
std::result::Result::Ok(_) => crate::core_extraction::CoreExtractionItemOutcome {
index,
status: CoreExtractionItemStatus::Extracted,
message: "extracted and committed".to_string(),
},
std::result::Result::Err(error) => {
let failure_result = ks_store::CoreExtractionFailure::new(
row.id,
bundle.ledger_identity,
"canonical_to_core_persist_failed",
error.to_string(),
);
if let std::result::Result::Ok(failure) = failure_result {
let _mark_result = store.mark_core_extraction_failed(&failure).await;
}
crate::core_extraction::CoreExtractionItemOutcome {
index,
status: CoreExtractionItemStatus::Failed,
message: error.to_string(),
}
},
};
}
/// Extracts one canonical raw transaction into a complete normalized core bundle.
pub fn extract_raw_transaction_to_core(
row: &ks_store::RawTransactionRow,
) -> ks_core::Result<ks_store::CoreExtractionBundle> {
let canonical_json = match &row.canonical_json {
std::option::Option::Some(value) => value.clone(),
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::invalid_state(
"canonical JSON is unavailable for core extraction",
));
},
};
let input_hash = match &row.canonical_json_hash {
std::option::Option::Some(value) if !value.trim().is_empty() => value.clone(),
_ => {
return std::result::Result::Err(ks_core::Error::invalid_state(
"canonical JSON hash is unavailable for core extraction",
));
},
};
let expected_format_version =
match i32::try_from(ks_lib::MD_CANONICAL_TRANSACTION_FORMAT_VERSION) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::invalid_state(format!(
"canonical format version conversion failed: {error}"
)));
},
};
if row.canonical_format_version != expected_format_version {
return std::result::Result::Err(ks_core::Error::invalid_state(format!(
"unsupported canonical format version in raw store: {}",
row.canonical_format_version
)));
}
let parse_result = serde_json::from_value::<ks_lib::MdCanonicalTransaction>(canonical_json);
let transaction = match parse_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::json(format!(
"cannot parse canonical transaction for core extraction: {error}"
)));
},
};
let validation_result = transaction.validate();
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
if transaction.primary_signature != row.signature {
return std::result::Result::Err(ks_core::Error::invalid_state(
"canonical transaction signature does not match raw row signature",
));
}
let row_slot_result = u64::try_from(row.slot);
let row_slot = match row_slot_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::invalid_state(format!(
"raw transaction slot is negative: {error}"
)));
},
};
if transaction.slot != row_slot {
return std::result::Result::Err(ks_core::Error::invalid_state(
"canonical transaction slot does not match raw row slot",
));
}
let computed_hash_result = transaction.canonical_json_hash();
let computed_hash = match computed_hash_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if computed_hash != input_hash {
return std::result::Result::Err(ks_core::Error::invalid_state(
"canonical transaction hash does not match the stored hash",
));
}
let ledger_identity_result = crate::core_extraction::ledger_identity_from_raw(row);
let ledger_identity = match ledger_identity_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::option::Option::Some(metadata) = &transaction.metadata {
return crate::core_extraction::build_bundle_with_metadata(
row,
&transaction,
metadata,
ledger_identity,
);
}
let core_transaction_result = ks_store::CoreTransactionInsert::new(
transaction.primary_signature.clone(),
transaction.slot,
false,
std::option::Option::None,
);
let core_transaction = match core_transaction_result {
std::result::Result::Ok(value) => value.with_raw_transaction_id(row.id),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let account_keys_result = crate::core_extraction::extract_account_keys(&transaction);
let account_keys = match account_keys_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let instruction_result = crate::core_extraction::extract_instructions(&transaction);
let (instructions, inner_instructions) = match instruction_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ks_store::CoreExtractionBundle {
raw_transaction_id: row.id,
ledger_identity,
transaction: core_transaction,
account_keys,
instructions,
inner_instructions,
logs: std::vec::Vec::new(),
balance_changes: std::vec::Vec::new(),
});
}
fn build_bundle_with_metadata(
row: &ks_store::RawTransactionRow,
transaction: &ks_lib::MdCanonicalTransaction,
metadata: &ks_lib::MdCanonicalTransactionMetadata,
ledger_identity: ks_store::ProcessingLedgerIdentity,
) -> ks_core::Result<ks_store::CoreExtractionBundle> {
let failed = metadata.status == ks_lib::MdCanonicalTransactionStatus::Failed;
let core_transaction_result = ks_store::CoreTransactionInsert::new(
transaction.primary_signature.clone(),
transaction.slot,
failed,
metadata.error.clone(),
);
let core_transaction = match core_transaction_result {
std::result::Result::Ok(value) => value.with_raw_transaction_id(row.id),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let account_keys_result = crate::core_extraction::extract_account_keys(transaction);
let account_keys = match account_keys_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let instruction_result = crate::core_extraction::extract_instructions(transaction);
let (instructions, inner_instructions) = match instruction_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let logs_result = crate::core_extraction::extract_logs(transaction, metadata);
let logs = match logs_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let balances_result = crate::core_extraction::extract_balance_changes(transaction, metadata);
let balance_changes = match balances_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ks_store::CoreExtractionBundle {
raw_transaction_id: row.id,
ledger_identity,
transaction: core_transaction,
account_keys,
instructions,
inner_instructions,
logs,
balance_changes,
});
}
fn ledger_identity_from_raw(
row: &ks_store::RawTransactionRow,
) -> ks_core::Result<ks_store::ProcessingLedgerIdentity> {
let input_hash = match &row.canonical_json_hash {
std::option::Option::Some(value) => value.clone(),
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::invalid_state(
"canonical JSON hash is required for the processing ledger",
));
},
};
return ks_store::ProcessingLedgerIdentity::new(
crate::CORE_EXTRACTION_STAGE,
crate::CORE_EXTRACTION_PROCESSOR_NAME,
crate::CORE_EXTRACTION_PROCESSOR_VERSION,
row.signature.clone(),
input_hash,
);
}
fn resolved_account_keys(
transaction: &ks_lib::MdCanonicalTransaction,
) -> std::vec::Vec<std::string::String> {
let mut keys = std::vec::Vec::with_capacity(transaction.message.resolved_account_count());
keys.extend(transaction.message.static_account_keys.iter().cloned());
keys.extend(transaction.message.loaded_addresses.writable.iter().cloned());
keys.extend(transaction.message.loaded_addresses.readonly.iter().cloned());
return keys;
}
fn extract_account_keys(
transaction: &ks_lib::MdCanonicalTransaction,
) -> ks_core::Result<std::vec::Vec<ks_store::CoreAccountKeyInsert>> {
let static_count = transaction.message.static_account_keys.len();
let required = usize::from(transaction.message.header.num_required_signatures);
let readonly_signed = usize::from(transaction.message.header.num_readonly_signed_accounts);
let readonly_unsigned = usize::from(transaction.message.header.num_readonly_unsigned_accounts);
let writable_signed_count = required - readonly_signed;
let writable_unsigned_end = static_count - readonly_unsigned;
let mut output = std::vec::Vec::with_capacity(transaction.message.resolved_account_count());
for (index, key) in transaction.message.static_account_keys.iter().enumerate() {
let account_index_result = u32::try_from(index);
let account_index = match account_index_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::invalid_state(format!(
"static account index overflow: {error}"
)));
},
};
let signer = index < required;
let writable = if signer {
index < writable_signed_count
} else {
index < writable_unsigned_end
};
let insert_result = ks_store::CoreAccountKeyInsert::new(
transaction.primary_signature.clone(),
transaction.slot,
account_index,
key.clone(),
ks_store::CoreAccountKeySource::Static,
writable,
signer,
std::option::Option::None,
);
let insert = match insert_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
output.push(insert);
}
for key in &transaction.message.loaded_addresses.writable {
let account_index_result = u32::try_from(output.len());
let account_index = match account_index_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::invalid_state(format!(
"loaded writable account index overflow: {error}"
)));
},
};
let insert_result = ks_store::CoreAccountKeyInsert::new(
transaction.primary_signature.clone(),
transaction.slot,
account_index,
key.clone(),
ks_store::CoreAccountKeySource::LoadedWritable,
true,
false,
std::option::Option::None,
);
let insert = match insert_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
output.push(insert);
}
for key in &transaction.message.loaded_addresses.readonly {
let account_index_result = u32::try_from(output.len());
let account_index = match account_index_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::invalid_state(format!(
"loaded readonly account index overflow: {error}"
)));
},
};
let insert_result = ks_store::CoreAccountKeyInsert::new(
transaction.primary_signature.clone(),
transaction.slot,
account_index,
key.clone(),
ks_store::CoreAccountKeySource::LoadedReadonly,
false,
false,
std::option::Option::None,
);
let insert = match insert_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
output.push(insert);
}
return std::result::Result::Ok(output);
}
fn extract_instructions(
transaction: &ks_lib::MdCanonicalTransaction,
) -> ks_core::Result<(
std::vec::Vec<ks_store::CoreInstructionInsert>,
std::vec::Vec<ks_store::CoreInnerInstructionInsert>,
)> {
let keys = crate::core_extraction::resolved_account_keys(transaction);
let mut instructions = std::vec::Vec::new();
for (index, instruction) in transaction.message.instructions.iter().enumerate() {
let path = index.to_string();
let resolved_result = crate::core_extraction::resolve_instruction(&keys, instruction);
let (program_id, accounts_json, payload_json, payload_hash) = match resolved_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let insert_result = ks_store::CoreInstructionInsert::new(
transaction.primary_signature.clone(),
transaction.slot,
path,
program_id,
accounts_json,
payload_json,
);
let insert = match insert_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let hash_result = insert.with_payload_json_hash(payload_hash);
let hashed = match hash_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
instructions.push(hashed);
}
let mut inner_instructions = std::vec::Vec::new();
if let std::option::Option::Some(metadata) = &transaction.metadata {
for group in &metadata.inner_instructions {
let parent_path = group.parent_instruction_index.to_string();
for (inner_index, instruction) in group.instructions.iter().enumerate() {
let path = format!("{parent_path}/{inner_index}");
let resolved_result =
crate::core_extraction::resolve_instruction(&keys, instruction);
let (program_id, accounts_json, payload_json, payload_hash) = match resolved_result
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let insert_result = ks_store::CoreInnerInstructionInsert::new(
transaction.primary_signature.clone(),
transaction.slot,
parent_path.clone(),
path,
program_id,
accounts_json,
payload_json,
);
let insert = match insert_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let hash_result = insert.with_payload_json_hash(payload_hash);
let hashed = match hash_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
inner_instructions.push(hashed);
}
}
}
return std::result::Result::Ok((instructions, inner_instructions));
}
fn resolve_instruction(
keys: &[std::string::String],
instruction: &ks_lib::MdCanonicalCompiledInstruction,
) -> ks_core::Result<(std::string::String, serde_json::Value, serde_json::Value, std::string::String)>
{
let program_id = match keys.get(usize::from(instruction.program_id_index)) {
std::option::Option::Some(value) => value.clone(),
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::invalid_state(
"instruction program id index is outside resolved accounts",
));
},
};
let mut accounts = std::vec::Vec::with_capacity(instruction.account_indexes.len());
for account_index in &instruction.account_indexes {
let account_key = match keys.get(usize::from(*account_index)) {
std::option::Option::Some(value) => value.clone(),
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::invalid_state(
"instruction account index is outside resolved accounts",
));
},
};
accounts.push(serde_json::json!({
"accountIndex": account_index,
"accountKey": account_key,
}));
}
let accounts_json = serde_json::Value::Array(accounts);
let payload_json = serde_json::json!({
"programIdIndex": instruction.program_id_index,
"dataBase64": instruction.data_base64,
"stackHeight": instruction.stack_height,
});
let payload_hash_result = crate::core_extraction::hash_json(&payload_json);
let payload_hash = match payload_hash_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok((program_id, accounts_json, payload_json, payload_hash));
}
fn extract_logs(
transaction: &ks_lib::MdCanonicalTransaction,
metadata: &ks_lib::MdCanonicalTransactionMetadata,
) -> ks_core::Result<std::vec::Vec<ks_store::CoreLogInsert>> {
let mut output = std::vec::Vec::with_capacity(metadata.log_messages.len());
let links = crate::core_extraction::resolve_log_links(transaction, metadata);
for (index, text) in metadata.log_messages.iter().enumerate() {
let log_index_result = u32::try_from(index);
let log_index = match log_index_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::invalid_state(format!(
"log index overflow: {error}"
)));
},
};
let link = match links.get(index) {
std::option::Option::Some(value) => value.clone(),
std::option::Option::None => (std::option::Option::None, std::option::Option::None),
};
let insert_result = ks_store::CoreLogInsert::new(
transaction.primary_signature.clone(),
transaction.slot,
log_index,
link.1,
link.0,
text.clone(),
);
let insert = match insert_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let hash = crate::core_extraction::hash_bytes(text.as_bytes());
let hash_result = insert.with_log_text_hash(hash);
let hashed = match hash_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
output.push(hashed);
}
return std::result::Result::Ok(output);
}
fn resolve_log_links(
transaction: &ks_lib::MdCanonicalTransaction,
metadata: &ks_lib::MdCanonicalTransactionMetadata,
) -> std::vec::Vec<(
std::option::Option<std::string::String>,
std::option::Option<std::string::String>,
)> {
let keys = crate::core_extraction::resolved_account_keys(transaction);
let mut top_level = std::vec::Vec::<(std::string::String, std::string::String)>::new();
for (index, instruction) in transaction.message.instructions.iter().enumerate() {
if let std::option::Option::Some(program_id) =
keys.get(usize::from(instruction.program_id_index))
{
top_level.push((program_id.clone(), index.to_string()));
}
}
let mut inner_by_parent = std::collections::BTreeMap::<
std::string::String,
std::vec::Vec<(std::string::String, std::string::String)>,
>::new();
for group in &metadata.inner_instructions {
let parent = group.parent_instruction_index.to_string();
let entries = inner_by_parent.entry(parent.clone()).or_default();
for (index, instruction) in group.instructions.iter().enumerate() {
if let std::option::Option::Some(program_id) =
keys.get(usize::from(instruction.program_id_index))
{
entries.push((program_id.clone(), format!("{parent}/{index}")));
}
}
}
let mut top_cursor = 0_usize;
let mut inner_cursors = std::collections::BTreeMap::<std::string::String, usize>::new();
let mut stack =
std::vec::Vec::<(std::string::String, std::option::Option<std::string::String>)>::new();
let mut output = std::vec::Vec::with_capacity(metadata.log_messages.len());
for text in &metadata.log_messages {
let program_line = crate::core_extraction::parse_program_log(text.as_str());
match program_line {
std::option::Option::Some((
program_id,
crate::core_extraction::ProgramLogKind::Invoke(depth),
)) => {
let path = if depth == 1 {
let mut resolved = std::option::Option::None;
while top_cursor < top_level.len() {
let candidate = &top_level[top_cursor];
top_cursor += 1;
if candidate.0 == program_id {
resolved = std::option::Option::Some(candidate.1.clone());
break;
}
}
resolved
} else {
let parent_path = stack.first().and_then(|entry| return entry.1.clone());
match parent_path {
std::option::Option::Some(parent) => {
let cursor = inner_cursors.entry(parent.clone()).or_insert(0);
let mut resolved = std::option::Option::None;
if let std::option::Option::Some(entries) =
inner_by_parent.get(parent.as_str())
{
while *cursor < entries.len() {
let candidate = &entries[*cursor];
*cursor += 1;
if candidate.0 == program_id {
resolved = std::option::Option::Some(candidate.1.clone());
break;
}
}
}
resolved
},
std::option::Option::None => std::option::Option::None,
}
};
stack.push((program_id.clone(), path.clone()));
output.push((std::option::Option::Some(program_id), path));
},
std::option::Option::Some((
program_id,
crate::core_extraction::ProgramLogKind::Terminal,
)) => {
let path = stack.last().and_then(|entry| {
return if entry.0 == program_id {
entry.1.clone()
} else {
std::option::Option::None
};
});
output.push((std::option::Option::Some(program_id.clone()), path));
if stack.last().map(|entry| return entry.0.as_str())
== std::option::Option::Some(program_id.as_str())
{
stack.pop();
}
},
std::option::Option::None => {
let current = stack.last().cloned();
match current {
std::option::Option::Some((program_id, path)) => {
output.push((std::option::Option::Some(program_id), path))
},
std::option::Option::None => {
output.push((std::option::Option::None, std::option::Option::None))
},
}
},
}
}
return output;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ProgramLogKind {
Invoke(u32),
Terminal,
}
fn parse_program_log(text: &str) -> std::option::Option<(std::string::String, ProgramLogKind)> {
let remainder = match text.strip_prefix("Program ") {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
let split = remainder.split_once(' ');
let (program_id, suffix) = match split {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
if let std::option::Option::Some(depth_text) =
suffix.strip_prefix("invoke [").and_then(|value| return value.strip_suffix(']'))
{
let parse_result = depth_text.parse::<u32>();
if let std::result::Result::Ok(depth) = parse_result {
return std::option::Option::Some((
program_id.to_string(),
ProgramLogKind::Invoke(depth),
));
}
}
if suffix == "success" || suffix.starts_with("failed:") {
return std::option::Option::Some((program_id.to_string(), ProgramLogKind::Terminal));
}
return std::option::Option::None;
}
fn extract_balance_changes(
transaction: &ks_lib::MdCanonicalTransaction,
metadata: &ks_lib::MdCanonicalTransactionMetadata,
) -> ks_core::Result<std::vec::Vec<ks_store::CoreBalanceChangeInsert>> {
let keys = crate::core_extraction::resolved_account_keys(transaction);
let mut output = std::vec::Vec::new();
for index in 0..metadata.pre_balances.len() {
let pre = metadata.pre_balances[index];
let post = metadata.post_balances[index];
let delta = i128::from(post) - i128::from(pre);
let account_index_result = u32::try_from(index);
let account_index = match account_index_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::invalid_state(format!(
"native balance account index overflow: {error}"
)));
},
};
let balance_index_result = u32::try_from(output.len());
let balance_index = match balance_index_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::invalid_state(format!(
"balance change index overflow: {error}"
)));
},
};
let account_key = keys.get(index).cloned();
let insert_result = ks_store::CoreBalanceChangeInsert::new(
transaction.primary_signature.clone(),
transaction.slot,
balance_index,
ks_store::CoreBalanceChangeKind::NativeLamports,
std::option::Option::Some(account_index),
account_key,
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(serde_json::Value::String(pre.to_string())),
std::option::Option::Some(serde_json::Value::String(post.to_string())),
std::option::Option::Some(serde_json::Value::String(delta.to_string())),
);
let insert = match insert_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
output.push(insert);
}
let mut token_keys = std::collections::BTreeSet::<(
u16,
std::string::String,
std::option::Option<std::string::String>,
)>::new();
for balance in &metadata.pre_token_balances {
token_keys.insert((
balance.account_index,
balance.mint.clone(),
balance.program_id.clone(),
));
}
for balance in &metadata.post_token_balances {
token_keys.insert((
balance.account_index,
balance.mint.clone(),
balance.program_id.clone(),
));
}
for key in token_keys {
let pre = metadata.pre_token_balances.iter().find(|balance| {
return balance.account_index == key.0
&& balance.mint == key.1
&& balance.program_id == key.2;
});
let post = metadata.post_token_balances.iter().find(|balance| {
return balance.account_index == key.0
&& balance.mint == key.1
&& balance.program_id == key.2;
});
let decimals = match (pre, post) {
(std::option::Option::Some(left), std::option::Option::Some(right)) => {
if left.decimals != right.decimals {
return std::result::Result::Err(ks_core::Error::invalid_state(
"token balance decimals changed between pre and post metadata",
));
}
left.decimals
},
(std::option::Option::Some(value), std::option::Option::None) => value.decimals,
(std::option::Option::None, std::option::Option::Some(value)) => value.decimals,
(std::option::Option::None, std::option::Option::None) => continue,
};
let pre_amount = match pre {
std::option::Option::Some(balance) => balance.amount.as_str(),
std::option::Option::None => "0",
};
let post_amount = match post {
std::option::Option::Some(balance) => balance.amount.as_str(),
std::option::Option::None => "0",
};
let delta_amount =
crate::core_extraction::subtract_unsigned_decimal(post_amount, pre_amount);
let delta_decimal =
crate::core_extraction::signed_fixed_decimal(delta_amount.as_str(), decimals);
let balance_index_result = u32::try_from(output.len());
let balance_index = match balance_index_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::invalid_state(format!(
"token balance change index overflow: {error}"
)));
},
};
let account_key = keys.get(usize::from(key.0)).cloned();
let owner = post
.and_then(|balance| return balance.owner.clone())
.or_else(|| return pre.and_then(|balance| return balance.owner.clone()));
let pre_json = match pre {
std::option::Option::Some(balance) => serde_json::json!({
"amount": balance.amount,
"decimals": balance.decimals,
"decimalAmount": balance.decimal_amount,
"programId": balance.program_id,
}),
std::option::Option::None => serde_json::json!({
"amount": "0",
"decimals": decimals,
"decimalAmount": crate::core_extraction::signed_fixed_decimal("0", decimals),
"programId": key.2.clone(),
}),
};
let post_json = match post {
std::option::Option::Some(balance) => serde_json::json!({
"amount": balance.amount,
"decimals": balance.decimals,
"decimalAmount": balance.decimal_amount,
"programId": balance.program_id,
}),
std::option::Option::None => serde_json::json!({
"amount": "0",
"decimals": decimals,
"decimalAmount": crate::core_extraction::signed_fixed_decimal("0", decimals),
"programId": key.2.clone(),
}),
};
let delta_json = serde_json::json!({
"amount": delta_amount,
"decimals": decimals,
"decimalAmount": delta_decimal,
"programId": key.2.clone(),
});
let insert_result = ks_store::CoreBalanceChangeInsert::new(
transaction.primary_signature.clone(),
transaction.slot,
balance_index,
ks_store::CoreBalanceChangeKind::TokenAmount,
std::option::Option::Some(u32::from(key.0)),
account_key,
std::option::Option::Some(key.1),
owner,
std::option::Option::Some(pre_json),
std::option::Option::Some(post_json),
std::option::Option::Some(delta_json),
);
let insert = match insert_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
output.push(insert);
}
return std::result::Result::Ok(output);
}
fn subtract_unsigned_decimal(left: &str, right: &str) -> std::string::String {
let ordering = crate::core_extraction::compare_unsigned_decimal(left, right);
return match ordering {
std::cmp::Ordering::Equal => "0".to_string(),
std::cmp::Ordering::Greater => crate::core_extraction::subtract_magnitude(left, right),
std::cmp::Ordering::Less => {
format!("-{}", crate::core_extraction::subtract_magnitude(right, left))
},
};
}
fn compare_unsigned_decimal(left: &str, right: &str) -> std::cmp::Ordering {
let left_trimmed = left.trim_start_matches('0');
let right_trimmed = right.trim_start_matches('0');
let left_value = if left_trimmed.is_empty() { "0" } else { left_trimmed };
let right_value = if right_trimmed.is_empty() { "0" } else { right_trimmed };
return match left_value.len().cmp(&right_value.len()) {
std::cmp::Ordering::Equal => left_value.cmp(right_value),
ordering => ordering,
};
}
fn subtract_magnitude(larger: &str, smaller: &str) -> std::string::String {
let mut result = std::vec::Vec::<u8>::new();
let larger_bytes = larger.as_bytes();
let smaller_bytes = smaller.as_bytes();
let mut borrow = 0_i16;
for offset in 0..larger_bytes.len() {
let larger_index = larger_bytes.len() - 1 - offset;
let mut digit = i16::from(larger_bytes[larger_index] - b'0') - borrow;
let smaller_digit = if offset < smaller_bytes.len() {
i16::from(smaller_bytes[smaller_bytes.len() - 1 - offset] - b'0')
} else {
0
};
if digit < smaller_digit {
digit += 10;
borrow = 1;
} else {
borrow = 0;
}
let conversion_result = u8::try_from(digit - smaller_digit);
let value = match conversion_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => 0,
};
result.push(value + b'0');
}
while result.len() > 1 && result.last() == std::option::Option::Some(&b'0') {
result.pop();
}
result.reverse();
let text_result = std::string::String::from_utf8(result);
return match text_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => "0".to_string(),
};
}
fn signed_fixed_decimal(amount: &str, decimals: u8) -> std::string::String {
let negative = amount.starts_with('-');
let magnitude = match amount.strip_prefix('-') {
std::option::Option::Some(value) => value,
std::option::Option::None => amount,
};
let scale = usize::from(decimals);
let unsigned = if scale == 0 {
magnitude.to_string()
} else if magnitude.len() > scale {
let split = magnitude.len() - scale;
format!("{}.{}", &magnitude[..split], &magnitude[split..])
} else {
format!("0.{}{}", "0".repeat(scale - magnitude.len()), magnitude)
};
if negative && magnitude != "0" {
return format!("-{unsigned}");
}
return unsigned;
}
fn hash_json(value: &serde_json::Value) -> ks_core::Result<std::string::String> {
let serialization_result = serde_json::to_vec(value);
let bytes = match serialization_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::json(format!(
"cannot serialize core payload for hashing: {error}"
)));
},
};
return std::result::Result::Ok(crate::core_extraction::hash_bytes(bytes.as_slice()));
}
fn hash_bytes(bytes: &[u8]) -> std::string::String {
let digest = sha2::Sha256::digest(bytes);
let mut output = std::string::String::with_capacity(64);
for byte in digest {
output.push_str(format!("{byte:02x}").as_str());
}
return output;
}
#[cfg(test)]
mod tests {
struct TestCoreExtractionStore {
rows: std::vec::Vec<ks_store::RawTransactionRow>,
current: std::sync::atomic::AtomicBool,
persist_calls: std::sync::atomic::AtomicU64,
failure_calls: std::sync::atomic::AtomicU64,
}
impl TestCoreExtractionStore {
fn new(rows: std::vec::Vec<ks_store::RawTransactionRow>, current: bool) -> Self {
return Self {
rows,
current: std::sync::atomic::AtomicBool::new(current),
persist_calls: std::sync::atomic::AtomicU64::new(0),
failure_calls: std::sync::atomic::AtomicU64::new(0),
};
}
}
#[async_trait::async_trait]
impl ks_store::CoreExtractionStore for TestCoreExtractionStore {
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn list_raw_transactions_for_core_extraction(
&self,
_filter: &ks_store::CoreExtractionSelectionFilter,
) -> ks_core::Result<std::vec::Vec<ks_store::RawTransactionRow>> {
return std::result::Result::Ok(self.rows.clone());
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn is_core_extraction_current(
&self,
_identity: &ks_store::ProcessingLedgerIdentity,
) -> ks_core::Result<bool> {
return std::result::Result::Ok(
self.current.load(std::sync::atomic::Ordering::Acquire),
);
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn persist_core_extraction(
&self,
_bundle: &ks_store::CoreExtractionBundle,
_force_replay: bool,
) -> ks_core::Result<ks_store::InsertOutcome> {
self.persist_calls.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
return std::result::Result::Ok(ks_store::InsertOutcome::new(1, 0, 0));
}
#[expect(
clippy::implicit_return,
reason = "async_trait expansion triggers implicit_return on generated async trait methods."
)]
async fn mark_core_extraction_failed(
&self,
_failure: &ks_store::CoreExtractionFailure,
) -> ks_core::Result<ks_store::InsertOutcome> {
self.failure_calls.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
return std::result::Result::Ok(ks_store::InsertOutcome::new(0, 1, 0));
}
}
struct TestCoreExtractionObserver {
cancelled: bool,
events: std::sync::Mutex<std::vec::Vec<crate::CoreExtractionProgressEvent>>,
}
impl TestCoreExtractionObserver {
fn new(cancelled: bool) -> Self {
return Self {
cancelled,
events: std::sync::Mutex::new(std::vec::Vec::new()),
};
}
}
impl crate::CoreExtractionObserver for TestCoreExtractionObserver {
fn on_progress(&self, event: &crate::CoreExtractionProgressEvent) {
let lock_result = self.events.lock();
if let std::result::Result::Ok(mut events) = lock_result {
events.push(event.clone());
}
}
fn is_cancelled(&self) -> bool {
return self.cancelled;
}
}
fn sample_transaction() -> ks_lib::MdCanonicalTransaction {
let token_pre = match ks_lib::MdCanonicalTokenBalance::new(
1,
"11111111111111111111111111111111",
std::option::Option::Some("SysvarC1ock11111111111111111111111111111111".to_string()),
std::option::Option::Some("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA".to_string()),
"100",
2,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("sample token pre balance failed: {error}"),
};
let token_post = match ks_lib::MdCanonicalTokenBalance::new(
1,
"11111111111111111111111111111111",
std::option::Option::Some("SysvarC1ock11111111111111111111111111111111".to_string()),
std::option::Option::Some("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA".to_string()),
"125",
2,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("sample token post balance failed: {error}"),
};
return ks_lib::MdCanonicalTransaction {
format_version: ks_lib::MD_CANONICAL_TRANSACTION_FORMAT_VERSION,
primary_signature: "2Ana1pUpv2ZbMVkwF5FXapYeBEjdxDatLn7nvJkhgTSXbs59SyZSx866bXirPgj8QQVB57uxHJBG1YFvkRbFj4T".to_string(),
slot: 42,
block_time: std::option::Option::Some(1_700_000_000),
version: ks_lib::MdCanonicalTransactionVersion::Legacy,
signatures: std::vec!["2Ana1pUpv2ZbMVkwF5FXapYeBEjdxDatLn7nvJkhgTSXbs59SyZSx866bXirPgj8QQVB57uxHJBG1YFvkRbFj4T".to_string()],
message: ks_lib::MdCanonicalTransactionMessage {
header: ks_lib::MdCanonicalMessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 1,
},
static_account_keys: std::vec![
"11111111111111111111111111111111".to_string(),
"SysvarC1ock11111111111111111111111111111111".to_string(),
],
recent_blockhash: "11111111111111111111111111111111".to_string(),
instructions: std::vec![ks_lib::MdCanonicalCompiledInstruction {
program_id_index: 0,
account_indexes: std::vec![1],
data_base64: "AQ==".to_string(),
stack_height: std::option::Option::Some(1),
}],
address_table_lookups: std::vec::Vec::new(),
loaded_addresses: ks_lib::MdCanonicalLoadedAddresses::default(),
},
metadata: std::option::Option::Some(ks_lib::MdCanonicalTransactionMetadata {
status: ks_lib::MdCanonicalTransactionStatus::Success,
error: std::option::Option::None,
fee: 5000,
pre_balances: std::vec![10_000, 1],
post_balances: std::vec![5_000, 1],
inner_instructions: std::vec![ks_lib::MdCanonicalInnerInstructionGroup {
parent_instruction_index: 0,
instructions: std::vec![ks_lib::MdCanonicalCompiledInstruction {
program_id_index: 0,
account_indexes: std::vec![1],
data_base64: "Ag==".to_string(),
stack_height: std::option::Option::Some(2),
}],
}],
log_messages: std::vec![
"Program 11111111111111111111111111111111 invoke [1]".to_string(),
"Program 11111111111111111111111111111111 invoke [2]".to_string(),
"Program 11111111111111111111111111111111 success".to_string(),
"Program 11111111111111111111111111111111 success".to_string(),
],
pre_token_balances: std::vec![token_pre],
post_token_balances: std::vec![token_post],
rewards: std::vec::Vec::new(),
return_data: std::option::Option::None,
compute_units_consumed: std::option::Option::Some(100),
cost_units: std::option::Option::Some(120),
}),
};
}
fn raw_row(transaction: &ks_lib::MdCanonicalTransaction) -> ks_store::RawTransactionRow {
let json = match transaction.to_canonical_json() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("sample canonical JSON failed: {error}"),
};
let hash = match transaction.canonical_json_hash() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("sample canonical hash failed: {error}"),
};
return ks_store::RawTransactionRow {
id: 1,
signature: transaction.primary_signature.clone(),
slot: 42,
canonical_json: std::option::Option::Some(json),
canonical_json_hash: std::option::Option::Some(hash),
canonical_format_version: 1,
retention_state: ks_store::RawPayloadRetentionState::Full,
processing_state: ks_store::RawPayloadProcessingState::Received,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
}
#[test]
fn extraction_resolves_accounts_instructions_logs_and_balances() {
let transaction = crate::core_extraction::tests::sample_transaction();
let row = crate::core_extraction::tests::raw_row(&transaction);
let result = crate::extract_raw_transaction_to_core(&row);
let bundle = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("core extraction failed: {error}"),
};
assert_eq!(bundle.account_keys.len(), 2);
assert_eq!(bundle.instructions.len(), 1);
assert_eq!(bundle.inner_instructions.len(), 1);
assert_eq!(bundle.logs.len(), 4);
assert_eq!(bundle.balance_changes.len(), 3);
assert_eq!(bundle.inner_instructions[0].instruction_path, "0/0");
assert_eq!(bundle.logs[1].instruction_path.as_deref(), std::option::Option::Some("0/0"));
}
#[test]
fn extraction_rejects_hash_mismatch() {
let transaction = crate::core_extraction::tests::sample_transaction();
let mut row = crate::core_extraction::tests::raw_row(&transaction);
row.canonical_json_hash = std::option::Option::Some("bad".to_string());
let result = crate::extract_raw_transaction_to_core(&row);
assert!(result.is_err());
}
#[test]
fn decimal_subtraction_handles_positive_negative_and_zero() {
assert_eq!(crate::core_extraction::subtract_unsigned_decimal("125", "100"), "25");
assert_eq!(crate::core_extraction::subtract_unsigned_decimal("100", "125"), "-25");
assert_eq!(crate::core_extraction::subtract_unsigned_decimal("000", "0"), "0");
assert_eq!(crate::core_extraction::signed_fixed_decimal("-25", 2), "-0.25");
}
#[test]
fn static_account_flags_follow_message_header() {
let transaction = crate::core_extraction::tests::sample_transaction();
let result = crate::core_extraction::extract_account_keys(&transaction);
let keys = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("account extraction failed: {error}"),
};
assert!(keys[0].signer);
assert!(keys[0].writable);
assert!(!keys[1].signer);
assert!(!keys[1].writable);
}
#[test]
fn version_zero_loaded_addresses_extend_resolved_account_space() {
let mut transaction = crate::core_extraction::tests::sample_transaction();
transaction.version = ks_lib::MdCanonicalTransactionVersion::Number(0);
transaction.message.loaded_addresses.writable =
std::vec!["Vote111111111111111111111111111111111111111".to_string(),];
transaction.message.loaded_addresses.readonly =
std::vec!["Stake11111111111111111111111111111111111111".to_string(),];
transaction.message.instructions[0].program_id_index = 2;
transaction.message.instructions[0].account_indexes = std::vec![3];
let result = crate::core_extraction::extract_account_keys(&transaction);
let keys = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("loaded account extraction failed: {error}"),
};
assert_eq!(keys.len(), 4);
assert_eq!(keys[2].source, ks_store::CoreAccountKeySource::LoadedWritable);
assert!(keys[2].writable);
assert!(!keys[2].signer);
assert_eq!(keys[3].source, ks_store::CoreAccountKeySource::LoadedReadonly);
assert!(!keys[3].writable);
let instruction_result = crate::core_extraction::extract_instructions(&transaction);
let (instructions, _) = match instruction_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("loaded instruction extraction failed: {error}")
},
};
assert_eq!(instructions[0].program_id, keys[2].account_key);
}
#[test]
fn failed_transaction_preserves_error_json() {
let mut transaction = crate::core_extraction::tests::sample_transaction();
let metadata = match transaction.metadata.as_mut() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("sample metadata is required"),
};
metadata.status = ks_lib::MdCanonicalTransactionStatus::Failed;
metadata.error =
std::option::Option::Some(serde_json::json!({"InstructionError": [0, "Custom"]}));
let expected_error = metadata.error.clone();
let row = crate::core_extraction::tests::raw_row(&transaction);
let result = crate::extract_raw_transaction_to_core(&row);
let bundle = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("failed transaction extraction failed: {error}")
},
};
assert!(bundle.transaction.failed);
assert_eq!(bundle.transaction.err_json, expected_error);
}
#[test]
fn transaction_without_metadata_keeps_structural_core_rows() {
let mut transaction = crate::core_extraction::tests::sample_transaction();
transaction.metadata = std::option::Option::None;
let row = crate::core_extraction::tests::raw_row(&transaction);
let result = crate::extract_raw_transaction_to_core(&row);
let bundle = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("metadata-free extraction failed: {error}"),
};
assert!(!bundle.transaction.failed);
assert_eq!(bundle.account_keys.len(), 2);
assert_eq!(bundle.instructions.len(), 1);
assert!(bundle.logs.is_empty());
assert!(bundle.balance_changes.is_empty());
}
#[test]
fn invalid_instruction_indexes_are_rejected() {
let transaction = crate::core_extraction::tests::sample_transaction();
let mut row = crate::core_extraction::tests::raw_row(&transaction);
let canonical_json = match row.canonical_json.as_mut() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("sample canonical JSON is required"),
};
canonical_json["message"]["instructions"][0]["programIdIndex"] = serde_json::json!(99);
let result = crate::extract_raw_transaction_to_core(&row);
assert!(result.is_err());
let mut account_row = crate::core_extraction::tests::raw_row(&transaction);
let account_json = match account_row.canonical_json.as_mut() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("sample canonical JSON is required"),
};
account_json["message"]["instructions"][0]["accountIndexes"] = serde_json::json!([99]);
let account_result = crate::extract_raw_transaction_to_core(&account_row);
assert!(account_result.is_err());
}
#[test]
fn token_account_created_or_closed_produces_deterministic_changes() {
let mut created = crate::core_extraction::tests::sample_transaction();
let created_metadata = match created.metadata.as_mut() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("sample metadata is required"),
};
created_metadata.pre_token_balances.clear();
let created_row = crate::core_extraction::tests::raw_row(&created);
let created_bundle = match crate::extract_raw_transaction_to_core(&created_row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("created token extraction failed: {error}"),
};
let created_token = match created_bundle.balance_changes.last() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("created token balance is required"),
};
assert_eq!(
created_token.pre_balance_json,
std::option::Option::Some(serde_json::json!({
"amount": "0",
"decimals": 2,
"decimalAmount": "0.00",
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
}))
);
let mut closed = crate::core_extraction::tests::sample_transaction();
let closed_metadata = match closed.metadata.as_mut() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("sample metadata is required"),
};
closed_metadata.post_token_balances.clear();
let closed_row = crate::core_extraction::tests::raw_row(&closed);
let closed_bundle = match crate::extract_raw_transaction_to_core(&closed_row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("closed token extraction failed: {error}"),
};
let closed_token = match closed_bundle.balance_changes.last() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("closed token balance is required"),
};
assert_eq!(
closed_token.post_balance_json,
std::option::Option::Some(serde_json::json!({
"amount": "0",
"decimals": 2,
"decimalAmount": "0.00",
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
}))
);
}
#[tokio::test]
async fn current_version_and_hash_are_skipped_without_persistence() {
let transaction = crate::core_extraction::tests::sample_transaction();
let row = crate::core_extraction::tests::raw_row(&transaction);
let store =
crate::core_extraction::tests::TestCoreExtractionStore::new(std::vec![row], true);
let observer = crate::core_extraction::tests::TestCoreExtractionObserver::new(false);
let request = crate::CoreExtractionRequest {
source: crate::CoreExtractionSource::Pending,
limit: 1,
max_concurrent_extractions: 1,
force_replay: false,
};
let result = crate::execute_core_extraction(&store, &request, &observer).await;
let summary = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("skip campaign failed: {error}"),
};
assert_eq!(summary.skipped, 1);
assert_eq!(summary.extracted, 0);
assert_eq!(store.persist_calls.load(std::sync::atomic::Ordering::Acquire), 0);
}
#[tokio::test]
async fn force_replay_persists_even_when_ledger_is_current() {
let transaction = crate::core_extraction::tests::sample_transaction();
let row = crate::core_extraction::tests::raw_row(&transaction);
let store =
crate::core_extraction::tests::TestCoreExtractionStore::new(std::vec![row], true);
let observer = crate::core_extraction::tests::TestCoreExtractionObserver::new(false);
let request = crate::CoreExtractionRequest {
source: crate::CoreExtractionSource::Pending,
limit: 1,
max_concurrent_extractions: 1,
force_replay: true,
};
let result = crate::execute_core_extraction(&store, &request, &observer).await;
let summary = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("force replay campaign failed: {error}"),
};
assert_eq!(summary.extracted, 1);
assert_eq!(summary.skipped, 0);
assert_eq!(store.persist_calls.load(std::sync::atomic::Ordering::Acquire), 1);
}
#[tokio::test]
async fn cancellation_before_admission_keeps_every_candidate_not_started() {
let transaction = crate::core_extraction::tests::sample_transaction();
let row = crate::core_extraction::tests::raw_row(&transaction);
let store =
crate::core_extraction::tests::TestCoreExtractionStore::new(std::vec![row], false);
let observer = crate::core_extraction::tests::TestCoreExtractionObserver::new(true);
let request = crate::CoreExtractionRequest {
source: crate::CoreExtractionSource::Pending,
limit: 1,
max_concurrent_extractions: 1,
force_replay: false,
};
let result = crate::execute_core_extraction(&store, &request, &observer).await;
let summary = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("cancelled campaign failed: {error}"),
};
assert!(summary.cancelled);
assert_eq!(summary.started, 0);
assert_eq!(summary.completed, 0);
assert_eq!(summary.not_started, 1);
}
}