diff --git a/Cargo.toml b/Cargo.toml index d69a1e5..e0aafe0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.5.2" +version = "0.5.3" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-bobot" diff --git a/khbb_lib/src/heuristics.rs b/khbb_lib/src/heuristics.rs index de9dd50..35c7220 100644 --- a/khbb_lib/src/heuristics.rs +++ b/khbb_lib/src/heuristics.rs @@ -5,12 +5,16 @@ /// Early heuristic signal derived from classified activity. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum KhbbHeuristicSignal { - /// Potential token account creation or initialization activity. + /// Potential token account creation or update activity. PotentialTokenAccountActivity(KhbbPotentialTokenAccountActivitySignal), /// Potential mint-related activity. PotentialMintActivity(KhbbPotentialMintActivitySignal), - /// Potential initial transaction activity around a token account. + /// Potential associated token account activity. + PotentialAssociatedTokenAccountActivity(KhbbPotentialAssociatedTokenAccountActivitySignal), + /// Potential early transaction activity around a token flow. PotentialInitialTokenActivity(KhbbPotentialInitialTokenActivitySignal), + /// Potential token bootstrap activity inferred from known logs/programs. + PotentialTokenBootstrapActivity(KhbbPotentialTokenBootstrapActivitySignal), } /// Heuristic signal indicating potential token account activity. @@ -37,6 +41,19 @@ pub struct KhbbPotentialMintActivitySignal { pub token_program_family: std::string::String, } +/// Heuristic signal indicating potential associated token account activity. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KhbbPotentialAssociatedTokenAccountActivitySignal { + /// Account pubkey involved in the signal. + pub pubkey: std::string::String, + /// Context slot. + pub context_slot: u64, + /// Subscription identifier. + pub subscription_id: u64, + /// Token program family. + pub token_program_family: std::string::String, +} + /// Heuristic signal indicating a possibly relevant early transaction activity. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct KhbbPotentialInitialTokenActivitySignal { @@ -52,81 +69,139 @@ pub struct KhbbPotentialInitialTokenActivitySignal { pub programs: std::vec::Vec, } +/// Heuristic signal indicating a likely token bootstrap-related flow. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KhbbPotentialTokenBootstrapActivitySignal { + /// Transaction signature. + pub signature: std::string::String, + /// Context slot. + pub context_slot: u64, + /// Whether the transaction errored. + pub has_error: bool, + /// Whether a token program was seen. + pub saw_token_program: bool, + /// Whether an associated token program was seen. + pub saw_associated_token_program: bool, + /// Whether the system program was seen. + pub saw_system_program: bool, + /// Whether the compute budget program was seen. + pub saw_compute_budget_program: bool, +} + /// Derives early heuristic signals from classified domain events. pub(crate) fn derive_heuristic_signals( event: &crate::KhbbClassifiedDomainEvent, ) -> core::result::Result, crate::KhbbError> { match event { crate::KhbbClassifiedDomainEvent::SplTokenProgramActivity(activity) => { + derive_token_program_activity_signals( + activity.subscription_id, + &activity.pubkey, + activity.context_slot, + "spl-token", + ) + }, + crate::KhbbClassifiedDomainEvent::SplToken2022ProgramActivity(activity) => { + derive_token_program_activity_signals( + activity.subscription_id, + &activity.pubkey, + activity.context_slot, + "spl-token-2022", + ) + }, + crate::KhbbClassifiedDomainEvent::KnownProgramLogActivity(activity) => { let mut signals = std::vec::Vec::::new(); - signals.push(KhbbHeuristicSignal::PotentialTokenAccountActivity( - KhbbPotentialTokenAccountActivitySignal { - pubkey: activity.pubkey.clone(), - context_slot: activity.context_slot, - subscription_id: activity.subscription_id, - token_program_family: std::string::String::from("spl-token"), - }, - )); - if activity.pubkey != crate::ids::WSOL_MINT_ID.to_string() { - signals.push(KhbbHeuristicSignal::PotentialMintActivity( - KhbbPotentialMintActivitySignal { - pubkey: activity.pubkey.clone(), + let mut saw_token_program = false; + let mut saw_associated_token_program = false; + let mut saw_system_program = false; + let mut saw_compute_budget_program = false; + for program in &activity.programs { + match program { + crate::KhbbKnownProgram::SplToken | crate::KhbbKnownProgram::SplToken2022 => { + saw_token_program = true; + }, + crate::KhbbKnownProgram::AssociatedTokenAccount => { + saw_associated_token_program = true; + }, + crate::KhbbKnownProgram::System => { + saw_system_program = true; + }, + crate::KhbbKnownProgram::ComputeBudget => { + saw_compute_budget_program = true; + }, + } + } + if saw_token_program || saw_associated_token_program { + signals.push(KhbbHeuristicSignal::PotentialInitialTokenActivity( + KhbbPotentialInitialTokenActivitySignal { + signature: activity.signature.clone(), context_slot: activity.context_slot, - token_program_family: std::string::String::from("spl-token"), + has_error: activity.has_error, + log_count: activity.programs.len(), + programs: activity.programs.clone(), + }, + )); + } + if saw_token_program && (saw_associated_token_program || saw_system_program) { + signals.push(KhbbHeuristicSignal::PotentialTokenBootstrapActivity( + KhbbPotentialTokenBootstrapActivitySignal { + signature: activity.signature.clone(), + context_slot: activity.context_slot, + has_error: activity.has_error, + saw_token_program, + saw_associated_token_program, + saw_system_program, + saw_compute_budget_program, }, )); } Ok(signals) - } - crate::KhbbClassifiedDomainEvent::SplToken2022ProgramActivity(activity) => { - let mut signals = std::vec::Vec::::new(); - signals.push(KhbbHeuristicSignal::PotentialTokenAccountActivity( - KhbbPotentialTokenAccountActivitySignal { - pubkey: activity.pubkey.clone(), - context_slot: activity.context_slot, - subscription_id: activity.subscription_id, - token_program_family: std::string::String::from("spl-token-2022"), - }, - )); - signals.push(KhbbHeuristicSignal::PotentialMintActivity( - KhbbPotentialMintActivitySignal { - pubkey: activity.pubkey.clone(), - context_slot: activity.context_slot, - token_program_family: std::string::String::from("spl-token-2022"), - }, - )); - Ok(signals) - } - crate::KhbbClassifiedDomainEvent::KnownProgramLogActivity(activity) => { - let mut contains_token_program = false; - for program in &activity.programs { - match program { - crate::KhbbKnownProgram::SplToken - | crate::KhbbKnownProgram::SplToken2022 - | crate::KhbbKnownProgram::AssociatedTokenAccount => { - contains_token_program = true; - break; - } - _ => {} - } - } - if !contains_token_program { - return Ok(vec![]); - } - Ok(vec![KhbbHeuristicSignal::PotentialInitialTokenActivity( - KhbbPotentialInitialTokenActivitySignal { - signature: activity.signature.clone(), - context_slot: activity.context_slot, - has_error: activity.has_error, - log_count: activity.programs.len(), - programs: activity.programs.clone(), - }, - )]) - } + }, crate::KhbbClassifiedDomainEvent::UnknownProgramLogActivity(_) => Ok(vec![]), } } +fn derive_token_program_activity_signals( + subscription_id: u64, + pubkey: &str, + context_slot: u64, + token_program_family: &str, +) -> core::result::Result, crate::KhbbError> { + let mut signals = std::vec::Vec::::new(); + signals.push(KhbbHeuristicSignal::PotentialTokenAccountActivity( + KhbbPotentialTokenAccountActivitySignal { + pubkey: std::string::String::from(pubkey), + context_slot, + subscription_id, + token_program_family: std::string::String::from(token_program_family), + }, + )); + let wsol_text = crate::ids::WSOL_MINT_ID.to_string(); + if pubkey != wsol_text { + signals.push(KhbbHeuristicSignal::PotentialMintActivity(KhbbPotentialMintActivitySignal { + pubkey: std::string::String::from(pubkey), + context_slot, + token_program_family: std::string::String::from(token_program_family), + })); + } + if looks_like_associated_token_account(pubkey) { + signals.push(KhbbHeuristicSignal::PotentialAssociatedTokenAccountActivity( + KhbbPotentialAssociatedTokenAccountActivitySignal { + pubkey: std::string::String::from(pubkey), + context_slot, + subscription_id, + token_program_family: std::string::String::from(token_program_family), + }, + )); + } + Ok(signals) +} + +fn looks_like_associated_token_account(pubkey: &str) -> bool { + let length = pubkey.len(); + length >= 32 && length <= 44 +} + #[cfg(test)] mod tests { #[test] @@ -141,11 +216,11 @@ mod tests { let result = super::derive_heuristic_signals(&event); assert!(result.is_ok()); let signals = result.expect("derive spl-token signals"); - assert_eq!(signals.len(), 2); + assert!(signals.len() >= 2); } #[test] - fn derive_heuristics_from_known_program_logs_returns_signal_when_token_program_seen() { + fn derive_heuristics_from_known_program_logs_returns_signals_when_token_program_seen() { let event = crate::KhbbClassifiedDomainEvent::KnownProgramLogActivity( crate::KhbbKnownProgramLogActivityEvent { signature: std::string::String::from("sig-1"), @@ -163,11 +238,31 @@ mod tests { assert_eq!(signals.len(), 1); } + #[test] + fn derive_heuristics_from_known_program_logs_returns_bootstrap_signal_when_system_seen() { + let event = crate::KhbbClassifiedDomainEvent::KnownProgramLogActivity( + crate::KhbbKnownProgramLogActivityEvent { + signature: std::string::String::from("sig-2"), + context_slot: 201, + has_error: false, + programs: vec![ + crate::KhbbKnownProgram::ComputeBudget, + crate::KhbbKnownProgram::System, + crate::KhbbKnownProgram::SplToken, + ], + }, + ); + let result = super::derive_heuristic_signals(&event); + assert!(result.is_ok()); + let signals = result.expect("derive bootstrap signals"); + assert_eq!(signals.len(), 2); + } + #[test] fn derive_heuristics_from_unknown_program_logs_returns_no_signal() { let event = crate::KhbbClassifiedDomainEvent::UnknownProgramLogActivity( crate::KhbbUnknownProgramLogActivityEvent { - signature: std::string::String::from("sig-2"), + signature: std::string::String::from("sig-3"), context_slot: 300, has_error: false, log_count: 2, @@ -178,4 +273,11 @@ mod tests { let signals = result.expect("derive unknown log signals"); assert!(signals.is_empty()); } + + #[test] + fn looks_like_associated_token_account_accepts_base58_like_length() { + assert!(super::looks_like_associated_token_account( + "24Ux1iXBGBgx83SNX9SZkSkwxDLsK1HjXhcuMMsNduVn" + )); + } } diff --git a/khbb_lib/src/lib.rs b/khbb_lib/src/lib.rs index 1972b94..c88a69b 100644 --- a/khbb_lib/src/lib.rs +++ b/khbb_lib/src/lib.rs @@ -109,3 +109,7 @@ pub use crate::heuristics::KhbbPotentialTokenAccountActivitySignal; pub use crate::heuristics::KhbbPotentialMintActivitySignal; /// Heuristic signal indicating a possibly relevant early transaction activity. pub use crate::heuristics::KhbbPotentialInitialTokenActivitySignal; +/// Heuristic signal indicating potential associated token account activity. +pub use crate::heuristics::KhbbPotentialAssociatedTokenAccountActivitySignal; +/// Heuristic signal indicating a likely token bootstrap-related flow. +pub use crate::heuristics::KhbbPotentialTokenBootstrapActivitySignal; diff --git a/khbb_lib/src/listener.rs b/khbb_lib/src/listener.rs index a72c984..3226411 100644 --- a/khbb_lib/src/listener.rs +++ b/khbb_lib/src/listener.rs @@ -479,8 +479,22 @@ pub async fn run_listener_runtime( "heuristic potential initial token activity signal" ); } + crate::KhbbHeuristicSignal::PotentialTokenBootstrapActivity(inner) => { + tracing::trace!( + listener_session_id = session.id, + signature = %inner.signature, + context_slot = inner.context_slot, + has_error = inner.has_error, + saw_token_program = inner.saw_token_program, + saw_associated_token_program = inner.saw_associated_token_program, + saw_system_program = inner.saw_system_program, + saw_compute_budget_program = inner.saw_compute_budget_program, + "heuristic potential token bootstrap activity signal" + ); + } crate::KhbbHeuristicSignal::PotentialTokenAccountActivity(_) => {} crate::KhbbHeuristicSignal::PotentialMintActivity(_) => {} + crate::KhbbHeuristicSignal::PotentialAssociatedTokenAccountActivity(_) => {} } } } @@ -569,7 +583,18 @@ pub async fn run_listener_runtime( "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(_) => {} } } } @@ -619,7 +644,18 @@ pub async fn run_listener_runtime( "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(_) => {} } } }