happy_path_mint_wallet.rs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  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::ProofsMethods;
  23. use cdk::nuts::{CurrencyUnit, MeltQuoteState, NotificationPayload, 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_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.melt_quote(invoice, None).await.unwrap();
  119. write
  120. .send(Message::Text(
  121. serde_json::to_string(&json!({
  122. "jsonrpc": "2.0",
  123. "id": 2,
  124. "method": "subscribe",
  125. "params": {
  126. "kind": "bolt11_melt_quote",
  127. "filters": [
  128. melt.id.clone(),
  129. ],
  130. "subId": "test-sub",
  131. }
  132. }))
  133. .unwrap()
  134. .into(),
  135. ))
  136. .await
  137. .unwrap();
  138. // Parse both JSON strings to objects and compare them instead of comparing strings directly
  139. let binding = reader.next().await.unwrap().unwrap();
  140. let response_str = binding.to_text().unwrap();
  141. let response_json: serde_json::Value =
  142. serde_json::from_str(response_str).expect("Valid JSON response");
  143. let expected_json: serde_json::Value = serde_json::from_str(
  144. r#"{"jsonrpc":"2.0","result":{"status":"OK","subId":"test-sub"},"id":2}"#,
  145. )
  146. .expect("Valid JSON expected");
  147. assert_eq!(response_json, expected_json);
  148. let mut metadata = HashMap::new();
  149. metadata.insert("test".to_string(), "value".to_string());
  150. let melt_response = wallet
  151. .melt_with_metadata(&melt.id, metadata.clone())
  152. .await
  153. .unwrap();
  154. assert!(melt_response.preimage.is_some());
  155. assert_eq!(melt_response.state, MeltQuoteState::Paid);
  156. let txs = wallet.list_transactions(None).await.unwrap();
  157. let tx = txs
  158. .into_iter()
  159. .find(|tx| tx.quote_id == Some(melt.id.clone()))
  160. .unwrap();
  161. assert_eq!(tx.amount, melt.amount);
  162. assert_eq!(tx.metadata, metadata);
  163. let mut notifications = get_notifications(&mut reader, Duration::from_millis(15000), 3).await;
  164. notifications.reverse();
  165. let (sub_id, payload) = notifications.pop().unwrap();
  166. // first message is the current state
  167. assert_eq!("test-sub", sub_id);
  168. let payload = match payload {
  169. NotificationPayload::MeltQuoteBolt11Response(melt) => melt,
  170. _ => panic!("Wrong payload"),
  171. };
  172. // assert_eq!(payload.amount + payload.fee_reserve, 50.into());
  173. assert_eq!(payload.quote.to_string(), melt.id);
  174. assert_eq!(payload.state, MeltQuoteState::Unpaid);
  175. // get current state
  176. let (sub_id, payload) = notifications.pop().unwrap();
  177. assert_eq!("test-sub", sub_id);
  178. let payload = match payload {
  179. NotificationPayload::MeltQuoteBolt11Response(melt) => melt,
  180. _ => panic!("Wrong payload"),
  181. };
  182. assert_eq!(payload.quote.to_string(), melt.id);
  183. assert_eq!(payload.state, MeltQuoteState::Pending);
  184. // get current state
  185. let (sub_id, payload) = notifications.pop().unwrap();
  186. assert_eq!("test-sub", sub_id);
  187. let payload = match payload {
  188. NotificationPayload::MeltQuoteBolt11Response(melt) => melt,
  189. _ => panic!("Wrong payload"),
  190. };
  191. assert_eq!(payload.amount, 50.into());
  192. assert_eq!(payload.quote.to_string(), melt.id);
  193. assert_eq!(payload.state, MeltQuoteState::Paid);
  194. }
  195. /// Tests basic minting functionality with payment verification
  196. ///
  197. /// This test focuses on the core minting process:
  198. /// 1. Creates a mint quote for a specific amount (100 sats)
  199. /// 2. Verifies the quote has the correct amount
  200. /// 3. Pays the invoice (or simulates payment in non-regtest environments)
  201. /// 4. Waits for the mint to recognize the payment
  202. /// 5. Mints tokens and verifies the correct amount was received
  203. ///
  204. /// This ensures the basic minting flow works correctly from quote to token issuance.
  205. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  206. async fn test_happy_mint() {
  207. let wallet = Wallet::new(
  208. &get_mint_url_from_env(),
  209. CurrencyUnit::Sat,
  210. Arc::new(memory::empty().await.unwrap()),
  211. Mnemonic::generate(12).unwrap().to_seed_normalized(""),
  212. None,
  213. )
  214. .expect("failed to create new wallet");
  215. let mint_amount = Amount::from(100);
  216. let mint_quote = wallet.mint_quote(mint_amount, None).await.unwrap();
  217. assert_eq!(mint_quote.amount, Some(mint_amount));
  218. let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
  219. pay_if_regtest(&get_test_temp_dir(), &invoice)
  220. .await
  221. .unwrap();
  222. let proofs = wallet
  223. .wait_and_mint_quote(
  224. mint_quote.clone(),
  225. SplitTarget::default(),
  226. None,
  227. tokio::time::Duration::from_secs(60),
  228. )
  229. .await
  230. .expect("payment");
  231. let mint_amount = proofs.total_amount().unwrap();
  232. assert!(mint_amount == 100.into());
  233. }
  234. /// Tests wallet restoration and proof state verification
  235. ///
  236. /// This test verifies the wallet restoration process:
  237. /// 1. Creates a wallet with a specific seed and mints tokens
  238. /// 2. Verifies the wallet has the expected balance
  239. /// 3. Creates a new wallet instance with the same seed but empty storage
  240. /// 4. Confirms the new wallet starts with zero balance
  241. /// 5. Restores the wallet state from the mint
  242. /// 6. Swaps the proofs to ensure they're valid
  243. /// 7. Verifies the restored wallet has the correct balance
  244. /// 8. Checks that the original proofs are now marked as spent
  245. ///
  246. /// This ensures wallet restoration works correctly and that
  247. /// the mint properly tracks spent proofs across wallet instances.
  248. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  249. async fn test_restore() {
  250. let seed = Mnemonic::generate(12).unwrap().to_seed_normalized("");
  251. let wallet = Wallet::new(
  252. &get_mint_url_from_env(),
  253. CurrencyUnit::Sat,
  254. Arc::new(memory::empty().await.unwrap()),
  255. seed,
  256. None,
  257. )
  258. .expect("failed to create new wallet");
  259. let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
  260. let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
  261. pay_if_regtest(&get_test_temp_dir(), &invoice)
  262. .await
  263. .unwrap();
  264. let _proofs = wallet
  265. .wait_and_mint_quote(
  266. mint_quote.clone(),
  267. SplitTarget::default(),
  268. None,
  269. tokio::time::Duration::from_secs(60),
  270. )
  271. .await
  272. .expect("payment");
  273. assert_eq!(wallet.total_balance().await.unwrap(), 100.into());
  274. let wallet_2 = Wallet::new(
  275. &get_mint_url_from_env(),
  276. CurrencyUnit::Sat,
  277. Arc::new(memory::empty().await.unwrap()),
  278. seed,
  279. None,
  280. )
  281. .expect("failed to create new wallet");
  282. assert_eq!(wallet_2.total_balance().await.unwrap(), 0.into());
  283. let restored = wallet_2.restore().await.unwrap();
  284. let proofs = wallet_2.get_unspent_proofs().await.unwrap();
  285. assert!(!proofs.is_empty());
  286. let expected_fee = wallet.get_proofs_fee(&proofs).await.unwrap();
  287. wallet_2
  288. .swap(None, SplitTarget::default(), proofs, None, false)
  289. .await
  290. .unwrap();
  291. assert_eq!(restored, 100.into());
  292. // Since we have to do a swap we expect to restore amount - fee
  293. assert_eq!(
  294. wallet_2.total_balance().await.unwrap(),
  295. Amount::from(100) - expected_fee
  296. );
  297. let proofs = wallet.get_unspent_proofs().await.unwrap();
  298. let states = wallet.check_proofs_spent(proofs).await.unwrap();
  299. for state in states {
  300. if state.state != State::Spent {
  301. panic!("All proofs should be spent");
  302. }
  303. }
  304. }
  305. /// Tests that the melt quote status can be checked after a melt has completed
  306. ///
  307. /// This test verifies:
  308. /// 1. Mint tokens
  309. /// 2. Create a melt quote and execute the melt
  310. /// 3. Check the melt quote status via the wallet
  311. /// 4. Verify the quote is in the Paid state
  312. ///
  313. /// This ensures the mint correctly reports the melt quote status after completion.
  314. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  315. async fn test_melt_quote_status_after_melt() {
  316. let wallet = Wallet::new(
  317. &get_mint_url_from_env(),
  318. CurrencyUnit::Sat,
  319. Arc::new(memory::empty().await.unwrap()),
  320. Mnemonic::generate(12).unwrap().to_seed_normalized(""),
  321. None,
  322. )
  323. .expect("failed to create new wallet");
  324. let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
  325. let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
  326. pay_if_regtest(&get_test_temp_dir(), &invoice)
  327. .await
  328. .unwrap();
  329. let proofs = wallet
  330. .wait_and_mint_quote(
  331. mint_quote.clone(),
  332. SplitTarget::default(),
  333. None,
  334. tokio::time::Duration::from_secs(60),
  335. )
  336. .await
  337. .expect("mint failed");
  338. let mint_amount = proofs.total_amount().unwrap();
  339. assert_eq!(mint_amount, 100.into());
  340. let invoice = create_invoice_for_env(Some(50)).await.unwrap();
  341. let melt_quote = wallet.melt_quote(invoice, None).await.unwrap();
  342. let melt_response = wallet.melt(&melt_quote.id).await.unwrap();
  343. assert_eq!(melt_response.state, MeltQuoteState::Paid);
  344. let quote_status = wallet.melt_quote_status(&melt_quote.id).await.unwrap();
  345. assert_eq!(
  346. quote_status.state,
  347. MeltQuoteState::Paid,
  348. "Melt quote should be in Paid state after successful melt"
  349. );
  350. let db_quote = wallet
  351. .localstore
  352. .get_melt_quote(&melt_quote.id)
  353. .await
  354. .unwrap()
  355. .unwrap();
  356. assert_eq!(
  357. db_quote.state,
  358. MeltQuoteState::Paid,
  359. "Melt quote should be in Paid state after successful melt"
  360. );
  361. }
  362. /// Tests that the melt quote status can be checked via MultiMintWallet after a melt has completed
  363. ///
  364. /// This test verifies the same flow as test_melt_quote_status_after_melt but using
  365. /// the MultiMintWallet abstraction:
  366. /// 1. Create a MultiMintWallet and add a mint
  367. /// 2. Mint tokens via the multi mint wallet
  368. /// 3. Create a melt quote and execute the melt
  369. /// 4. Check the melt quote status via check_melt_quote
  370. /// 5. Verify the quote is in the Paid state
  371. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  372. async fn test_melt_quote_status_after_melt_multi_mint_wallet() {
  373. let seed = Mnemonic::generate(12).unwrap().to_seed_normalized("");
  374. let localstore = Arc::new(memory::empty().await.unwrap());
  375. let multi_mint_wallet = MultiMintWallet::new(localstore.clone(), seed, CurrencyUnit::Sat)
  376. .await
  377. .expect("failed to create multi mint wallet");
  378. let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
  379. multi_mint_wallet
  380. .add_mint(mint_url.clone())
  381. .await
  382. .expect("failed to add mint");
  383. let mint_quote = multi_mint_wallet
  384. .mint_quote(&mint_url, 100.into(), None)
  385. .await
  386. .unwrap();
  387. let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
  388. pay_if_regtest(&get_test_temp_dir(), &invoice)
  389. .await
  390. .unwrap();
  391. let _proofs = multi_mint_wallet
  392. .wait_for_mint_quote(&mint_url, &mint_quote.id, SplitTarget::default(), None, 60)
  393. .await
  394. .expect("mint failed");
  395. let balance = multi_mint_wallet.total_balance().await.unwrap();
  396. assert_eq!(balance, 100.into());
  397. let invoice = create_invoice_for_env(Some(50)).await.unwrap();
  398. let melt_quote = multi_mint_wallet
  399. .melt_quote(&mint_url, invoice, None)
  400. .await
  401. .unwrap();
  402. let melt_response = multi_mint_wallet
  403. .melt_with_mint(&mint_url, &melt_quote.id)
  404. .await
  405. .unwrap();
  406. assert_eq!(melt_response.state, MeltQuoteState::Paid);
  407. let quote_status = multi_mint_wallet
  408. .check_melt_quote(&mint_url, &melt_quote.id)
  409. .await
  410. .unwrap();
  411. assert_eq!(
  412. quote_status.state,
  413. MeltQuoteState::Paid,
  414. "Melt quote should be in Paid state after successful melt (via MultiMintWallet)"
  415. );
  416. use cdk_common::database::WalletDatabase;
  417. let db_quote = localstore
  418. .get_melt_quote(&melt_quote.id)
  419. .await
  420. .unwrap()
  421. .unwrap();
  422. assert_eq!(
  423. db_quote.state,
  424. MeltQuoteState::Paid,
  425. "Melt quote should be in Paid state after successful melt"
  426. );
  427. }
  428. /// Tests that change outputs in a melt quote are correctly handled
  429. ///
  430. /// This test verifies the following workflow:
  431. /// 1. Mint 100 sats of tokens
  432. /// 2. Create a melt quote for 9 sats (which requires 100 sats input with 91 sats change)
  433. /// 3. Manually construct a melt request with proofs and blinded messages for change
  434. /// 4. Verify that the change proofs in the response match what's reported by the quote status
  435. ///
  436. /// This ensures the mint correctly processes change outputs during melting operations
  437. /// and that the wallet can properly verify the change amounts match expectations.
  438. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  439. async fn test_fake_melt_change_in_quote() {
  440. let wallet = Wallet::new(
  441. &get_mint_url_from_env(),
  442. CurrencyUnit::Sat,
  443. Arc::new(memory::empty().await.unwrap()),
  444. Mnemonic::generate(12).unwrap().to_seed_normalized(""),
  445. None,
  446. )
  447. .expect("failed to create new wallet");
  448. let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
  449. let bolt11 = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
  450. pay_if_regtest(&get_test_temp_dir(), &bolt11).await.unwrap();
  451. let _proofs = wallet
  452. .wait_and_mint_quote(
  453. mint_quote.clone(),
  454. SplitTarget::default(),
  455. None,
  456. tokio::time::Duration::from_secs(60),
  457. )
  458. .await
  459. .expect("payment");
  460. let invoice = create_invoice_for_env(Some(9)).await.unwrap();
  461. let proofs = wallet.get_unspent_proofs().await.unwrap();
  462. let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
  463. let keyset = wallet.fetch_active_keyset().await.unwrap();
  464. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  465. let premint_secrets = PreMintSecrets::random(
  466. keyset.id,
  467. 100.into(),
  468. &SplitTarget::default(),
  469. &fee_and_amounts,
  470. )
  471. .unwrap();
  472. let client = HttpClient::new(get_mint_url_from_env().parse().unwrap(), None);
  473. let melt_request = MeltRequest::new(
  474. melt_quote.id.clone(),
  475. proofs.clone(),
  476. Some(premint_secrets.blinded_messages()),
  477. );
  478. let melt_response = client.post_melt(melt_request).await.unwrap();
  479. assert!(melt_response.change.is_some());
  480. let check = wallet.melt_quote_status(&melt_quote.id).await.unwrap();
  481. let mut melt_change = melt_response.change.unwrap();
  482. melt_change.sort_by(|a, b| a.amount.cmp(&b.amount));
  483. let mut check = check.change.unwrap();
  484. check.sort_by(|a, b| a.amount.cmp(&b.amount));
  485. assert_eq!(melt_change, check);
  486. }
  487. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  488. async fn test_pay_invoice_twice() {
  489. let ln_backend = match env::var("LN_BACKEND") {
  490. Ok(val) => Some(val),
  491. Err(_) => env::var("CDK_MINTD_LN_BACKEND").ok(),
  492. };
  493. if ln_backend.map(|ln| ln.to_uppercase()) == Some("FAKEWALLET".to_string()) {
  494. // We can only perform this test on regtest backends as fake wallet just marks the quote as paid
  495. return;
  496. }
  497. let wallet = Wallet::new(
  498. &get_mint_url_from_env(),
  499. CurrencyUnit::Sat,
  500. Arc::new(memory::empty().await.unwrap()),
  501. Mnemonic::generate(12).unwrap().to_seed_normalized(""),
  502. None,
  503. )
  504. .expect("failed to create new wallet");
  505. let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
  506. pay_if_regtest(&get_test_temp_dir(), &mint_quote.request.parse().unwrap())
  507. .await
  508. .unwrap();
  509. let proofs = wallet
  510. .wait_and_mint_quote(
  511. mint_quote.clone(),
  512. SplitTarget::default(),
  513. None,
  514. tokio::time::Duration::from_secs(60),
  515. )
  516. .await
  517. .expect("payment");
  518. let mint_amount = proofs.total_amount().unwrap();
  519. assert_eq!(mint_amount, 100.into());
  520. let invoice = create_invoice_for_env(Some(25)).await.unwrap();
  521. let melt_quote = wallet.melt_quote(invoice.clone(), None).await.unwrap();
  522. let melt = wallet.melt(&melt_quote.id).await.unwrap();
  523. // Creating a second quote for the same invoice is allowed
  524. let melt_quote_two = wallet.melt_quote(invoice, None).await.unwrap();
  525. // But attempting to melt (pay) the second quote should fail
  526. // since the first quote with the same lookup_id is already paid
  527. let melt_two = wallet.melt(&melt_quote_two.id).await;
  528. match melt_two {
  529. Err(err) => {
  530. let err_str = err.to_string().to_lowercase();
  531. if !err_str.contains("duplicate")
  532. && !err_str.contains("already paid")
  533. && !err_str.contains("request already paid")
  534. {
  535. panic!(
  536. "Expected duplicate/already paid error, got: {}",
  537. err.to_string()
  538. );
  539. }
  540. }
  541. Ok(_) => {
  542. panic!("Should not have allowed second payment");
  543. }
  544. }
  545. let balance = wallet.total_balance().await.unwrap();
  546. assert_eq!(balance, (Amount::from(100) - melt.fee_paid - melt.amount));
  547. }