Skip to main content

cdk_sqlite/wallet/
mod.rs

1//! SQLite Wallet Database
2
3use cdk_sql_common::SQLWalletDatabase;
4
5use crate::common::SqliteConnectionManager;
6
7pub mod memory;
8
9/// Mint SQLite implementation with rusqlite
10pub 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        // Create a temporary database
66        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        // Create a proof with DLEQ
79        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        // Create DLEQ components
84        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        // Add DLEQ to the proof
101        proof.dleq = Some(dleq);
102
103        // Create ProofInfo
104        let proof_info =
105            ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
106
107        // Store the proof in the database
108        db.update_proofs(vec![proof_info.clone()], vec![])
109            .await
110            .unwrap();
111
112        // Retrieve the proof from the database
113        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        // Verify we got back exactly one proof
124        assert_eq!(retrieved_proofs.len(), 1);
125
126        // Verify the DLEQ data was preserved
127        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        // Verify DLEQ components match what we stored
133        assert_eq!(retrieved_dleq.e.to_string(), e.to_string());
134        assert_eq!(retrieved_dleq.s.to_string(), s.to_string());
135        assert_eq!(retrieved_dleq.r.to_string(), r.to_string());
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};
142        use cdk_common::wallet::MintQuote;
143        use cdk_common::Amount;
144
145        // Create a temporary database
146        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        // Test PaymentMethod variants
160        let mint_url = MintUrl::from_str("https://example.com").unwrap();
161        let payment_methods = [
162            PaymentMethod::Known(KnownMethod::Bolt11),
163            PaymentMethod::Known(KnownMethod::Bolt11),
164            PaymentMethod::Custom("custom".to_string()),
165        ];
166
167        for (i, payment_method) in payment_methods.iter().enumerate() {
168            let quote = MintQuote {
169                id: format!("test_quote_{}", i),
170                mint_url: mint_url.clone(),
171                amount: Some(Amount::from(100)),
172                unit: CurrencyUnit::Sat,
173                request: "test_request".to_string(),
174                state: MintQuoteState::Unpaid,
175                expiry: 1000000000,
176                secret_key: None,
177                payment_method: payment_method.clone(),
178                amount_issued: Amount::from(0),
179                amount_paid: Amount::from(0),
180                estimated_blocks: None,
181                used_by_operation: None,
182                version: 0,
183            };
184
185            // Store the quote
186            db.add_mint_quote(quote.clone()).await.unwrap();
187
188            // Retrieve and verify
189            let retrieved = db.get_mint_quote(&quote.id).await.unwrap().unwrap();
190            assert_eq!(retrieved.payment_method, *payment_method);
191            assert_eq!(retrieved.amount_issued, Amount::from(0));
192            assert_eq!(retrieved.amount_paid, Amount::from(0));
193        }
194    }
195
196    #[tokio::test]
197    async fn test_get_proofs_by_ys_empty_errors() {
198        use cdk_common::database::Error;
199
200        let path = std::env::temp_dir().to_path_buf().join(format!(
201            "cdk-test-proofs-by-ys-empty-{}.sqlite",
202            uuid::Uuid::new_v4()
203        ));
204
205        #[cfg(feature = "sqlcipher")]
206        let db = WalletSqliteDatabase::new((path, "password".to_string()))
207            .await
208            .unwrap();
209
210        #[cfg(not(feature = "sqlcipher"))]
211        let db = WalletSqliteDatabase::new(path).await.unwrap();
212
213        let result = db.get_proofs_by_ys(vec![]).await;
214        assert!(matches!(result, Err(Error::EmptyInClause(_))));
215    }
216
217    #[tokio::test]
218    async fn test_get_proofs_by_ys() {
219        use cdk_common::mint_url::MintUrl;
220        use cdk_common::nuts::{CurrencyUnit, Id, Proof, SecretKey};
221        use cdk_common::wallet::ProofInfo;
222        use cdk_common::Amount;
223
224        let path = std::env::temp_dir().to_path_buf().join(format!(
225            "cdk-test-proofs-by-ys-{}.sqlite",
226            uuid::Uuid::new_v4()
227        ));
228
229        #[cfg(feature = "sqlcipher")]
230        let db = WalletSqliteDatabase::new((path, "password".to_string()))
231            .await
232            .unwrap();
233
234        #[cfg(not(feature = "sqlcipher"))]
235        let db = WalletSqliteDatabase::new(path).await.unwrap();
236
237        let keyset_id = Id::from_str("00deadbeef123456").unwrap();
238        let mint_url = MintUrl::from_str("https://example.com").unwrap();
239
240        let mut proof_infos = vec![];
241        let mut expected_ys = vec![];
242
243        for _i in 0..5 {
244            let secret = Secret::generate();
245            let secret_key = SecretKey::generate();
246            let c = secret_key.public_key();
247            let proof = Proof::new(Amount::from(64), keyset_id, secret, c);
248            let proof_info =
249                ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
250
251            expected_ys.push(proof_info.y);
252            proof_infos.push(proof_info);
253        }
254
255        db.update_proofs(proof_infos.clone(), vec![]).await.unwrap();
256
257        // Retrieve all proofs by their Y values
258        let retrieved_proofs = db.get_proofs_by_ys(expected_ys.clone()).await.unwrap();
259        assert_eq!(retrieved_proofs.len(), 5);
260        for retrieved_proof in &retrieved_proofs {
261            assert!(expected_ys.contains(&retrieved_proof.y));
262        }
263
264        // Retrieve subset of proofs (first 3)
265        let subset_ys = expected_ys[0..3].to_vec();
266        let subset_proofs = db.get_proofs_by_ys(subset_ys.clone()).await.unwrap();
267        assert_eq!(subset_proofs.len(), 3);
268        for retrieved_proof in &subset_proofs {
269            assert!(subset_ys.contains(&retrieved_proof.y));
270        }
271
272        // Retrieve with non-existent Y values returns only existing ones
273        let non_existent_secret_key = SecretKey::generate();
274        let non_existent_y = non_existent_secret_key.public_key();
275        let mixed_ys = vec![expected_ys[0], non_existent_y, expected_ys[1]];
276        let mixed_proofs = db.get_proofs_by_ys(mixed_ys).await.unwrap();
277        assert_eq!(mixed_proofs.len(), 2);
278
279        // Verify retrieved proof data matches original
280        let single_y = vec![expected_ys[2]];
281        let single_proof = db.get_proofs_by_ys(single_y).await.unwrap();
282        assert_eq!(single_proof.len(), 1);
283        assert_eq!(single_proof[0].y, proof_infos[2].y);
284        assert_eq!(single_proof[0].proof.amount, proof_infos[2].proof.amount);
285        assert_eq!(single_proof[0].mint_url, proof_infos[2].mint_url);
286        assert_eq!(single_proof[0].state, proof_infos[2].state);
287    }
288
289    #[tokio::test]
290    async fn test_get_unissued_mint_quotes() {
291        use cdk_common::mint_url::MintUrl;
292        use cdk_common::nuts::{CurrencyUnit, MintQuoteState, PaymentMethod};
293        use cdk_common::wallet::MintQuote;
294        use cdk_common::Amount;
295
296        // Create a temporary database
297        let path = std::env::temp_dir().to_path_buf().join(format!(
298            "cdk-test-unpaid-quotes-{}.sqlite",
299            uuid::Uuid::new_v4()
300        ));
301
302        #[cfg(feature = "sqlcipher")]
303        let db = WalletSqliteDatabase::new((path, "password".to_string()))
304            .await
305            .unwrap();
306
307        #[cfg(not(feature = "sqlcipher"))]
308        let db = WalletSqliteDatabase::new(path).await.unwrap();
309
310        let mint_url = MintUrl::from_str("https://example.com").unwrap();
311
312        // Quote 1: Fully paid and issued (should NOT be returned)
313        let quote1 = MintQuote {
314            id: "quote_fully_paid".to_string(),
315            mint_url: mint_url.clone(),
316            amount: Some(Amount::from(100)),
317            unit: CurrencyUnit::Sat,
318            request: "test_request_1".to_string(),
319            state: MintQuoteState::Paid,
320            expiry: 1000000000,
321            secret_key: None,
322            payment_method: PaymentMethod::Known(KnownMethod::Bolt11),
323            amount_issued: Amount::from(100),
324            amount_paid: Amount::from(100),
325            estimated_blocks: None,
326            used_by_operation: None,
327            version: 0,
328        };
329
330        // Quote 2: Paid but not yet issued (should be returned - has pending balance)
331        let quote2 = MintQuote {
332            id: "quote_pending_balance".to_string(),
333            mint_url: mint_url.clone(),
334            amount: Some(Amount::from(100)),
335            unit: CurrencyUnit::Sat,
336            request: "test_request_2".to_string(),
337            state: MintQuoteState::Paid,
338            expiry: 1000000000,
339            secret_key: None,
340            payment_method: PaymentMethod::Known(KnownMethod::Bolt11),
341            amount_issued: Amount::from(0),
342            amount_paid: Amount::from(100),
343            estimated_blocks: None,
344            used_by_operation: None,
345            version: 0,
346        };
347
348        // Quote 3: Bolt12 quote with no balance (should be returned - bolt12 is reusable)
349        let quote3 = MintQuote {
350            id: "quote_bolt12".to_string(),
351            mint_url: mint_url.clone(),
352            amount: Some(Amount::from(100)),
353            unit: CurrencyUnit::Sat,
354            request: "test_request_3".to_string(),
355            state: MintQuoteState::Unpaid,
356            expiry: 1000000000,
357            secret_key: None,
358            payment_method: PaymentMethod::Known(KnownMethod::Bolt12),
359            amount_issued: Amount::from(0),
360            amount_paid: Amount::from(0),
361            estimated_blocks: None,
362            used_by_operation: None,
363            version: 0,
364        };
365
366        // Quote 4: Unpaid bolt11 quote (should be returned - wallet needs to check with mint)
367        let quote4 = MintQuote {
368            id: "quote_unpaid".to_string(),
369            mint_url: mint_url.clone(),
370            amount: Some(Amount::from(100)),
371            unit: CurrencyUnit::Sat,
372            request: "test_request_4".to_string(),
373            state: MintQuoteState::Unpaid,
374            expiry: 1000000000,
375            secret_key: None,
376            payment_method: PaymentMethod::Known(KnownMethod::Bolt11),
377            amount_issued: Amount::from(0),
378            amount_paid: Amount::from(0),
379            estimated_blocks: None,
380            used_by_operation: None,
381            version: 0,
382        };
383
384        // Add all quotes to the database
385        db.add_mint_quote(quote1).await.unwrap();
386        db.add_mint_quote(quote2.clone()).await.unwrap();
387        db.add_mint_quote(quote3.clone()).await.unwrap();
388        db.add_mint_quote(quote4.clone()).await.unwrap();
389
390        // Get unissued mint quotes
391        let unissued_quotes = db.get_unissued_mint_quotes().await.unwrap();
392
393        // Should return 3 quotes: quote2, quote3, and quote4
394        // - quote2: bolt11 with amount_issued = 0 (needs minting)
395        // - quote3: bolt12 (always returned, reusable)
396        // - quote4: bolt11 with amount_issued = 0 (check with mint if paid)
397        assert_eq!(unissued_quotes.len(), 3);
398
399        // Verify the returned quotes are the expected ones
400        let quote_ids: Vec<&str> = unissued_quotes.iter().map(|q| q.id.as_str()).collect();
401        assert!(quote_ids.contains(&"quote_pending_balance"));
402        assert!(quote_ids.contains(&"quote_bolt12"));
403        assert!(quote_ids.contains(&"quote_unpaid"));
404
405        // Verify that fully paid and issued quote is not returned
406        assert!(!quote_ids.contains(&"quote_fully_paid"));
407    }
408}