ecash_client/
storage.rs

1use crate::error::Result;
2use chrono::{DateTime, Utc};
3use ecash_core::Token;
4use rusqlite::{params, Connection};
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct StoredToken {
10    pub id: String,
11    pub token: Token,
12    pub status: TokenStatus,
13    pub created_at: DateTime<Utc>,
14    pub spent_at: Option<DateTime<Utc>>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
18pub enum TokenStatus {
19    Available,
20    Spent,
21    Pending,
22}
23
24impl std::fmt::Display for TokenStatus {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        let s = match self {
27            TokenStatus::Available => "available",
28            TokenStatus::Spent => "spent",
29            TokenStatus::Pending => "pending",
30        };
31        write!(f, "{}", s)
32    }
33}
34
35impl TokenStatus {
36    fn from_str(s: &str) -> Self {
37        match s {
38            "available" => TokenStatus::Available,
39            "spent" => TokenStatus::Spent,
40            "pending" => TokenStatus::Pending,
41            _ => TokenStatus::Available,
42        }
43    }
44}
45
46pub struct WalletStorage {
47    conn: Connection,
48}
49
50impl WalletStorage {
51    pub fn new(db_path: &str) -> Result<Self> {
52        let conn = Connection::open(db_path)?;
53        
54        conn.execute(
55            r#"
56            CREATE TABLE IF NOT EXISTS tokens (
57                id TEXT PRIMARY KEY,
58                token_data TEXT NOT NULL,
59                status TEXT NOT NULL,
60                created_at TEXT NOT NULL,
61                spent_at TEXT
62            )
63            "#,
64            [],
65        )?;
66        
67        conn.execute(
68            r#"
69            CREATE TABLE IF NOT EXISTS transactions (
70                id TEXT PRIMARY KEY,
71                tx_type TEXT NOT NULL,
72                amount INTEGER NOT NULL,
73                token_count INTEGER NOT NULL,
74                created_at TEXT NOT NULL,
75                metadata TEXT
76            )
77            "#,
78            [],
79        )?;
80        
81        conn.execute(
82            "CREATE INDEX IF NOT EXISTS idx_tokens_status ON tokens(status)",
83            [],
84        )?;
85        
86        Ok(Self { conn })
87    }
88
89    pub fn store_token(&self, token: Token) -> Result<StoredToken> {
90        let stored = StoredToken {
91            id: Uuid::new_v4().to_string(),
92            token,
93            status: TokenStatus::Available,
94            created_at: Utc::now(),
95            spent_at: None,
96        };
97        
98        let token_json = serde_json::to_string(&stored.token)?;
99        
100        self.conn.execute(
101            "INSERT INTO tokens (id, token_data, status, created_at, spent_at) VALUES (?1, ?2, ?3, ?4, ?5)",
102            params![
103                &stored.id,
104                token_json,
105                stored.status.to_string(),
106                stored.created_at.to_rfc3339(),
107                stored.spent_at.map(|dt| dt.to_rfc3339()),
108            ],
109        )?;
110        
111        Ok(stored)
112    }
113
114    pub fn get_available_tokens(&self) -> Result<Vec<StoredToken>> {
115        let mut stmt = self.conn.prepare(
116            "SELECT id, token_data, status, created_at, spent_at FROM tokens WHERE status = 'available' ORDER BY created_at"
117        )?;
118        
119        let tokens = stmt.query_map([], |row| {
120            let token_json: String = row.get(1)?;
121            let token: Token = serde_json::from_str(&token_json).unwrap();
122            let created_str: String = row.get(3)?;
123            let spent_str: Option<String> = row.get(4)?;
124            
125            Ok(StoredToken {
126                id: row.get(0)?,
127                token,
128                status: TokenStatus::from_str(&row.get::<_, String>(2)?),
129                created_at: DateTime::parse_from_rfc3339(&created_str).unwrap().with_timezone(&Utc),
130                spent_at: spent_str.and_then(|s| DateTime::parse_from_rfc3339(&s).ok().map(|dt| dt.with_timezone(&Utc))),
131            })
132        })?
133        .collect::<std::result::Result<Vec<_>, _>>()?;
134        
135        Ok(tokens)
136    }
137
138    pub fn mark_tokens_spent(&self, token_ids: &[String]) -> Result<()> {
139        let tx = self.conn.unchecked_transaction()?;
140        
141        for id in token_ids {
142            tx.execute(
143                "UPDATE tokens SET status = 'spent', spent_at = ?1 WHERE id = ?2",
144                params![Utc::now().to_rfc3339(), id],
145            )?;
146        }
147        
148        tx.commit()?;
149        Ok(())
150    }
151
152    pub fn get_balance(&self) -> Result<u64> {
153        let mut stmt = self.conn.prepare(
154            "SELECT token_data FROM tokens WHERE status = 'available'"
155        )?;
156        
157        let mut total = 0u64;
158        let tokens = stmt.query_map([], |row| {
159            let token_json: String = row.get(0)?;
160            Ok(token_json)
161        })?;
162        
163        for token_result in tokens {
164            let token_json = token_result?;
165            let token: Token = serde_json::from_str(&token_json)?;
166            total += token.denomination;
167        }
168        
169        Ok(total)
170    }
171
172    pub fn log_transaction(&self, tx_type: &str, amount: u64, token_count: usize, metadata: Option<String>) -> Result<()> {
173        self.conn.execute(
174            "INSERT INTO transactions (id, tx_type, amount, token_count, created_at, metadata) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
175            params![
176                Uuid::new_v4().to_string(),
177                tx_type,
178                amount as i64,
179                token_count as i64,
180                Utc::now().to_rfc3339(),
181                metadata,
182            ],
183        )?;
184        
185        Ok(())
186    }
187}