Skip to main content

cdk_spilman/
client_storage.rs

1//! Client-side storage abstraction for Spilman payment channels
2//!
3//! This module provides storage traits and implementations for managing
4//! client-side channel state. It separates immutable funding data from
5//! mutable payment state, mirroring the server-side pattern.
6
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9
10// ============================================================================
11// Data Structures
12// ============================================================================
13
14/// Immutable funding data (saved once when channel is opened)
15///
16/// This data is set at channel creation time and never changes.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ClientChannelFunding {
19    /// Serialized channel parameters (JSON)
20    pub params_json: String,
21    /// Serialized funding proofs (JSON array)
22    pub funding_proofs_json: String,
23    /// Hex-encoded hashed ECDH channel secret (32 bytes)
24    pub channel_secret_hex: String,
25    /// Serialized keyset info (JSON)
26    pub keyset_info_json: String,
27    /// Sender's public key for this channel (hex)
28    pub sender_pubkey_hex: String,
29    /// Maximum value the receiver can claim
30    pub capacity: u64,
31    /// Nominal funding token amount
32    pub funding_token_amount: u64,
33    /// Mint URL associated with the channel
34    pub mint_url: String,
35    /// Unix timestamp when channel was created
36    pub created_at: u64,
37}
38
39/// Mutable payment state (updated on each payment)
40///
41/// This tracks the current state of payments made through the channel.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ClientPaymentState {
44    /// Last signed balance (cumulative, monotonically increasing)
45    pub balance: u64,
46    /// Last signature corresponding to the balance
47    pub signature: String,
48    /// Number of payments made through this channel
49    pub payment_count: u64,
50    /// Unix timestamp of the last payment
51    pub last_payment_at: u64,
52}
53
54/// Channel lifecycle state
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
56pub enum ClientChannelState {
57    /// Channel is open and can accept payments
58    #[default]
59    Open,
60    /// Channel is closed, no more payments allowed
61    Closed,
62}
63
64// ============================================================================
65// Storage Trait
66// ============================================================================
67
68/// Storage trait for client channel data
69///
70/// Implementations handle persistence of channel funding data and payment state.
71/// The trait separates immutable funding data from mutable payment state.
72pub trait ClientStorage {
73    // === Funding Data (immutable after creation) ===
74
75    /// Save funding data for a new channel
76    fn save_funding(&mut self, channel_id: &str, funding: ClientChannelFunding);
77
78    /// Get funding data for a channel
79    fn get_funding(&self, channel_id: &str) -> Option<&ClientChannelFunding>;
80
81    // === Payment State (mutable) ===
82
83    /// Get the current payment state for a channel
84    fn get_payment_state(&self, channel_id: &str) -> Option<&ClientPaymentState>;
85
86    /// Save/update payment state for a channel
87    fn save_payment_state(&mut self, channel_id: &str, state: ClientPaymentState);
88
89    // === Lifecycle ===
90
91    /// Get the lifecycle state of a channel
92    fn get_state(&self, channel_id: &str) -> ClientChannelState;
93
94    /// Mark a channel as closed
95    fn set_closed(&mut self, channel_id: &str);
96
97    // === Management ===
98
99    /// List all stored channel IDs
100    fn list_channel_ids(&self) -> Vec<String>;
101
102    /// Delete a channel and all its data
103    fn delete(&mut self, channel_id: &str);
104}
105
106// ============================================================================
107// In-Memory Implementation
108// ============================================================================
109
110/// In-memory storage implementation
111///
112/// Stores all channel data in HashMaps. Data is lost when the process exits.
113/// Suitable for testing, demos, and short-lived applications.
114#[derive(Debug, Default)]
115pub struct MemoryClientStorage {
116    funding: HashMap<String, ClientChannelFunding>,
117    payments: HashMap<String, ClientPaymentState>,
118    closed: HashSet<String>,
119}
120
121impl MemoryClientStorage {
122    /// Create a new empty in-memory storage
123    pub fn new() -> Self {
124        Self::default()
125    }
126
127    /// Get the number of stored channels
128    pub fn channel_count(&self) -> usize {
129        self.funding.len()
130    }
131}
132
133impl ClientStorage for MemoryClientStorage {
134    fn save_funding(&mut self, channel_id: &str, funding: ClientChannelFunding) {
135        self.funding.insert(channel_id.to_string(), funding);
136    }
137
138    fn get_funding(&self, channel_id: &str) -> Option<&ClientChannelFunding> {
139        self.funding.get(channel_id)
140    }
141
142    fn get_payment_state(&self, channel_id: &str) -> Option<&ClientPaymentState> {
143        self.payments.get(channel_id)
144    }
145
146    fn save_payment_state(&mut self, channel_id: &str, state: ClientPaymentState) {
147        self.payments.insert(channel_id.to_string(), state);
148    }
149
150    fn get_state(&self, channel_id: &str) -> ClientChannelState {
151        if self.closed.contains(channel_id) {
152            ClientChannelState::Closed
153        } else if self.funding.contains_key(channel_id) {
154            ClientChannelState::Open
155        } else {
156            // Channel doesn't exist, treat as closed
157            ClientChannelState::Closed
158        }
159    }
160
161    fn set_closed(&mut self, channel_id: &str) {
162        self.closed.insert(channel_id.to_string());
163    }
164
165    fn list_channel_ids(&self) -> Vec<String> {
166        self.funding.keys().cloned().collect()
167    }
168
169    fn delete(&mut self, channel_id: &str) {
170        self.funding.remove(channel_id);
171        self.payments.remove(channel_id);
172        self.closed.remove(channel_id);
173    }
174}
175
176// ============================================================================
177// Tests
178// ============================================================================
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    fn make_test_funding() -> ClientChannelFunding {
185        ClientChannelFunding {
186            params_json: r#"{"test": true}"#.to_string(),
187            funding_proofs_json: "[]".to_string(),
188            channel_secret_hex: "aa".repeat(32),
189            keyset_info_json: "{}".to_string(),
190            sender_pubkey_hex: "02".to_string() + &"bb".repeat(32),
191            capacity: 1000,
192            funding_token_amount: 1100,
193            mint_url: "https://mint.example.com".to_string(),
194            created_at: 1234567890,
195        }
196    }
197
198    fn make_test_payment_state(balance: u64) -> ClientPaymentState {
199        ClientPaymentState {
200            balance,
201            signature: "sig".to_string(),
202            payment_count: 1,
203            last_payment_at: 1234567890,
204        }
205    }
206
207    #[test]
208    fn test_memory_storage_funding() {
209        let mut storage = MemoryClientStorage::new();
210        let channel_id = "test_channel_1";
211
212        // Initially empty
213        assert!(storage.get_funding(channel_id).is_none());
214        assert_eq!(storage.channel_count(), 0);
215
216        // Save funding
217        storage.save_funding(channel_id, make_test_funding());
218
219        // Now retrievable
220        let funding = storage.get_funding(channel_id).unwrap();
221        assert_eq!(funding.capacity, 1000);
222        assert_eq!(storage.channel_count(), 1);
223
224        // State should be Open
225        assert_eq!(storage.get_state(channel_id), ClientChannelState::Open);
226    }
227
228    #[test]
229    fn test_memory_storage_payments() {
230        let mut storage = MemoryClientStorage::new();
231        let channel_id = "test_channel_1";
232
233        storage.save_funding(channel_id, make_test_funding());
234
235        // Initially no payment state
236        assert!(storage.get_payment_state(channel_id).is_none());
237
238        // Save payment state
239        storage.save_payment_state(channel_id, make_test_payment_state(100));
240
241        let state = storage.get_payment_state(channel_id).unwrap();
242        assert_eq!(state.balance, 100);
243        assert_eq!(state.payment_count, 1);
244
245        // Update payment state
246        storage.save_payment_state(channel_id, make_test_payment_state(200));
247
248        let state = storage.get_payment_state(channel_id).unwrap();
249        assert_eq!(state.balance, 200);
250    }
251
252    #[test]
253    fn test_memory_storage_lifecycle() {
254        let mut storage = MemoryClientStorage::new();
255        let channel_id = "test_channel_1";
256
257        // Unknown channel is Closed
258        assert_eq!(storage.get_state(channel_id), ClientChannelState::Closed);
259
260        // After funding, it's Open
261        storage.save_funding(channel_id, make_test_funding());
262        assert_eq!(storage.get_state(channel_id), ClientChannelState::Open);
263
264        // Mark closed
265        storage.set_closed(channel_id);
266        assert_eq!(storage.get_state(channel_id), ClientChannelState::Closed);
267    }
268
269    #[test]
270    fn test_memory_storage_delete() {
271        let mut storage = MemoryClientStorage::new();
272        let channel_id = "test_channel_1";
273
274        storage.save_funding(channel_id, make_test_funding());
275        storage.save_payment_state(channel_id, make_test_payment_state(100));
276        storage.set_closed(channel_id);
277
278        assert_eq!(storage.channel_count(), 1);
279
280        // Delete
281        storage.delete(channel_id);
282
283        assert_eq!(storage.channel_count(), 0);
284        assert!(storage.get_funding(channel_id).is_none());
285        assert!(storage.get_payment_state(channel_id).is_none());
286        assert_eq!(storage.get_state(channel_id), ClientChannelState::Closed);
287    }
288
289    #[test]
290    fn test_memory_storage_list() {
291        let mut storage = MemoryClientStorage::new();
292
293        storage.save_funding("channel_1", make_test_funding());
294        storage.save_funding("channel_2", make_test_funding());
295        storage.save_funding("channel_3", make_test_funding());
296
297        let mut ids = storage.list_channel_ids();
298        ids.sort();
299
300        assert_eq!(ids, vec!["channel_1", "channel_2", "channel_3"]);
301    }
302}