use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug, Clone, Copy)]
pub struct PendingTx {
pub tx_hash: [u8; 32],
pub nonce: u64,
pub submitted_at_ms: u64,
}
#[derive(Debug)]
pub struct NonceManager {
next: AtomicU64,
pending: Mutex<HashMap<u64, PendingTx>>,
}
impl NonceManager {
pub fn new(on_chain_nonce: u64) -> Self {
Self {
next: AtomicU64::new(on_chain_nonce),
pending: Mutex::new(HashMap::new()),
}
}
#[inline]
pub fn acquire(&self) -> u64 {
self.next.fetch_add(1, Ordering::Relaxed)
}
#[inline]
pub fn peek(&self) -> u64 {
self.next.load(Ordering::Relaxed)
}
pub fn track(&self, nonce: u64, tx_hash: [u8; 32], submitted_at_ms: u64) {
let tx = PendingTx {
tx_hash,
nonce,
submitted_at_ms,
};
self.pending.lock().unwrap().insert(nonce, tx);
}
pub fn confirm(&self, nonce: u64) -> Option<PendingTx> {
self.pending.lock().unwrap().remove(&nonce)
}
pub fn release(&self, nonce: u64) -> bool {
self.pending.lock().unwrap().remove(&nonce);
let rewound = self
.next
.compare_exchange(nonce + 1, nonce, Ordering::Relaxed, Ordering::Relaxed)
.is_ok();
tracing::debug!(nonce, rewound, "nonce released");
rewound
}
pub fn resync(&self, on_chain_nonce: u64) {
let old = self.next.swap(on_chain_nonce, Ordering::Relaxed);
self.pending.lock().unwrap().clear();
tracing::info!(
old_nonce = old,
new_nonce = on_chain_nonce,
"nonce resynced"
);
}
pub fn pending_count(&self) -> usize {
self.pending.lock().unwrap().len()
}
pub fn pending_snapshot(&self) -> Vec<PendingTx> {
self.pending.lock().unwrap().values().copied().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn acquire_is_monotonic() {
let mgr = NonceManager::new(0);
let nonces: Vec<u64> = (0..100).map(|_| mgr.acquire()).collect();
for i in 1..nonces.len() {
assert_eq!(nonces[i], nonces[i - 1] + 1);
}
}
#[test]
fn acquire_starts_at_on_chain_nonce() {
let mgr = NonceManager::new(999);
assert_eq!(mgr.acquire(), 999);
assert_eq!(mgr.acquire(), 1000);
}
#[test]
fn peek_does_not_advance() {
let mgr = NonceManager::new(10);
assert_eq!(mgr.peek(), 10);
assert_eq!(mgr.peek(), 10);
assert_eq!(mgr.acquire(), 10);
assert_eq!(mgr.peek(), 11);
}
#[test]
fn track_and_confirm() {
let mgr = NonceManager::new(0);
let n = mgr.acquire();
mgr.track(n, [0xAA; 32], 1000);
assert_eq!(mgr.pending_count(), 1);
let tx = mgr.confirm(n).unwrap();
assert_eq!(tx.nonce, 0);
assert_eq!(tx.tx_hash, [0xAA; 32]);
assert_eq!(tx.submitted_at_ms, 1000);
assert_eq!(mgr.pending_count(), 0);
}
#[test]
fn confirm_unknown_nonce_returns_none() {
let mgr = NonceManager::new(0);
assert!(mgr.confirm(42).is_none());
}
#[test]
fn release_rewinds_if_last_acquired() {
let mgr = NonceManager::new(5);
let n = mgr.acquire(); assert!(mgr.release(n)); assert_eq!(mgr.peek(), 5);
assert_eq!(mgr.acquire(), 5); }
#[test]
fn release_does_not_rewind_if_another_acquired_after() {
let mgr = NonceManager::new(5);
let n1 = mgr.acquire(); let _n2 = mgr.acquire(); assert!(!mgr.release(n1)); assert_eq!(mgr.peek(), 7); }
#[test]
fn release_removes_from_pending() {
let mgr = NonceManager::new(0);
let n = mgr.acquire();
mgr.track(n, [0xBB; 32], 500);
assert_eq!(mgr.pending_count(), 1);
mgr.release(n);
assert_eq!(mgr.pending_count(), 0);
}
#[test]
fn resync_resets_everything() {
let mgr = NonceManager::new(0);
for _ in 0..5 {
let n = mgr.acquire();
mgr.track(n, [0x11; 32], 100);
}
assert_eq!(mgr.pending_count(), 5);
assert_eq!(mgr.peek(), 5);
mgr.resync(100);
assert_eq!(mgr.peek(), 100);
assert_eq!(mgr.pending_count(), 0);
}
#[test]
fn pending_snapshot_returns_all() {
let mgr = NonceManager::new(0);
mgr.track(0, [0x01; 32], 100);
mgr.track(1, [0x02; 32], 200);
mgr.track(2, [0x03; 32], 300);
let snap = mgr.pending_snapshot();
assert_eq!(snap.len(), 3);
}
#[test]
fn struct_sizes() {
assert_eq!(std::mem::size_of::<PendingTx>(), 48);
assert_eq!(std::mem::align_of::<PendingTx>(), 8);
}
#[test]
fn concurrent_acquire_no_duplicates() {
use std::sync::Arc;
use std::thread;
let mgr = Arc::new(NonceManager::new(0));
let mut handles = Vec::new();
for _ in 0..8 {
let mgr = Arc::clone(&mgr);
handles.push(thread::spawn(move || {
(0..1000).map(|_| mgr.acquire()).collect::<Vec<_>>()
}));
}
let mut all: Vec<u64> = handles
.into_iter()
.flat_map(|h| h.join().unwrap())
.collect();
all.sort();
all.dedup();
assert_eq!(all.len(), 8000, "no duplicate nonces across 8 threads");
assert_eq!(all[0], 0);
assert_eq!(all[all.len() - 1], 7999);
}
}