agent-pay 0.1.0

L402 + DID-signed invoices: agent-to-agent Lightning payments (Rust port of @p-vbordei/agent-pay)
Documentation
//! Cache to reject replayed preimages within their invoice's TTL.

use std::collections::HashMap;
use std::collections::VecDeque;
use std::sync::Mutex;

/// Now provider returning milliseconds since the Unix epoch.
pub type NowFn = Box<dyn Fn() -> u64 + Send + Sync>;

pub struct ReplayCache {
    map: Mutex<Inner>,
    max_entries: usize,
    now: NowFn,
}

struct Inner {
    expirations: HashMap<String, u64>,
    order: VecDeque<String>,
}

fn default_now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

impl Default for ReplayCache {
    fn default() -> Self {
        Self::new(100_000, Box::new(default_now_ms))
    }
}

impl ReplayCache {
    pub fn new(max_entries: usize, now: NowFn) -> Self {
        Self {
            map: Mutex::new(Inner {
                expirations: HashMap::new(),
                order: VecDeque::new(),
            }),
            max_entries,
            now,
        }
    }

    pub fn mark_used(&self, key: &str, expires_at_ms: u64) {
        let mut inner = self.map.lock().unwrap();
        if inner.expirations.len() >= self.max_entries {
            if let Some(oldest) = inner.order.pop_front() {
                inner.expirations.remove(&oldest);
            }
        }
        if inner
            .expirations
            .insert(key.to_string(), expires_at_ms)
            .is_none()
        {
            inner.order.push_back(key.to_string());
        }
    }

    pub fn is_used(&self, key: &str) -> bool {
        let mut inner = self.map.lock().unwrap();
        let Some(exp) = inner.expirations.get(key).copied() else {
            return false;
        };
        if exp <= (self.now)() {
            inner.expirations.remove(key);
            // Lazy: leave order entry; it'll be skipped if popped.
            return false;
        }
        true
    }
}