use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
#[derive(Debug, Clone)]
pub struct CuCostTable {
costs: HashMap<String, u32>,
default_cost: u32,
}
impl CuCostTable {
pub fn new(default_cost: u32) -> Self {
Self {
costs: HashMap::new(),
default_cost,
}
}
pub fn alchemy_defaults() -> Self {
let mut table = Self::new(50);
let defaults = [
("eth_blockNumber", 10),
("eth_getBalance", 19),
("eth_getTransactionCount", 26),
("eth_call", 26),
("eth_estimateGas", 87),
("eth_sendRawTransaction", 250),
("eth_getTransactionReceipt", 15),
("eth_getBlockByNumber", 16),
("eth_getLogs", 75),
("eth_subscribe", 10),
("eth_getCode", 19),
("eth_getStorageAt", 17),
("eth_gasPrice", 19),
("eth_feeHistory", 10),
("eth_maxPriorityFeePerGas", 10),
("eth_getTransactionByHash", 17),
("debug_traceTransaction", 309),
("trace_block", 500),
("trace_transaction", 309),
];
for (method, cost) in defaults {
table.costs.insert(method.to_string(), cost);
}
table
}
pub fn set_cost(&mut self, method: &str, cost: u32) {
self.costs.insert(method.to_string(), cost);
}
pub fn cost_for(&self, method: &str) -> u32 {
self.costs.get(method).copied().unwrap_or(self.default_cost)
}
}
impl Default for CuCostTable {
fn default() -> Self {
Self::alchemy_defaults()
}
}
#[derive(Debug, Clone)]
pub struct CuBudgetConfig {
pub monthly_budget: u64,
pub alert_threshold: f64,
pub throttle_near_limit: bool,
}
impl Default for CuBudgetConfig {
fn default() -> Self {
Self {
monthly_budget: 0, alert_threshold: 0.8,
throttle_near_limit: false,
}
}
}
pub struct CuTracker {
url: String,
cost_table: CuCostTable,
config: CuBudgetConfig,
consumed: AtomicU64,
per_method: Mutex<HashMap<String, u64>>,
}
impl CuTracker {
pub fn new(url: impl Into<String>, cost_table: CuCostTable, config: CuBudgetConfig) -> Self {
Self {
url: url.into(),
cost_table,
config,
consumed: AtomicU64::new(0),
per_method: Mutex::new(HashMap::new()),
}
}
pub fn record(&self, method: &str) {
let cost = self.cost_table.cost_for(method) as u64;
self.consumed.fetch_add(cost, Ordering::Relaxed);
let mut pm = self.per_method.lock().unwrap();
*pm.entry(method.to_string()).or_insert(0) += cost;
}
pub fn cost_for(&self, method: &str) -> u32 {
self.cost_table.cost_for(method)
}
pub fn consumed(&self) -> u64 {
self.consumed.load(Ordering::Relaxed)
}
pub fn remaining(&self) -> u64 {
if self.config.monthly_budget == 0 {
return u64::MAX;
}
self.config
.monthly_budget
.saturating_sub(self.consumed.load(Ordering::Relaxed))
}
pub fn usage_fraction(&self) -> f64 {
if self.config.monthly_budget == 0 {
return 0.0;
}
self.consumed.load(Ordering::Relaxed) as f64 / self.config.monthly_budget as f64
}
pub fn is_alert(&self) -> bool {
self.config.monthly_budget > 0 && self.usage_fraction() >= self.config.alert_threshold
}
pub fn is_exhausted(&self) -> bool {
self.config.monthly_budget > 0
&& self.consumed.load(Ordering::Relaxed) >= self.config.monthly_budget
}
pub fn should_throttle(&self) -> bool {
self.config.throttle_near_limit && self.is_alert()
}
pub fn reset(&self) {
self.consumed.store(0, Ordering::Relaxed);
let mut pm = self.per_method.lock().unwrap();
pm.clear();
}
pub fn per_method_usage(&self) -> HashMap<String, u64> {
let pm = self.per_method.lock().unwrap();
pm.clone()
}
pub fn url(&self) -> &str {
&self.url
}
pub fn snapshot(&self) -> CuSnapshot {
CuSnapshot {
url: self.url.clone(),
consumed: self.consumed.load(Ordering::Relaxed),
budget: self.config.monthly_budget,
usage_fraction: self.usage_fraction(),
alert: self.is_alert(),
exhausted: self.is_exhausted(),
per_method: self.per_method_usage(),
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct CuSnapshot {
pub url: String,
pub consumed: u64,
pub budget: u64,
pub usage_fraction: f64,
pub alert: bool,
pub exhausted: bool,
pub per_method: HashMap<String, u64>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cost_table_defaults() {
let table = CuCostTable::alchemy_defaults();
assert_eq!(table.cost_for("eth_blockNumber"), 10);
assert_eq!(table.cost_for("eth_call"), 26);
assert_eq!(table.cost_for("eth_sendRawTransaction"), 250);
assert_eq!(table.cost_for("debug_traceTransaction"), 309);
assert_eq!(table.cost_for("unknown_method"), 50); }
#[test]
fn tracker_records_consumption() {
let tracker = CuTracker::new(
"https://rpc.example.com",
CuCostTable::alchemy_defaults(),
CuBudgetConfig::default(),
);
tracker.record("eth_blockNumber"); tracker.record("eth_call"); tracker.record("eth_getLogs");
assert_eq!(tracker.consumed(), 10 + 26 + 75);
}
#[test]
fn budget_tracking() {
let tracker = CuTracker::new(
"https://rpc.example.com",
CuCostTable::alchemy_defaults(),
CuBudgetConfig {
monthly_budget: 1000,
alert_threshold: 0.8,
throttle_near_limit: true,
},
);
for _ in 0..75 {
tracker.record("eth_blockNumber"); }
assert_eq!(tracker.consumed(), 750);
assert!(!tracker.is_alert());
assert!(!tracker.should_throttle());
assert_eq!(tracker.remaining(), 250);
for _ in 0..10 {
tracker.record("eth_blockNumber");
}
assert!(tracker.is_alert());
assert!(tracker.should_throttle());
assert!(!tracker.is_exhausted());
}
#[test]
fn budget_exhaustion() {
let tracker = CuTracker::new(
"https://rpc.example.com",
CuCostTable::alchemy_defaults(),
CuBudgetConfig {
monthly_budget: 100,
..Default::default()
},
);
for _ in 0..10 {
tracker.record("eth_blockNumber"); }
assert!(tracker.is_exhausted());
assert_eq!(tracker.remaining(), 0);
}
#[test]
fn unlimited_budget() {
let tracker = CuTracker::new(
"https://rpc.example.com",
CuCostTable::alchemy_defaults(),
CuBudgetConfig::default(), );
for _ in 0..1000 {
tracker.record("eth_call");
}
assert_eq!(tracker.remaining(), u64::MAX);
assert!(!tracker.is_alert());
assert!(!tracker.is_exhausted());
}
#[test]
fn per_method_breakdown() {
let tracker = CuTracker::new(
"https://rpc.example.com",
CuCostTable::alchemy_defaults(),
CuBudgetConfig::default(),
);
tracker.record("eth_blockNumber");
tracker.record("eth_blockNumber");
tracker.record("eth_call");
let breakdown = tracker.per_method_usage();
assert_eq!(*breakdown.get("eth_blockNumber").unwrap(), 20);
assert_eq!(*breakdown.get("eth_call").unwrap(), 26);
}
#[test]
fn reset_clears_counters() {
let tracker = CuTracker::new(
"https://rpc.example.com",
CuCostTable::alchemy_defaults(),
CuBudgetConfig::default(),
);
tracker.record("eth_blockNumber");
assert!(tracker.consumed() > 0);
tracker.reset();
assert_eq!(tracker.consumed(), 0);
assert!(tracker.per_method_usage().is_empty());
}
#[test]
fn snapshot_serializable() {
let tracker = CuTracker::new(
"https://rpc.example.com",
CuCostTable::alchemy_defaults(),
CuBudgetConfig {
monthly_budget: 1000,
..Default::default()
},
);
tracker.record("eth_call");
let snap = tracker.snapshot();
let json = serde_json::to_string(&snap).unwrap();
assert!(json.contains("\"consumed\":26"));
assert!(json.contains("\"budget\":1000"));
}
}