1066 lines
46 KiB
Rust
1066 lines
46 KiB
Rust
// file: kb_lib/src/local_pipeline_replay.rs
|
|
|
|
//! Local pipeline replay from already persisted raw transaction data.
|
|
//!
|
|
//! This service does not fetch historical transactions from Solana RPC. It
|
|
//! reuses rows already present in `k_sol_chain_transactions` and replays the
|
|
//! deterministic local pipeline over their signatures.
|
|
|
|
const LOCAL_PIPELINE_DEX_DECODER_SCOPE: &str = "dex_decode.local_pipeline";
|
|
const LOCAL_PIPELINE_DEX_DECODER_VERSION: &str = "dex_decode.v0.7.51.raydium_amm_v4_max_decoder";
|
|
|
|
fn default_skip_certified_dex_decode() -> bool {
|
|
return true;
|
|
}
|
|
|
|
fn default_defer_instruction_observation_index_refresh() -> bool {
|
|
return true;
|
|
}
|
|
|
|
/// Configuration for a local pipeline replay pass.
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct LocalPipelineReplayConfig {
|
|
/// Maximum number of persisted transactions to replay.
|
|
pub limit: std::option::Option<i64>,
|
|
/// Whether token metadata backfill should run after local replay.
|
|
pub refresh_missing_token_metadata: bool,
|
|
/// Maximum number of missing token metadata rows to resolve.
|
|
pub token_metadata_limit: std::option::Option<i64>,
|
|
/// Whether locally replayed market materialization tables are reset before replay.
|
|
pub reset_market_materialization_before_replay: bool,
|
|
/// Whether DEX decoding may be skipped when the replay ledger certifies it is safe.
|
|
#[serde(default = "default_skip_certified_dex_decode")]
|
|
pub skip_certified_dex_decode: bool,
|
|
/// Whether DEX decoding must run even when the replay ledger certifies a safe prior pass.
|
|
#[serde(default)]
|
|
pub force_decode_replay: bool,
|
|
/// Whether instruction observation indexing is deferred and refreshed once after replay.
|
|
#[serde(default = "default_defer_instruction_observation_index_refresh")]
|
|
pub defer_instruction_observation_index_refresh: bool,
|
|
}
|
|
|
|
impl Default for LocalPipelineReplayConfig {
|
|
fn default() -> Self {
|
|
return Self {
|
|
limit: Some(10_000),
|
|
refresh_missing_token_metadata: false,
|
|
token_metadata_limit: Some(250),
|
|
reset_market_materialization_before_replay: true,
|
|
skip_certified_dex_decode: true,
|
|
force_decode_replay: false,
|
|
defer_instruction_observation_index_refresh: true,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// Summary of a local pipeline replay pass.
|
|
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct LocalPipelineReplayResult {
|
|
/// Number of transaction signatures selected for replay.
|
|
pub selected_transaction_count: usize,
|
|
/// Number of transactions replayed without a fatal per-signature error.
|
|
pub replayed_transaction_count: usize,
|
|
/// Number of transactions that produced a decode error.
|
|
pub decode_error_count: usize,
|
|
/// Number of transactions that produced a detect error.
|
|
pub detect_error_count: usize,
|
|
/// Number of transactions that produced a trade aggregation error.
|
|
pub trade_aggregation_error_count: usize,
|
|
/// Number of transactions that produced a non-trade materialization error.
|
|
pub non_trade_materialization_error_count: usize,
|
|
/// Number of transactions that produced a candle aggregation error.
|
|
pub pair_candle_error_count: usize,
|
|
/// Number of transactions that produced an analytic signal error.
|
|
pub analytic_signal_error_count: usize,
|
|
/// Total decoded events returned by replayed decode calls.
|
|
pub decoded_event_count: usize,
|
|
/// Number of transactions where DEX decoding was skipped through the replay ledger.
|
|
pub decode_skipped_count: usize,
|
|
/// Number of persisted decoded events covered by skipped decode ledger rows.
|
|
pub decode_skipped_event_count: usize,
|
|
/// Number of replay ledger rows upserted by this replay pass.
|
|
pub decode_ledger_upsert_count: usize,
|
|
/// Number of replay ledger rows marked unsafe for future decode skip.
|
|
pub decode_ledger_unsafe_count: usize,
|
|
/// Total detection results returned by replayed detect calls.
|
|
pub detection_count: usize,
|
|
/// Total trade aggregation results returned by replayed aggregation calls.
|
|
pub trade_event_count: usize,
|
|
/// Total liquidity event materialization results returned by replayed non-trade calls.
|
|
pub liquidity_event_count: usize,
|
|
/// Total pool lifecycle event materialization results returned by replayed non-trade calls.
|
|
pub pool_lifecycle_event_count: usize,
|
|
/// Total fee event materialization results returned by replayed non-trade calls.
|
|
pub fee_event_count: usize,
|
|
/// Total reward event materialization results returned by replayed non-trade calls.
|
|
pub reward_event_count: usize,
|
|
/// Total pool administration event materialization results returned by replayed non-trade calls.
|
|
pub pool_admin_event_count: usize,
|
|
/// Total orderbook event materialization results returned by replayed non-trade calls.
|
|
pub orderbook_event_count: usize,
|
|
/// Total token-account event materialization results returned by replayed non-trade calls.
|
|
pub token_account_event_count: usize,
|
|
/// Total candle upsert results returned by replayed candle calls.
|
|
///
|
|
/// This is a replay write/result counter, not the number of distinct rows
|
|
/// currently persisted in `k_sol_pair_candles`. Use local diagnostics for the
|
|
/// persisted row count.
|
|
pub pair_candle_upsert_count: usize,
|
|
/// Total analytic signal upsert results returned by replayed analytic calls.
|
|
///
|
|
/// This is a replay write/result counter, not the number of distinct rows
|
|
/// currently persisted in the analytic signal table.
|
|
pub analytic_signal_upsert_count: usize,
|
|
/// Total transaction classification rows upserted during replay.
|
|
pub transaction_classification_count: usize,
|
|
/// Number of transactions that produced a classification error.
|
|
pub transaction_classification_error_count: usize,
|
|
/// Number of token metadata rows updated after replay.
|
|
pub token_metadata_updated_count: usize,
|
|
/// Number of pair symbols updated after replay.
|
|
pub pair_symbol_updated_count: usize,
|
|
/// Number of derived market materialization rows deleted before replay.
|
|
pub reset_market_materialization_deleted_count: u64,
|
|
/// Total instruction source rows scanned by the observation index refresh.
|
|
pub instruction_observation_scanned_count: usize,
|
|
/// Total instruction-observation rows upserted by the observation index refresh.
|
|
pub instruction_observation_upserted_count: usize,
|
|
/// Number of errors outside per-signature replay.
|
|
pub global_error_count: usize,
|
|
}
|
|
|
|
/// Local pipeline replay service.
|
|
#[derive(Debug, Clone)]
|
|
pub struct LocalPipelineReplayService {
|
|
database: std::sync::Arc<crate::Database>,
|
|
http_pool: std::option::Option<std::sync::Arc<crate::HttpEndpointPool>>,
|
|
http_role: std::string::String,
|
|
}
|
|
|
|
impl LocalPipelineReplayService {
|
|
/// Creates a new local-only pipeline replay service.
|
|
pub fn new(database: std::sync::Arc<crate::Database>) -> Self {
|
|
return Self {
|
|
database,
|
|
http_pool: None,
|
|
http_role: "local_metadata".to_string(),
|
|
};
|
|
}
|
|
|
|
/// Creates a new pipeline replay service able to refresh token metadata over Solana HTTP RPC.
|
|
pub fn new_with_http_pool(
|
|
http_pool: std::sync::Arc<crate::HttpEndpointPool>,
|
|
database: std::sync::Arc<crate::Database>,
|
|
http_role: std::string::String,
|
|
) -> Self {
|
|
return Self {
|
|
database,
|
|
http_pool: Some(http_pool),
|
|
http_role,
|
|
};
|
|
}
|
|
|
|
/// Replays the local pipeline from persisted raw chain transaction rows.
|
|
pub async fn replay_local_pipeline(
|
|
&self,
|
|
config: &crate::LocalPipelineReplayConfig,
|
|
) -> Result<crate::LocalPipelineReplayResult, crate::Error> {
|
|
let signatures_result = crate::query_chain_transactions_list_signatures_for_replay(
|
|
self.database.as_ref(),
|
|
config.limit,
|
|
)
|
|
.await;
|
|
let signatures = match signatures_result {
|
|
Ok(signatures) => signatures,
|
|
Err(error) => return Err(error),
|
|
};
|
|
let mut reset_market_materialization_deleted_count = 0_u64;
|
|
if config.reset_market_materialization_before_replay {
|
|
let reset_result =
|
|
crate::query_trade_market_materialization_delete_all(self.database.as_ref()).await;
|
|
reset_market_materialization_deleted_count = match reset_result {
|
|
Ok(deleted_count) => deleted_count,
|
|
Err(error) => return Err(error),
|
|
};
|
|
tracing::debug!(
|
|
deleted_count = reset_market_materialization_deleted_count,
|
|
"local pipeline replay reset market materialization tables"
|
|
);
|
|
}
|
|
let dex_decode = crate::DexDecodeService::new(self.database.clone());
|
|
let dex_detect = crate::DexDetectService::new(self.database.clone());
|
|
let trade_aggregation = crate::TradeAggregationService::new(self.database.clone());
|
|
let non_trade_materialization =
|
|
crate::NonTradeEventMaterializationService::new(self.database.clone());
|
|
let pair_candle_aggregation =
|
|
crate::PairCandleAggregationService::new(self.database.clone());
|
|
let pair_analytic_signal = crate::PairAnalyticSignalService::new(self.database.clone());
|
|
let transaction_classification =
|
|
crate::TransactionClassificationService::new(self.database.clone());
|
|
let instruction_observation_index =
|
|
crate::InstructionObservationIndexService::new(self.database.clone());
|
|
let mut result = LocalPipelineReplayResult {
|
|
selected_transaction_count: signatures.len(),
|
|
reset_market_materialization_deleted_count,
|
|
..Default::default()
|
|
};
|
|
for signature in signatures {
|
|
tracing::debug!(
|
|
signature = %signature,
|
|
"replaying local pipeline for persisted transaction"
|
|
);
|
|
let transaction_result = crate::query_chain_transactions_get_by_signature(
|
|
self.database.as_ref(),
|
|
signature.as_str(),
|
|
)
|
|
.await;
|
|
let transaction = match transaction_result {
|
|
Ok(Some(transaction)) => transaction,
|
|
Ok(None) => {
|
|
result.global_error_count += 1;
|
|
tracing::warn!(
|
|
signature = %signature,
|
|
"local pipeline replay transaction row disappeared before replay"
|
|
);
|
|
continue;
|
|
},
|
|
Err(error) => {
|
|
result.global_error_count += 1;
|
|
tracing::warn!(
|
|
signature = %signature,
|
|
error = %error,
|
|
"local pipeline replay transaction lookup failed"
|
|
);
|
|
continue;
|
|
},
|
|
};
|
|
let transaction_id = match transaction.id {
|
|
Some(transaction_id) => transaction_id,
|
|
None => {
|
|
result.global_error_count += 1;
|
|
tracing::warn!(
|
|
signature = %signature,
|
|
"local pipeline replay transaction row has no persisted id"
|
|
);
|
|
continue;
|
|
},
|
|
};
|
|
let decode_skip_ledger_result = self
|
|
.get_certified_dex_decode_skip_ledger(config, transaction_id, signature.as_str())
|
|
.await;
|
|
let decode_skip_ledger = match decode_skip_ledger_result {
|
|
Ok(decode_skip_ledger) => decode_skip_ledger,
|
|
Err(error) => return Err(error),
|
|
};
|
|
match decode_skip_ledger {
|
|
Some(ledger) => {
|
|
result.decode_skipped_count += 1;
|
|
let ledger_event_count = usize::try_from(ledger.event_count);
|
|
match ledger_event_count {
|
|
Ok(event_count) => {
|
|
result.decode_skipped_event_count += event_count;
|
|
},
|
|
Err(error) => {
|
|
result.global_error_count += 1;
|
|
tracing::warn!(
|
|
signature = %signature,
|
|
event_count = ledger.event_count,
|
|
error = %error,
|
|
"local pipeline replay could not convert skipped event count"
|
|
);
|
|
},
|
|
}
|
|
tracing::debug!(
|
|
signature = %signature,
|
|
event_count = ledger.event_count,
|
|
decoder_version = %ledger.decoder_version,
|
|
"local pipeline replay skipped certified DEX decode step"
|
|
);
|
|
},
|
|
None => {
|
|
let replay_scope_delete_result =
|
|
crate::query_dex_decoded_events_delete_local_replay_scope_by_transaction_id(
|
|
self.database.as_ref(),
|
|
transaction_id,
|
|
)
|
|
.await;
|
|
match replay_scope_delete_result {
|
|
Ok(deleted_count) => {
|
|
result.reset_market_materialization_deleted_count = result
|
|
.reset_market_materialization_deleted_count
|
|
.saturating_add(deleted_count);
|
|
if deleted_count > 0 {
|
|
tracing::debug!(
|
|
signature = %signature,
|
|
transaction_id,
|
|
deleted_count,
|
|
"local pipeline replay deleted stale local DEX replay scope before decode"
|
|
);
|
|
}
|
|
},
|
|
Err(error) => return Err(error),
|
|
}
|
|
let decode_result =
|
|
dex_decode.decode_transaction_by_signature(signature.as_str()).await;
|
|
match decode_result {
|
|
Ok(decoded_events) => {
|
|
result.decoded_event_count += decoded_events.len();
|
|
let ledger_result = self
|
|
.record_dex_decode_replay_ledger(
|
|
transaction_id,
|
|
signature.as_str(),
|
|
&decoded_events,
|
|
)
|
|
.await;
|
|
match ledger_result {
|
|
Ok(ledger) => {
|
|
result.decode_ledger_upsert_count += 1;
|
|
if ledger.force_replay_required {
|
|
result.decode_ledger_unsafe_count += 1;
|
|
}
|
|
},
|
|
Err(error) => {
|
|
result.global_error_count += 1;
|
|
tracing::warn!(
|
|
signature = %signature,
|
|
error = %error,
|
|
"local pipeline replay could not record successful decode ledger row"
|
|
);
|
|
},
|
|
}
|
|
},
|
|
Err(error) => {
|
|
result.decode_error_count += 1;
|
|
let ledger_result = self
|
|
.record_failed_dex_decode_replay_ledger(
|
|
transaction_id,
|
|
signature.as_str(),
|
|
error.to_string(),
|
|
)
|
|
.await;
|
|
match ledger_result {
|
|
Ok(_) => {
|
|
result.decode_ledger_upsert_count += 1;
|
|
result.decode_ledger_unsafe_count += 1;
|
|
},
|
|
Err(ledger_error) => {
|
|
result.global_error_count += 1;
|
|
tracing::warn!(
|
|
signature = %signature,
|
|
error = %ledger_error,
|
|
"local pipeline replay could not record failed decode ledger row"
|
|
);
|
|
},
|
|
}
|
|
tracing::warn!(
|
|
signature = %signature,
|
|
error = %error,
|
|
"local pipeline replay decode step failed"
|
|
);
|
|
continue;
|
|
},
|
|
}
|
|
},
|
|
}
|
|
let detect_result =
|
|
dex_detect.detect_transaction_by_signature(signature.as_str()).await;
|
|
match detect_result {
|
|
Ok(detections) => {
|
|
result.detection_count += detections.len();
|
|
},
|
|
Err(error) => {
|
|
result.detect_error_count += 1;
|
|
tracing::warn!(
|
|
signature = %signature,
|
|
error = %error,
|
|
"local pipeline replay detect step failed; continuing with aggregation"
|
|
);
|
|
},
|
|
}
|
|
let non_trade_result = non_trade_materialization
|
|
.record_transaction_by_signature(signature.as_str())
|
|
.await;
|
|
match non_trade_result {
|
|
Ok(non_trade_result) => {
|
|
result.liquidity_event_count += non_trade_result.liquidity_event_count;
|
|
result.pool_lifecycle_event_count +=
|
|
non_trade_result.pool_lifecycle_event_count;
|
|
result.fee_event_count += non_trade_result.fee_event_count;
|
|
result.reward_event_count += non_trade_result.reward_event_count;
|
|
result.pool_admin_event_count += non_trade_result.pool_admin_event_count;
|
|
result.orderbook_event_count += non_trade_result.orderbook_event_count;
|
|
result.token_account_event_count += non_trade_result.token_account_event_count;
|
|
},
|
|
Err(error) => {
|
|
result.non_trade_materialization_error_count += 1;
|
|
tracing::warn!(
|
|
signature = %signature,
|
|
error = %error,
|
|
"local pipeline replay non-trade materialization step failed"
|
|
);
|
|
continue;
|
|
},
|
|
}
|
|
let trade_result =
|
|
trade_aggregation.record_transaction_by_signature(signature.as_str()).await;
|
|
match trade_result {
|
|
Ok(trade_results) => {
|
|
result.trade_event_count += trade_results.len();
|
|
},
|
|
Err(error) => {
|
|
result.trade_aggregation_error_count += 1;
|
|
tracing::warn!(
|
|
signature = %signature,
|
|
error = %error,
|
|
"local pipeline replay trade aggregation step failed"
|
|
);
|
|
continue;
|
|
},
|
|
}
|
|
let candle_result = pair_candle_aggregation
|
|
.record_transaction_by_signature(signature.as_str())
|
|
.await;
|
|
match candle_result {
|
|
Ok(candle_results) => {
|
|
result.pair_candle_upsert_count += candle_results.len();
|
|
},
|
|
Err(error) => {
|
|
result.pair_candle_error_count += 1;
|
|
tracing::warn!(
|
|
signature = %signature,
|
|
error = %error,
|
|
"local pipeline replay candle aggregation step failed"
|
|
);
|
|
},
|
|
}
|
|
let analytic_result =
|
|
pair_analytic_signal.record_transaction_by_signature(signature.as_str()).await;
|
|
match analytic_result {
|
|
Ok(analytic_results) => {
|
|
result.analytic_signal_upsert_count += analytic_results.len();
|
|
},
|
|
Err(error) => {
|
|
result.analytic_signal_error_count += 1;
|
|
tracing::warn!(
|
|
signature = %signature,
|
|
error = %error,
|
|
"local pipeline replay analytic signal step failed"
|
|
);
|
|
},
|
|
}
|
|
let classification_result = transaction_classification
|
|
.classify_transaction_by_signature(signature.as_str())
|
|
.await;
|
|
match classification_result {
|
|
Ok(_) => {
|
|
result.transaction_classification_count += 1;
|
|
},
|
|
Err(error) => {
|
|
result.transaction_classification_error_count += 1;
|
|
tracing::warn!(
|
|
signature = %signature,
|
|
error = %error,
|
|
"local pipeline replay transaction classification step failed"
|
|
);
|
|
},
|
|
}
|
|
if !config.defer_instruction_observation_index_refresh {
|
|
let instruction_index_result =
|
|
instruction_observation_index.refresh_signature(signature.as_str()).await;
|
|
match instruction_index_result {
|
|
Ok(index_result) => {
|
|
result.instruction_observation_scanned_count +=
|
|
index_result.scanned_instruction_count;
|
|
result.instruction_observation_upserted_count +=
|
|
index_result.upserted_observation_count;
|
|
tracing::debug!(
|
|
signature = %signature,
|
|
upserted_observation_count = index_result.upserted_observation_count,
|
|
"instruction observation index refreshed during local replay"
|
|
);
|
|
},
|
|
Err(error) => {
|
|
tracing::warn!(
|
|
signature = %signature,
|
|
error = %error,
|
|
"instruction observation index refresh failed during local replay"
|
|
);
|
|
},
|
|
}
|
|
}
|
|
result.replayed_transaction_count += 1;
|
|
}
|
|
if config.defer_instruction_observation_index_refresh {
|
|
let instruction_index_result =
|
|
instruction_observation_index.refresh_replay_window(config.limit).await;
|
|
match instruction_index_result {
|
|
Ok(index_result) => {
|
|
result.instruction_observation_scanned_count +=
|
|
index_result.scanned_instruction_count;
|
|
result.instruction_observation_upserted_count +=
|
|
index_result.upserted_observation_count;
|
|
tracing::debug!(
|
|
scanned_instruction_count = index_result.scanned_instruction_count,
|
|
upserted_observation_count = index_result.upserted_observation_count,
|
|
"instruction observation index refreshed after local replay"
|
|
);
|
|
},
|
|
Err(error) => {
|
|
result.global_error_count += 1;
|
|
tracing::warn!(
|
|
error = %error,
|
|
"instruction observation index refresh failed after local replay"
|
|
);
|
|
},
|
|
}
|
|
}
|
|
if config.refresh_missing_token_metadata {
|
|
let metadata_service = match &self.http_pool {
|
|
Some(http_pool) => crate::TokenMetadataBackfillService::new(
|
|
http_pool.clone(),
|
|
self.database.clone(),
|
|
self.http_role.clone(),
|
|
),
|
|
None => crate::TokenMetadataBackfillService::new_local(self.database.clone()),
|
|
};
|
|
let metadata_result = metadata_service
|
|
.backfill_missing_token_metadata(config.token_metadata_limit)
|
|
.await;
|
|
match metadata_result {
|
|
Ok(metadata_result) => {
|
|
result.token_metadata_updated_count += metadata_result.updated_token_count;
|
|
},
|
|
Err(error) => {
|
|
result.global_error_count += 1;
|
|
tracing::warn!(
|
|
error = %error,
|
|
"token metadata refresh failed after local pipeline replay"
|
|
);
|
|
},
|
|
}
|
|
}
|
|
self.refresh_event_coverage_best_effort().await;
|
|
return Ok(result);
|
|
}
|
|
|
|
async fn refresh_event_coverage_best_effort(&self) {
|
|
let cpmm_cleanup_result =
|
|
crate::query_dex_decoded_events_delete_replaced_raydium_cpmm_instruction_audits(
|
|
self.database.as_ref(),
|
|
None,
|
|
)
|
|
.await;
|
|
match cpmm_cleanup_result {
|
|
Ok(deleted_count) => {
|
|
if deleted_count > 0 {
|
|
tracing::info!(
|
|
deleted_count = deleted_count,
|
|
"replaced Raydium CPMM instruction audits cleaned before dex event coverage refresh"
|
|
);
|
|
}
|
|
},
|
|
Err(error) => {
|
|
tracing::warn!(
|
|
error = %error,
|
|
"Raydium CPMM replaced instruction-audit cleanup failed before dex event coverage refresh"
|
|
);
|
|
},
|
|
}
|
|
|
|
let cleanup_result =
|
|
crate::query_dex_decoded_events_delete_replaced_raydium_clmm_instruction_audits(
|
|
self.database.as_ref(),
|
|
None,
|
|
)
|
|
.await;
|
|
match cleanup_result {
|
|
Ok(deleted_count) => {
|
|
if deleted_count > 0 {
|
|
tracing::info!(
|
|
deleted_count = deleted_count,
|
|
"replaced Raydium CLMM instruction audits cleaned before dex event coverage refresh"
|
|
);
|
|
}
|
|
},
|
|
Err(error) => {
|
|
tracing::warn!(
|
|
error = %error,
|
|
"Raydium CLMM replaced instruction-audit cleanup failed before dex event coverage refresh"
|
|
);
|
|
},
|
|
}
|
|
|
|
let upstream_cleanup_result =
|
|
crate::query_dex_decoded_events_delete_locally_covered_upstream_instruction_matches(
|
|
self.database.as_ref(),
|
|
None,
|
|
)
|
|
.await;
|
|
match upstream_cleanup_result {
|
|
Ok(deleted_count) => {
|
|
if deleted_count > 0 {
|
|
tracing::info!(
|
|
deleted_count = deleted_count,
|
|
"locally covered upstream instruction matches cleaned before dex event coverage refresh"
|
|
);
|
|
}
|
|
},
|
|
Err(error) => {
|
|
tracing::warn!(
|
|
error = %error,
|
|
"locally covered upstream instruction-match cleanup failed before dex event coverage refresh"
|
|
);
|
|
},
|
|
}
|
|
|
|
let coverage_service = crate::DexEventCoverageService::new(self.database.clone());
|
|
let refresh_result = coverage_service.refresh_local_counts(None).await;
|
|
match refresh_result {
|
|
Ok(refresh_result) => {
|
|
tracing::debug!(
|
|
upserted_entry_count = refresh_result.upserted_entry_count,
|
|
refreshed_entry_count = refresh_result.refreshed_entry_count,
|
|
summary_count = refresh_result.summaries.len(),
|
|
"dex event coverage refreshed after local pipeline replay"
|
|
);
|
|
},
|
|
Err(error) => {
|
|
tracing::warn!(
|
|
error = %error,
|
|
"dex event coverage refresh failed after local pipeline replay"
|
|
);
|
|
},
|
|
}
|
|
|
|
let post_refresh_cpmm_cleanup_result =
|
|
crate::query_dex_decoded_events_delete_replaced_raydium_cpmm_instruction_audits(
|
|
self.database.as_ref(),
|
|
None,
|
|
)
|
|
.await;
|
|
match post_refresh_cpmm_cleanup_result {
|
|
Ok(deleted_count) => {
|
|
if deleted_count > 0 {
|
|
tracing::info!(
|
|
deleted_count = deleted_count,
|
|
"replaced Raydium CPMM instruction audits cleaned after dex event coverage refresh"
|
|
);
|
|
let second_refresh_result = coverage_service.refresh_local_counts(None).await;
|
|
match second_refresh_result {
|
|
Ok(second_refresh_result) => {
|
|
tracing::debug!(
|
|
upserted_entry_count = second_refresh_result.upserted_entry_count,
|
|
refreshed_entry_count = second_refresh_result.refreshed_entry_count,
|
|
summary_count = second_refresh_result.summaries.len(),
|
|
"dex event coverage refreshed after Raydium CPMM instruction-audit cleanup"
|
|
);
|
|
},
|
|
Err(error) => {
|
|
tracing::warn!(
|
|
error = %error,
|
|
"dex event coverage refresh failed after Raydium CPMM instruction-audit cleanup"
|
|
);
|
|
},
|
|
}
|
|
}
|
|
},
|
|
Err(error) => {
|
|
tracing::warn!(
|
|
error = %error,
|
|
"Raydium CPMM replaced instruction-audit cleanup failed after dex event coverage refresh"
|
|
);
|
|
},
|
|
}
|
|
|
|
let post_refresh_upstream_cleanup_result =
|
|
crate::query_dex_decoded_events_delete_locally_covered_upstream_instruction_matches(
|
|
self.database.as_ref(),
|
|
None,
|
|
)
|
|
.await;
|
|
match post_refresh_upstream_cleanup_result {
|
|
Ok(deleted_count) => {
|
|
if deleted_count > 0 {
|
|
tracing::info!(
|
|
deleted_count = deleted_count,
|
|
"locally covered upstream instruction matches cleaned after dex event coverage refresh"
|
|
);
|
|
let second_refresh_result = coverage_service.refresh_local_counts(None).await;
|
|
match second_refresh_result {
|
|
Ok(second_refresh_result) => {
|
|
tracing::debug!(
|
|
upserted_entry_count = second_refresh_result.upserted_entry_count,
|
|
refreshed_entry_count = second_refresh_result.refreshed_entry_count,
|
|
summary_count = second_refresh_result.summaries.len(),
|
|
"dex event coverage refreshed after upstream instruction-match cleanup"
|
|
);
|
|
},
|
|
Err(error) => {
|
|
tracing::warn!(
|
|
error = %error,
|
|
"dex event coverage refresh failed after upstream instruction-match cleanup"
|
|
);
|
|
},
|
|
}
|
|
}
|
|
},
|
|
Err(error) => {
|
|
tracing::warn!(
|
|
error = %error,
|
|
"locally covered upstream instruction-match cleanup failed after dex event coverage refresh"
|
|
);
|
|
},
|
|
}
|
|
}
|
|
|
|
async fn get_certified_dex_decode_skip_ledger(
|
|
&self,
|
|
config: &crate::LocalPipelineReplayConfig,
|
|
transaction_id: i64,
|
|
signature: &str,
|
|
) -> Result<std::option::Option<crate::DexDecodeReplayLedgerDto>, crate::Error> {
|
|
if config.force_decode_replay {
|
|
return Ok(None);
|
|
}
|
|
if !config.skip_certified_dex_decode {
|
|
return Ok(None);
|
|
}
|
|
let ledger_result = crate::query_dex_decode_replay_ledger_get_by_transaction(
|
|
self.database.as_ref(),
|
|
transaction_id,
|
|
LOCAL_PIPELINE_DEX_DECODER_SCOPE,
|
|
LOCAL_PIPELINE_DEX_DECODER_VERSION,
|
|
)
|
|
.await;
|
|
let ledger_option = match ledger_result {
|
|
Ok(ledger_option) => ledger_option,
|
|
Err(error) => return Err(error),
|
|
};
|
|
match ledger_option {
|
|
Some(ledger) => {
|
|
if ledger.can_skip_decode() {
|
|
let persisted_count_result = self
|
|
.count_persisted_decoded_events_for_skip(transaction_id, signature)
|
|
.await;
|
|
let persisted_count = match persisted_count_result {
|
|
Ok(persisted_count) => persisted_count,
|
|
Err(error) => return Err(error),
|
|
};
|
|
if persisted_count >= ledger.event_count {
|
|
return Ok(Some(ledger));
|
|
}
|
|
tracing::debug!(
|
|
signature = %signature,
|
|
ledger_event_count = ledger.event_count,
|
|
persisted_event_count = persisted_count,
|
|
"local pipeline replay ledger is certified but persisted decoded events are missing"
|
|
);
|
|
return Ok(None);
|
|
}
|
|
tracing::debug!(
|
|
signature = %signature,
|
|
decode_status = %ledger.decode_status,
|
|
certainty = %ledger.certainty,
|
|
force_replay_required = ledger.force_replay_required,
|
|
"local pipeline replay ledger requires DEX decode"
|
|
);
|
|
return Ok(None);
|
|
},
|
|
None => return Ok(None),
|
|
}
|
|
}
|
|
|
|
async fn count_persisted_decoded_events_for_skip(
|
|
&self,
|
|
transaction_id: i64,
|
|
signature: &str,
|
|
) -> Result<i64, crate::Error> {
|
|
let events_result = crate::query_dex_decoded_events_list_by_transaction_id(
|
|
self.database.as_ref(),
|
|
transaction_id,
|
|
)
|
|
.await;
|
|
let events = match events_result {
|
|
Ok(events) => events,
|
|
Err(error) => return Err(error),
|
|
};
|
|
let count_result = i64::try_from(events.len());
|
|
match count_result {
|
|
Ok(count) => return Ok(count),
|
|
Err(error) => {
|
|
return Err(crate::Error::Db(format!(
|
|
"cannot convert persisted decoded event count for signature '{}' to i64: {}",
|
|
signature, error
|
|
)));
|
|
},
|
|
}
|
|
}
|
|
|
|
async fn record_dex_decode_replay_ledger(
|
|
&self,
|
|
transaction_id: i64,
|
|
signature: &str,
|
|
decoded_events: &[crate::DexDecodedEventDto],
|
|
) -> Result<crate::DexDecodeReplayLedgerDto, crate::Error> {
|
|
let ledger_result =
|
|
build_success_dex_decode_replay_ledger(transaction_id, signature, decoded_events);
|
|
let ledger = match ledger_result {
|
|
Ok(ledger) => ledger,
|
|
Err(error) => return Err(error),
|
|
};
|
|
let upsert_result =
|
|
crate::query_dex_decode_replay_ledger_upsert(self.database.as_ref(), &ledger).await;
|
|
match upsert_result {
|
|
Ok(_) => return Ok(ledger),
|
|
Err(error) => return Err(error),
|
|
}
|
|
}
|
|
|
|
async fn record_failed_dex_decode_replay_ledger(
|
|
&self,
|
|
transaction_id: i64,
|
|
signature: &str,
|
|
error_message: std::string::String,
|
|
) -> Result<crate::DexDecodeReplayLedgerDto, crate::Error> {
|
|
let ledger = crate::DexDecodeReplayLedgerDto::new(
|
|
transaction_id,
|
|
signature.to_string(),
|
|
LOCAL_PIPELINE_DEX_DECODER_SCOPE.to_string(),
|
|
LOCAL_PIPELINE_DEX_DECODER_VERSION.to_string(),
|
|
crate::DexDecodeReplayLedgerDto::STATUS_FAILED.to_string(),
|
|
crate::DexDecodeReplayLedgerDto::CERTAINTY_UNSAFE.to_string(),
|
|
0,
|
|
0,
|
|
true,
|
|
Some(format!("decode failed: {error_message}")),
|
|
);
|
|
let upsert_result =
|
|
crate::query_dex_decode_replay_ledger_upsert(self.database.as_ref(), &ledger).await;
|
|
match upsert_result {
|
|
Ok(_) => return Ok(ledger),
|
|
Err(error) => return Err(error),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn build_success_dex_decode_replay_ledger(
|
|
transaction_id: i64,
|
|
signature: &str,
|
|
decoded_events: &[crate::DexDecodedEventDto],
|
|
) -> Result<crate::DexDecodeReplayLedgerDto, crate::Error> {
|
|
let event_count_result = i64::try_from(decoded_events.len());
|
|
let event_count = match event_count_result {
|
|
Ok(event_count) => event_count,
|
|
Err(error) => {
|
|
return Err(crate::Error::Db(format!(
|
|
"cannot convert decoded event count '{}' to i64: {}",
|
|
decoded_events.len(),
|
|
error
|
|
)));
|
|
},
|
|
};
|
|
let effective_event_count_usize = count_effective_decoded_events(decoded_events);
|
|
let effective_event_count_result = i64::try_from(effective_event_count_usize);
|
|
let effective_event_count = match effective_event_count_result {
|
|
Ok(effective_event_count) => effective_event_count,
|
|
Err(error) => {
|
|
return Err(crate::Error::Db(format!(
|
|
"cannot convert effective decoded event count '{}' to i64: {}",
|
|
effective_event_count_usize, error
|
|
)));
|
|
},
|
|
};
|
|
let instruction_audit_count = event_count - effective_event_count;
|
|
let distinct_token_mint_count_usize = count_distinct_decoded_event_token_mints(decoded_events);
|
|
let distinct_token_mint_count_result = i64::try_from(distinct_token_mint_count_usize);
|
|
let distinct_token_mint_count = match distinct_token_mint_count_result {
|
|
Ok(distinct_token_mint_count) => distinct_token_mint_count,
|
|
Err(error) => {
|
|
return Err(crate::Error::Db(format!(
|
|
"cannot convert distinct token mint count '{}' to i64: {}",
|
|
distinct_token_mint_count_usize, error
|
|
)));
|
|
},
|
|
};
|
|
let force_replay_required = effective_event_count > 1 || distinct_token_mint_count > 2;
|
|
let decode_status = if event_count == 0 {
|
|
crate::DexDecodeReplayLedgerDto::STATUS_NO_EVENTS.to_string()
|
|
} else {
|
|
crate::DexDecodeReplayLedgerDto::STATUS_DECODED.to_string()
|
|
};
|
|
let certainty = if force_replay_required {
|
|
crate::DexDecodeReplayLedgerDto::CERTAINTY_UNSAFE.to_string()
|
|
} else {
|
|
crate::DexDecodeReplayLedgerDto::CERTAINTY_SURE.to_string()
|
|
};
|
|
let status_reason = build_dex_decode_replay_ledger_status_reason(
|
|
event_count,
|
|
effective_event_count,
|
|
instruction_audit_count,
|
|
distinct_token_mint_count,
|
|
force_replay_required,
|
|
);
|
|
return Ok(crate::DexDecodeReplayLedgerDto::new(
|
|
transaction_id,
|
|
signature.to_string(),
|
|
LOCAL_PIPELINE_DEX_DECODER_SCOPE.to_string(),
|
|
LOCAL_PIPELINE_DEX_DECODER_VERSION.to_string(),
|
|
decode_status,
|
|
certainty,
|
|
event_count,
|
|
distinct_token_mint_count,
|
|
force_replay_required,
|
|
Some(status_reason),
|
|
));
|
|
}
|
|
|
|
fn count_effective_decoded_events(decoded_events: &[crate::DexDecodedEventDto]) -> usize {
|
|
let mut count = 0_usize;
|
|
for event in decoded_events {
|
|
if is_replay_audit_only_event(event) {
|
|
continue;
|
|
}
|
|
count += 1;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
fn is_replay_audit_only_event(event: &crate::DexDecodedEventDto) -> bool {
|
|
if event.event_kind.ends_with(".instruction_audit") {
|
|
return true;
|
|
}
|
|
if event.event_kind.ends_with("_audit") {
|
|
return true;
|
|
}
|
|
if event.event_kind == crate::UPSTREAM_REGISTRY_INSTRUCTION_MATCH_EVENT_KIND {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fn count_distinct_decoded_event_token_mints(decoded_events: &[crate::DexDecodedEventDto]) -> usize {
|
|
let mut mints = std::collections::BTreeSet::<std::string::String>::new();
|
|
for event in decoded_events {
|
|
insert_optional_mint(&mut mints, &event.lp_mint);
|
|
insert_optional_mint(&mut mints, &event.token_a_mint);
|
|
insert_optional_mint(&mut mints, &event.token_b_mint);
|
|
}
|
|
return mints.len();
|
|
}
|
|
|
|
fn insert_optional_mint(
|
|
mints: &mut std::collections::BTreeSet<std::string::String>,
|
|
mint_option: &std::option::Option<std::string::String>,
|
|
) {
|
|
if let Some(mint) = mint_option {
|
|
let trimmed = mint.trim();
|
|
if !trimmed.is_empty() {
|
|
mints.insert(trimmed.to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
fn build_dex_decode_replay_ledger_status_reason(
|
|
event_count: i64,
|
|
effective_event_count: i64,
|
|
instruction_audit_count: i64,
|
|
distinct_token_mint_count: i64,
|
|
force_replay_required: bool,
|
|
) -> std::string::String {
|
|
if event_count == 0 {
|
|
return "decode completed with no persisted DEX event".to_string();
|
|
}
|
|
if force_replay_required {
|
|
return format!(
|
|
"decode completed but remains unsafe for skip: event_count={event_count}, effective_event_count={effective_event_count}, instruction_audit_count={instruction_audit_count}, distinct_token_mint_count={distinct_token_mint_count}"
|
|
);
|
|
}
|
|
return format!(
|
|
"decode completed and certified for skip: event_count={event_count}, effective_event_count={effective_event_count}, instruction_audit_count={instruction_audit_count}, distinct_token_mint_count={distinct_token_mint_count}"
|
|
);
|
|
}
|
|
|
|
/// Replays the local pipeline from persisted raw chain transaction rows.
|
|
pub async fn replay_local_pipeline(
|
|
database: std::sync::Arc<crate::Database>,
|
|
config: &crate::LocalPipelineReplayConfig,
|
|
) -> Result<crate::LocalPipelineReplayResult, crate::Error> {
|
|
let service = crate::LocalPipelineReplayService::new(database);
|
|
return service.replay_local_pipeline(config).await;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
fn make_decoded_event(
|
|
event_kind: &str,
|
|
token_a_mint: std::option::Option<&str>,
|
|
token_b_mint: std::option::Option<&str>,
|
|
) -> crate::DexDecodedEventDto {
|
|
return crate::DexDecodedEventDto::new(
|
|
1,
|
|
Some(10),
|
|
"meteora_dlmm".to_string(),
|
|
crate::METEORA_DLMM_PROGRAM_ID.to_string(),
|
|
event_kind.to_string(),
|
|
Some("pool".to_string()),
|
|
None,
|
|
token_a_mint.map(|value| return value.to_string()),
|
|
token_b_mint.map(|value| return value.to_string()),
|
|
None,
|
|
"{}".to_string(),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ledger_certifies_one_effective_event_with_instruction_audits() {
|
|
let events = vec![
|
|
make_decoded_event("meteora_dlmm.swap", Some("mint-a"), Some("mint-b")),
|
|
make_decoded_event("meteora_dlmm.instruction_audit", None, None),
|
|
make_decoded_event("meteora_dlmm.instruction_audit", None, None),
|
|
];
|
|
let ledger = super::build_success_dex_decode_replay_ledger(1, "sig", events.as_slice())
|
|
.expect("ledger must build");
|
|
assert_eq!(ledger.event_count, 3);
|
|
assert_eq!(ledger.distinct_token_mint_count, 2);
|
|
assert!(!ledger.force_replay_required);
|
|
assert_eq!(ledger.certainty, crate::DexDecodeReplayLedgerDto::CERTAINTY_SURE);
|
|
assert!(ledger.can_skip_decode());
|
|
}
|
|
|
|
#[test]
|
|
fn ledger_treats_audit_suffix_events_as_audit_only() {
|
|
let events = vec![
|
|
make_decoded_event("openbook_v2.settle_funds_audit", Some("mint-a"), Some("mint-b")),
|
|
make_decoded_event("openbook_v2.order_place_audit", None, None),
|
|
];
|
|
let ledger = super::build_success_dex_decode_replay_ledger(1, "sig", events.as_slice())
|
|
.expect("ledger must build");
|
|
assert_eq!(ledger.event_count, 2);
|
|
assert_eq!(
|
|
ledger.status_reason.as_deref(),
|
|
Some(
|
|
"decode completed and certified for skip: event_count=2, effective_event_count=0, instruction_audit_count=2, distinct_token_mint_count=2"
|
|
)
|
|
);
|
|
assert!(!ledger.force_replay_required);
|
|
assert!(ledger.can_skip_decode());
|
|
}
|
|
|
|
#[test]
|
|
fn ledger_keeps_multiple_effective_events_unsafe() {
|
|
let events = vec![
|
|
make_decoded_event("meteora_dlmm.swap", Some("mint-a"), Some("mint-b")),
|
|
make_decoded_event("meteora_dlmm.swap", Some("mint-a"), Some("mint-b")),
|
|
make_decoded_event("meteora_dlmm.instruction_audit", None, None),
|
|
];
|
|
let ledger = super::build_success_dex_decode_replay_ledger(1, "sig", events.as_slice())
|
|
.expect("ledger must build");
|
|
assert_eq!(ledger.event_count, 3);
|
|
assert!(ledger.force_replay_required);
|
|
assert_eq!(ledger.certainty, crate::DexDecodeReplayLedgerDto::CERTAINTY_UNSAFE);
|
|
assert!(!ledger.can_skip_decode());
|
|
}
|
|
}
|