#![forbid(unsafe_code)]
use crate::solana_sdk::pubkey::Pubkey;
use crate::{error::Result, events::TallyEvent, SimpleTallyClient, TallyError};
use anchor_client::solana_account_decoder::UiAccountEncoding;
use anchor_client::solana_client::rpc_client::GetConfirmedSignaturesForAddress2Config;
use anchor_client::solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig};
use anchor_client::solana_client::rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType};
use anchor_client::solana_sdk::{commitment_config::CommitmentConfig, signature::Signature};
use anyhow::Context;
use chrono::{DateTime, Utc};
use lru::LruCache;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::num::NonZeroUsize;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use tracing::{debug, info, trace, warn};
#[derive(Debug, Clone)]
pub struct EventQueryConfig {
pub max_events_per_query: usize,
pub max_signatures_per_batch: usize,
pub commitment: CommitmentConfig,
pub enable_cache: bool,
pub cache_ttl_seconds: u64,
pub max_cache_size: usize,
}
impl Default for EventQueryConfig {
fn default() -> Self {
Self {
max_events_per_query: 1000,
max_signatures_per_batch: 100,
commitment: CommitmentConfig::confirmed(),
enable_cache: true,
cache_ttl_seconds: 300, max_cache_size: 1000,
}
}
}
#[derive(Debug, Clone)]
pub struct EventQueryClientConfig {
pub rpc_url: String,
pub program_id: Pubkey,
pub query_config: EventQueryConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParsedEvent {
pub signature: Signature,
pub slot: u64,
pub block_time: Option<i64>,
pub success: bool,
pub event: TallyEvent,
pub log_index: usize,
}
#[derive(Debug, Clone)]
struct CacheEntry {
events: Vec<ParsedEvent>,
cached_at: DateTime<Utc>,
ttl_seconds: u64,
}
impl CacheEntry {
fn new(events: Vec<ParsedEvent>, ttl_seconds: u64) -> Self {
Self {
events,
cached_at: Utc::now(),
ttl_seconds,
}
}
fn is_expired(&self) -> bool {
let ttl_i64 = i64::try_from(self.ttl_seconds).unwrap_or(i64::MAX);
let duration = chrono::Duration::seconds(ttl_i64);
let expiry = self
.cached_at
.checked_add_signed(duration)
.unwrap_or(self.cached_at);
Utc::now() > expiry
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
struct QueryKey {
merchant: Pubkey,
query_type: QueryType,
limit: usize,
from_slot: Option<u64>,
to_slot: Option<u64>,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
enum QueryType {
Recent,
DateRange,
MerchantEvents,
}
pub struct EventQueryClient {
sdk_client: Arc<SimpleTallyClient>,
program_id: Pubkey,
config: EventQueryConfig,
cache: Arc<Mutex<LruCache<QueryKey, CacheEntry>>>,
}
impl EventQueryClient {
pub fn new(config: EventQueryClientConfig) -> Result<Self> {
let sdk_client = Arc::new(
SimpleTallyClient::new(&config.rpc_url)
.context("Failed to create SimpleTallyClient")?,
);
let cache_size = NonZeroUsize::new(config.query_config.max_cache_size)
.context("Cache size must be greater than 0")?;
let cache = Arc::new(Mutex::new(LruCache::new(cache_size)));
info!(
service = "tally-sdk",
component = "event_query_client",
event = "client_created",
rpc_url = %config.rpc_url,
program_id = %config.program_id,
max_events_per_query = config.query_config.max_events_per_query,
cache_enabled = config.query_config.enable_cache,
"EventQueryClient initialized successfully"
);
Ok(Self {
sdk_client,
program_id: config.program_id,
config: config.query_config,
cache,
})
}
pub fn new_with_program_id(
rpc_url: String,
query_config: Option<EventQueryConfig>,
) -> Result<Self> {
let config = EventQueryClientConfig {
rpc_url,
program_id: crate::program_id(),
query_config: query_config.unwrap_or_default(),
};
Self::new(config)
}
pub async fn get_recent_events(
&self,
merchant: &Pubkey,
limit: usize,
) -> Result<Vec<ParsedEvent>> {
let start_time = Instant::now();
let query_key = Self::build_query_key(merchant, QueryType::Recent, limit, None, None);
if let Some(cached) = self.try_get_cached_events(&query_key, merchant) {
return Ok(cached);
}
debug!(
service = "tally-sdk",
component = "event_query_client",
event = "query_recent_events",
merchant = %merchant,
limit = limit,
"Querying recent events for merchant"
);
let sorted_events = self.fetch_and_sort_events(merchant, limit).await?;
self.try_cache_events(query_key, &sorted_events);
Self::log_query_success(merchant, &sorted_events, start_time);
Ok(sorted_events)
}
const fn build_query_key(
merchant: &Pubkey,
query_type: QueryType,
limit: usize,
from_slot: Option<u64>,
to_slot: Option<u64>,
) -> QueryKey {
QueryKey {
merchant: *merchant,
query_type,
limit,
from_slot,
to_slot,
}
}
fn try_get_cached_events(
&self,
query_key: &QueryKey,
merchant: &Pubkey,
) -> Option<Vec<ParsedEvent>> {
if !self.config.enable_cache {
return None;
}
if let Some(cached_events) = self.get_from_cache(query_key) {
debug!(
service = "tally-sdk",
component = "event_query_client",
event = "cache_hit",
merchant = %merchant,
cached_event_count = cached_events.len(),
"Returning cached recent events"
);
return Some(cached_events);
}
None
}
async fn fetch_and_sort_events(
&self,
merchant: &Pubkey,
limit: usize,
) -> Result<Vec<ParsedEvent>> {
let signatures = self.get_merchant_signatures(merchant, limit).await?;
let events = self.parse_events_from_signatures(&signatures).await?;
Ok(Self::sort_and_limit_events(events, limit))
}
fn sort_and_limit_events(mut events: Vec<ParsedEvent>, limit: usize) -> Vec<ParsedEvent> {
events.sort_by(|a, b| b.slot.cmp(&a.slot));
events.truncate(limit);
events
}
fn try_cache_events(&self, query_key: QueryKey, events: &[ParsedEvent]) {
if self.config.enable_cache {
self.store_in_cache(query_key, events.to_vec());
}
}
fn log_query_success(merchant: &Pubkey, events: &[ParsedEvent], start_time: Instant) {
info!(
service = "tally-sdk",
component = "event_query_client",
event = "recent_events_retrieved",
merchant = %merchant,
event_count = events.len(),
duration_ms = start_time.elapsed().as_millis(),
"Successfully retrieved recent events"
);
}
pub async fn get_events_by_date_range(
&self,
merchant: &Pubkey,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> Result<Vec<ParsedEvent>> {
let start_time = Instant::now();
debug!(
service = "tally-sdk",
component = "event_query_client",
event = "query_events_by_date_range",
merchant = %merchant,
from = %from,
to = %to,
"Querying events by date range"
);
let (from_slot, to_slot) = self.convert_date_range_to_slots(from, to)?;
let query_key = Self::build_date_range_query_key(
merchant,
from_slot,
to_slot,
self.config.max_events_per_query,
);
if let Some(cached) = self.try_get_cached_date_range_events(&query_key, merchant) {
return Ok(cached);
}
let sorted_events = self
.fetch_filter_and_sort_events_by_date(merchant, from, to, from_slot, to_slot)
.await?;
self.try_cache_events(query_key.clone(), &sorted_events);
Self::log_date_range_query_success(
merchant,
&sorted_events,
from_slot,
to_slot,
start_time,
);
Ok(sorted_events)
}
fn convert_date_range_to_slots(
&self,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> Result<(u64, u64)> {
let from_slot = self.timestamp_to_approximate_slot(from.timestamp())?;
let to_slot = self.timestamp_to_approximate_slot(to.timestamp())?;
Ok((from_slot, to_slot))
}
const fn build_date_range_query_key(
merchant: &Pubkey,
from_slot: u64,
to_slot: u64,
limit: usize,
) -> QueryKey {
QueryKey {
merchant: *merchant,
query_type: QueryType::DateRange,
limit,
from_slot: Some(from_slot),
to_slot: Some(to_slot),
}
}
fn try_get_cached_date_range_events(
&self,
query_key: &QueryKey,
merchant: &Pubkey,
) -> Option<Vec<ParsedEvent>> {
if !self.config.enable_cache {
return None;
}
if let Some(cached_events) = self.get_from_cache(query_key) {
debug!(
service = "tally-sdk",
component = "event_query_client",
event = "cache_hit",
merchant = %merchant,
cached_event_count = cached_events.len(),
"Returning cached date range events"
);
return Some(cached_events);
}
None
}
async fn fetch_filter_and_sort_events_by_date(
&self,
merchant: &Pubkey,
from: DateTime<Utc>,
to: DateTime<Utc>,
from_slot: u64,
to_slot: u64,
) -> Result<Vec<ParsedEvent>> {
let signatures = self
.get_merchant_signatures_in_slot_range(merchant, from_slot, to_slot)
.await?;
let events = self.parse_events_from_signatures(&signatures).await?;
let filtered_events = Self::filter_events_by_date_range(events, from, to);
Ok(Self::sort_events_by_block_time(filtered_events))
}
fn filter_events_by_date_range(
events: Vec<ParsedEvent>,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> Vec<ParsedEvent> {
events
.into_iter()
.filter(|event| Self::is_event_in_date_range(event, from, to))
.collect()
}
fn is_event_in_date_range(event: &ParsedEvent, from: DateTime<Utc>, to: DateTime<Utc>) -> bool {
event
.block_time
.and_then(|block_time| DateTime::from_timestamp(block_time, 0))
.is_some_and(|event_time| event_time >= from && event_time <= to)
}
fn sort_events_by_block_time(mut events: Vec<ParsedEvent>) -> Vec<ParsedEvent> {
events.sort_by(|a, b| b.block_time.unwrap_or(0).cmp(&a.block_time.unwrap_or(0)));
events
}
fn log_date_range_query_success(
merchant: &Pubkey,
events: &[ParsedEvent],
from_slot: u64,
to_slot: u64,
start_time: Instant,
) {
info!(
service = "tally-sdk",
component = "event_query_client",
event = "date_range_events_retrieved",
merchant = %merchant,
event_count = events.len(),
from_slot = from_slot,
to_slot = to_slot,
duration_ms = start_time.elapsed().as_millis(),
"Successfully retrieved events by date range"
);
}
pub async fn get_merchant_events(
&self,
merchant: &Pubkey,
limit: usize,
) -> Result<Vec<ParsedEvent>> {
let start_time = Instant::now();
let query_key = Self::build_merchant_events_query_key(merchant, limit);
if let Some(cached) = self.try_get_cached_merchant_events(&query_key, merchant) {
return Ok(cached);
}
debug!(
service = "tally-sdk",
component = "event_query_client",
event = "query_merchant_events",
merchant = %merchant,
limit = limit,
"Querying all events for merchant"
);
let sorted_events = self
.fetch_parse_and_sort_merchant_events(merchant, limit)
.await?;
self.try_cache_events(query_key, &sorted_events);
Self::log_merchant_events_success(merchant, &sorted_events, start_time);
Ok(sorted_events)
}
const fn build_merchant_events_query_key(merchant: &Pubkey, limit: usize) -> QueryKey {
QueryKey {
merchant: *merchant,
query_type: QueryType::MerchantEvents,
limit,
from_slot: None,
to_slot: None,
}
}
fn try_get_cached_merchant_events(
&self,
query_key: &QueryKey,
merchant: &Pubkey,
) -> Option<Vec<ParsedEvent>> {
if !self.config.enable_cache {
return None;
}
if let Some(cached_events) = self.get_from_cache(query_key) {
debug!(
service = "tally-sdk",
component = "event_query_client",
event = "cache_hit",
merchant = %merchant,
cached_event_count = cached_events.len(),
"Returning cached merchant events"
);
return Some(cached_events);
}
None
}
async fn fetch_parse_and_sort_merchant_events(
&self,
merchant: &Pubkey,
limit: usize,
) -> Result<Vec<ParsedEvent>> {
let signature_limit = limit.saturating_mul(2);
let signatures = self
.get_merchant_signatures(merchant, signature_limit)
.await?;
let events = self.parse_events_from_signatures(&signatures).await?;
Ok(Self::sort_and_limit_events(events, limit))
}
fn log_merchant_events_success(merchant: &Pubkey, events: &[ParsedEvent], start_time: Instant) {
info!(
service = "tally-sdk",
component = "event_query_client",
event = "merchant_events_retrieved",
merchant = %merchant,
event_count = events.len(),
duration_ms = start_time.elapsed().as_millis(),
"Successfully retrieved merchant events"
);
}
#[allow(clippy::unused_async)] async fn get_merchant_signatures(
&self,
merchant: &Pubkey,
limit: usize,
) -> Result<Vec<Signature>> {
let merchant_signatures = self
.sdk_client
.get_confirmed_signatures_for_address(
merchant,
Some(GetConfirmedSignaturesForAddress2Config {
limit: Some(limit.min(1000)), commitment: Some(self.config.commitment),
..Default::default()
}),
)
.map_err(|e| TallyError::RpcError(format!("Failed to get merchant signatures: {e}")))?;
let mut signatures = HashSet::new();
for sig_info in merchant_signatures {
if let Ok(signature) = Signature::from_str(&sig_info.signature) {
signatures.insert(signature);
}
}
let plans = self.get_merchant_plans(merchant)?;
for plan_address in &plans {
let plan_signatures = self
.sdk_client
.get_confirmed_signatures_for_address(
plan_address,
Some(GetConfirmedSignaturesForAddress2Config {
limit: Some(limit.min(1000)),
commitment: Some(self.config.commitment),
..Default::default()
}),
)
.map_err(|e| TallyError::RpcError(format!("Failed to get plan signatures: {e}")))?;
for sig_info in plan_signatures {
if let Ok(signature) = Signature::from_str(&sig_info.signature) {
signatures.insert(signature);
}
}
}
for plan_address in &plans {
let subscriptions = self.get_plan_subscriptions(plan_address)?;
for subscription_address in subscriptions {
let sub_signatures = self
.sdk_client
.get_confirmed_signatures_for_address(
&subscription_address,
Some(GetConfirmedSignaturesForAddress2Config {
limit: Some(limit.min(1000)),
commitment: Some(self.config.commitment),
..Default::default()
}),
)
.map_err(|e| {
TallyError::RpcError(format!("Failed to get subscription signatures: {e}"))
})?;
for sig_info in sub_signatures {
if let Ok(signature) = Signature::from_str(&sig_info.signature) {
signatures.insert(signature);
}
}
}
}
let result: Vec<Signature> = signatures.into_iter().collect();
debug!(
service = "tally-sdk",
component = "event_query_client",
event = "signatures_collected",
merchant = %merchant,
signature_count = result.len(),
plan_count = plans.len(),
"Collected transaction signatures for merchant"
);
Ok(result)
}
async fn get_merchant_signatures_in_slot_range(
&self,
merchant: &Pubkey,
_from_slot: u64,
_to_slot: u64,
) -> Result<Vec<Signature>> {
let signature_limit = self.config.max_events_per_query.saturating_mul(2);
let signatures = self
.get_merchant_signatures(merchant, signature_limit)
.await?;
Ok(signatures)
}
async fn parse_events_from_signatures(
&self,
signatures: &[Signature],
) -> Result<Vec<ParsedEvent>> {
let all_events = self.process_signature_batches(signatures).await;
Self::log_parsed_events_summary(signatures, &all_events);
Ok(all_events)
}
async fn process_signature_batches(&self, signatures: &[Signature]) -> Vec<ParsedEvent> {
let mut all_events = Vec::new();
for chunk in signatures.chunks(self.config.max_signatures_per_batch) {
let batch_events = self.process_signature_chunk(chunk);
all_events.extend(batch_events);
self.apply_batch_rate_limit(chunk.len()).await;
}
all_events
}
fn process_signature_chunk(&self, chunk: &[Signature]) -> Vec<ParsedEvent> {
let batch_events = Vec::new();
for signature in chunk {
self.try_fetch_and_log_transaction(signature);
}
batch_events
}
fn try_fetch_and_log_transaction(&self, signature: &Signature) {
match self.sdk_client.get_transaction(signature) {
Ok(_transaction) => {
Self::log_transaction_received(signature);
}
Err(e) => {
Self::log_transaction_fetch_error(signature, &e);
}
}
}
fn log_transaction_received(signature: &Signature) {
debug!(
service = "tally-sdk",
component = "event_query_client",
event = "transaction_received",
signature = %signature,
"Transaction data received - event parsing temporarily disabled"
);
}
fn log_transaction_fetch_error<E: std::fmt::Display>(signature: &Signature, error: &E) {
trace!(
service = "tally-sdk",
component = "event_query_client",
event = "transaction_fetch_error",
signature = %signature,
error = %error,
"Failed to fetch transaction details"
);
}
async fn apply_batch_rate_limit(&self, chunk_len: usize) {
if chunk_len == self.config.max_signatures_per_batch {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
}
fn log_parsed_events_summary(signatures: &[Signature], events: &[ParsedEvent]) {
debug!(
service = "tally-sdk",
component = "event_query_client",
event = "events_parsed",
signature_count = signatures.len(),
event_count = events.len(),
"Parsed events from transaction signatures"
);
}
fn get_merchant_plans(&self, merchant: &Pubkey) -> Result<Vec<Pubkey>> {
let config = RpcProgramAccountsConfig {
filters: Some(vec![
RpcFilterType::Memcmp(Memcmp::new(
8, MemcmpEncodedBytes::Base58(merchant.to_string()),
)),
]),
account_config: RpcAccountInfoConfig {
encoding: Some(UiAccountEncoding::Base64),
commitment: Some(self.config.commitment),
..Default::default()
},
with_context: Some(false),
sort_results: None,
};
let accounts = self
.sdk_client
.rpc()
.get_program_accounts_with_config(&self.program_id, config)
.map_err(|e| TallyError::RpcError(format!("Failed to get merchant plans: {e}")))?;
let plan_addresses: Vec<Pubkey> = accounts.into_iter().map(|(pubkey, _)| pubkey).collect();
debug!(
service = "tally-sdk",
component = "event_query_client",
event = "merchant_plans_retrieved",
merchant = %merchant,
plan_count = plan_addresses.len(),
"Retrieved plan addresses for merchant"
);
Ok(plan_addresses)
}
fn get_plan_subscriptions(&self, plan: &Pubkey) -> Result<Vec<Pubkey>> {
let config = RpcProgramAccountsConfig {
filters: Some(vec![
RpcFilterType::Memcmp(Memcmp::new(
8, MemcmpEncodedBytes::Base58(plan.to_string()),
)),
]),
account_config: RpcAccountInfoConfig {
encoding: Some(UiAccountEncoding::Base64),
commitment: Some(self.config.commitment),
..Default::default()
},
with_context: Some(false),
sort_results: None,
};
let accounts = self
.sdk_client
.rpc()
.get_program_accounts_with_config(&self.program_id, config)
.map_err(|e| TallyError::RpcError(format!("Failed to get plan subscriptions: {e}")))?;
let subscription_addresses: Vec<Pubkey> =
accounts.into_iter().map(|(pubkey, _)| pubkey).collect();
trace!(
service = "tally-sdk",
component = "event_query_client",
event = "plan_subscriptions_retrieved",
plan = %plan,
subscription_count = subscription_addresses.len(),
"Retrieved subscription addresses for plan"
);
Ok(subscription_addresses)
}
fn timestamp_to_approximate_slot(&self, timestamp: i64) -> Result<u64> {
const SLOT_DURATION_MS: i64 = 400;
let current_slot = self
.sdk_client
.get_slot()
.map_err(|e| TallyError::RpcError(format!("Failed to get current slot: {e}")))?;
let current_time = Utc::now().timestamp();
let time_diff_seconds = current_time.saturating_sub(timestamp);
let time_diff_ms = time_diff_seconds.saturating_mul(1000);
let slot_diff = time_diff_ms / SLOT_DURATION_MS;
let approximate_slot = if slot_diff > 0 {
current_slot.saturating_sub(u64::try_from(slot_diff).unwrap_or(u64::MAX))
} else {
let abs_diff = slot_diff.unsigned_abs();
current_slot.saturating_add(abs_diff)
};
trace!(
service = "tally-sdk",
component = "event_query_client",
event = "timestamp_to_slot_conversion",
timestamp = timestamp,
current_slot = current_slot,
approximate_slot = approximate_slot,
"Converted timestamp to approximate slot"
);
Ok(approximate_slot)
}
fn get_from_cache(&self, key: &QueryKey) -> Option<Vec<ParsedEvent>> {
if let Ok(mut cache) = self.cache.lock() {
if let Some(entry) = cache.get(key) {
if !entry.is_expired() {
return Some(entry.events.clone());
}
cache.pop(key);
}
}
None
}
fn store_in_cache(&self, key: QueryKey, events: Vec<ParsedEvent>) {
if let Ok(mut cache) = self.cache.lock() {
let entry = CacheEntry::new(events, self.config.cache_ttl_seconds);
cache.put(key, entry);
}
}
pub fn clear_cache(&self) {
if let Ok(mut cache) = self.cache.lock() {
cache.clear();
}
info!(
service = "tally-sdk",
component = "event_query_client",
event = "cache_cleared",
"Query cache has been cleared"
);
}
#[must_use]
pub fn get_cache_stats(&self) -> HashMap<String, u64> {
let mut stats = HashMap::new();
if let Ok(cache) = self.cache.lock() {
stats.insert("cache_size".to_string(), cache.len() as u64);
stats.insert("cache_capacity".to_string(), cache.cap().get() as u64);
}
stats
}
pub fn health_check(&self) -> bool {
match self.sdk_client.get_health() {
Ok(()) => {
debug!(
service = "tally-sdk",
component = "event_query_client",
event = "health_check_success",
"RPC client health check passed"
);
true
}
Err(e) => {
warn!(
service = "tally-sdk",
component = "event_query_client",
event = "health_check_failed",
error = %e,
"RPC client health check failed"
);
false
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_config() -> EventQueryClientConfig {
EventQueryClientConfig {
rpc_url: "http://localhost:8899".to_string(),
program_id: crate::program_id(),
query_config: EventQueryConfig::default(),
}
}
#[test]
fn test_event_query_client_creation() {
let config = create_test_config();
let client = EventQueryClient::new(config);
assert!(client.is_ok());
}
#[test]
fn test_event_query_config_default() {
let config = EventQueryConfig::default();
assert_eq!(config.max_events_per_query, 1000);
assert_eq!(config.max_signatures_per_batch, 100);
assert!(config.enable_cache);
assert_eq!(config.cache_ttl_seconds, 300);
}
#[test]
fn test_cache_entry_expiry() {
let events = vec![];
let entry = CacheEntry::new(events, 1);
assert!(!entry.is_expired());
let mut expired_entry = entry;
expired_entry.cached_at = Utc::now() - chrono::Duration::seconds(2);
assert!(expired_entry.is_expired());
}
#[test]
fn test_query_key_equality() {
let merchant = Pubkey::new_unique();
let key1 = QueryKey {
merchant,
query_type: QueryType::Recent,
limit: 100,
from_slot: None,
to_slot: None,
};
let key2 = QueryKey {
merchant,
query_type: QueryType::Recent,
limit: 100,
from_slot: None,
to_slot: None,
};
assert_eq!(key1, key2);
let key3 = QueryKey {
merchant,
query_type: QueryType::MerchantEvents,
limit: 100,
from_slot: None,
to_slot: None,
};
assert_ne!(key1, key3);
}
#[tokio::test]
async fn test_timestamp_to_slot_conversion() {
let config = create_test_config();
let _client = EventQueryClient::new(config).unwrap();
let _current_time = Utc::now().timestamp();
}
#[test]
fn test_cache_operations() {
let config = create_test_config();
let client = EventQueryClient::new(config).unwrap();
let key = QueryKey {
merchant: Pubkey::new_unique(),
query_type: QueryType::Recent,
limit: 100,
from_slot: None,
to_slot: None,
};
assert!(client.get_from_cache(&key).is_none());
let events = vec![];
client.store_in_cache(key.clone(), events);
assert!(client.get_from_cache(&key).is_some());
client.clear_cache();
assert!(client.get_from_cache(&key).is_none());
}
#[test]
fn test_cache_stats() {
let config = create_test_config();
let client = EventQueryClient::new(config).unwrap();
let stats = client.get_cache_stats();
assert!(stats.contains_key("cache_size"));
assert!(stats.contains_key("cache_capacity"));
assert_eq!(stats["cache_size"], 0);
}
}