diff --git a/Cargo.toml b/Cargo.toml index 09f5ed9..00d0ad1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.5.5" +version = "0.5.6" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-bobot" diff --git a/khbb_lib/src/lib.rs b/khbb_lib/src/lib.rs index febb150..8019d81 100644 --- a/khbb_lib/src/lib.rs +++ b/khbb_lib/src/lib.rs @@ -26,6 +26,7 @@ mod ids; mod heuristics; mod account_enrichment; mod enriched_classifier; +mod signal_correlation; /// Runs the listener application bootstrap workflow. pub use crate::app::run_listener_app; @@ -127,3 +128,11 @@ pub use crate::enriched_classifier::KhbbConfirmedTokenAccountActivityEvent; pub use crate::enriched_classifier::KhbbConfirmedMintAccountActivityEvent; /// Unknown token-program-owned account activity. pub use crate::enriched_classifier::KhbbUnknownTokenProgramAccountActivityEvent; +/// Correlated signal produced from enriched and heuristic activity. +pub use crate::signal_correlation::KhbbCorrelatedSignal; +/// Correlated signal indicating a confirmed token account update. +pub use crate::signal_correlation::KhbbConfirmedTokenAccountUpdateSignal; +/// Correlated signal indicating a potential new token mint. +pub use crate::signal_correlation::KhbbPotentialNewTokenMintSignal; +/// Correlated signal indicating a potential token bootstrap flow. +pub use crate::signal_correlation::KhbbPotentialTokenBootstrapFlowSignal; diff --git a/khbb_lib/src/listener.rs b/khbb_lib/src/listener.rs index c0042ff..24ee1ce 100644 --- a/khbb_lib/src/listener.rs +++ b/khbb_lib/src/listener.rs @@ -560,52 +560,54 @@ pub async fn run_listener_runtime( classified.clone(), ), ); - match heuristic_result { - Ok(signals) => { - for signal in signals { - match signal { - crate::KhbbHeuristicSignal::PotentialTokenAccountActivity(inner) => { - tracing::trace!( - listener_session_id = session.id, - pubkey = %inner.pubkey, - context_slot = inner.context_slot, - subscription_id = inner.subscription_id, - token_program_family = %inner.token_program_family, - "heuristic potential token account activity signal" - ); + let heuristic_signals = match heuristic_result { + Ok(signals) => { + for signal in &signals { + match signal { + crate::KhbbHeuristicSignal::PotentialTokenAccountActivity(inner) => { + tracing::trace!( + listener_session_id = session.id, + pubkey = %inner.pubkey, + context_slot = inner.context_slot, + subscription_id = inner.subscription_id, + token_program_family = %inner.token_program_family, + "heuristic potential token account activity signal" + ); + } + crate::KhbbHeuristicSignal::PotentialMintActivity(inner) => { + tracing::trace!( + listener_session_id = session.id, + pubkey = %inner.pubkey, + context_slot = inner.context_slot, + token_program_family = %inner.token_program_family, + "heuristic potential mint activity signal" + ); + } + crate::KhbbHeuristicSignal::PotentialAssociatedTokenAccountActivity(inner) => { + tracing::trace!( + listener_session_id = session.id, + pubkey = %inner.pubkey, + context_slot = inner.context_slot, + subscription_id = inner.subscription_id, + token_program_family = %inner.token_program_family, + "heuristic potential associated token account activity signal" + ); + } + crate::KhbbHeuristicSignal::PotentialInitialTokenActivity(_) => {} + crate::KhbbHeuristicSignal::PotentialTokenBootstrapActivity(_) => {} } - crate::KhbbHeuristicSignal::PotentialMintActivity(inner) => { - tracing::trace!( - listener_session_id = session.id, - pubkey = %inner.pubkey, - context_slot = inner.context_slot, - token_program_family = %inner.token_program_family, - "heuristic potential mint activity signal" - ); - } - crate::KhbbHeuristicSignal::PotentialAssociatedTokenAccountActivity(inner) => { - tracing::trace!( - listener_session_id = session.id, - pubkey = %inner.pubkey, - context_slot = inner.context_slot, - subscription_id = inner.subscription_id, - token_program_family = %inner.token_program_family, - "heuristic potential associated token account activity signal" - ); - } - crate::KhbbHeuristicSignal::PotentialInitialTokenActivity(_) => {} - crate::KhbbHeuristicSignal::PotentialTokenBootstrapActivity(_) => {} } + signals } - } - Err(error) => { - tracing::error!( - listener_session_id = session.id, - error = %error, - "failed to derive heuristic signals from spl-token program activity" - ); - } - } + Err(error) => { + tracing::error!( + listener_session_id = session.id, + error = %error, + "failed to derive heuristic signals from spl-token program activity" + ); + vec![] + } + }; let account_info_result = http_client .get_account_info( &classified.pubkey, @@ -671,6 +673,51 @@ pub async fn run_listener_runtime( lamports = ?inner.lamports, "confirmed token account activity event" ); + let correlated_result = + crate::signal_correlation::correlate_signals( + &crate::KhbbEnrichedConfirmedEvent::ConfirmedTokenAccountActivity( + inner.clone(), + ), + &heuristic_signals, + ); + match correlated_result { + Ok(signals) => { + for signal in signals { + match signal { + crate::KhbbCorrelatedSignal::ConfirmedTokenAccountUpdate(correlated) => { + tracing::trace!( + listener_session_id = session.id, + pubkey = %correlated.pubkey, + context_slot = correlated.context_slot, + owner = ?correlated.owner, + lamports = ?correlated.lamports, + "correlated confirmed token account update signal" + ); + } + crate::KhbbCorrelatedSignal::PotentialTokenBootstrapFlow(correlated) => { + tracing::trace!( + listener_session_id = session.id, + pubkey = ?correlated.pubkey, + context_slot = correlated.context_slot, + saw_token_program = correlated.saw_token_program, + saw_associated_token_account = correlated.saw_associated_token_account, + saw_bootstrap_logs = correlated.saw_bootstrap_logs, + "correlated potential token bootstrap flow signal" + ); + } + crate::KhbbCorrelatedSignal::PotentialNewTokenMint(_) => {} + } + } + } + Err(error) => { + tracing::error!( + listener_session_id = session.id, + error = %error, + pubkey = %classified.pubkey, + "failed to correlate confirmed token account activity" + ); + } + } } Ok(Some(crate::KhbbEnrichedConfirmedEvent::ConfirmedMintAccountActivity(inner))) => { tracing::trace!( @@ -681,6 +728,41 @@ pub async fn run_listener_runtime( lamports = ?inner.lamports, "confirmed mint account activity event" ); + let correlated_result = + crate::signal_correlation::correlate_signals( + &crate::KhbbEnrichedConfirmedEvent::ConfirmedMintAccountActivity( + inner.clone(), + ), + &heuristic_signals, + ); + match correlated_result { + Ok(signals) => { + for signal in signals { + match signal { + crate::KhbbCorrelatedSignal::PotentialNewTokenMint(correlated) => { + tracing::trace!( + listener_session_id = session.id, + pubkey = %correlated.pubkey, + context_slot = correlated.context_slot, + owner = ?correlated.owner, + lamports = ?correlated.lamports, + "correlated potential new token mint signal" + ); + } + crate::KhbbCorrelatedSignal::ConfirmedTokenAccountUpdate(_) => {} + crate::KhbbCorrelatedSignal::PotentialTokenBootstrapFlow(_) => {} + } + } + } + Err(error) => { + tracing::error!( + listener_session_id = session.id, + error = %error, + pubkey = %classified.pubkey, + "failed to correlate confirmed mint account activity" + ); + } + } } Ok(Some(crate::KhbbEnrichedConfirmedEvent::UnknownTokenProgramAccountActivity(inner))) => { tracing::trace!( @@ -691,6 +773,42 @@ pub async fn run_listener_runtime( lamports = ?inner.lamports, "unknown token-program-owned account activity event" ); + let correlated_result = + crate::signal_correlation::correlate_signals( + &crate::KhbbEnrichedConfirmedEvent::UnknownTokenProgramAccountActivity( + inner.clone(), + ), + &heuristic_signals, + ); + match correlated_result { + Ok(signals) => { + for signal in signals { + match signal { + crate::KhbbCorrelatedSignal::PotentialTokenBootstrapFlow(correlated) => { + tracing::trace!( + listener_session_id = session.id, + pubkey = ?correlated.pubkey, + context_slot = correlated.context_slot, + saw_token_program = correlated.saw_token_program, + saw_associated_token_account = correlated.saw_associated_token_account, + saw_bootstrap_logs = correlated.saw_bootstrap_logs, + "correlated potential token bootstrap flow signal" + ); + } + crate::KhbbCorrelatedSignal::ConfirmedTokenAccountUpdate(_) => {} + crate::KhbbCorrelatedSignal::PotentialNewTokenMint(_) => {} + } + } + } + Err(error) => { + tracing::error!( + listener_session_id = session.id, + error = %error, + pubkey = %classified.pubkey, + "failed to correlate unknown token-program-owned account activity" + ); + } + } } Ok(None) => {} Err(error) => { diff --git a/khbb_lib/src/signal_correlation.rs b/khbb_lib/src/signal_correlation.rs new file mode 100644 index 0000000..4879192 --- /dev/null +++ b/khbb_lib/src/signal_correlation.rs @@ -0,0 +1,231 @@ +// file: khbb_lib/src/signal_correlation.rs + +//! Local correlation of enriched and heuristic signals into stronger trading-oriented signals. + +/// Correlated signal produced from enriched and heuristic activity. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum KhbbCorrelatedSignal { + /// Confirmed token account update. + ConfirmedTokenAccountUpdate(KhbbConfirmedTokenAccountUpdateSignal), + /// Potential new token mint. + PotentialNewTokenMint(KhbbPotentialNewTokenMintSignal), + /// Potential token bootstrap flow. + PotentialTokenBootstrapFlow(KhbbPotentialTokenBootstrapFlowSignal), +} + +/// Correlated signal indicating a confirmed token account update. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KhbbConfirmedTokenAccountUpdateSignal { + /// Account pubkey. + pub pubkey: std::string::String, + /// Context slot. + pub context_slot: u64, + /// Owner program if available. + pub owner: std::option::Option, + /// Lamports if available. + pub lamports: std::option::Option, +} + +/// Correlated signal indicating a potential new token mint. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KhbbPotentialNewTokenMintSignal { + /// Account pubkey. + pub pubkey: std::string::String, + /// Context slot. + pub context_slot: u64, + /// Owner program if available. + pub owner: std::option::Option, + /// Lamports if available. + pub lamports: std::option::Option, +} + +/// Correlated signal indicating a potential token bootstrap flow. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KhbbPotentialTokenBootstrapFlowSignal { + /// Account pubkey if one is available from the enriched side. + pub pubkey: std::option::Option, + /// Context slot. + pub context_slot: u64, + /// Whether token program activity was seen. + pub saw_token_program: bool, + /// Whether associated token account activity was seen. + pub saw_associated_token_account: bool, + /// Whether bootstrap-style logs were seen. + pub saw_bootstrap_logs: bool, +} + +/// Correlates an enriched confirmed event with heuristic signals. +pub(crate) fn correlate_signals( + enriched_event: &crate::KhbbEnrichedConfirmedEvent, + heuristic_signals: &[crate::KhbbHeuristicSignal], +) -> core::result::Result, crate::KhbbError> { + let mut results = std::vec::Vec::::new(); + match enriched_event { + crate::KhbbEnrichedConfirmedEvent::ConfirmedTokenAccountActivity(event) => { + results.push(KhbbCorrelatedSignal::ConfirmedTokenAccountUpdate( + KhbbConfirmedTokenAccountUpdateSignal { + pubkey: event.pubkey.clone(), + context_slot: event.context_slot, + owner: event.owner.clone(), + lamports: event.lamports, + }, + )); + let mut saw_token_program = false; + let mut saw_associated_token_account = false; + let mut saw_bootstrap_logs = false; + for signal in heuristic_signals { + match signal { + crate::KhbbHeuristicSignal::PotentialTokenAccountActivity(inner) => { + if inner.pubkey == event.pubkey { + saw_token_program = true; + } + }, + crate::KhbbHeuristicSignal::PotentialAssociatedTokenAccountActivity(inner) => { + if inner.pubkey == event.pubkey { + saw_associated_token_account = true; + } + }, + crate::KhbbHeuristicSignal::PotentialTokenBootstrapActivity(_) => { + saw_bootstrap_logs = true; + }, + _ => {}, + } + } + if saw_token_program && (saw_associated_token_account || saw_bootstrap_logs) { + results.push(KhbbCorrelatedSignal::PotentialTokenBootstrapFlow( + KhbbPotentialTokenBootstrapFlowSignal { + pubkey: Some(event.pubkey.clone()), + context_slot: event.context_slot, + saw_token_program, + saw_associated_token_account, + saw_bootstrap_logs, + }, + )); + } + }, + crate::KhbbEnrichedConfirmedEvent::ConfirmedMintAccountActivity(event) => { + results.push(KhbbCorrelatedSignal::PotentialNewTokenMint( + KhbbPotentialNewTokenMintSignal { + pubkey: event.pubkey.clone(), + context_slot: event.context_slot, + owner: event.owner.clone(), + lamports: event.lamports, + }, + )); + }, + crate::KhbbEnrichedConfirmedEvent::UnknownTokenProgramAccountActivity(event) => { + let mut saw_bootstrap_logs = false; + for signal in heuristic_signals { + if let crate::KhbbHeuristicSignal::PotentialTokenBootstrapActivity(_) = signal { + saw_bootstrap_logs = true; + } + } + if saw_bootstrap_logs { + results.push(KhbbCorrelatedSignal::PotentialTokenBootstrapFlow( + KhbbPotentialTokenBootstrapFlowSignal { + pubkey: Some(event.pubkey.clone()), + context_slot: event.context_slot, + saw_token_program: true, + saw_associated_token_account: false, + saw_bootstrap_logs, + }, + )); + } + }, + } + Ok(results) +} + +#[cfg(test)] +mod tests { + #[test] + fn correlate_confirmed_token_account_returns_confirmed_update() { + let enriched = crate::KhbbEnrichedConfirmedEvent::ConfirmedTokenAccountActivity( + crate::KhbbConfirmedTokenAccountActivityEvent { + pubkey: std::string::String::from("SomePubkey"), + context_slot: 100, + owner: Some(crate::ids::SPL_TOKEN_PROGRAM_ID.to_string()), + lamports: Some(123), + }, + ); + let heuristics = vec![crate::KhbbHeuristicSignal::PotentialTokenAccountActivity( + crate::KhbbPotentialTokenAccountActivitySignal { + pubkey: std::string::String::from("SomePubkey"), + context_slot: 100, + subscription_id: 1, + token_program_family: std::string::String::from("spl-token"), + }, + )]; + let result = super::correlate_signals(&enriched, &heuristics); + assert!(result.is_ok()); + let signals = result.expect("correlate token account"); + assert_eq!(signals.len(), 1); + match &signals[0] { + super::KhbbCorrelatedSignal::ConfirmedTokenAccountUpdate(inner) => { + assert_eq!(inner.pubkey, "SomePubkey"); + }, + _ => { + panic!("expected confirmed token account update"); + }, + } + } + + #[test] + fn correlate_confirmed_mint_returns_potential_new_token_mint() { + let enriched = crate::KhbbEnrichedConfirmedEvent::ConfirmedMintAccountActivity( + crate::KhbbConfirmedMintAccountActivityEvent { + pubkey: std::string::String::from("MintPubkey"), + context_slot: 200, + owner: Some(crate::ids::SPL_TOKEN_PROGRAM_ID.to_string()), + lamports: Some(456), + }, + ); + let heuristics = vec![]; + let result = super::correlate_signals(&enriched, &heuristics); + assert!(result.is_ok()); + let signals = result.expect("correlate mint"); + assert_eq!(signals.len(), 1); + match &signals[0] { + super::KhbbCorrelatedSignal::PotentialNewTokenMint(inner) => { + assert_eq!(inner.pubkey, "MintPubkey"); + }, + _ => { + panic!("expected potential new token mint"); + }, + } + } + + #[test] + fn correlate_confirmed_token_account_returns_bootstrap_flow_when_context_matches() { + let enriched = crate::KhbbEnrichedConfirmedEvent::ConfirmedTokenAccountActivity( + crate::KhbbConfirmedTokenAccountActivityEvent { + pubkey: std::string::String::from("AtaLikePubkey"), + context_slot: 300, + owner: Some(crate::ids::SPL_TOKEN_PROGRAM_ID.to_string()), + lamports: Some(789), + }, + ); + let heuristics = vec![ + crate::KhbbHeuristicSignal::PotentialTokenAccountActivity( + crate::KhbbPotentialTokenAccountActivitySignal { + pubkey: std::string::String::from("AtaLikePubkey"), + context_slot: 300, + subscription_id: 2, + token_program_family: std::string::String::from("spl-token"), + }, + ), + crate::KhbbHeuristicSignal::PotentialAssociatedTokenAccountActivity( + crate::KhbbPotentialAssociatedTokenAccountActivitySignal { + pubkey: std::string::String::from("AtaLikePubkey"), + context_slot: 300, + subscription_id: 2, + token_program_family: std::string::String::from("spl-token"), + }, + ), + ]; + let result = super::correlate_signals(&enriched, &heuristics); + assert!(result.is_ok()); + let signals = result.expect("correlate bootstrap"); + assert_eq!(signals.len(), 2); + } +}