use crate::config::ClientConfig;
use crate::error::{Error, Result};
use governor::{DefaultDirectRateLimiter, Quota, RateLimiter};
use moka::future::Cache;
use reqwest::Client;
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::num::NonZeroU32;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
pub mod endpoints;
pub mod types;
pub use endpoints::*;
pub use types::*;
#[derive(Clone)]
pub struct BscScanClient {
config: Arc<ClientConfig>,
http_client: Client,
rate_limiter: Arc<DefaultDirectRateLimiter>,
cache: Cache<String, Value>,
api_key_index: Arc<AtomicUsize>,
}
impl BscScanClient {
pub fn new(api_key: impl Into<String>) -> Result<Self> {
let config = ClientConfig::new(api_key);
Self::with_config(config)
}
pub fn testnet(api_key: impl Into<String>) -> Result<Self> {
let config = ClientConfig::testnet(api_key);
Self::with_config(config)
}
pub fn with_config(config: ClientConfig) -> Result<Self> {
config.validate()?;
let http_client = Client::builder()
.timeout(config.timeout())
.build()
.map_err(|e| Error::InvalidConfig(format!("Failed to create HTTP client: {}", e)))?;
let rate_limit = NonZeroU32::new(config.rate_limit_per_second)
.ok_or_else(|| Error::InvalidConfig("Rate limit must be greater than 0".to_string()))?;
let quota = Quota::per_second(rate_limit);
let rate_limiter = Arc::new(RateLimiter::direct(quota));
let cache = Cache::builder()
.max_capacity(config.cache_max_size)
.time_to_live(config.cache_ttl())
.build();
Ok(Self {
config: Arc::new(config),
http_client,
rate_limiter,
cache,
api_key_index: Arc::new(AtomicUsize::new(0)),
})
}
fn get_api_key(&self) -> &str {
let index = self.api_key_index.fetch_add(1, Ordering::Relaxed);
&self.config.api_keys[index % self.config.api_keys.len()]
}
pub(crate) async fn request<T: DeserializeOwned>(
&self,
module: &str,
action: &str,
params: &[(&str, &str)],
) -> Result<T> {
let cache_key = format!(
"{}:{}:{}",
module,
action,
params
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("&")
);
if self.config.cache_ttl_seconds > 0 {
if let Some(cached) = self.cache.get(&cache_key).await {
return serde_json::from_value(cached)
.map_err(Error::Serialization);
}
}
self.rate_limiter.until_ready().await;
let api_key = self.get_api_key();
let mut url = reqwest::Url::parse(&self.config.base_url)
.map_err(|e| Error::InvalidConfig(format!("Invalid base URL: {}", e)))?;
{
let mut query_pairs = url.query_pairs_mut();
query_pairs.append_pair("module", module);
query_pairs.append_pair("action", action);
query_pairs.append_pair("apikey", api_key);
query_pairs.append_pair("chainid", &self.config.chain_id.to_string());
for (key, value) in params {
query_pairs.append_pair(key, value);
}
}
let response = self
.http_client
.get(url)
.send()
.await
.map_err(Error::HttpRequest)?;
let status = response.status();
let body: Value = response.json().await.map_err(Error::HttpRequest)?;
if !status.is_success() {
return Err(Error::api_error(format!(
"HTTP {}: {}",
status,
body.get("message")
.and_then(|v| v.as_str())
.unwrap_or("Unknown error")
)));
}
if module == "proxy" {
if let Some(error) = body.get("error") {
let code = error.get("code").and_then(|v| v.as_i64()).unwrap_or(0);
let message = error.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown error");
return Err(Error::api_error(format!("JSON-RPC Error {}: {}", code, message)));
}
let result = body
.get("result")
.ok_or_else(|| Error::api_error("Missing 'result' field in proxy response"))?
.clone();
if self.config.cache_ttl_seconds > 0 {
self.cache.insert(cache_key, result.clone()).await;
}
return serde_json::from_value(result.clone()).map_err(|e| {
if let Some(msg) = result.as_str() {
Error::api_error(msg.to_string())
} else {
Error::Serialization(e)
}
});
}
let api_status = body
.get("status")
.and_then(|v| v.as_str())
.unwrap_or("0");
let message = body
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("Unknown");
if api_status == "0" && message != "No transactions found" && message != "NOTOK" {
return Err(Error::api_error(message));
}
let result = body
.get("result")
.ok_or_else(|| Error::api_error("Missing 'result' field in response"))?
.clone();
if self.config.cache_ttl_seconds > 0 {
self.cache.insert(cache_key, result.clone()).await;
}
serde_json::from_value(result.clone()).map_err(|e| {
if let Some(msg) = result.as_str() {
Error::api_error(msg.to_string())
} else {
Error::Serialization(e)
}
})
}
pub(crate) async fn request_simple<T: DeserializeOwned>(
&self,
module: &str,
action: &str,
params: &[(&str, &str)],
) -> Result<T> {
self.request(module, action, params).await
}
pub async fn clear_cache(&self) {
self.cache.invalidate_all();
}
pub fn cache_stats(&self) -> (u64, u64) {
(self.cache.entry_count(), self.cache.weighted_size())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_creation() {
let client = BscScanClient::new("test-key");
assert!(client.is_ok());
}
#[test]
fn test_testnet_client() {
let client = BscScanClient::testnet("test-key");
assert!(client.is_ok());
let client = client.unwrap();
assert_eq!(client.config.chain_id, 11155111);
}
#[test]
fn test_api_key_rotation() {
let config = ClientConfig::builder()
.api_key("key1")
.api_key("key2")
.api_key("key3")
.build()
.unwrap();
let client = BscScanClient::with_config(config).unwrap();
assert_eq!(client.get_api_key(), "key1");
assert_eq!(client.get_api_key(), "key2");
assert_eq!(client.get_api_key(), "key3");
assert_eq!(client.get_api_key(), "key1"); }
}