async_melt.rs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910
  1. //! Async Melt Integration Tests
  2. //!
  3. //! This file contains tests for async melt functionality using the Prefer: respond-async header.
  4. //!
  5. //! Test Scenarios:
  6. //! - Async melt returns PENDING state immediately
  7. //! - Synchronous melt still works correctly (backward compatibility)
  8. //! - Background task completion
  9. //! - Quote polling pattern
  10. use std::collections::HashSet;
  11. use std::sync::Arc;
  12. use bip39::Mnemonic;
  13. use cashu::PaymentMethod;
  14. use cdk::amount::SplitTarget;
  15. use cdk::nuts::{CurrencyUnit, MeltQuoteState, State};
  16. use cdk::wallet::{MeltOutcome, Wallet, WalletTrait};
  17. use cdk::StreamExt;
  18. use cdk_fake_wallet::{create_fake_invoice, FakeInvoiceDescription};
  19. use cdk_sqlite::wallet::memory;
  20. const MINT_URL: &str = "http://127.0.0.1:8086";
  21. /// Test: Async melt returns PENDING state immediately
  22. ///
  23. /// This test validates that when calling melt with Prefer: respond-async header,
  24. /// the mint returns immediately with PENDING state.
  25. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  26. async fn test_async_melt_returns_pending() {
  27. let wallet = Wallet::new(
  28. MINT_URL,
  29. CurrencyUnit::Sat,
  30. Arc::new(memory::empty().await.unwrap()),
  31. Mnemonic::generate(12).unwrap().to_seed_normalized(""),
  32. None,
  33. )
  34. .expect("failed to create new wallet");
  35. // Step 1: Mint some tokens
  36. let mint_quote = wallet
  37. .mint_quote(PaymentMethod::BOLT11, Some(100.into()), None, None)
  38. .await
  39. .unwrap();
  40. let mut proof_streams = wallet.proof_stream(mint_quote.clone(), SplitTarget::default(), None);
  41. let proofs_before = proof_streams
  42. .next()
  43. .await
  44. .expect("payment")
  45. .expect("no error");
  46. // Collect Y values of proofs before melt
  47. let ys_before: HashSet<_> = proofs_before
  48. .iter()
  49. .map(|p| p.y().expect("Invalid proof Y value").clone())
  50. .collect();
  51. let balance = wallet.total_balance().await.unwrap();
  52. assert_eq!(balance, 100.into());
  53. // Step 2: Create a melt quote
  54. let fake_invoice_description = FakeInvoiceDescription {
  55. pay_invoice_state: MeltQuoteState::Paid,
  56. check_payment_state: MeltQuoteState::Paid,
  57. pay_err: false,
  58. check_err: false,
  59. };
  60. let invoice: cashu::Bolt11Invoice = create_fake_invoice(
  61. 50_000, // 50 sats in millisats
  62. serde_json::to_string(&fake_invoice_description).unwrap(),
  63. );
  64. let melt_quote = wallet
  65. .melt_quote(PaymentMethod::BOLT11, invoice.to_string(), None, None)
  66. .await
  67. .unwrap();
  68. // Step 3: Call melt (wallet handles proof selection internally)
  69. // This should complete and return the final state
  70. let prepared = wallet
  71. .prepare_melt(&melt_quote.id, std::collections::HashMap::new())
  72. .await
  73. .unwrap();
  74. // Collect Y values of proofs that will be used in the melt
  75. let proofs_to_use: HashSet<_> = prepared
  76. .proofs()
  77. .iter()
  78. .chain(prepared.proofs_to_swap().iter())
  79. .map(|p| p.y().expect("Invalid proof Y value").clone())
  80. .collect();
  81. let confirmed = prepared.confirm().await.unwrap();
  82. // Step 4: Verify the melt completed successfully
  83. assert_eq!(
  84. confirmed.state(),
  85. MeltQuoteState::Paid,
  86. "Melt should complete with PAID state"
  87. );
  88. // Step 5: Verify balance reduced (100 - 50 - fees)
  89. let final_balance = wallet.total_balance().await.unwrap();
  90. assert!(
  91. final_balance < 100.into(),
  92. "Balance should be reduced after melt. Initial: 100, Final: {}",
  93. final_balance
  94. );
  95. // Step 6: Verify no proofs are pending
  96. let pending_proofs = wallet
  97. .get_proofs_with(Some(vec![State::Pending]), None)
  98. .await
  99. .unwrap();
  100. assert!(
  101. pending_proofs.is_empty(),
  102. "No proofs should be in pending state after melt completes"
  103. );
  104. // Step 7: Verify proofs used in melt are marked as Spent
  105. let proofs_after = wallet.get_proofs_with(None, None).await.unwrap();
  106. let ys_after: HashSet<_> = proofs_after
  107. .iter()
  108. .map(|p| p.y().expect("Invalid proof Y value").clone())
  109. .collect();
  110. // All original proofs should still exist (not deleted)
  111. for y in &ys_before {
  112. assert!(
  113. ys_after.contains(y),
  114. "Original proof with Y={} should still exist after melt",
  115. y
  116. );
  117. }
  118. // Verify the specific proofs used are in Spent state
  119. let spent_proofs = wallet
  120. .get_proofs_with(Some(vec![State::Spent]), None)
  121. .await
  122. .unwrap();
  123. let spent_ys: HashSet<_> = spent_proofs
  124. .iter()
  125. .map(|p| p.y().expect("Invalid proof Y value").clone())
  126. .collect();
  127. for y in &proofs_to_use {
  128. assert!(
  129. spent_ys.contains(y),
  130. "Proof with Y={} that was used in melt should be marked as Spent",
  131. y
  132. );
  133. }
  134. }
  135. /// Test: Synchronous melt still works correctly
  136. ///
  137. /// This test ensures backward compatibility - melt without Prefer header
  138. /// still blocks until completion and returns the final state.
  139. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  140. async fn test_sync_melt_completes_fully() {
  141. let wallet = Wallet::new(
  142. MINT_URL,
  143. CurrencyUnit::Sat,
  144. Arc::new(memory::empty().await.unwrap()),
  145. Mnemonic::generate(12).unwrap().to_seed_normalized(""),
  146. None,
  147. )
  148. .expect("failed to create new wallet");
  149. // Step 1: Mint some tokens
  150. let mint_quote = wallet
  151. .mint_quote(PaymentMethod::BOLT11, Some(100.into()), None, None)
  152. .await
  153. .unwrap();
  154. let mut proof_streams = wallet.proof_stream(mint_quote.clone(), SplitTarget::default(), None);
  155. let proofs_before = proof_streams
  156. .next()
  157. .await
  158. .expect("payment")
  159. .expect("no error");
  160. // Collect Y values of proofs before melt
  161. let ys_before: HashSet<_> = proofs_before
  162. .iter()
  163. .map(|p| p.y().expect("Invalid proof Y value").clone())
  164. .collect();
  165. let balance = wallet.total_balance().await.unwrap();
  166. assert_eq!(balance, 100.into());
  167. // Step 2: Create a melt quote
  168. let fake_invoice_description = FakeInvoiceDescription {
  169. pay_invoice_state: MeltQuoteState::Paid,
  170. check_payment_state: MeltQuoteState::Paid,
  171. pay_err: false,
  172. check_err: false,
  173. };
  174. let invoice = create_fake_invoice(
  175. 50_000, // 50 sats in millisats
  176. serde_json::to_string(&fake_invoice_description).unwrap(),
  177. );
  178. let melt_quote = wallet
  179. .melt_quote(PaymentMethod::BOLT11, invoice.to_string(), None, None)
  180. .await
  181. .unwrap();
  182. // Step 3: Call melt with prepare/confirm pattern
  183. let prepared = wallet
  184. .prepare_melt(&melt_quote.id, std::collections::HashMap::new())
  185. .await
  186. .unwrap();
  187. // Collect Y values of proofs that will be used in the melt
  188. let proofs_to_use: HashSet<_> = prepared
  189. .proofs()
  190. .iter()
  191. .chain(prepared.proofs_to_swap().iter())
  192. .map(|p| p.y().expect("Invalid proof Y value").clone())
  193. .collect();
  194. let confirmed = prepared.confirm().await.unwrap();
  195. // Step 5: Verify response shows payment completed
  196. assert_eq!(
  197. confirmed.state(),
  198. MeltQuoteState::Paid,
  199. "Melt should return PAID state"
  200. );
  201. // Step 6: Verify the quote is PAID in the mint
  202. let quote_state = wallet
  203. .check_melt_quote_status(&melt_quote.id)
  204. .await
  205. .unwrap();
  206. assert_eq!(
  207. quote_state.state,
  208. MeltQuoteState::Paid,
  209. "Quote should be PAID"
  210. );
  211. // Step 7: Verify balance reduced after melt
  212. let final_balance = wallet.total_balance().await.unwrap();
  213. assert!(
  214. final_balance < 100.into(),
  215. "Balance should be reduced after melt. Initial: 100, Final: {}",
  216. final_balance
  217. );
  218. // Step 8: Verify no proofs are pending
  219. let pending_proofs = wallet
  220. .get_proofs_with(Some(vec![State::Pending]), None)
  221. .await
  222. .unwrap();
  223. assert!(
  224. pending_proofs.is_empty(),
  225. "No proofs should be in pending state after melt completes"
  226. );
  227. // Step 9: Verify proofs used in melt are marked as Spent
  228. let proofs_after = wallet.get_proofs_with(None, None).await.unwrap();
  229. let ys_after: HashSet<_> = proofs_after
  230. .iter()
  231. .map(|p| p.y().expect("Invalid proof Y value").clone())
  232. .collect();
  233. // All original proofs should still exist (not deleted)
  234. for y in &ys_before {
  235. assert!(
  236. ys_after.contains(y),
  237. "Original proof with Y={} should still exist after melt",
  238. y
  239. );
  240. }
  241. // Verify the specific proofs used are in Spent state
  242. let spent_proofs = wallet
  243. .get_proofs_with(Some(vec![State::Spent]), None)
  244. .await
  245. .unwrap();
  246. let spent_ys: HashSet<_> = spent_proofs
  247. .iter()
  248. .map(|p| p.y().expect("Invalid proof Y value").clone())
  249. .collect();
  250. for y in &proofs_to_use {
  251. assert!(
  252. spent_ys.contains(y),
  253. "Proof with Y={} that was used in melt should be marked as Spent",
  254. y
  255. );
  256. }
  257. }
  258. /// Test: confirm_prefer_async returns Pending when mint supports async
  259. ///
  260. /// This test validates that confirm_prefer_async() returns MeltOutcome::Pending
  261. /// when the mint accepts the async request and returns PENDING state.
  262. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  263. async fn test_confirm_prefer_async_returns_pending_immediately() {
  264. let wallet = Wallet::new(
  265. MINT_URL,
  266. CurrencyUnit::Sat,
  267. Arc::new(memory::empty().await.unwrap()),
  268. Mnemonic::generate(12).unwrap().to_seed_normalized(""),
  269. None,
  270. )
  271. .expect("failed to create new wallet");
  272. // Step 1: Mint some tokens
  273. let mint_quote = wallet
  274. .mint_quote(PaymentMethod::BOLT11, Some(100.into()), None, None)
  275. .await
  276. .unwrap();
  277. let mut proof_streams = wallet.proof_stream(mint_quote.clone(), SplitTarget::default(), None);
  278. let _proofs = proof_streams
  279. .next()
  280. .await
  281. .expect("payment")
  282. .expect("no error");
  283. let balance = wallet.total_balance().await.unwrap();
  284. assert_eq!(balance, 100.into());
  285. // Step 2: Create a melt quote with Pending state
  286. let fake_invoice_description = FakeInvoiceDescription {
  287. pay_invoice_state: MeltQuoteState::Pending,
  288. check_payment_state: MeltQuoteState::Pending,
  289. pay_err: false,
  290. check_err: false,
  291. };
  292. let invoice = create_fake_invoice(
  293. 50_000, // 50 sats in millisats
  294. serde_json::to_string(&fake_invoice_description).unwrap(),
  295. );
  296. let melt_quote = wallet
  297. .melt_quote(PaymentMethod::BOLT11, invoice.to_string(), None, None)
  298. .await
  299. .unwrap();
  300. // Step 3: Call confirm_prefer_async
  301. let prepared = wallet
  302. .prepare_melt(&melt_quote.id, std::collections::HashMap::new())
  303. .await
  304. .unwrap();
  305. let result = prepared.confirm_prefer_async().await.unwrap();
  306. // Step 4: Verify we got Pending result
  307. assert!(
  308. matches!(result, MeltOutcome::Pending(_)),
  309. "confirm_prefer_async should return MeltOutcome::Pending when mint supports async"
  310. );
  311. // Step 5: Verify proofs are in pending state
  312. let pending_proofs = wallet
  313. .get_proofs_with(Some(vec![State::Pending]), None)
  314. .await
  315. .unwrap();
  316. assert!(
  317. !pending_proofs.is_empty(),
  318. "Proofs should be in pending state"
  319. );
  320. // Note: Fake wallet may complete immediately even with Pending state configured.
  321. // The key assertion is that confirm_prefer_async returns MeltOutcome::Pending,
  322. // which proves the API is working correctly.
  323. }
  324. /// Test: Pending melt from confirm_prefer_async can be awaited
  325. ///
  326. /// This test validates that when confirm_prefer_async() returns MeltOutcome::Pending,
  327. /// the pending melt can be awaited to completion.
  328. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  329. async fn test_confirm_prefer_async_pending_can_be_awaited() {
  330. let wallet = Wallet::new(
  331. MINT_URL,
  332. CurrencyUnit::Sat,
  333. Arc::new(memory::empty().await.unwrap()),
  334. Mnemonic::generate(12).unwrap().to_seed_normalized(""),
  335. None,
  336. )
  337. .expect("failed to create new wallet");
  338. // Step 1: Mint some tokens
  339. let mint_quote = wallet
  340. .mint_quote(PaymentMethod::BOLT11, Some(100.into()), None, None)
  341. .await
  342. .unwrap();
  343. let mut proof_streams = wallet.proof_stream(mint_quote.clone(), SplitTarget::default(), None);
  344. let proofs_before = proof_streams
  345. .next()
  346. .await
  347. .expect("payment")
  348. .expect("no error");
  349. // Collect Y values of proofs before melt
  350. let ys_before: HashSet<_> = proofs_before
  351. .iter()
  352. .map(|p| p.y().expect("Invalid proof Y value").clone())
  353. .collect();
  354. let fake_invoice_description = FakeInvoiceDescription {
  355. pay_invoice_state: MeltQuoteState::Paid,
  356. check_payment_state: MeltQuoteState::Paid,
  357. pay_err: false,
  358. check_err: false,
  359. };
  360. let invoice = create_fake_invoice(
  361. 50_000,
  362. serde_json::to_string(&fake_invoice_description).unwrap(),
  363. );
  364. let melt_quote = wallet
  365. .melt_quote(PaymentMethod::BOLT11, invoice.to_string(), None, None)
  366. .await
  367. .unwrap();
  368. // Step 3: Call confirm_prefer_async
  369. let prepared = wallet
  370. .prepare_melt(&melt_quote.id, std::collections::HashMap::new())
  371. .await
  372. .unwrap();
  373. // Collect Y values of proofs that will be used in the melt
  374. let proofs_to_use: HashSet<_> = prepared
  375. .proofs()
  376. .iter()
  377. .chain(prepared.proofs_to_swap().iter())
  378. .map(|p| p.y().expect("Invalid proof Y value").clone())
  379. .collect();
  380. let result = prepared.confirm_prefer_async().await.unwrap();
  381. // Step 4: If we got Pending, await it
  382. let finalized = match result {
  383. MeltOutcome::Paid(_melt) => panic!("We expect it to be pending"),
  384. MeltOutcome::Pending(pending) => {
  385. // This is the key test - awaiting the pending melt
  386. let melt = pending.await.unwrap();
  387. melt
  388. }
  389. };
  390. // Step 5: Verify final state
  391. assert_eq!(
  392. finalized.state(),
  393. MeltQuoteState::Paid,
  394. "Awaited melt should complete to PAID state"
  395. );
  396. // Step 6: Verify balance reduced after awaiting
  397. let final_balance = wallet.total_balance().await.unwrap();
  398. assert!(
  399. final_balance < 100.into(),
  400. "Balance should be reduced after melt completes. Initial: 100, Final: {}",
  401. final_balance
  402. );
  403. // Step 7: Verify no proofs are pending
  404. let pending_proofs = wallet
  405. .get_proofs_with(Some(vec![State::Pending]), None)
  406. .await
  407. .unwrap();
  408. assert!(
  409. pending_proofs.is_empty(),
  410. "No proofs should be in pending state after melt completes"
  411. );
  412. // Step 8: Verify proofs used in melt are marked as Spent after awaiting
  413. let proofs_after = wallet.get_proofs_with(None, None).await.unwrap();
  414. let ys_after: HashSet<_> = proofs_after
  415. .iter()
  416. .map(|p| p.y().expect("Invalid proof Y value").clone())
  417. .collect();
  418. // All original proofs should still exist (not deleted)
  419. for y in &ys_before {
  420. assert!(
  421. ys_after.contains(y),
  422. "Original proof with Y={} should still exist after awaiting",
  423. y
  424. );
  425. }
  426. // Verify the specific proofs used are in Spent state
  427. let spent_proofs = wallet
  428. .get_proofs_with(Some(vec![State::Spent]), None)
  429. .await
  430. .unwrap();
  431. let spent_ys: HashSet<_> = spent_proofs
  432. .iter()
  433. .map(|p| p.y().expect("Invalid proof Y value").clone())
  434. .collect();
  435. for y in &proofs_to_use {
  436. assert!(
  437. spent_ys.contains(y),
  438. "Proof with Y={} that was used in melt should be marked as Spent after awaiting",
  439. y
  440. );
  441. }
  442. }
  443. /// Test: Pending melt can be dropped and polled elsewhere
  444. ///
  445. /// This test validates that when confirm_prefer_async() returns MeltOutcome::Pending,
  446. /// the caller can drop the pending handle and poll the status via check_melt_quote_status().
  447. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  448. async fn test_confirm_prefer_async_pending_can_be_dropped_and_polled() {
  449. let wallet = Wallet::new(
  450. MINT_URL,
  451. CurrencyUnit::Sat,
  452. Arc::new(memory::empty().await.unwrap()),
  453. Mnemonic::generate(12).unwrap().to_seed_normalized(""),
  454. None,
  455. )
  456. .expect("failed to create new wallet");
  457. // Step 1: Mint some tokens
  458. let mint_quote = wallet
  459. .mint_quote(PaymentMethod::BOLT11, Some(100.into()), None, None)
  460. .await
  461. .unwrap();
  462. let mut proof_streams = wallet.proof_stream(mint_quote.clone(), SplitTarget::default(), None);
  463. let proofs_before = proof_streams
  464. .next()
  465. .await
  466. .expect("payment")
  467. .expect("no error");
  468. // Collect Y values of proofs before melt
  469. let ys_before: HashSet<_> = proofs_before
  470. .iter()
  471. .map(|p| p.y().expect("Invalid proof Y value").clone())
  472. .collect();
  473. // Step 2: Create a melt quote
  474. let fake_invoice_description = FakeInvoiceDescription {
  475. pay_invoice_state: MeltQuoteState::Paid,
  476. check_payment_state: MeltQuoteState::Paid,
  477. pay_err: false,
  478. check_err: false,
  479. };
  480. let invoice = create_fake_invoice(
  481. 50_000,
  482. serde_json::to_string(&fake_invoice_description).unwrap(),
  483. );
  484. let melt_quote = wallet
  485. .melt_quote(PaymentMethod::BOLT11, invoice.to_string(), None, None)
  486. .await
  487. .unwrap();
  488. let quote_id = melt_quote.id.clone();
  489. // Step 3: Call confirm_prefer_async
  490. let prepared = wallet
  491. .prepare_melt(&melt_quote.id, std::collections::HashMap::new())
  492. .await
  493. .unwrap();
  494. // Collect Y values of proofs that will be used in the melt
  495. let proofs_to_use: HashSet<_> = prepared
  496. .proofs()
  497. .iter()
  498. .chain(prepared.proofs_to_swap().iter())
  499. .map(|p| p.y().expect("Invalid proof Y value").clone())
  500. .collect();
  501. let result = prepared.confirm_prefer_async().await.unwrap();
  502. // Step 4: Drop the pending handle (simulating caller not awaiting)
  503. match result {
  504. MeltOutcome::Paid(_) => {
  505. panic!("We expect it to be pending");
  506. }
  507. MeltOutcome::Pending(_) => {
  508. // Drop the pending handle - don't await
  509. }
  510. }
  511. // Step 5: Poll the quote status
  512. let mut attempts = 0;
  513. let max_attempts = 10;
  514. let mut final_state = MeltQuoteState::Unknown;
  515. while attempts < max_attempts {
  516. let quote = wallet.check_melt_quote_status(&quote_id).await.unwrap();
  517. final_state = quote.state;
  518. if matches!(final_state, MeltQuoteState::Paid | MeltQuoteState::Failed) {
  519. break;
  520. }
  521. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  522. attempts += 1;
  523. }
  524. // Step 6: Verify final state
  525. assert_eq!(
  526. final_state,
  527. MeltQuoteState::Paid,
  528. "Quote should reach PAID state after polling"
  529. );
  530. // Step 7: Verify balance reduced after polling shows Paid
  531. let final_balance = wallet.total_balance().await.unwrap();
  532. assert!(
  533. final_balance < 100.into(),
  534. "Balance should be reduced after melt completes via polling. Initial: 100, Final: {}",
  535. final_balance
  536. );
  537. // Step 8: Verify no proofs are pending
  538. let pending_proofs = wallet
  539. .get_proofs_with(Some(vec![State::Pending]), None)
  540. .await
  541. .unwrap();
  542. assert!(
  543. pending_proofs.is_empty(),
  544. "No proofs should be in pending state after polling shows Paid"
  545. );
  546. // Step 9: Verify proofs used in melt are marked as Spent after polling
  547. let proofs_after = wallet.get_proofs_with(None, None).await.unwrap();
  548. let ys_after: HashSet<_> = proofs_after
  549. .iter()
  550. .map(|p| p.y().expect("Invalid proof Y value").clone())
  551. .collect();
  552. // All original proofs should still exist (not deleted)
  553. for y in &ys_before {
  554. assert!(
  555. ys_after.contains(y),
  556. "Original proof with Y={} should still exist after polling",
  557. y
  558. );
  559. }
  560. // Verify the specific proofs used are in Spent state
  561. let spent_proofs = wallet
  562. .get_proofs_with(Some(vec![State::Spent]), None)
  563. .await
  564. .unwrap();
  565. let spent_ys: HashSet<_> = spent_proofs
  566. .iter()
  567. .map(|p| p.y().expect("Invalid proof Y value").clone())
  568. .collect();
  569. for y in &proofs_to_use {
  570. assert!(
  571. spent_ys.contains(y),
  572. "Proof with Y={} that was used in melt should be marked as Spent after polling",
  573. y
  574. );
  575. }
  576. }
  577. /// Test: Compare confirm() vs confirm_prefer_async() behavior
  578. ///
  579. /// This test validates the difference between blocking confirm() and
  580. /// non-blocking confirm_prefer_async() methods.
  581. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  582. async fn test_confirm_vs_confirm_prefer_async_behavior() {
  583. // Create two wallets for the comparison
  584. let wallet_a = Wallet::new(
  585. MINT_URL,
  586. CurrencyUnit::Sat,
  587. Arc::new(memory::empty().await.unwrap()),
  588. Mnemonic::generate(12).unwrap().to_seed_normalized(""),
  589. None,
  590. )
  591. .expect("failed to create wallet A");
  592. let wallet_b = Wallet::new(
  593. MINT_URL,
  594. CurrencyUnit::Sat,
  595. Arc::new(memory::empty().await.unwrap()),
  596. Mnemonic::generate(12).unwrap().to_seed_normalized(""),
  597. None,
  598. )
  599. .expect("failed to create wallet B");
  600. // Step 1: Fund both wallets and collect their proof Y values
  601. let mint_quote_a = wallet_a
  602. .mint_quote(PaymentMethod::BOLT11, Some(100.into()), None, None)
  603. .await
  604. .unwrap();
  605. let mut proof_streams_a =
  606. wallet_a.proof_stream(mint_quote_a.clone(), SplitTarget::default(), None);
  607. let proofs_before_a = proof_streams_a
  608. .next()
  609. .await
  610. .expect("payment")
  611. .expect("no error");
  612. // Collect Y values of proofs before melt for wallet A
  613. let ys_before_a: HashSet<_> = proofs_before_a
  614. .iter()
  615. .map(|p| p.y().expect("Invalid proof Y value").clone())
  616. .collect();
  617. let mint_quote_b = wallet_b
  618. .mint_quote(PaymentMethod::BOLT11, Some(100.into()), None, None)
  619. .await
  620. .unwrap();
  621. let mut proof_streams_b =
  622. wallet_b.proof_stream(mint_quote_b.clone(), SplitTarget::default(), None);
  623. let proofs_before_b = proof_streams_b
  624. .next()
  625. .await
  626. .expect("payment")
  627. .expect("no error");
  628. // Collect Y values of proofs before melt for wallet B
  629. let ys_before_b: HashSet<_> = proofs_before_b
  630. .iter()
  631. .map(|p| p.y().expect("Invalid proof Y value").clone())
  632. .collect();
  633. // Step 2: Create melt quotes for both wallets (separate invoices with unique payment hashes)
  634. let fake_invoice_description = FakeInvoiceDescription {
  635. pay_invoice_state: MeltQuoteState::Paid,
  636. check_payment_state: MeltQuoteState::Paid,
  637. pay_err: false,
  638. check_err: false,
  639. };
  640. let invoice_a = create_fake_invoice(
  641. 50_000,
  642. serde_json::to_string(&fake_invoice_description).unwrap(),
  643. );
  644. let melt_quote_a = wallet_a
  645. .melt_quote(PaymentMethod::BOLT11, invoice_a.to_string(), None, None)
  646. .await
  647. .unwrap();
  648. // Create separate invoice for wallet B (different payment hash)
  649. let invoice_b = create_fake_invoice(
  650. 50_000,
  651. serde_json::to_string(&fake_invoice_description).unwrap(),
  652. );
  653. let melt_quote_b = wallet_b
  654. .melt_quote(PaymentMethod::BOLT11, invoice_b.to_string(), None, None)
  655. .await
  656. .unwrap();
  657. // Step 3: Wallet A uses confirm() - blocks until completion
  658. let prepared_a = wallet_a
  659. .prepare_melt(&melt_quote_a.id, std::collections::HashMap::new())
  660. .await
  661. .unwrap();
  662. // Collect Y values of proofs that will be used in the melt for wallet A
  663. let proofs_to_use_a: HashSet<_> = prepared_a
  664. .proofs()
  665. .iter()
  666. .chain(prepared_a.proofs_to_swap().iter())
  667. .map(|p| p.y().expect("Invalid proof Y value").clone())
  668. .collect();
  669. let finalized_a = prepared_a.confirm().await.unwrap();
  670. // Step 4: Wallet B uses confirm_prefer_async() - returns immediately
  671. let prepared_b = wallet_b
  672. .prepare_melt(&melt_quote_b.id, std::collections::HashMap::new())
  673. .await
  674. .unwrap();
  675. // Collect Y values of proofs that will be used in the melt for wallet B
  676. let proofs_to_use_b: HashSet<_> = prepared_b
  677. .proofs()
  678. .iter()
  679. .chain(prepared_b.proofs_to_swap().iter())
  680. .map(|p| p.y().expect("Invalid proof Y value").clone())
  681. .collect();
  682. let result_b = prepared_b.confirm_prefer_async().await.unwrap();
  683. // Step 5: Both should complete successfully
  684. assert_eq!(
  685. finalized_a.state(),
  686. MeltQuoteState::Paid,
  687. "Wallet A (confirm) should complete successfully"
  688. );
  689. let finalized_b = match result_b {
  690. MeltOutcome::Paid(melt) => melt,
  691. MeltOutcome::Pending(pending) => pending.await.unwrap(),
  692. };
  693. assert_eq!(
  694. finalized_b.state(),
  695. MeltQuoteState::Paid,
  696. "Wallet B (confirm_prefer_async) should complete successfully"
  697. );
  698. // Step 6: Verify both wallets have reduced balances
  699. let balance_a = wallet_a.total_balance().await.unwrap();
  700. let balance_b = wallet_b.total_balance().await.unwrap();
  701. assert!(
  702. balance_a < 100.into(),
  703. "Wallet A balance should be reduced. Initial: 100, Final: {}",
  704. balance_a
  705. );
  706. assert!(
  707. balance_b < 100.into(),
  708. "Wallet B balance should be reduced. Initial: 100, Final: {}",
  709. balance_b
  710. );
  711. // Step 7: Verify no proofs are pending in either wallet
  712. let pending_a = wallet_a
  713. .get_proofs_with(Some(vec![State::Pending]), None)
  714. .await
  715. .unwrap();
  716. let pending_b = wallet_b
  717. .get_proofs_with(Some(vec![State::Pending]), None)
  718. .await
  719. .unwrap();
  720. assert!(
  721. pending_a.is_empty(),
  722. "Wallet A should have no pending proofs"
  723. );
  724. assert!(
  725. pending_b.is_empty(),
  726. "Wallet B should have no pending proofs"
  727. );
  728. // Step 8: Verify original proofs are marked as Spent in both wallets
  729. let proofs_after_a = wallet_a.get_proofs_with(None, None).await.unwrap();
  730. let proofs_after_b = wallet_b.get_proofs_with(None, None).await.unwrap();
  731. let ys_after_a: HashSet<_> = proofs_after_a
  732. .iter()
  733. .map(|p| p.y().expect("Invalid proof Y value").clone())
  734. .collect();
  735. let ys_after_b: HashSet<_> = proofs_after_b
  736. .iter()
  737. .map(|p| p.y().expect("Invalid proof Y value").clone())
  738. .collect();
  739. // All original proofs should still exist (not deleted)
  740. for y in &ys_before_a {
  741. assert!(
  742. ys_after_a.contains(y),
  743. "Wallet A original proof with Y={} should still exist after melt",
  744. y
  745. );
  746. }
  747. for y in &ys_before_b {
  748. assert!(
  749. ys_after_b.contains(y),
  750. "Wallet B original proof with Y={} should still exist after melt",
  751. y
  752. );
  753. }
  754. // Verify the specific proofs used are in Spent state
  755. let spent_a = wallet_a
  756. .get_proofs_with(Some(vec![State::Spent]), None)
  757. .await
  758. .unwrap();
  759. let spent_b = wallet_b
  760. .get_proofs_with(Some(vec![State::Spent]), None)
  761. .await
  762. .unwrap();
  763. let spent_ys_a: HashSet<_> = spent_a
  764. .iter()
  765. .map(|p| p.y().expect("Invalid proof Y value").clone())
  766. .collect();
  767. let spent_ys_b: HashSet<_> = spent_b
  768. .iter()
  769. .map(|p| p.y().expect("Invalid proof Y value").clone())
  770. .collect();
  771. for y in &proofs_to_use_a {
  772. assert!(
  773. spent_ys_a.contains(y),
  774. "Wallet A proof with Y={} that was used in melt should be marked as Spent",
  775. y
  776. );
  777. }
  778. for y in &proofs_to_use_b {
  779. assert!(
  780. spent_ys_b.contains(y),
  781. "Wallet B proof with Y={} that was used in melt should be marked as Spent",
  782. y
  783. );
  784. }
  785. }