mod.rs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. //! SQLite Wallet Database
  2. use cdk_sql_common::SQLWalletDatabase;
  3. use crate::common::SqliteConnectionManager;
  4. pub mod memory;
  5. /// Mint SQLite implementation with rusqlite
  6. pub type WalletSqliteDatabase = SQLWalletDatabase<SqliteConnectionManager>;
  7. #[cfg(test)]
  8. mod tests {
  9. use cdk_common::wallet_db_test;
  10. use super::memory;
  11. async fn provide_db(_test_name: String) -> super::WalletSqliteDatabase {
  12. memory::empty().await.unwrap()
  13. }
  14. wallet_db_test!(provide_db);
  15. use std::str::FromStr;
  16. use cdk_common::database::WalletDatabase;
  17. use cdk_common::nuts::{ProofDleq, State};
  18. use cdk_common::secret::Secret;
  19. use crate::WalletSqliteDatabase;
  20. #[tokio::test]
  21. #[cfg(feature = "sqlcipher")]
  22. async fn test_sqlcipher() {
  23. use cdk_common::mint_url::MintUrl;
  24. use cdk_common::MintInfo;
  25. use super::*;
  26. let path = std::env::temp_dir()
  27. .to_path_buf()
  28. .join(format!("cdk-test-{}.sqlite", uuid::Uuid::new_v4()));
  29. let db = WalletSqliteDatabase::new((path, "password".to_string()))
  30. .await
  31. .unwrap();
  32. let mint_info = MintInfo::new().description("test");
  33. let mint_url = MintUrl::from_str("https://mint.xyz").unwrap();
  34. let mut tx = db.begin_db_transaction().await.expect("tx");
  35. tx.add_mint(mint_url.clone(), Some(mint_info.clone()))
  36. .await
  37. .unwrap();
  38. tx.commit().await.expect("commit");
  39. let res = db.get_mint(mint_url).await.unwrap();
  40. assert_eq!(mint_info, res.clone().unwrap());
  41. assert_eq!("test", &res.unwrap().description.unwrap());
  42. }
  43. #[tokio::test]
  44. async fn test_proof_with_dleq() {
  45. use cdk_common::common::ProofInfo;
  46. use cdk_common::mint_url::MintUrl;
  47. use cdk_common::nuts::{CurrencyUnit, Id, Proof, PublicKey, SecretKey};
  48. use cdk_common::Amount;
  49. // Create a temporary database
  50. let path = std::env::temp_dir()
  51. .to_path_buf()
  52. .join(format!("cdk-test-dleq-{}.sqlite", uuid::Uuid::new_v4()));
  53. #[cfg(feature = "sqlcipher")]
  54. let db = WalletSqliteDatabase::new((path, "password".to_string()))
  55. .await
  56. .unwrap();
  57. #[cfg(not(feature = "sqlcipher"))]
  58. let db = WalletSqliteDatabase::new(path).await.unwrap();
  59. // Create a proof with DLEQ
  60. let keyset_id = Id::from_str("00deadbeef123456").unwrap();
  61. let mint_url = MintUrl::from_str("https://example.com").unwrap();
  62. let secret = Secret::new("test_secret_for_dleq");
  63. // Create DLEQ components
  64. let e = SecretKey::generate();
  65. let s = SecretKey::generate();
  66. let r = SecretKey::generate();
  67. let dleq = ProofDleq::new(e.clone(), s.clone(), r.clone());
  68. let mut proof = Proof::new(
  69. Amount::from(64),
  70. keyset_id,
  71. secret,
  72. PublicKey::from_hex(
  73. "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
  74. )
  75. .unwrap(),
  76. );
  77. // Add DLEQ to the proof
  78. proof.dleq = Some(dleq);
  79. // Create ProofInfo
  80. let proof_info =
  81. ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
  82. let mut tx = db.begin_db_transaction().await.expect("tx");
  83. // Store the proof in the database
  84. tx.update_proofs(vec![proof_info.clone()], vec![])
  85. .await
  86. .unwrap();
  87. tx.commit().await.expect("commit");
  88. // Retrieve the proof from the database
  89. let retrieved_proofs = db
  90. .get_proofs(
  91. Some(mint_url),
  92. Some(CurrencyUnit::Sat),
  93. Some(vec![State::Unspent]),
  94. None,
  95. )
  96. .await
  97. .unwrap();
  98. // Verify we got back exactly one proof
  99. assert_eq!(retrieved_proofs.len(), 1);
  100. // Verify the DLEQ data was preserved
  101. let retrieved_proof = &retrieved_proofs[0];
  102. assert!(retrieved_proof.proof.dleq.is_some());
  103. let retrieved_dleq = retrieved_proof.proof.dleq.as_ref().unwrap();
  104. // Verify DLEQ components match what we stored
  105. assert_eq!(retrieved_dleq.e.to_string(), e.to_string());
  106. assert_eq!(retrieved_dleq.s.to_string(), s.to_string());
  107. assert_eq!(retrieved_dleq.r.to_string(), r.to_string());
  108. }
  109. #[tokio::test]
  110. async fn test_mint_quote_payment_method_read_and_write() {
  111. use cdk_common::mint_url::MintUrl;
  112. use cdk_common::nuts::{CurrencyUnit, MintQuoteState, PaymentMethod};
  113. use cdk_common::wallet::MintQuote;
  114. use cdk_common::Amount;
  115. // Create a temporary database
  116. let path = std::env::temp_dir().to_path_buf().join(format!(
  117. "cdk-test-migration-{}.sqlite",
  118. uuid::Uuid::new_v4()
  119. ));
  120. #[cfg(feature = "sqlcipher")]
  121. let db = WalletSqliteDatabase::new((path, "password".to_string()))
  122. .await
  123. .unwrap();
  124. #[cfg(not(feature = "sqlcipher"))]
  125. let db = WalletSqliteDatabase::new(path).await.unwrap();
  126. // Test PaymentMethod variants
  127. let mint_url = MintUrl::from_str("https://example.com").unwrap();
  128. let payment_methods = [
  129. PaymentMethod::Bolt11,
  130. PaymentMethod::Bolt12,
  131. PaymentMethod::Custom("custom".to_string()),
  132. ];
  133. let mut tx = db.begin_db_transaction().await.expect("begin");
  134. for (i, payment_method) in payment_methods.iter().enumerate() {
  135. let quote = MintQuote {
  136. id: format!("test_quote_{}", i),
  137. mint_url: mint_url.clone(),
  138. amount: Some(Amount::from(100)),
  139. unit: CurrencyUnit::Sat,
  140. request: "test_request".to_string(),
  141. state: MintQuoteState::Unpaid,
  142. expiry: 1000000000,
  143. secret_key: None,
  144. payment_method: payment_method.clone(),
  145. amount_issued: Amount::from(0),
  146. amount_paid: Amount::from(0),
  147. };
  148. // Store the quote
  149. tx.add_mint_quote(quote.clone()).await.unwrap();
  150. // Retrieve and verify
  151. let retrieved = tx.get_mint_quote(&quote.id).await.unwrap().unwrap();
  152. assert_eq!(retrieved.payment_method, *payment_method);
  153. assert_eq!(retrieved.amount_issued, Amount::from(0));
  154. assert_eq!(retrieved.amount_paid, Amount::from(0));
  155. }
  156. tx.commit().await.expect("commit");
  157. }
  158. #[tokio::test]
  159. async fn test_get_proofs_by_ys() {
  160. use cdk_common::common::ProofInfo;
  161. use cdk_common::mint_url::MintUrl;
  162. use cdk_common::nuts::{CurrencyUnit, Id, Proof, SecretKey};
  163. use cdk_common::Amount;
  164. // Create a temporary database
  165. let path = std::env::temp_dir().to_path_buf().join(format!(
  166. "cdk-test-proofs-by-ys-{}.sqlite",
  167. uuid::Uuid::new_v4()
  168. ));
  169. #[cfg(feature = "sqlcipher")]
  170. let db = WalletSqliteDatabase::new((path, "password".to_string()))
  171. .await
  172. .unwrap();
  173. #[cfg(not(feature = "sqlcipher"))]
  174. let db = WalletSqliteDatabase::new(path).await.unwrap();
  175. // Create multiple proofs
  176. let keyset_id = Id::from_str("00deadbeef123456").unwrap();
  177. let mint_url = MintUrl::from_str("https://example.com").unwrap();
  178. let mut proof_infos = vec![];
  179. let mut expected_ys = vec![];
  180. // Generate valid public keys using SecretKey
  181. for _i in 0..5 {
  182. let secret = Secret::generate();
  183. // Generate a valid public key from a secret key
  184. let secret_key = SecretKey::generate();
  185. let c = secret_key.public_key();
  186. let proof = Proof::new(Amount::from(64), keyset_id, secret, c);
  187. let proof_info =
  188. ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
  189. expected_ys.push(proof_info.y);
  190. proof_infos.push(proof_info);
  191. }
  192. // Store all proofs in the database
  193. let mut tx = db.begin_db_transaction().await.unwrap();
  194. tx.update_proofs(proof_infos.clone(), vec![]).await.unwrap();
  195. tx.commit().await.unwrap();
  196. // Test 1: Retrieve all proofs by their Y values
  197. let retrieved_proofs = db.get_proofs_by_ys(expected_ys.clone()).await.unwrap();
  198. assert_eq!(retrieved_proofs.len(), 5);
  199. for retrieved_proof in &retrieved_proofs {
  200. assert!(expected_ys.contains(&retrieved_proof.y));
  201. }
  202. // Test 2: Retrieve subset of proofs (first 3)
  203. let subset_ys = expected_ys[0..3].to_vec();
  204. let subset_proofs = db.get_proofs_by_ys(subset_ys.clone()).await.unwrap();
  205. assert_eq!(subset_proofs.len(), 3);
  206. for retrieved_proof in &subset_proofs {
  207. assert!(subset_ys.contains(&retrieved_proof.y));
  208. }
  209. // Test 3: Retrieve with non-existent Y values
  210. let non_existent_secret_key = SecretKey::generate();
  211. let non_existent_y = non_existent_secret_key.public_key();
  212. let mixed_ys = vec![expected_ys[0], non_existent_y, expected_ys[1]];
  213. let mixed_proofs = db.get_proofs_by_ys(mixed_ys).await.unwrap();
  214. // Should only return the 2 that exist
  215. assert_eq!(mixed_proofs.len(), 2);
  216. // Test 4: Empty input returns empty result
  217. let empty_result = db.get_proofs_by_ys(vec![]).await.unwrap();
  218. assert_eq!(empty_result.len(), 0);
  219. // Test 5: Verify retrieved proof data matches original
  220. let single_y = vec![expected_ys[2]];
  221. let single_proof = db.get_proofs_by_ys(single_y).await.unwrap();
  222. assert_eq!(single_proof.len(), 1);
  223. assert_eq!(single_proof[0].y, proof_infos[2].y);
  224. assert_eq!(single_proof[0].proof.amount, proof_infos[2].proof.amount);
  225. assert_eq!(single_proof[0].mint_url, proof_infos[2].mint_url);
  226. assert_eq!(single_proof[0].state, proof_infos[2].state);
  227. }
  228. #[tokio::test]
  229. async fn test_get_unissued_mint_quotes() {
  230. use cdk_common::mint_url::MintUrl;
  231. use cdk_common::nuts::{CurrencyUnit, MintQuoteState, PaymentMethod};
  232. use cdk_common::wallet::MintQuote;
  233. use cdk_common::Amount;
  234. // Create a temporary database
  235. let path = std::env::temp_dir().to_path_buf().join(format!(
  236. "cdk-test-unpaid-quotes-{}.sqlite",
  237. uuid::Uuid::new_v4()
  238. ));
  239. #[cfg(feature = "sqlcipher")]
  240. let db = WalletSqliteDatabase::new((path, "password".to_string()))
  241. .await
  242. .unwrap();
  243. #[cfg(not(feature = "sqlcipher"))]
  244. let db = WalletSqliteDatabase::new(path).await.unwrap();
  245. let mint_url = MintUrl::from_str("https://example.com").unwrap();
  246. // Quote 1: Fully paid and issued (should NOT be returned)
  247. let quote1 = MintQuote {
  248. id: "quote_fully_paid".to_string(),
  249. mint_url: mint_url.clone(),
  250. amount: Some(Amount::from(100)),
  251. unit: CurrencyUnit::Sat,
  252. request: "test_request_1".to_string(),
  253. state: MintQuoteState::Paid,
  254. expiry: 1000000000,
  255. secret_key: None,
  256. payment_method: PaymentMethod::Bolt11,
  257. amount_issued: Amount::from(100),
  258. amount_paid: Amount::from(100),
  259. };
  260. // Quote 2: Paid but not yet issued (should be returned - has pending balance)
  261. let quote2 = MintQuote {
  262. id: "quote_pending_balance".to_string(),
  263. mint_url: mint_url.clone(),
  264. amount: Some(Amount::from(100)),
  265. unit: CurrencyUnit::Sat,
  266. request: "test_request_2".to_string(),
  267. state: MintQuoteState::Paid,
  268. expiry: 1000000000,
  269. secret_key: None,
  270. payment_method: PaymentMethod::Bolt11,
  271. amount_issued: Amount::from(0),
  272. amount_paid: Amount::from(100),
  273. };
  274. // Quote 3: Bolt12 quote with no balance (should be returned - bolt12 is reusable)
  275. let quote3 = MintQuote {
  276. id: "quote_bolt12".to_string(),
  277. mint_url: mint_url.clone(),
  278. amount: Some(Amount::from(100)),
  279. unit: CurrencyUnit::Sat,
  280. request: "test_request_3".to_string(),
  281. state: MintQuoteState::Unpaid,
  282. expiry: 1000000000,
  283. secret_key: None,
  284. payment_method: PaymentMethod::Bolt12,
  285. amount_issued: Amount::from(0),
  286. amount_paid: Amount::from(0),
  287. };
  288. // Quote 4: Unpaid bolt11 quote (should be returned - wallet needs to check with mint)
  289. let quote4 = MintQuote {
  290. id: "quote_unpaid".to_string(),
  291. mint_url: mint_url.clone(),
  292. amount: Some(Amount::from(100)),
  293. unit: CurrencyUnit::Sat,
  294. request: "test_request_4".to_string(),
  295. state: MintQuoteState::Unpaid,
  296. expiry: 1000000000,
  297. secret_key: None,
  298. payment_method: PaymentMethod::Bolt11,
  299. amount_issued: Amount::from(0),
  300. amount_paid: Amount::from(0),
  301. };
  302. {
  303. let mut tx = db.begin_db_transaction().await.unwrap();
  304. // Add all quotes to the database
  305. tx.add_mint_quote(quote1).await.unwrap();
  306. tx.add_mint_quote(quote2.clone()).await.unwrap();
  307. tx.add_mint_quote(quote3.clone()).await.unwrap();
  308. tx.add_mint_quote(quote4.clone()).await.unwrap();
  309. tx.commit().await.unwrap();
  310. }
  311. // Get unissued mint quotes
  312. let unissued_quotes = db.get_unissued_mint_quotes().await.unwrap();
  313. // Should return 3 quotes: quote2, quote3, and quote4
  314. // - quote2: bolt11 with amount_issued = 0 (needs minting)
  315. // - quote3: bolt12 (always returned, reusable)
  316. // - quote4: bolt11 with amount_issued = 0 (check with mint if paid)
  317. assert_eq!(unissued_quotes.len(), 3);
  318. // Verify the returned quotes are the expected ones
  319. let quote_ids: Vec<&str> = unissued_quotes.iter().map(|q| q.id.as_str()).collect();
  320. assert!(quote_ids.contains(&"quote_pending_balance"));
  321. assert!(quote_ids.contains(&"quote_bolt12"));
  322. assert!(quote_ids.contains(&"quote_unpaid"));
  323. // Verify that fully paid and issued quote is not returned
  324. assert!(!quote_ids.contains(&"quote_fully_paid"));
  325. }
  326. }