use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex, OnceLock};
use alloy::primitives::{Address, B256};
use crate::connection::SentTx;
#[derive(Debug, Default)]
pub(crate) struct NonceState {
pub next: Option<u64>,
pub last_hash: Option<B256>,
}
impl NonceState {
pub(crate) fn reset(&mut self) {
self.next = None;
self.last_hash = None;
}
}
type Registry<K, V> = OnceLock<Mutex<HashMap<K, Arc<tokio::sync::Mutex<V>>>>>;
static NONCES: Registry<(String, u64, Address), NonceState> = OnceLock::new();
static APPROVALS: Registry<(String, u64, Address, Address, Address), ()> = OnceLock::new();
fn entry<K: std::hash::Hash + Eq, V: Default>(registry: &Registry<K, V>, key: K) -> Arc<tokio::sync::Mutex<V>> {
let map = registry.get_or_init(|| Mutex::new(HashMap::new()));
let mut map = map.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
Arc::clone(map.entry(key).or_default())
}
pub(crate) const SENT_LOG_CAPACITY: usize = 256;
#[derive(Debug, Default)]
pub(crate) struct SentLog {
entries: VecDeque<SentTx>,
}
impl SentLog {
pub(crate) fn record(&mut self, tx: SentTx) {
self.entries.retain(|t| t.hash != tx.hash);
self.entries.push_back(tx);
while self.entries.len() > SENT_LOG_CAPACITY {
self.entries.pop_front();
}
}
pub(crate) fn find(&self, hash: B256) -> Option<&SentTx> {
self.entries.iter().rev().find(|t| t.hash == hash || t.replaces.contains(&hash))
}
}
type SentRegistry = OnceLock<Mutex<HashMap<(String, u64, Address), Arc<Mutex<SentLog>>>>>;
static SENT: SentRegistry = OnceLock::new();
pub(crate) fn sent_log(scope: &str, chain_id: u64, account: Address) -> Arc<Mutex<SentLog>> {
let map = SENT.get_or_init(|| Mutex::new(HashMap::new()));
let mut map = map.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
Arc::clone(map.entry((scope.to_owned(), chain_id, account)).or_default())
}
pub(crate) fn nonce_lock(scope: &str, chain_id: u64, account: Address) -> Arc<tokio::sync::Mutex<NonceState>> {
entry(&NONCES, (scope.to_owned(), chain_id, account))
}
pub(crate) fn approval_lock(
scope: &str,
chain_id: u64,
owner: Address,
token: Address,
spender: Address,
) -> Arc<tokio::sync::Mutex<()>> {
entry(&APPROVALS, (scope.to_owned(), chain_id, owner, token, spender))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn locks_are_shared_per_key() {
let a = nonce_lock("http://a/", 1, Address::repeat_byte(1));
let b = nonce_lock("http://a/", 1, Address::repeat_byte(1));
let c = nonce_lock("http://a/", 2, Address::repeat_byte(1));
let d = nonce_lock("http://b/", 1, Address::repeat_byte(1));
assert!(Arc::ptr_eq(&a, &b));
assert!(!Arc::ptr_eq(&a, &c));
assert!(!Arc::ptr_eq(&a, &d));
let x = approval_lock("http://a/", 1, Address::ZERO, Address::repeat_byte(2), Address::repeat_byte(3));
let y = approval_lock("http://a/", 1, Address::ZERO, Address::repeat_byte(2), Address::repeat_byte(3));
assert!(Arc::ptr_eq(&x, &y));
let p = sent_log("http://a/", 1, Address::repeat_byte(1));
let q = sent_log("http://a/", 1, Address::repeat_byte(1));
assert!(Arc::ptr_eq(&p, &q));
assert!(!Arc::ptr_eq(&p, &sent_log("http://b/", 1, Address::repeat_byte(1))));
}
#[test]
fn sent_log_finds_the_latest_version_and_is_bounded() {
let tx = |n: u8| crate::connection::tests::sent_tx(B256::repeat_byte(n), u64::from(n));
let mut log = SentLog::default();
let first = tx(1);
log.record(first.clone());
let mut bumped = tx(2);
bumped.nonce = first.nonce;
bumped.replaces = vec![first.hash];
log.record(bumped.clone());
assert_eq!(log.find(first.hash), Some(&bumped));
assert_eq!(log.find(bumped.hash), Some(&bumped));
assert_eq!(log.find(B256::repeat_byte(9)), None);
log.record(bumped.clone());
assert_eq!(log.entries.len(), 2);
for i in 0..SENT_LOG_CAPACITY + 10 {
let mut t = tx(3);
t.hash = B256::from(alloy::primitives::U256::from(i + 1000));
log.record(t);
}
assert_eq!(log.entries.len(), SENT_LOG_CAPACITY);
assert_eq!(log.find(first.hash), None, "the oldest entries are forgotten");
}
}