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                used_by_operation: None,
181                version: 0,
182            };
183
184            // Store the quote
185            db.add_mint_quote(quote.clone()).await.unwrap();
186
187            // Retrieve and verify
188            let retrieved = db.get_mint_quote(&quote.id).await.unwrap().unwrap();
189            assert_eq!(retrieved.payment_method, *payment_method);
190            assert_eq!(retrieved.amount_issued, Amount::from(0));
191            assert_eq!(retrieved.amount_paid, Amount::from(0));
192        }
193    }
194
195    #[tokio::test]
196    async fn test_get_proofs_by_ys() {
197        use cdk_common::mint_url::MintUrl;
198        use cdk_common::nuts::{CurrencyUnit, Id, Proof, SecretKey};
199        use cdk_common::wallet::ProofInfo;
200        use cdk_common::Amount;
201
202        // Create a temporary database
203        let path = std::env::temp_dir().to_path_buf().join(format!(
204            "cdk-test-proofs-by-ys-{}.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        // Create multiple proofs
217        let keyset_id = Id::from_str("00deadbeef123456").unwrap();
218        let mint_url = MintUrl::from_str("https://example.com").unwrap();
219
220        let mut proof_infos = vec![];
221        let mut expected_ys = vec![];
222
223        // Generate valid public keys using SecretKey
224        for _i in 0..5 {
225            let secret = Secret::generate();
226
227            // Generate a valid public key from a secret key
228            let secret_key = SecretKey::generate();
229            let c = secret_key.public_key();
230
231            let proof = Proof::new(Amount::from(64), keyset_id, secret, c);
232
233            let proof_info =
234                ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
235
236            expected_ys.push(proof_info.y);
237            proof_infos.push(proof_info);
238        }
239
240        // Store all proofs in the database
241        db.update_proofs(proof_infos.clone(), vec![]).await.unwrap();
242
243        // Test 1: Retrieve all proofs by their Y values
244        let retrieved_proofs = db.get_proofs_by_ys(expected_ys.clone()).await.unwrap();
245
246        assert_eq!(retrieved_proofs.len(), 5);
247        for retrieved_proof in &retrieved_proofs {
248            assert!(expected_ys.contains(&retrieved_proof.y));
249        }
250
251        // Test 2: Retrieve subset of proofs (first 3)
252        let subset_ys = expected_ys[0..3].to_vec();
253        let subset_proofs = db.get_proofs_by_ys(subset_ys.clone()).await.unwrap();
254
255        assert_eq!(subset_proofs.len(), 3);
256        for retrieved_proof in &subset_proofs {
257            assert!(subset_ys.contains(&retrieved_proof.y));
258        }
259
260        // Test 3: Retrieve with non-existent Y values
261        let non_existent_secret_key = SecretKey::generate();
262        let non_existent_y = non_existent_secret_key.public_key();
263        let mixed_ys = vec![expected_ys[0], non_existent_y, expected_ys[1]];
264        let mixed_proofs = db.get_proofs_by_ys(mixed_ys).await.unwrap();
265
266        // Should only return the 2 that exist
267        assert_eq!(mixed_proofs.len(), 2);
268
269        // Test 4: Empty input returns empty result
270        let empty_result = db.get_proofs_by_ys(vec![]).await.unwrap();
271        assert_eq!(empty_result.len(), 0);
272
273        // Test 5: Verify retrieved proof data matches original
274        let single_y = vec![expected_ys[2]];
275        let single_proof = db.get_proofs_by_ys(single_y).await.unwrap();
276
277        assert_eq!(single_proof.len(), 1);
278        assert_eq!(single_proof[0].y, proof_infos[2].y);
279        assert_eq!(single_proof[0].proof.amount, proof_infos[2].proof.amount);
280        assert_eq!(single_proof[0].mint_url, proof_infos[2].mint_url);
281        assert_eq!(single_proof[0].state, proof_infos[2].state);
282    }
283
284    #[tokio::test]
285    async fn test_get_unissued_mint_quotes() {
286        use cdk_common::mint_url::MintUrl;
287        use cdk_common::nuts::{CurrencyUnit, MintQuoteState, PaymentMethod};
288        use cdk_common::wallet::MintQuote;
289        use cdk_common::Amount;
290
291        // Create a temporary database
292        let path = std::env::temp_dir().to_path_buf().join(format!(
293            "cdk-test-unpaid-quotes-{}.sqlite",
294            uuid::Uuid::new_v4()
295        ));
296
297        #[cfg(feature = "sqlcipher")]
298        let db = WalletSqliteDatabase::new((path, "password".to_string()))
299            .await
300            .unwrap();
301
302        #[cfg(not(feature = "sqlcipher"))]
303        let db = WalletSqliteDatabase::new(path).await.unwrap();
304
305        let mint_url = MintUrl::from_str("https://example.com").unwrap();
306
307        // Quote 1: Fully paid and issued (should NOT be returned)
308        let quote1 = MintQuote {
309            id: "quote_fully_paid".to_string(),
310            mint_url: mint_url.clone(),
311            amount: Some(Amount::from(100)),
312            unit: CurrencyUnit::Sat,
313            request: "test_request_1".to_string(),
314            state: MintQuoteState::Paid,
315            expiry: 1000000000,
316            secret_key: None,
317            payment_method: PaymentMethod::Known(KnownMethod::Bolt11),
318            amount_issued: Amount::from(100),
319            amount_paid: Amount::from(100),
320            used_by_operation: None,
321            version: 0,
322        };
323
324        // Quote 2: Paid but not yet issued (should be returned - has pending balance)
325        let quote2 = MintQuote {
326            id: "quote_pending_balance".to_string(),
327            mint_url: mint_url.clone(),
328            amount: Some(Amount::from(100)),
329            unit: CurrencyUnit::Sat,
330            request: "test_request_2".to_string(),
331            state: MintQuoteState::Paid,
332            expiry: 1000000000,
333            secret_key: None,
334            payment_method: PaymentMethod::Known(KnownMethod::Bolt11),
335            amount_issued: Amount::from(0),
336            amount_paid: Amount::from(100),
337            used_by_operation: None,
338            version: 0,
339        };
340
341        // Quote 3: Bolt12 quote with no balance (should be returned - bolt12 is reusable)
342        let quote3 = MintQuote {
343            id: "quote_bolt12".to_string(),
344            mint_url: mint_url.clone(),
345            amount: Some(Amount::from(100)),
346            unit: CurrencyUnit::Sat,
347            request: "test_request_3".to_string(),
348            state: MintQuoteState::Unpaid,
349            expiry: 1000000000,
350            secret_key: None,
351            payment_method: PaymentMethod::Known(KnownMethod::Bolt12),
352            amount_issued: Amount::from(0),
353            amount_paid: Amount::from(0),
354            used_by_operation: None,
355            version: 0,
356        };
357
358        // Quote 4: Unpaid bolt11 quote (should be returned - wallet needs to check with mint)
359        let quote4 = MintQuote {
360            id: "quote_unpaid".to_string(),
361            mint_url: mint_url.clone(),
362            amount: Some(Amount::from(100)),
363            unit: CurrencyUnit::Sat,
364            request: "test_request_4".to_string(),
365            state: MintQuoteState::Unpaid,
366            expiry: 1000000000,
367            secret_key: None,
368            payment_method: PaymentMethod::Known(KnownMethod::Bolt11),
369            amount_issued: Amount::from(0),
370            amount_paid: Amount::from(0),
371            used_by_operation: None,
372            version: 0,
373        };
374
375        // Add all quotes to the database
376        db.add_mint_quote(quote1).await.unwrap();
377        db.add_mint_quote(quote2.clone()).await.unwrap();
378        db.add_mint_quote(quote3.clone()).await.unwrap();
379        db.add_mint_quote(quote4.clone()).await.unwrap();
380
381        // Get unissued mint quotes
382        let unissued_quotes = db.get_unissued_mint_quotes().await.unwrap();
383
384        // Should return 3 quotes: quote2, quote3, and quote4
385        // - quote2: bolt11 with amount_issued = 0 (needs minting)
386        // - quote3: bolt12 (always returned, reusable)
387        // - quote4: bolt11 with amount_issued = 0 (check with mint if paid)
388        assert_eq!(unissued_quotes.len(), 3);
389
390        // Verify the returned quotes are the expected ones
391        let quote_ids: Vec<&str> = unissued_quotes.iter().map(|q| q.id.as_str()).collect();
392        assert!(quote_ids.contains(&"quote_pending_balance"));
393        assert!(quote_ids.contains(&"quote_bolt12"));
394        assert!(quote_ids.contains(&"quote_unpaid"));
395
396        // Verify that fully paid and issued quote is not returned
397        assert!(!quote_ids.contains(&"quote_fully_paid"));
398    }
399}