happy_path_mint_wallet.rs 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026
  1. //! Integration tests for mint-wallet interactions that should work across all mint implementations
  2. //!
  3. //! These tests verify the core functionality of the wallet-mint interaction protocol,
  4. //! including minting, melting, and wallet restoration. They are designed to be
  5. //! implementation-agnostic and should pass against any compliant Cashu mint,
  6. //! including Nutshell, CDK, and other implementations that follow the Cashu NUTs.
  7. //!
  8. //! The tests use environment variables to determine which mint to connect to and
  9. //! whether to use real Lightning Network payments (regtest mode) or simulated payments.
  10. use core::panic;
  11. use std::collections::HashMap;
  12. use std::env;
  13. use std::fmt::Debug;
  14. use std::path::PathBuf;
  15. use std::str::FromStr;
  16. use std::sync::Arc;
  17. use std::time::Duration;
  18. use bip39::Mnemonic;
  19. use cashu::{MeltRequest, PreMintSecrets};
  20. use cdk::amount::{Amount, SplitTarget};
  21. use cdk::mint_url::MintUrl;
  22. use cdk::nuts::nut00::{KnownMethod, ProofsMethods};
  23. use cdk::nuts::{CurrencyUnit, MeltQuoteState, NotificationPayload, PaymentMethod, State};
  24. use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletRepositoryBuilder, WalletTrait};
  25. use cdk_integration_tests::{create_invoice_for_env, get_mint_url_from_env, pay_if_regtest};
  26. use cdk_sqlite::wallet::memory;
  27. use futures::{SinkExt, StreamExt};
  28. use lightning_invoice::Bolt11Invoice;
  29. use serde_json::json;
  30. use tokio::time::timeout;
  31. use tokio_tungstenite::connect_async;
  32. use tokio_tungstenite::tungstenite::protocol::Message;
  33. // Helper function to get temp directory from environment or fallback
  34. fn get_test_temp_dir() -> PathBuf {
  35. match env::var("CDK_ITESTS_DIR") {
  36. Ok(dir) => PathBuf::from(dir),
  37. Err(_) => panic!("Unknown test dir"),
  38. }
  39. }
  40. async fn get_notifications<T: StreamExt<Item = Result<Message, E>> + Unpin, E: Debug>(
  41. reader: &mut T,
  42. timeout_to_wait: Duration,
  43. total: usize,
  44. ) -> Vec<(String, NotificationPayload<String>)> {
  45. let mut results = Vec::new();
  46. for _ in 0..total {
  47. let msg = timeout(timeout_to_wait, reader.next())
  48. .await
  49. .expect("timeout")
  50. .unwrap()
  51. .unwrap();
  52. let mut response: serde_json::Value =
  53. serde_json::from_str(msg.to_text().unwrap()).expect("valid json");
  54. let mut params_raw = response
  55. .as_object_mut()
  56. .expect("object")
  57. .remove("params")
  58. .expect("valid params");
  59. let params_map = params_raw.as_object_mut().expect("params is object");
  60. results.push((
  61. params_map
  62. .remove("subId")
  63. .unwrap()
  64. .as_str()
  65. .unwrap()
  66. .to_string(),
  67. serde_json::from_value(params_map.remove("payload").unwrap()).unwrap(),
  68. ))
  69. }
  70. results
  71. }
  72. /// Tests a complete mint-melt round trip with WebSocket notifications
  73. ///
  74. /// This test verifies the full lifecycle of tokens:
  75. /// 1. Creates a mint quote and pays the invoice
  76. /// 2. Mints tokens and verifies the correct amount
  77. /// 3. Creates a melt quote to spend tokens
  78. /// 4. Subscribes to WebSocket notifications for the melt process
  79. /// 5. Executes the melt and verifies the payment was successful
  80. /// 6. Validates all WebSocket notifications received during the process
  81. ///
  82. /// This ensures the entire mint-melt flow works correctly and that
  83. /// WebSocket notifications are properly sent at each state transition.
  84. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  85. async fn test_happy_mint_melt_round_trip() {
  86. let wallet = Wallet::new(
  87. &get_mint_url_from_env(),
  88. CurrencyUnit::Sat,
  89. Arc::new(memory::empty().await.unwrap()),
  90. Mnemonic::generate(12).unwrap().to_seed_normalized(""),
  91. None,
  92. )
  93. .expect("failed to create new wallet");
  94. let (ws_stream, _) = connect_async(format!(
  95. "{}/v1/ws",
  96. get_mint_url_from_env().replace("http", "ws")
  97. ))
  98. .await
  99. .expect("Failed to connect");
  100. let (mut write, mut reader) = ws_stream.split();
  101. let mint_quote = wallet
  102. .mint_quote(PaymentMethod::BOLT11, Some(100.into()), None, None)
  103. .await
  104. .unwrap();
  105. let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
  106. pay_if_regtest(&get_test_temp_dir(), &invoice)
  107. .await
  108. .unwrap();
  109. let proofs = wallet
  110. .wait_and_mint_quote(
  111. mint_quote.clone(),
  112. SplitTarget::default(),
  113. None,
  114. tokio::time::Duration::from_secs(60),
  115. )
  116. .await
  117. .expect("payment");
  118. let mint_amount = proofs.total_amount().unwrap();
  119. assert!(mint_amount == 100.into());
  120. let invoice = create_invoice_for_env(Some(50)).await.unwrap();
  121. let melt = wallet
  122. .melt_quote(
  123. PaymentMethod::Known(KnownMethod::Bolt11),
  124. invoice.to_string(),
  125. None,
  126. None,
  127. )
  128. .await
  129. .unwrap();
  130. write
  131. .send(Message::Text(
  132. serde_json::to_string(&json!({
  133. "jsonrpc": "2.0",
  134. "id": 2,
  135. "method": "subscribe",
  136. "params": {
  137. "kind": "bolt11_melt_quote",
  138. "filters": [
  139. melt.id.clone(),
  140. ],
  141. "subId": "test-sub",
  142. }
  143. }))
  144. .unwrap()
  145. .into(),
  146. ))
  147. .await
  148. .unwrap();
  149. // Parse both JSON strings to objects and compare them instead of comparing strings directly
  150. let binding = reader.next().await.unwrap().unwrap();
  151. let response_str = binding.to_text().unwrap();
  152. let response_json: serde_json::Value =
  153. serde_json::from_str(response_str).expect("Valid JSON response");
  154. let expected_json: serde_json::Value = serde_json::from_str(
  155. r#"{"jsonrpc":"2.0","result":{"status":"OK","subId":"test-sub"},"id":2}"#,
  156. )
  157. .expect("Valid JSON expected");
  158. assert_eq!(response_json, expected_json);
  159. // Read the initial state notification before starting the melt to ensure we capture Unpaid
  160. let initial_notification =
  161. get_notifications(&mut reader, Duration::from_millis(15000), 1).await;
  162. let (sub_id, payload) = &initial_notification[0];
  163. assert_eq!("test-sub", sub_id);
  164. let initial_melt = match payload {
  165. NotificationPayload::MeltQuoteBolt11Response(m) => m,
  166. _ => panic!("Wrong payload"),
  167. };
  168. assert_eq!(initial_melt.state, MeltQuoteState::Unpaid);
  169. assert_eq!(initial_melt.quote.to_string(), melt.id);
  170. // Now start the melt
  171. let mut metadata = HashMap::new();
  172. metadata.insert("test".to_string(), "value".to_string());
  173. let prepared = wallet
  174. .prepare_melt(&melt.id, metadata.clone())
  175. .await
  176. .unwrap();
  177. let melt_response = prepared.confirm().await.unwrap();
  178. assert!(melt_response.payment_proof().is_some());
  179. assert_eq!(melt_response.state(), MeltQuoteState::Paid);
  180. let txs = wallet.list_transactions(None).await.unwrap();
  181. let tx = txs
  182. .into_iter()
  183. .find(|tx| tx.quote_id == Some(melt.id.clone()))
  184. .unwrap();
  185. assert_eq!(tx.amount, melt.amount);
  186. assert_eq!(tx.metadata, metadata);
  187. // Read remaining notifications (Pending -> Paid)
  188. let notifications = get_notifications(&mut reader, Duration::from_millis(15000), 2).await;
  189. let (sub_id, payload) = &notifications[0];
  190. assert_eq!("test-sub", sub_id);
  191. let pending_melt = match payload {
  192. NotificationPayload::MeltQuoteBolt11Response(m) => m,
  193. _ => panic!("Wrong payload"),
  194. };
  195. assert_eq!(pending_melt.state, MeltQuoteState::Pending);
  196. assert_eq!(pending_melt.quote.to_string(), melt.id);
  197. let (sub_id, payload) = &notifications[1];
  198. assert_eq!("test-sub", sub_id);
  199. let final_melt = match payload {
  200. NotificationPayload::MeltQuoteBolt11Response(m) => m,
  201. _ => panic!("Wrong payload"),
  202. };
  203. assert_eq!(final_melt.state, MeltQuoteState::Paid);
  204. assert_eq!(final_melt.amount, 50.into());
  205. assert_eq!(final_melt.quote.to_string(), melt.id);
  206. }
  207. /// Tests basic minting functionality with payment verification
  208. ///
  209. /// This test focuses on the core minting process:
  210. /// 1. Creates a mint quote for a specific amount (100 sats)
  211. /// 2. Verifies the quote has the correct amount
  212. /// 3. Pays the invoice (or simulates payment in non-regtest environments)
  213. /// 4. Waits for the mint to recognize the payment
  214. /// 5. Mints tokens and verifies the correct amount was received
  215. ///
  216. /// This ensures the basic minting flow works correctly from quote to token issuance.
  217. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  218. async fn test_happy_mint() {
  219. let wallet = Wallet::new(
  220. &get_mint_url_from_env(),
  221. CurrencyUnit::Sat,
  222. Arc::new(memory::empty().await.unwrap()),
  223. Mnemonic::generate(12).unwrap().to_seed_normalized(""),
  224. None,
  225. )
  226. .expect("failed to create new wallet");
  227. let mint_amount = Amount::from(100);
  228. let mint_quote = wallet
  229. .mint_quote(PaymentMethod::BOLT11, Some(mint_amount), None, None)
  230. .await
  231. .unwrap();
  232. assert_eq!(mint_quote.amount, Some(mint_amount));
  233. let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
  234. pay_if_regtest(&get_test_temp_dir(), &invoice)
  235. .await
  236. .unwrap();
  237. let proofs = wallet
  238. .wait_and_mint_quote(
  239. mint_quote.clone(),
  240. SplitTarget::default(),
  241. None,
  242. tokio::time::Duration::from_secs(60),
  243. )
  244. .await
  245. .expect("payment");
  246. let mint_amount = proofs.total_amount().unwrap();
  247. assert!(mint_amount == 100.into());
  248. }
  249. /// Tests wallet restoration and proof state verification
  250. ///
  251. /// This test verifies the wallet restoration process:
  252. /// 1. Creates a wallet with a specific seed and mints tokens
  253. /// 2. Verifies the wallet has the expected balance
  254. /// 3. Creates a new wallet instance with the same seed but empty storage
  255. /// 4. Confirms the new wallet starts with zero balance
  256. /// 5. Restores the wallet state from the mint
  257. /// 6. Swaps the proofs to ensure they're valid
  258. /// 7. Verifies the restored wallet has the correct balance
  259. /// 8. Checks that the original proofs are now marked as spent
  260. ///
  261. /// This ensures wallet restoration works correctly and that
  262. /// the mint properly tracks spent proofs across wallet instances.
  263. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  264. async fn test_restore() {
  265. let seed = Mnemonic::generate(12).unwrap().to_seed_normalized("");
  266. let wallet = Wallet::new(
  267. &get_mint_url_from_env(),
  268. CurrencyUnit::Sat,
  269. Arc::new(memory::empty().await.unwrap()),
  270. seed,
  271. None,
  272. )
  273. .expect("failed to create new wallet");
  274. let mint_quote = wallet
  275. .mint_quote(PaymentMethod::BOLT11, Some(100.into()), None, None)
  276. .await
  277. .unwrap();
  278. let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
  279. pay_if_regtest(&get_test_temp_dir(), &invoice)
  280. .await
  281. .unwrap();
  282. let _proofs = wallet
  283. .wait_and_mint_quote(
  284. mint_quote.clone(),
  285. SplitTarget::default(),
  286. None,
  287. tokio::time::Duration::from_secs(60),
  288. )
  289. .await
  290. .expect("payment");
  291. assert_eq!(wallet.total_balance().await.unwrap(), 100.into());
  292. let wallet_2 = Wallet::new(
  293. &get_mint_url_from_env(),
  294. CurrencyUnit::Sat,
  295. Arc::new(memory::empty().await.unwrap()),
  296. seed,
  297. None,
  298. )
  299. .expect("failed to create new wallet");
  300. assert_eq!(wallet_2.total_balance().await.unwrap(), 0.into());
  301. let restored = wallet_2.restore().await.unwrap();
  302. let proofs = wallet_2.get_unspent_proofs().await.unwrap();
  303. assert!(!proofs.is_empty());
  304. let expected_fee = wallet.get_proofs_fee(&proofs).await.unwrap().total;
  305. wallet_2
  306. .swap(None, SplitTarget::default(), proofs, None, false)
  307. .await
  308. .unwrap();
  309. assert_eq!(restored.unspent, 100.into());
  310. // Since we have to do a swap we expect to restore amount - fee
  311. assert_eq!(
  312. wallet_2.total_balance().await.unwrap(),
  313. Amount::from(100) - expected_fee
  314. );
  315. let proofs = wallet.get_unspent_proofs().await.unwrap();
  316. let states = wallet.check_proofs_spent(proofs).await.unwrap();
  317. for state in states {
  318. if state.state != State::Spent {
  319. panic!("All proofs should be spent");
  320. }
  321. }
  322. }
  323. /// Tests wallet restoration with a large number of proofs (3000)
  324. ///
  325. /// This test verifies the restore process works correctly with many proofs,
  326. /// which is important for testing database performance (especially PostgreSQL)
  327. /// and ensuring the restore batching logic handles large proof sets:
  328. /// 1. Creates a wallet and mints 3000 sats as individual 1-sat proofs
  329. /// 2. Creates a new wallet instance with the same seed but empty storage
  330. /// 3. Restores the wallet state from the mint (requires ~30 restore batches)
  331. /// 4. Verifies all 3000 proofs are correctly restored
  332. /// 5. Swaps the proofs to ensure they're valid
  333. /// 6. Checks that the original proofs are now marked as spent
  334. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  335. async fn test_restore_large_proof_count() {
  336. let seed = Mnemonic::generate(12).unwrap().to_seed_normalized("");
  337. let wallet = Wallet::new(
  338. &get_mint_url_from_env(),
  339. CurrencyUnit::Sat,
  340. Arc::new(memory::empty().await.unwrap()),
  341. seed,
  342. None,
  343. )
  344. .expect("failed to create new wallet");
  345. let mint_amount: u64 = 3000;
  346. let batch_size: u64 = 999; // Keep under 1000 outputs per request
  347. // Mint in batches to avoid exceeding the 1000 output limit per request
  348. let mut total_proofs = 0usize;
  349. let mut remaining = mint_amount;
  350. while remaining > 0 {
  351. let batch = remaining.min(batch_size);
  352. let mint_quote = wallet
  353. .mint_quote(PaymentMethod::BOLT11, Some(batch.into()), None, None)
  354. .await
  355. .unwrap();
  356. let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
  357. pay_if_regtest(&get_test_temp_dir(), &invoice)
  358. .await
  359. .unwrap();
  360. // Mint with SplitTarget::Value(1) to create individual 1-sat proofs
  361. let proofs = wallet
  362. .wait_and_mint_quote(
  363. mint_quote.clone(),
  364. SplitTarget::Value(1.into()),
  365. None,
  366. tokio::time::Duration::from_secs(120),
  367. )
  368. .await
  369. .expect("payment");
  370. total_proofs += proofs.len();
  371. remaining -= batch;
  372. }
  373. assert_eq!(total_proofs, mint_amount as usize);
  374. assert_eq!(wallet.total_balance().await.unwrap(), mint_amount.into());
  375. let wallet_2 = Wallet::new(
  376. &get_mint_url_from_env(),
  377. CurrencyUnit::Sat,
  378. Arc::new(memory::empty().await.unwrap()),
  379. seed,
  380. None,
  381. )
  382. .expect("failed to create new wallet");
  383. assert_eq!(wallet_2.total_balance().await.unwrap(), 0.into());
  384. let restored = wallet_2.restore().await.unwrap();
  385. let proofs = wallet_2.get_unspent_proofs().await.unwrap();
  386. assert_eq!(proofs.len(), mint_amount as usize);
  387. assert_eq!(restored.unspent, mint_amount.into());
  388. // Swap in batches to avoid exceeding the 1000 input limit per request
  389. let mut total_fee = Amount::ZERO;
  390. for batch in proofs.chunks(batch_size as usize) {
  391. let batch_vec = batch.to_vec();
  392. let batch_fee = wallet_2.get_proofs_fee(&batch_vec).await.unwrap().total;
  393. total_fee += batch_fee;
  394. wallet_2
  395. .swap(None, SplitTarget::default(), batch.to_vec(), None, false)
  396. .await
  397. .unwrap();
  398. }
  399. // Since we have to do a swap we expect to restore amount - fee
  400. assert_eq!(
  401. wallet_2.total_balance().await.unwrap(),
  402. Amount::from(mint_amount) - total_fee
  403. );
  404. let proofs = wallet.get_unspent_proofs().await.unwrap();
  405. // Check proofs in batches to avoid large queries
  406. for batch in proofs.chunks(100) {
  407. let states = wallet.check_proofs_spent(batch.to_vec()).await.unwrap();
  408. for state in states {
  409. if state.state != State::Spent {
  410. panic!("All proofs should be spent");
  411. }
  412. }
  413. }
  414. }
  415. /// Tests that wallet restore correctly handles non-sequential counter values
  416. ///
  417. /// This test verifies that after restoring a wallet where there were gaps in the
  418. /// counter sequence (e.g., due to failed operations or multi-device usage), the
  419. /// wallet can continue to operate without errors.
  420. ///
  421. /// Test scenario:
  422. /// 1. Wallet mints proofs using counters 0-N
  423. /// 2. Counter is incremented to simulate failed operations that consumed counter values
  424. /// 3. Wallet mints more proofs using counters at higher values
  425. /// 4. New wallet restores from seed and finds proofs at non-sequential counter positions
  426. /// 5. Wallet should be able to continue normal operations (swaps) after restore
  427. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  428. async fn test_restore_with_counter_gap() {
  429. let seed = Mnemonic::generate(12).unwrap().to_seed_normalized("");
  430. let wallet = Wallet::new(
  431. &get_mint_url_from_env(),
  432. CurrencyUnit::Sat,
  433. Arc::new(memory::empty().await.unwrap()),
  434. seed,
  435. None,
  436. )
  437. .expect("failed to create new wallet");
  438. // Mint first batch of proofs (uses counters starting at 0)
  439. let mint_quote = wallet
  440. .mint_quote(PaymentMethod::BOLT11, Some(100.into()), None, None)
  441. .await
  442. .unwrap();
  443. let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
  444. pay_if_regtest(&get_test_temp_dir(), &invoice)
  445. .await
  446. .unwrap();
  447. let _proofs1 = wallet
  448. .wait_and_mint_quote(
  449. mint_quote.clone(),
  450. SplitTarget::default(),
  451. None,
  452. tokio::time::Duration::from_secs(60),
  453. )
  454. .await
  455. .expect("first mint failed");
  456. assert_eq!(wallet.total_balance().await.unwrap(), 100.into());
  457. // Get the active keyset ID to increment counter
  458. let active_keyset = wallet.fetch_active_keyset().await.unwrap();
  459. let keyset_id = active_keyset.id;
  460. // Create a gap in the counter sequence
  461. // This simulates failed operations or multi-device usage where counter values
  462. // were consumed but no signatures were obtained
  463. let gap_size = 50u32;
  464. wallet
  465. .localstore
  466. .increment_keyset_counter(&keyset_id, gap_size)
  467. .await
  468. .unwrap();
  469. // Mint second batch of proofs (uses counters after the gap)
  470. let mint_quote2 = wallet
  471. .mint_quote(PaymentMethod::BOLT11, Some(100.into()), None, None)
  472. .await
  473. .unwrap();
  474. let invoice2 = Bolt11Invoice::from_str(&mint_quote2.request).unwrap();
  475. pay_if_regtest(&get_test_temp_dir(), &invoice2)
  476. .await
  477. .unwrap();
  478. let _proofs2 = wallet
  479. .wait_and_mint_quote(
  480. mint_quote2.clone(),
  481. SplitTarget::default(),
  482. None,
  483. tokio::time::Duration::from_secs(60),
  484. )
  485. .await
  486. .expect("second mint failed");
  487. assert_eq!(wallet.total_balance().await.unwrap(), 200.into());
  488. // Create a new wallet with the same seed (simulating wallet restore scenario)
  489. let wallet_restored = Wallet::new(
  490. &get_mint_url_from_env(),
  491. CurrencyUnit::Sat,
  492. Arc::new(memory::empty().await.unwrap()),
  493. seed,
  494. None,
  495. )
  496. .expect("failed to create restored wallet");
  497. assert_eq!(wallet_restored.total_balance().await.unwrap(), 0.into());
  498. // Restore the wallet - this should find proofs at non-sequential counter positions
  499. let restored = wallet_restored.restore().await.unwrap();
  500. assert_eq!(restored.unspent, 200.into());
  501. let proofs = wallet_restored.get_unspent_proofs().await.unwrap();
  502. assert!(!proofs.is_empty());
  503. // Swap the restored proofs to verify they are valid
  504. let expected_fee = wallet_restored.get_proofs_fee(&proofs).await.unwrap().total;
  505. wallet_restored
  506. .swap(None, SplitTarget::default(), proofs, None, false)
  507. .await
  508. .expect("first swap after restore failed");
  509. let balance_after_first_swap = Amount::from(200) - expected_fee;
  510. assert_eq!(
  511. wallet_restored.total_balance().await.unwrap(),
  512. balance_after_first_swap
  513. );
  514. // Perform multiple swaps to verify the wallet can continue operating
  515. // after restore with non-sequential counter values
  516. for i in 0..gap_size {
  517. let proofs = wallet_restored.get_unspent_proofs().await.unwrap();
  518. if proofs.is_empty() {
  519. break;
  520. }
  521. let swap_result = wallet_restored
  522. .swap(None, SplitTarget::default(), proofs.clone(), None, false)
  523. .await;
  524. match swap_result {
  525. Ok(_) => {
  526. // Swap succeeded, continue
  527. }
  528. Err(e) => {
  529. let error_str = format!("{:?}", e);
  530. if error_str.contains("BlindedMessageAlreadySigned") {
  531. panic!(
  532. "Got 'blinded message already signed' error on swap {} after restore. \
  533. Counter was not correctly set after restoring with non-sequential values.",
  534. i + 1
  535. );
  536. } else {
  537. // Some other error - might be expected (e.g., insufficient funds due to fees)
  538. break;
  539. }
  540. }
  541. }
  542. }
  543. }
  544. /// Tests that the melt quote status can be checked after a melt has completed
  545. ///
  546. /// This test verifies:
  547. /// 1. Mint tokens
  548. /// 2. Create a melt quote and execute the melt
  549. /// 3. Check the melt quote status via the wallet
  550. /// 4. Verify the quote is in the Paid state
  551. ///
  552. /// This ensures the mint correctly reports the melt quote status after completion.
  553. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  554. async fn test_melt_quote_status_after_melt() {
  555. let wallet = Wallet::new(
  556. &get_mint_url_from_env(),
  557. CurrencyUnit::Sat,
  558. Arc::new(memory::empty().await.unwrap()),
  559. Mnemonic::generate(12).unwrap().to_seed_normalized(""),
  560. None,
  561. )
  562. .expect("failed to create new wallet");
  563. let mint_quote = wallet
  564. .mint_quote(PaymentMethod::BOLT11, Some(100.into()), None, None)
  565. .await
  566. .unwrap();
  567. let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
  568. pay_if_regtest(&get_test_temp_dir(), &invoice)
  569. .await
  570. .unwrap();
  571. let proofs = wallet
  572. .wait_and_mint_quote(
  573. mint_quote.clone(),
  574. SplitTarget::default(),
  575. None,
  576. tokio::time::Duration::from_secs(60),
  577. )
  578. .await
  579. .expect("mint failed");
  580. let mint_amount = proofs.total_amount().unwrap();
  581. assert_eq!(mint_amount, 100.into());
  582. let invoice = create_invoice_for_env(Some(50)).await.unwrap();
  583. let melt_quote = wallet
  584. .melt_quote(
  585. PaymentMethod::Known(KnownMethod::Bolt11),
  586. invoice.to_string(),
  587. None,
  588. None,
  589. )
  590. .await
  591. .unwrap();
  592. let prepared = wallet
  593. .prepare_melt(&melt_quote.id, std::collections::HashMap::new())
  594. .await
  595. .unwrap();
  596. let melt_response = prepared.confirm().await.unwrap();
  597. assert_eq!(melt_response.state(), MeltQuoteState::Paid);
  598. let quote_status = wallet
  599. .check_melt_quote_status(&melt_quote.id)
  600. .await
  601. .unwrap();
  602. assert_eq!(
  603. quote_status.state,
  604. MeltQuoteState::Paid,
  605. "Melt quote should be in Paid state after successful melt"
  606. );
  607. let db_quote = wallet
  608. .localstore
  609. .get_melt_quote(&melt_quote.id)
  610. .await
  611. .unwrap()
  612. .unwrap();
  613. assert_eq!(
  614. db_quote.state,
  615. MeltQuoteState::Paid,
  616. "Melt quote should be in Paid state after successful melt"
  617. );
  618. }
  619. /// Tests that the melt quote status can be checked via WalletRepository after a melt has completed
  620. ///
  621. /// This test verifies the same flow as test_melt_quote_status_after_melt but using
  622. /// the WalletRepository abstraction:
  623. /// 1. Create a WalletRepository and add a mint
  624. /// 2. Mint tokens via the wallet repository
  625. /// 3. Create a melt quote and execute the melt
  626. /// 4. Check the melt quote status via check_melt_quote
  627. /// 5. Verify the quote is in the Paid state
  628. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  629. async fn test_melt_quote_status_after_melt_wallet_repository() {
  630. let seed = Mnemonic::generate(12).unwrap().to_seed_normalized("");
  631. let localstore = Arc::new(memory::empty().await.unwrap());
  632. let wallet_repository = WalletRepositoryBuilder::new()
  633. .localstore(localstore.clone())
  634. .seed(seed)
  635. .build()
  636. .await
  637. .expect("failed to create wallet repository");
  638. let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
  639. wallet_repository
  640. .add_wallet(mint_url.clone())
  641. .await
  642. .expect("failed to add mint");
  643. // Get the wallet from the repository to call methods directly
  644. let wallet = wallet_repository
  645. .get_wallet(&mint_url, &CurrencyUnit::Sat)
  646. .await
  647. .expect("failed to get wallet");
  648. let mint_quote = wallet
  649. .mint_quote(
  650. PaymentMethod::Known(KnownMethod::Bolt11),
  651. Some(100.into()),
  652. None,
  653. None,
  654. )
  655. .await
  656. .unwrap();
  657. let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
  658. pay_if_regtest(&get_test_temp_dir(), &invoice)
  659. .await
  660. .unwrap();
  661. let _proofs = wallet
  662. .wait_and_mint_quote(
  663. mint_quote.clone(),
  664. SplitTarget::default(),
  665. None,
  666. Duration::from_secs(60),
  667. )
  668. .await
  669. .expect("mint failed");
  670. let balances = wallet_repository.total_balance().await.unwrap();
  671. let balance = balances
  672. .get(&CurrencyUnit::Sat)
  673. .copied()
  674. .unwrap_or(Amount::ZERO);
  675. assert_eq!(balance, 100.into());
  676. let invoice = create_invoice_for_env(Some(50)).await.unwrap();
  677. let melt_quote = wallet
  678. .melt_quote(
  679. PaymentMethod::Known(KnownMethod::Bolt11),
  680. invoice.to_string(),
  681. None,
  682. None,
  683. )
  684. .await
  685. .unwrap();
  686. let melt_response: cdk::types::FinalizedMelt = wallet
  687. .prepare_melt(&melt_quote.id, HashMap::new())
  688. .await
  689. .unwrap()
  690. .confirm()
  691. .await
  692. .unwrap();
  693. assert_eq!(melt_response.state(), MeltQuoteState::Paid);
  694. let quote_status = wallet
  695. .check_melt_quote_status(&melt_quote.id)
  696. .await
  697. .unwrap();
  698. assert_eq!(
  699. quote_status.state,
  700. MeltQuoteState::Paid,
  701. "Melt quote should be in Paid state after successful melt (via WalletRepository)"
  702. );
  703. use cdk_common::database::WalletDatabase;
  704. let db_quote = localstore
  705. .get_melt_quote(&melt_quote.id)
  706. .await
  707. .unwrap()
  708. .unwrap();
  709. assert_eq!(
  710. db_quote.state,
  711. MeltQuoteState::Paid,
  712. "Melt quote should be in Paid state after successful melt"
  713. );
  714. }
  715. /// Tests that change outputs in a melt quote are correctly handled
  716. ///
  717. /// This test verifies the following workflow:
  718. /// 1. Mint 100 sats of tokens
  719. /// 2. Create a melt quote for 9 sats (which requires 100 sats input with 91 sats change)
  720. /// 3. Manually construct a melt request with proofs and blinded messages for change
  721. /// 4. Verify that the change proofs in the response match what's reported by the quote status
  722. ///
  723. /// This ensures the mint correctly processes change outputs during melting operations
  724. /// and that the wallet can properly verify the change amounts match expectations.
  725. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  726. async fn test_fake_melt_change_in_quote() {
  727. let wallet = Wallet::new(
  728. &get_mint_url_from_env(),
  729. CurrencyUnit::Sat,
  730. Arc::new(memory::empty().await.unwrap()),
  731. Mnemonic::generate(12).unwrap().to_seed_normalized(""),
  732. None,
  733. )
  734. .expect("failed to create new wallet");
  735. let mint_quote = wallet
  736. .mint_quote(PaymentMethod::BOLT11, Some(100.into()), None, None)
  737. .await
  738. .unwrap();
  739. let bolt11 = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
  740. pay_if_regtest(&get_test_temp_dir(), &bolt11).await.unwrap();
  741. let _proofs = wallet
  742. .wait_and_mint_quote(
  743. mint_quote.clone(),
  744. SplitTarget::default(),
  745. None,
  746. tokio::time::Duration::from_secs(60),
  747. )
  748. .await
  749. .expect("payment");
  750. let invoice = create_invoice_for_env(Some(9)).await.unwrap();
  751. let proofs = wallet.get_unspent_proofs().await.unwrap();
  752. let melt_quote = wallet
  753. .melt_quote(
  754. PaymentMethod::Known(KnownMethod::Bolt11),
  755. invoice.to_string(),
  756. None,
  757. None,
  758. )
  759. .await
  760. .unwrap();
  761. let keyset = wallet.fetch_active_keyset().await.unwrap();
  762. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  763. let premint_secrets = PreMintSecrets::random(
  764. keyset.id,
  765. 100.into(),
  766. &SplitTarget::default(),
  767. &fee_and_amounts,
  768. )
  769. .unwrap();
  770. let client = HttpClient::new(get_mint_url_from_env().parse().unwrap(), None);
  771. let melt_request = MeltRequest::new(
  772. melt_quote.id.clone(),
  773. proofs.clone(),
  774. Some(premint_secrets.blinded_messages()),
  775. );
  776. let melt_response = client
  777. .post_melt(&PaymentMethod::Known(KnownMethod::Bolt11), melt_request)
  778. .await
  779. .unwrap();
  780. assert!(melt_response.change.is_some());
  781. let check = client.get_melt_quote_status(&melt_quote.id).await.unwrap();
  782. let mut melt_change = melt_response.change.unwrap();
  783. melt_change.sort_by(|a, b| a.amount.cmp(&b.amount));
  784. let mut check = check.change.unwrap();
  785. check.sort_by(|a, b| a.amount.cmp(&b.amount));
  786. assert_eq!(melt_change, check);
  787. }
  788. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  789. async fn test_pay_invoice_twice() {
  790. let ln_backend = match env::var("LN_BACKEND") {
  791. Ok(val) => Some(val),
  792. Err(_) => env::var("CDK_MINTD_LN_BACKEND").ok(),
  793. };
  794. if ln_backend.map(|ln| ln.to_uppercase()) == Some("FAKEWALLET".to_string()) {
  795. // We can only perform this test on regtest backends as fake wallet just marks the quote as paid
  796. return;
  797. }
  798. let wallet = Wallet::new(
  799. &get_mint_url_from_env(),
  800. CurrencyUnit::Sat,
  801. Arc::new(memory::empty().await.unwrap()),
  802. Mnemonic::generate(12).unwrap().to_seed_normalized(""),
  803. None,
  804. )
  805. .expect("failed to create new wallet");
  806. let mint_quote = wallet
  807. .mint_quote(PaymentMethod::BOLT11, Some(100.into()), None, None)
  808. .await
  809. .unwrap();
  810. pay_if_regtest(&get_test_temp_dir(), &mint_quote.request.parse().unwrap())
  811. .await
  812. .unwrap();
  813. let proofs = wallet
  814. .wait_and_mint_quote(
  815. mint_quote.clone(),
  816. SplitTarget::default(),
  817. None,
  818. tokio::time::Duration::from_secs(60),
  819. )
  820. .await
  821. .expect("payment");
  822. let mint_amount = proofs.total_amount().unwrap();
  823. assert_eq!(mint_amount, 100.into());
  824. let invoice = create_invoice_for_env(Some(25)).await.unwrap();
  825. let melt_quote = wallet
  826. .melt_quote(
  827. PaymentMethod::Known(KnownMethod::Bolt11),
  828. invoice.to_string(),
  829. None,
  830. None,
  831. )
  832. .await
  833. .unwrap();
  834. let prepared = wallet
  835. .prepare_melt(&melt_quote.id, std::collections::HashMap::new())
  836. .await
  837. .unwrap();
  838. let melt = prepared.confirm().await.unwrap();
  839. // Creating a second quote for the same invoice is allowed
  840. let melt_quote_two = wallet
  841. .melt_quote(
  842. PaymentMethod::Known(KnownMethod::Bolt11),
  843. invoice.to_string(),
  844. None,
  845. None,
  846. )
  847. .await
  848. .unwrap();
  849. // But attempting to melt (pay) the second quote should fail
  850. // since the first quote with the same lookup_id is already paid
  851. let melt_two = async {
  852. let prepared = wallet
  853. .prepare_melt(&melt_quote_two.id, std::collections::HashMap::new())
  854. .await?;
  855. prepared.confirm().await
  856. }
  857. .await;
  858. match melt_two {
  859. Err(err) => {
  860. let err_str = err.to_string().to_lowercase();
  861. if !err_str.contains("duplicate")
  862. && !err_str.contains("already paid")
  863. && !err_str.contains("request already paid")
  864. {
  865. panic!(
  866. "Expected duplicate/already paid error, got: {}",
  867. err.to_string()
  868. );
  869. }
  870. }
  871. Ok(_) => {
  872. panic!("Should not have allowed second payment");
  873. }
  874. }
  875. let balance = wallet.total_balance().await.unwrap();
  876. assert_eq!(
  877. balance,
  878. (Amount::from(100) - melt.fee_paid() - melt.amount())
  879. );
  880. }