This commit is contained in:
2026-04-18 22:50:18 +02:00
parent d7668c003b
commit 6dab32d4c5
5 changed files with 321 additions and 1 deletions

View File

@@ -8,7 +8,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.5.3" version = "0.5.4"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-bobot" repository = "https://git.sasedev.com/Sasedev/khadhroony-bobot"

View File

@@ -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<std::string::String>,
/// Lamports if available.
pub lamports: std::option::Option<u64>,
/// Parsed kind if inferable.
pub kind: std::option::Option<KhbbEnrichedAccountKind>,
}
/// 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<solana_account_decoder_client_types::UiAccount>,
>,
) -> core::result::Result<KhbbEnrichedAccountSnapshot, crate::KhbbError> {
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<KhbbEnrichedAccountKind> {
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());
}
}

View File

@@ -24,6 +24,7 @@ mod program_registry;
mod domain_classifier; mod domain_classifier;
mod ids; mod ids;
mod heuristics; mod heuristics;
mod account_enrichment;
/// Runs the listener application bootstrap workflow. /// Runs the listener application bootstrap workflow.
pub use crate::app::run_listener_app; pub use crate::app::run_listener_app;
@@ -113,3 +114,7 @@ pub use crate::heuristics::KhbbPotentialInitialTokenActivitySignal;
pub use crate::heuristics::KhbbPotentialAssociatedTokenAccountActivitySignal; pub use crate::heuristics::KhbbPotentialAssociatedTokenAccountActivitySignal;
/// Heuristic signal indicating a likely token bootstrap-related flow. /// Heuristic signal indicating a likely token bootstrap-related flow.
pub use crate::heuristics::KhbbPotentialTokenBootstrapActivitySignal; 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;

View File

@@ -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))) => { Ok(Some(crate::KhbbClassifiedDomainEvent::SplToken2022ProgramActivity(classified))) => {
tracing::trace!( 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(Some(_)) => {}
Ok(None) => {} Ok(None) => {}

View File

@@ -232,6 +232,40 @@ impl KhbbSolanaHttpRpcClient {
) )
.await .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<solana_account_decoder_client_types::UiAccount>,
>,
>,
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::<solana_rpc_client_api::response::Response<
std::option::Option<solana_account_decoder_client_types::UiAccount>,
>, serde_json::Value>(
solana_rpc_client_api::request::RpcRequest::GetAccountInfo,
params,
id,
)
.await
}
} }
pub(crate) fn rpc_request_method_name( 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::GetHealth => Ok("getHealth"),
solana_rpc_client_api::request::RpcRequest::GetSlot => Ok("getSlot"), solana_rpc_client_api::request::RpcRequest::GetSlot => Ok("getSlot"),
solana_rpc_client_api::request::RpcRequest::GetVersion => Ok("getVersion"), solana_rpc_client_api::request::RpcRequest::GetVersion => Ok("getVersion"),
solana_rpc_client_api::request::RpcRequest::GetAccountInfo => Ok("getAccountInfo"),
_ => Err(crate::KhbbError::Config { _ => Err(crate::KhbbError::Config {
message: std::format!( message: std::format!(
"unsupported rpc request variant in khbb minimal http client: {:?}", "unsupported rpc request variant in khbb minimal http client: {:?}",
@@ -404,4 +439,13 @@ mod tests {
let result = super::KhbbSolanaHttpRpcClient::new(config); let result = super::KhbbSolanaHttpRpcClient::new(config);
assert!(result.is_ok()); 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");
}
} }