happy_path_mint_wallet.rs 31 KB

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