use std::sync::{
Arc,
atomic::{AtomicU64, Ordering},
};
use dashmap::DashMap;
use thiserror::Error;
use crate::signing::encoding::utc_now_ms;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum NonceError {
#[error("system clock is before UNIX epoch")]
ClockBeforeEpoch,
}
#[derive(Debug, Default)]
pub struct NonceManager {
states: DashMap<(String, u64), Arc<AtomicU64>>,
}
impl NonceManager {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn next_nonce(&self, wallet: &str, subaccount_id: u64) -> Result<u64, NonceError> {
let now_ms = utc_now_ms().map_err(|_| NonceError::ClockBeforeEpoch)?;
Ok(self.next_nonce_at(wallet, subaccount_id, now_ms))
}
pub fn next_nonce_at(&self, wallet: &str, subaccount_id: u64, now_ms: u64) -> u64 {
let state = self.state_for(wallet, subaccount_id);
let initial = now_ms.saturating_mul(10);
loop {
let last = state.load(Ordering::Acquire);
let candidate = if initial > last { initial } else { last + 1 };
if state
.compare_exchange_weak(last, candidate, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
return candidate;
}
}
}
pub fn refresh(&self, wallet: &str, subaccount_id: u64, last_seen_nonce: u64) {
let state = self.state_for(wallet, subaccount_id);
loop {
let current = state.load(Ordering::Acquire);
if last_seen_nonce <= current {
return;
}
if state
.compare_exchange_weak(
current,
last_seen_nonce,
Ordering::AcqRel,
Ordering::Acquire,
)
.is_ok()
{
return;
}
}
}
#[must_use]
pub fn last_issued(&self, wallet: &str, subaccount_id: u64) -> Option<u64> {
self.states
.get(&Self::normalize_key(wallet, subaccount_id))
.map(|s| s.load(Ordering::Acquire))
.filter(|n| *n != 0)
}
fn state_for(&self, wallet: &str, subaccount_id: u64) -> Arc<AtomicU64> {
let entry = self
.states
.entry(Self::normalize_key(wallet, subaccount_id))
.or_insert_with(|| Arc::new(AtomicU64::new(0)));
entry.value().clone()
}
fn normalize_key(wallet: &str, subaccount_id: u64) -> (String, u64) {
(wallet.to_ascii_lowercase(), subaccount_id)
}
}
#[cfg(test)]
mod tests {
use std::{sync::Arc as StdArc, thread};
use rstest::rstest;
use super::*;
const WALLET_A: &str = "0x000000000000000000000000000000000000aaaa";
const WALLET_B: &str = "0x000000000000000000000000000000000000bbbb";
#[rstest]
fn test_next_nonce_at_first_call_concatenates_zero_suffix() {
let mgr = NonceManager::new();
let nonce = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
assert_eq!(nonce, 17_000_000_000_000);
}
#[rstest]
fn test_sequential_calls_within_same_ms_are_monotonic() {
let mgr = NonceManager::new();
let n1 = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
let n2 = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
let n3 = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
assert!(
n1 < n2 && n2 < n3,
"nonces must be monotonic, was {n1}, {n2}, {n3}"
);
assert_eq!(n2 - n1, 1);
assert_eq!(n3 - n2, 1);
}
#[rstest]
fn test_advancing_clock_jumps_to_new_prefix() {
let mgr = NonceManager::new();
let n1 = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
let n2 = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_001);
assert_eq!(n1, 17_000_000_000_000);
assert_eq!(n2, 17_000_000_000_010);
assert!(n2 > n1);
}
#[rstest]
fn test_distinct_keys_track_independent_state() {
let mgr = NonceManager::new();
let a = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
let b = mgr.next_nonce_at(WALLET_B, 1, 1_700_000_000_000);
assert_eq!(a, b);
let a2 = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
assert_eq!(a2, a + 1);
assert_eq!(mgr.last_issued(WALLET_B, 1), Some(b));
}
#[rstest]
fn test_distinct_subaccounts_track_independent_state() {
let mgr = NonceManager::new();
let a1 = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
let a2 = mgr.next_nonce_at(WALLET_A, 2, 1_700_000_000_000);
assert_eq!(a1, a2);
}
#[rstest]
fn test_refresh_advances_last_issued() {
let mgr = NonceManager::new();
mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
mgr.refresh(WALLET_A, 1, 99_999_999_999_999);
let n = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
assert_eq!(n, 99_999_999_999_999 + 1);
}
#[rstest]
fn test_refresh_never_rewinds_below_local_state() {
let mgr = NonceManager::new();
let high = mgr.next_nonce_at(WALLET_A, 1, 9_000_000_000_000);
mgr.refresh(WALLET_A, 1, 1_000);
let next = mgr.next_nonce_at(WALLET_A, 1, 1_000);
assert!(
next > high,
"refresh below local state must not rewind, last={high}, next={next}",
);
}
#[rstest]
fn test_checksum_and_lowercase_wallet_share_state() {
let mgr = NonceManager::new();
let lowercase = "0x000000000000000000000000000000000000abcd";
let checksum = "0x000000000000000000000000000000000000ABCD";
let n1 = mgr.next_nonce_at(lowercase, 1, 1_700_000_000_000);
let n2 = mgr.next_nonce_at(checksum, 1, 1_700_000_000_000);
assert_eq!(
n2,
n1 + 1,
"checksum and lowercase forms of the same address must share one nonce stream",
);
}
#[rstest]
fn test_last_issued_finds_state_regardless_of_address_case() {
let mgr = NonceManager::new();
let lowercase = "0x000000000000000000000000000000000000abcd";
let checksum = "0x000000000000000000000000000000000000ABCD";
let issued = mgr.next_nonce_at(lowercase, 1, 1_700_000_000_000);
assert_eq!(mgr.last_issued(lowercase, 1), Some(issued));
assert_eq!(
mgr.last_issued(checksum, 1),
Some(issued),
"lookup must normalize the same way as next_nonce/refresh",
);
}
#[rstest]
fn test_last_issued_reports_latest_value() {
let mgr = NonceManager::new();
assert_eq!(mgr.last_issued(WALLET_A, 1), None);
let n = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
assert_eq!(mgr.last_issued(WALLET_A, 1), Some(n));
}
#[rstest]
fn test_concurrent_callers_see_no_duplicates_or_gaps() {
let mgr = StdArc::new(NonceManager::new());
let threads = 8;
let per_thread = 250;
let now_ms = 1_700_000_000_000;
let handles: Vec<_> = (0..threads)
.map(|_| {
let mgr = StdArc::clone(&mgr);
thread::spawn(move || -> Vec<u64> {
(0..per_thread)
.map(|_| mgr.next_nonce_at(WALLET_A, 1, now_ms))
.collect()
})
})
.collect();
let mut all = Vec::with_capacity(threads * per_thread);
for h in handles {
all.extend(h.join().unwrap());
}
all.sort_unstable();
let total = (threads * per_thread) as u64;
let expected: Vec<u64> = (0..total).map(|i| now_ms * 10 + i).collect();
assert_eq!(
all, expected,
"concurrent issuance must be contiguous from ms*10",
);
}
#[rstest]
fn test_next_nonce_uses_system_clock_when_called_without_injection() {
let mgr = NonceManager::new();
let n = mgr.next_nonce(WALLET_A, 1).unwrap();
assert!(n > 17_000_000_000_000, "nonce too small: {n}");
}
}