From 6dab32d4c5ad4ae14f2910873454269029f1ee66 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sat, 18 Apr 2026 22:50:18 +0200 Subject: [PATCH] 0.5.4 --- Cargo.toml | 2 +- khbb_lib/src/account_enrichment.rs | 111 ++++++++++++++++++++ khbb_lib/src/lib.rs | 5 + khbb_lib/src/listener.rs | 160 +++++++++++++++++++++++++++++ khbb_lib/src/solana_rpc_http.rs | 44 ++++++++ 5 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 khbb_lib/src/account_enrichment.rs diff --git a/Cargo.toml b/Cargo.toml index e0aafe0..fd550a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.5.3" +version = "0.5.4" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-bobot" diff --git a/khbb_lib/src/account_enrichment.rs b/khbb_lib/src/account_enrichment.rs new file mode 100644 index 0000000..95d0d5e --- /dev/null +++ b/khbb_lib/src/account_enrichment.rs @@ -0,0 +1,111 @@ +// file: khbb_lib/src/account_enrichment.rs + +//! Targeted account enrichment derived from HTTP RPC account lookups. + +/// Minimal enriched account category derived from RPC account data. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum KhbbEnrichedAccountKind { + /// Account looks like a token mint account. + PotentialMintAccount, + /// Account looks like a token holding account. + PotentialTokenAccount, + /// Account exists but does not currently match a recognized token shape. + UnknownAccount, +} + +/// Minimal enriched account snapshot. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KhbbEnrichedAccountSnapshot { + /// Queried pubkey. + pub pubkey: std::string::String, + /// Slot used by the RPC context. + pub context_slot: u64, + /// Whether the account exists. + pub exists: bool, + /// Owner program if available. + pub owner: std::option::Option, + /// Lamports if available. + pub lamports: std::option::Option, + /// Parsed kind if inferable. + pub kind: std::option::Option, +} + +/// Builds an enriched account snapshot from an RPC `getAccountInfo` response. +pub(crate) fn build_enriched_account_snapshot( + pubkey: &str, + response: &solana_rpc_client_api::response::Response< + std::option::Option, + >, +) -> core::result::Result { + let account_option = &response.value; + match account_option { + Some(account) => { + let owner = Some(account.owner.clone()); + let lamports = Some(account.lamports); + let kind = infer_account_kind(account); + Ok(KhbbEnrichedAccountSnapshot { + pubkey: std::string::String::from(pubkey), + context_slot: response.context.slot, + exists: true, + owner, + lamports, + kind, + }) + }, + None => Ok(KhbbEnrichedAccountSnapshot { + pubkey: std::string::String::from(pubkey), + context_slot: response.context.slot, + exists: false, + owner: None, + lamports: None, + kind: None, + }), + } +} + +fn infer_account_kind( + account: &solana_account_decoder_client_types::UiAccount, +) -> std::option::Option { + let owner = account.owner.as_str(); + let spl_token_owner = crate::ids::SPL_TOKEN_PROGRAM_ID.to_string(); + let spl_token_2022_owner = crate::ids::SPL_TOKEN_2022_PROGRAM_ID.to_string(); + if owner != spl_token_owner && owner != spl_token_2022_owner { + return Some(KhbbEnrichedAccountKind::UnknownAccount); + } + match &account.data { + solana_account_decoder_client_types::UiAccountData::Json(parsed) => { + match parsed.program.as_str() { + "spl-token" | "spl-token-2022" => { + let parsed_type = parsed.parsed.get("type").and_then(serde_json::Value::as_str); + match parsed_type { + Some("mint") => Some(KhbbEnrichedAccountKind::PotentialMintAccount), + Some("account") => Some(KhbbEnrichedAccountKind::PotentialTokenAccount), + _ => Some(KhbbEnrichedAccountKind::UnknownAccount), + } + }, + _ => Some(KhbbEnrichedAccountKind::UnknownAccount), + } + }, + _ => Some(KhbbEnrichedAccountKind::UnknownAccount), + } +} + +#[cfg(test)] +mod tests { + #[test] + fn build_enriched_account_snapshot_returns_missing_when_value_is_none() { + let response = solana_rpc_client_api::response::Response { + context: solana_rpc_client_api::response::RpcResponseContext { + slot: 123, + api_version: None, + }, + value: None, + }; + let result = super::build_enriched_account_snapshot("SomePubkey", &response); + assert!(result.is_ok()); + let snapshot = result.expect("snapshot"); + assert!(!snapshot.exists); + assert_eq!(snapshot.context_slot, 123); + assert!(snapshot.kind.is_none()); + } +} diff --git a/khbb_lib/src/lib.rs b/khbb_lib/src/lib.rs index c88a69b..537113c 100644 --- a/khbb_lib/src/lib.rs +++ b/khbb_lib/src/lib.rs @@ -24,6 +24,7 @@ mod program_registry; mod domain_classifier; mod ids; mod heuristics; +mod account_enrichment; /// Runs the listener application bootstrap workflow. pub use crate::app::run_listener_app; @@ -113,3 +114,7 @@ pub use crate::heuristics::KhbbPotentialInitialTokenActivitySignal; pub use crate::heuristics::KhbbPotentialAssociatedTokenAccountActivitySignal; /// Heuristic signal indicating a likely token bootstrap-related flow. pub use crate::heuristics::KhbbPotentialTokenBootstrapActivitySignal; +/// Minimal enriched account category derived from RPC account data. +pub use crate::account_enrichment::KhbbEnrichedAccountKind; +/// Minimal enriched account snapshot. +pub use crate::account_enrichment::KhbbEnrichedAccountSnapshot; diff --git a/khbb_lib/src/listener.rs b/khbb_lib/src/listener.rs index 3226411..d97b092 100644 --- a/khbb_lib/src/listener.rs +++ b/khbb_lib/src/listener.rs @@ -606,6 +606,86 @@ pub async fn run_listener_runtime( ); } } + let account_info_result = http_client + .get_account_info( + &classified.pubkey, + 10_000u64 + .saturating_add(tick_count) + .saturating_add(classified.subscription_id), + ) + .await; + match account_info_result { + Ok(call_output) => { + let status = if call_output.response.error.is_some() { + "error" + } else { + "ok" + }; + let insert_result = crate::storage::insert_raw_http_rpc_message( + pool, + session.id, + call_output.request_id as i64, + &call_output.method, + &call_output.request_body, + &call_output.response_body, + status, + ) + .await; + if let Err(error) = insert_result { + tracing::error!( + listener_session_id = session.id, + error = %error, + "failed to store enriched getAccountInfo rpc message" + ); + } + match call_output.response.result { + Some(response_value) => { + let enrichment_result = + crate::account_enrichment::build_enriched_account_snapshot( + &classified.pubkey, + &response_value, + ); + match enrichment_result { + Ok(snapshot) => { + tracing::trace!( + listener_session_id = session.id, + pubkey = %snapshot.pubkey, + context_slot = snapshot.context_slot, + exists = snapshot.exists, + owner = ?snapshot.owner, + lamports = ?snapshot.lamports, + kind = ?snapshot.kind, + "enriched token program account snapshot" + ); + } + Err(error) => { + tracing::error!( + listener_session_id = session.id, + error = %error, + pubkey = %classified.pubkey, + "failed to build enriched account snapshot" + ); + } + } + } + None => { + tracing::trace!( + listener_session_id = session.id, + pubkey = %classified.pubkey, + "getAccountInfo returned no result payload" + ); + } + } + } + Err(error) => { + tracing::error!( + listener_session_id = session.id, + error = %error, + pubkey = %classified.pubkey, + "failed to enrich spl-token account via getAccountInfo" + ); + } + } } Ok(Some(crate::KhbbClassifiedDomainEvent::SplToken2022ProgramActivity(classified))) => { tracing::trace!( @@ -667,6 +747,86 @@ pub async fn run_listener_runtime( ); } } + let account_info_result = http_client + .get_account_info( + &classified.pubkey, + 10_000u64 + .saturating_add(tick_count) + .saturating_add(classified.subscription_id), + ) + .await; + match account_info_result { + Ok(call_output) => { + let status = if call_output.response.error.is_some() { + "error" + } else { + "ok" + }; + let insert_result = crate::storage::insert_raw_http_rpc_message( + pool, + session.id, + call_output.request_id as i64, + &call_output.method, + &call_output.request_body, + &call_output.response_body, + status, + ) + .await; + if let Err(error) = insert_result { + tracing::error!( + listener_session_id = session.id, + error = %error, + "failed to store enriched getAccountInfo rpc message" + ); + } + match call_output.response.result { + Some(response_value) => { + let enrichment_result = + crate::account_enrichment::build_enriched_account_snapshot( + &classified.pubkey, + &response_value, + ); + match enrichment_result { + Ok(snapshot) => { + tracing::trace!( + listener_session_id = session.id, + pubkey = %snapshot.pubkey, + context_slot = snapshot.context_slot, + exists = snapshot.exists, + owner = ?snapshot.owner, + lamports = ?snapshot.lamports, + kind = ?snapshot.kind, + "enriched token program account snapshot" + ); + } + Err(error) => { + tracing::error!( + listener_session_id = session.id, + error = %error, + pubkey = %classified.pubkey, + "failed to build enriched account snapshot" + ); + } + } + } + None => { + tracing::trace!( + listener_session_id = session.id, + pubkey = %classified.pubkey, + "getAccountInfo returned no result payload" + ); + } + } + } + Err(error) => { + tracing::error!( + listener_session_id = session.id, + error = %error, + pubkey = %classified.pubkey, + "failed to enrich spl-token-2022 account via getAccountInfo" + ); + } + } } Ok(Some(_)) => {} Ok(None) => {} diff --git a/khbb_lib/src/solana_rpc_http.rs b/khbb_lib/src/solana_rpc_http.rs index d065728..69ff3a7 100644 --- a/khbb_lib/src/solana_rpc_http.rs +++ b/khbb_lib/src/solana_rpc_http.rs @@ -232,6 +232,40 @@ impl KhbbSolanaHttpRpcClient { ) .await } + + /// Calls the `getAccountInfo` RPC method using JSON-parsed account encoding. + pub async fn get_account_info( + &self, + pubkey: &str, + id: u64, + ) -> core::result::Result< + KhbbHttpRpcCallOutput< + solana_rpc_client_api::response::Response< + std::option::Option, + >, + >, + crate::KhbbError, + > { + if pubkey.trim().is_empty() { + return Err(crate::KhbbError::Config { + message: std::string::String::from("get_account_info pubkey must not be empty"), + }); + } + let params = serde_json::json!([ + pubkey, + { + "encoding": "jsonParsed" + } + ]); + self.send_json_rpc_request::, + >, serde_json::Value>( + solana_rpc_client_api::request::RpcRequest::GetAccountInfo, + params, + id, + ) + .await + } } pub(crate) fn rpc_request_method_name( @@ -241,6 +275,7 @@ pub(crate) fn rpc_request_method_name( solana_rpc_client_api::request::RpcRequest::GetHealth => Ok("getHealth"), solana_rpc_client_api::request::RpcRequest::GetSlot => Ok("getSlot"), solana_rpc_client_api::request::RpcRequest::GetVersion => Ok("getVersion"), + solana_rpc_client_api::request::RpcRequest::GetAccountInfo => Ok("getAccountInfo"), _ => Err(crate::KhbbError::Config { message: std::format!( "unsupported rpc request variant in khbb minimal http client: {:?}", @@ -404,4 +439,13 @@ mod tests { let result = super::KhbbSolanaHttpRpcClient::new(config); assert!(result.is_ok()); } + + #[test] + fn rpc_request_method_name_maps_get_account_info_variant() { + let result = super::rpc_request_method_name( + solana_rpc_client_api::request::RpcRequest::GetAccountInfo, + ); + assert!(result.is_ok()); + assert_eq!(result.expect("getAccountInfo"), "getAccountInfo"); + } }