0.5.4
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
111
khbb_lib/src/account_enrichment.rs
Normal file
111
khbb_lib/src/account_enrichment.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) => {}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user