Skip to main content

r402_evm/batch_settlement/
store.rs

1//! Channel accounting store for batch-settlement servers/facilitators.
2//!
3//! Integrators replace [`MemoryChannelStore`] with durable backends; the trait
4//! is the only extension point (no plugin framework).
5
6use std::collections::HashMap;
7use std::sync::{Arc, Mutex};
8
9use alloy_primitives::B256;
10
11use super::types::ChannelState;
12use crate::chain::TokenAmount;
13
14/// Read/write channel accounting keyed by channel id.
15pub trait ChannelStore: Send + Sync {
16    /// Loads channel state (default empty if unknown).
17    fn get(&self, channel_id: &B256) -> ChannelState;
18
19    /// Persists channel state.
20    fn put(&self, channel_id: B256, state: ChannelState);
21
22    /// Atomically reserves a charge: requires
23    /// `max_claimable >= charged_cumulative + charge` and then
24    /// `charged_cumulative += charge`.
25    ///
26    /// # Errors
27    ///
28    /// Returns `false` when the voucher ceiling is insufficient.
29    fn try_charge(
30        &self,
31        channel_id: B256,
32        charge: TokenAmount,
33        max_claimable: TokenAmount,
34    ) -> bool {
35        let mut state = self.get(&channel_id);
36        let next = state.charged_cumulative.0.saturating_add(charge.0);
37        if next > max_claimable.0 {
38            return false;
39        }
40        state.charged_cumulative = TokenAmount::from(next);
41        self.put(channel_id, state);
42        true
43    }
44}
45
46/// Process-local in-memory store (tests and single-node demos).
47#[derive(Debug, Default, Clone)]
48pub struct MemoryChannelStore {
49    inner: Arc<Mutex<HashMap<B256, ChannelState>>>,
50}
51
52impl MemoryChannelStore {
53    /// Empty store.
54    #[must_use]
55    pub fn new() -> Self {
56        Self::default()
57    }
58}
59
60impl ChannelStore for MemoryChannelStore {
61    fn get(&self, channel_id: &B256) -> ChannelState {
62        self.inner
63            .lock()
64            .unwrap_or_else(std::sync::PoisonError::into_inner)
65            .get(channel_id)
66            .copied()
67            .unwrap_or_default()
68    }
69
70    fn put(&self, channel_id: B256, state: ChannelState) {
71        let _ = self
72            .inner
73            .lock()
74            .unwrap_or_else(std::sync::PoisonError::into_inner)
75            .insert(channel_id, state);
76    }
77
78    fn try_charge(
79        &self,
80        channel_id: B256,
81        charge: TokenAmount,
82        max_claimable: TokenAmount,
83    ) -> bool {
84        let mut guard = self
85            .inner
86            .lock()
87            .unwrap_or_else(std::sync::PoisonError::into_inner);
88        let entry = guard.entry(channel_id).or_default();
89        let next = entry.charged_cumulative.0.saturating_add(charge.0);
90        if next > max_claimable.0 {
91            return false;
92        }
93        entry.charged_cumulative = TokenAmount::from(next);
94        drop(guard);
95        true
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use alloy_primitives::U256;
102
103    use super::*;
104
105    #[test]
106    fn charge_monotonic() {
107        let store = MemoryChannelStore::new();
108        let id = B256::repeat_byte(0xab);
109        assert!(store.try_charge(
110            id,
111            TokenAmount::from(U256::from(10_u64)),
112            TokenAmount::from(U256::from(100_u64))
113        ));
114        assert_eq!(store.get(&id).charged_cumulative.0, U256::from(10_u64));
115        assert!(!store.try_charge(
116            id,
117            TokenAmount::from(U256::from(100_u64)),
118            TokenAmount::from(U256::from(100_u64))
119        ));
120        assert!(store.try_charge(
121            id,
122            TokenAmount::from(U256::from(90_u64)),
123            TokenAmount::from(U256::from(100_u64))
124        ));
125    }
126}