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_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        // 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 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            // Store the quote
188            db.add_mint_quote(quote.clone()).await.unwrap();
189
190            // Retrieve and verify
191            let retrieved = db.get_mint_quote(&quote.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        // Retrieve all proofs by their Y values
261        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        // Retrieve subset of proofs (first 3)
268        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        // Retrieve with non-existent Y values returns only existing ones
276        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        // Verify retrieved proof data matches original
283        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        // Create a temporary database
300        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        // Quote 1: Fully paid and issued (should NOT be returned)
316        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        // Quote 2: Paid but not yet issued (should be returned - has pending balance)
335        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        // Quote 3: Bolt12 quote with no balance (should be returned - bolt12 is reusable)
354        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        // Quote 4: Unpaid bolt11 quote (should be returned - wallet needs to check with mint)
373        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        // Add all quotes to the database
392        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        // Get unissued mint quotes
398        let unissued_quotes = db.get_unissued_mint_quotes().await.unwrap();
399
400        // Should return 3 quotes: quote2, quote3, and quote4
401        // - quote2: bolt11 with amount_issued = 0 (needs minting)
402        // - quote3: bolt12 (always returned, reusable)
403        // - quote4: bolt11 with amount_issued = 0 (check with mint if paid)
404        assert_eq!(unissued_quotes.len(), 3);
405
406        // Verify the returned quotes are the expected ones
407        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        // Verify that fully paid and issued quote is not returned
413        assert!(!quote_ids.contains(&"quote_fully_paid"));
414    }
415}