1use cdk_sql_common::SQLWalletDatabase;
4
5use crate::common::SqliteConnectionManager;
6
7pub mod memory;
8
9pub type WalletSqliteDatabase = SQLWalletDatabase<SqliteConnectionManager>;
11
12#[cfg(test)]
13mod tests {
14 use cdk_common::wallet_db_test;
15
16 use super::memory;
17
18 async fn provide_db(_test_name: String) -> super::WalletSqliteDatabase {
19 memory::empty().await.unwrap()
20 }
21
22 wallet_db_test!(provide_db);
23 use std::str::FromStr;
24
25 use cdk_common::database::WalletDatabase;
26 use cdk_common::nut00::KnownMethod;
27 use cdk_common::nuts::{ProofDleq, State};
28 use cdk_common::secret::Secret;
29
30 use crate::WalletSqliteDatabase;
31
32 #[tokio::test]
33 #[cfg(feature = "sqlcipher")]
34 async fn test_sqlcipher() {
35 use cdk_common::mint_url::MintUrl;
36 use cdk_common::MintInfo;
37
38 use super::*;
39 let path = std::env::temp_dir()
40 .to_path_buf()
41 .join(format!("cdk-test-{}.sqlite", uuid::Uuid::new_v4()));
42 let db = WalletSqliteDatabase::new((path, "password".to_string()))
43 .await
44 .unwrap();
45
46 let mint_info = MintInfo::new().description("test");
47 let mint_url = MintUrl::from_str("https://mint.xyz").unwrap();
48
49 db.add_mint(mint_url.clone(), Some(mint_info.clone()))
50 .await
51 .unwrap();
52
53 let res = db.get_mint(mint_url).await.unwrap();
54 assert_eq!(mint_info, res.clone().unwrap());
55 assert_eq!("test", &res.unwrap().description.unwrap());
56 }
57
58 #[tokio::test]
59 async fn test_proof_with_dleq() {
60 use cdk_common::mint_url::MintUrl;
61 use cdk_common::nuts::{CurrencyUnit, Id, Proof, PublicKey, SecretKey};
62 use cdk_common::wallet::ProofInfo;
63 use cdk_common::Amount;
64
65 let path = std::env::temp_dir()
67 .to_path_buf()
68 .join(format!("cdk-test-dleq-{}.sqlite", uuid::Uuid::new_v4()));
69
70 #[cfg(feature = "sqlcipher")]
71 let db = WalletSqliteDatabase::new((path, "password".to_string()))
72 .await
73 .unwrap();
74
75 #[cfg(not(feature = "sqlcipher"))]
76 let db = WalletSqliteDatabase::new(path).await.unwrap();
77
78 let keyset_id = Id::from_str("00deadbeef123456").unwrap();
80 let mint_url = MintUrl::from_str("https://example.com").unwrap();
81 let secret = Secret::new("test_secret_for_dleq");
82
83 let e = SecretKey::generate();
85 let s = SecretKey::generate();
86 let r = SecretKey::generate();
87
88 let dleq = ProofDleq::new(e.clone(), s.clone(), r.clone());
89
90 let mut proof = Proof::new(
91 Amount::from(64),
92 keyset_id,
93 secret,
94 PublicKey::from_hex(
95 "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
96 )
97 .unwrap(),
98 );
99
100 proof.dleq = Some(dleq);
102
103 let proof_info =
105 ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
106
107 db.update_proofs(vec![proof_info.clone()], vec![])
109 .await
110 .unwrap();
111
112 let retrieved_proofs = db
114 .get_proofs(
115 Some(mint_url),
116 Some(CurrencyUnit::Sat),
117 Some(vec![State::Unspent]),
118 None,
119 )
120 .await
121 .unwrap();
122
123 assert_eq!(retrieved_proofs.len(), 1);
125
126 let retrieved_proof = &retrieved_proofs[0];
128 assert!(retrieved_proof.proof.dleq.is_some());
129
130 let retrieved_dleq = retrieved_proof.proof.dleq.as_ref().unwrap();
131
132 assert_eq!(retrieved_dleq.e.to_secret_hex(), e.to_secret_hex());
134 assert_eq!(retrieved_dleq.s.to_secret_hex(), s.to_secret_hex());
135 assert_eq!(retrieved_dleq.r.to_secret_hex(), r.to_secret_hex());
136 }
137
138 #[tokio::test]
139 async fn test_mint_quote_payment_method_read_and_write() {
140 use cdk_common::mint_url::MintUrl;
141 use cdk_common::nuts::{CurrencyUnit, MintQuoteState, PaymentMethod, SecretKey};
142 use cdk_common::wallet::MintQuote;
143 use cdk_common::Amount;
144
145 let path = std::env::temp_dir().to_path_buf().join(format!(
147 "cdk-test-migration-{}.sqlite",
148 uuid::Uuid::new_v4()
149 ));
150
151 #[cfg(feature = "sqlcipher")]
152 let db = WalletSqliteDatabase::new((path, "password".to_string()))
153 .await
154 .unwrap();
155
156 #[cfg(not(feature = "sqlcipher"))]
157 let db = WalletSqliteDatabase::new(path).await.unwrap();
158
159 let mint_url = MintUrl::from_str("https://example.com").unwrap();
161 let quote_signing_key = SecretKey::generate();
162 let payment_methods = [
163 PaymentMethod::Known(KnownMethod::Bolt11),
164 PaymentMethod::Known(KnownMethod::Bolt11),
165 PaymentMethod::Custom("custom".to_string()),
166 ];
167
168 for (i, payment_method) in payment_methods.iter().enumerate() {
169 let quote = MintQuote {
170 id: format!("test_quote_{}", i),
171 mint_url: mint_url.clone(),
172 amount: Some(Amount::from(100)),
173 unit: CurrencyUnit::Sat,
174 request: "test_request".to_string(),
175 state: MintQuoteState::Unpaid,
176 expiry: 1000000000,
177 secret_key: Some(quote_signing_key.clone()),
178 payment_method: payment_method.clone(),
179 amount_issued: Amount::from(0),
180 amount_paid: Amount::from(0),
181 updated_at: 0,
182 estimated_blocks: None,
183 used_by_operation: None,
184 version: 0,
185 };
186
187 db.add_mint_quote(quote.clone()).await.unwrap();
189
190 let retrieved = db.get_mint_quote("e.id).await.unwrap().unwrap();
192 assert_eq!(retrieved.payment_method, *payment_method);
193 assert_eq!(retrieved.secret_key, Some(quote_signing_key.clone()));
194 assert_eq!(retrieved.amount_issued, Amount::from(0));
195 assert_eq!(retrieved.amount_paid, Amount::from(0));
196 }
197 }
198
199 #[tokio::test]
200 async fn test_get_proofs_by_ys_empty_errors() {
201 use cdk_common::database::Error;
202
203 let path = std::env::temp_dir().to_path_buf().join(format!(
204 "cdk-test-proofs-by-ys-empty-{}.sqlite",
205 uuid::Uuid::new_v4()
206 ));
207
208 #[cfg(feature = "sqlcipher")]
209 let db = WalletSqliteDatabase::new((path, "password".to_string()))
210 .await
211 .unwrap();
212
213 #[cfg(not(feature = "sqlcipher"))]
214 let db = WalletSqliteDatabase::new(path).await.unwrap();
215
216 let result = db.get_proofs_by_ys(vec![]).await;
217 assert!(matches!(result, Err(Error::EmptyInClause(_))));
218 }
219
220 #[tokio::test]
221 async fn test_get_proofs_by_ys() {
222 use cdk_common::mint_url::MintUrl;
223 use cdk_common::nuts::{CurrencyUnit, Id, Proof, SecretKey};
224 use cdk_common::wallet::ProofInfo;
225 use cdk_common::Amount;
226
227 let path = std::env::temp_dir().to_path_buf().join(format!(
228 "cdk-test-proofs-by-ys-{}.sqlite",
229 uuid::Uuid::new_v4()
230 ));
231
232 #[cfg(feature = "sqlcipher")]
233 let db = WalletSqliteDatabase::new((path, "password".to_string()))
234 .await
235 .unwrap();
236
237 #[cfg(not(feature = "sqlcipher"))]
238 let db = WalletSqliteDatabase::new(path).await.unwrap();
239
240 let keyset_id = Id::from_str("00deadbeef123456").unwrap();
241 let mint_url = MintUrl::from_str("https://example.com").unwrap();
242
243 let mut proof_infos = vec![];
244 let mut expected_ys = vec![];
245
246 for _i in 0..5 {
247 let secret = Secret::generate();
248 let secret_key = SecretKey::generate();
249 let c = secret_key.public_key();
250 let proof = Proof::new(Amount::from(64), keyset_id, secret, c);
251 let proof_info =
252 ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
253
254 expected_ys.push(proof_info.y);
255 proof_infos.push(proof_info);
256 }
257
258 db.update_proofs(proof_infos.clone(), vec![]).await.unwrap();
259
260 let retrieved_proofs = db.get_proofs_by_ys(expected_ys.clone()).await.unwrap();
262 assert_eq!(retrieved_proofs.len(), 5);
263 for retrieved_proof in &retrieved_proofs {
264 assert!(expected_ys.contains(&retrieved_proof.y));
265 }
266
267 let subset_ys = expected_ys[0..3].to_vec();
269 let subset_proofs = db.get_proofs_by_ys(subset_ys.clone()).await.unwrap();
270 assert_eq!(subset_proofs.len(), 3);
271 for retrieved_proof in &subset_proofs {
272 assert!(subset_ys.contains(&retrieved_proof.y));
273 }
274
275 let non_existent_secret_key = SecretKey::generate();
277 let non_existent_y = non_existent_secret_key.public_key();
278 let mixed_ys = vec![expected_ys[0], non_existent_y, expected_ys[1]];
279 let mixed_proofs = db.get_proofs_by_ys(mixed_ys).await.unwrap();
280 assert_eq!(mixed_proofs.len(), 2);
281
282 let single_y = vec![expected_ys[2]];
284 let single_proof = db.get_proofs_by_ys(single_y).await.unwrap();
285 assert_eq!(single_proof.len(), 1);
286 assert_eq!(single_proof[0].y, proof_infos[2].y);
287 assert_eq!(single_proof[0].proof.amount, proof_infos[2].proof.amount);
288 assert_eq!(single_proof[0].mint_url, proof_infos[2].mint_url);
289 assert_eq!(single_proof[0].state, proof_infos[2].state);
290 }
291
292 #[tokio::test]
293 async fn test_get_unissued_mint_quotes() {
294 use cdk_common::mint_url::MintUrl;
295 use cdk_common::nuts::{CurrencyUnit, MintQuoteState, PaymentMethod};
296 use cdk_common::wallet::MintQuote;
297 use cdk_common::Amount;
298
299 let path = std::env::temp_dir().to_path_buf().join(format!(
301 "cdk-test-unpaid-quotes-{}.sqlite",
302 uuid::Uuid::new_v4()
303 ));
304
305 #[cfg(feature = "sqlcipher")]
306 let db = WalletSqliteDatabase::new((path, "password".to_string()))
307 .await
308 .unwrap();
309
310 #[cfg(not(feature = "sqlcipher"))]
311 let db = WalletSqliteDatabase::new(path).await.unwrap();
312
313 let mint_url = MintUrl::from_str("https://example.com").unwrap();
314
315 let quote1 = MintQuote {
317 id: "quote_fully_paid".to_string(),
318 mint_url: mint_url.clone(),
319 amount: Some(Amount::from(100)),
320 unit: CurrencyUnit::Sat,
321 request: "test_request_1".to_string(),
322 state: MintQuoteState::Paid,
323 expiry: 1000000000,
324 secret_key: None,
325 payment_method: PaymentMethod::Known(KnownMethod::Bolt11),
326 amount_issued: Amount::from(100),
327 amount_paid: Amount::from(100),
328 updated_at: 0,
329 estimated_blocks: None,
330 used_by_operation: None,
331 version: 0,
332 };
333
334 let quote2 = MintQuote {
336 id: "quote_pending_balance".to_string(),
337 mint_url: mint_url.clone(),
338 amount: Some(Amount::from(100)),
339 unit: CurrencyUnit::Sat,
340 request: "test_request_2".to_string(),
341 state: MintQuoteState::Paid,
342 expiry: 1000000000,
343 secret_key: None,
344 payment_method: PaymentMethod::Known(KnownMethod::Bolt11),
345 amount_issued: Amount::from(0),
346 amount_paid: Amount::from(100),
347 updated_at: 0,
348 estimated_blocks: None,
349 used_by_operation: None,
350 version: 0,
351 };
352
353 let quote3 = MintQuote {
355 id: "quote_bolt12".to_string(),
356 mint_url: mint_url.clone(),
357 amount: Some(Amount::from(100)),
358 unit: CurrencyUnit::Sat,
359 request: "test_request_3".to_string(),
360 state: MintQuoteState::Unpaid,
361 expiry: 1000000000,
362 secret_key: None,
363 payment_method: PaymentMethod::Known(KnownMethod::Bolt12),
364 amount_issued: Amount::from(0),
365 amount_paid: Amount::from(0),
366 updated_at: 0,
367 estimated_blocks: None,
368 used_by_operation: None,
369 version: 0,
370 };
371
372 let quote4 = MintQuote {
374 id: "quote_unpaid".to_string(),
375 mint_url: mint_url.clone(),
376 amount: Some(Amount::from(100)),
377 unit: CurrencyUnit::Sat,
378 request: "test_request_4".to_string(),
379 state: MintQuoteState::Unpaid,
380 expiry: 1000000000,
381 secret_key: None,
382 payment_method: PaymentMethod::Known(KnownMethod::Bolt11),
383 amount_issued: Amount::from(0),
384 amount_paid: Amount::from(0),
385 updated_at: 0,
386 estimated_blocks: None,
387 used_by_operation: None,
388 version: 0,
389 };
390
391 db.add_mint_quote(quote1).await.unwrap();
393 db.add_mint_quote(quote2.clone()).await.unwrap();
394 db.add_mint_quote(quote3.clone()).await.unwrap();
395 db.add_mint_quote(quote4.clone()).await.unwrap();
396
397 let unissued_quotes = db.get_unissued_mint_quotes().await.unwrap();
399
400 assert_eq!(unissued_quotes.len(), 3);
405
406 let quote_ids: Vec<&str> = unissued_quotes.iter().map(|q| q.id.as_str()).collect();
408 assert!(quote_ids.contains(&"quote_pending_balance"));
409 assert!(quote_ids.contains(&"quote_bolt12"));
410 assert!(quote_ids.contains(&"quote_unpaid"));
411
412 assert!(!quote_ids.contains(&"quote_fully_paid"));
414 }
415}