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}