multi_mint_wallet.rs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767
  1. //! Integration tests for MultiMintWallet
  2. //!
  3. //! These tests verify the multi-mint wallet functionality including:
  4. //! - Basic mint/melt operations across multiple mints
  5. //! - Token receive and send operations
  6. //! - Automatic mint selection for melts
  7. //! - Cross-mint transfers
  8. //!
  9. //! Tests use the fake wallet backend for deterministic behavior.
  10. use std::env;
  11. use std::path::PathBuf;
  12. use std::str::FromStr;
  13. use std::sync::Arc;
  14. use bip39::Mnemonic;
  15. use cdk::amount::{Amount, SplitTarget};
  16. use cdk::mint_url::MintUrl;
  17. use cdk::nuts::nut00::ProofsMethods;
  18. use cdk::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState, PaymentMethod, Token};
  19. use cdk::wallet::{MultiMintReceiveOptions, MultiMintWallet, SendOptions};
  20. use cdk_integration_tests::{create_invoice_for_env, get_mint_url_from_env, pay_if_regtest};
  21. use cdk_sqlite::wallet::memory;
  22. use lightning_invoice::Bolt11Invoice;
  23. // Helper function to get temp directory from environment or fallback
  24. fn get_test_temp_dir() -> PathBuf {
  25. match env::var("CDK_ITESTS_DIR") {
  26. Ok(dir) => PathBuf::from(dir),
  27. Err(_) => panic!("Unknown test dir"),
  28. }
  29. }
  30. /// Helper to create a MultiMintWallet with a fresh seed and in-memory database
  31. async fn create_test_multi_mint_wallet() -> MultiMintWallet {
  32. let seed = Mnemonic::generate(12).unwrap().to_seed_normalized("");
  33. let localstore = Arc::new(memory::empty().await.unwrap());
  34. MultiMintWallet::new(localstore, seed, CurrencyUnit::Sat)
  35. .await
  36. .expect("failed to create multi mint wallet")
  37. }
  38. /// Helper to fund a MultiMintWallet at a specific mint
  39. async fn fund_multi_mint_wallet(
  40. wallet: &MultiMintWallet,
  41. mint_url: &MintUrl,
  42. amount: Amount,
  43. ) -> Amount {
  44. let mint_quote = wallet
  45. .mint_quote(mint_url, PaymentMethod::BOLT11, Some(amount), None, None)
  46. .await
  47. .unwrap();
  48. let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
  49. pay_if_regtest(&get_test_temp_dir(), &invoice)
  50. .await
  51. .unwrap();
  52. let proofs = wallet
  53. .wait_for_mint_quote(
  54. mint_url,
  55. &mint_quote.id,
  56. SplitTarget::default(),
  57. None,
  58. std::time::Duration::from_secs(60),
  59. )
  60. .await
  61. .expect("mint failed");
  62. proofs.total_amount().unwrap()
  63. }
  64. /// Test the direct mint() function on MultiMintWallet
  65. ///
  66. /// This test verifies:
  67. /// 1. Create a mint quote
  68. /// 2. Pay the invoice
  69. /// 3. Poll until quote is paid (like a real wallet would)
  70. /// 4. Call mint() directly (not wait_for_mint_quote)
  71. /// 5. Verify tokens are received
  72. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  73. async fn test_multi_mint_wallet_mint() {
  74. let multi_mint_wallet = create_test_multi_mint_wallet().await;
  75. let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
  76. multi_mint_wallet
  77. .add_mint(mint_url.clone())
  78. .await
  79. .expect("failed to add mint");
  80. // Create mint quote
  81. let mint_quote = multi_mint_wallet
  82. .mint_quote(
  83. &mint_url,
  84. PaymentMethod::BOLT11,
  85. Some(100.into()),
  86. None,
  87. None,
  88. )
  89. .await
  90. .unwrap();
  91. // Pay the invoice (in regtest mode) - for fake wallet, payment is simulated automatically
  92. let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
  93. pay_if_regtest(&get_test_temp_dir(), &invoice)
  94. .await
  95. .unwrap();
  96. // Poll for quote to be paid (like a real wallet would)
  97. let mut quote_status = multi_mint_wallet
  98. .refresh_mint_quote(&mint_url, &mint_quote.id)
  99. .await
  100. .unwrap();
  101. let timeout = tokio::time::Duration::from_secs(30);
  102. let start = tokio::time::Instant::now();
  103. while quote_status.state != MintQuoteState::Paid && quote_status.state != MintQuoteState::Issued
  104. {
  105. if start.elapsed() > timeout {
  106. panic!(
  107. "Timeout waiting for quote to be paid, state: {:?}",
  108. quote_status.state
  109. );
  110. }
  111. tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
  112. quote_status = multi_mint_wallet
  113. .refresh_mint_quote(&mint_url, &mint_quote.id)
  114. .await
  115. .unwrap();
  116. }
  117. // Call mint() directly (quote should be Paid at this point)
  118. let proofs = multi_mint_wallet
  119. .mint(&mint_url, &mint_quote.id, SplitTarget::default(), None)
  120. .await
  121. .unwrap();
  122. let minted_amount = proofs.total_amount().unwrap();
  123. assert_eq!(minted_amount, 100.into(), "Should mint exactly 100 sats");
  124. // Verify balance
  125. let balance = multi_mint_wallet.total_balance().await.unwrap();
  126. assert_eq!(balance, 100.into(), "Total balance should be 100 sats");
  127. }
  128. /// Test the melt() function with automatic mint selection
  129. ///
  130. /// This test verifies:
  131. /// 1. Fund wallet at a mint
  132. /// 2. Call melt() without specifying mint (auto-selection)
  133. /// 3. Verify payment is made
  134. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  135. async fn test_multi_mint_wallet_melt_auto_select() {
  136. let multi_mint_wallet = create_test_multi_mint_wallet().await;
  137. let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
  138. multi_mint_wallet
  139. .add_mint(mint_url.clone())
  140. .await
  141. .expect("failed to add mint");
  142. // Fund the wallet
  143. let funded_amount = fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
  144. assert_eq!(funded_amount, 100.into());
  145. // Create an invoice to pay
  146. let invoice = create_invoice_for_env(Some(50)).await.unwrap();
  147. // Use melt() with auto-selection (no specific mint specified)
  148. let melt_result = multi_mint_wallet.melt(&invoice, None, None).await.unwrap();
  149. assert_eq!(
  150. melt_result.state(),
  151. MeltQuoteState::Paid,
  152. "Melt should be paid"
  153. );
  154. assert_eq!(melt_result.amount(), 50.into(), "Should melt 50 sats");
  155. // Verify balance decreased
  156. let balance = multi_mint_wallet.total_balance().await.unwrap();
  157. assert!(
  158. balance < 100.into(),
  159. "Balance should be less than 100 after melt"
  160. );
  161. }
  162. /// Test the receive() function on MultiMintWallet
  163. ///
  164. /// This test verifies:
  165. /// 1. Create a token from a wallet
  166. /// 2. Receive the token in a different MultiMintWallet
  167. /// 3. Verify the token value is received
  168. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  169. async fn test_multi_mint_wallet_receive() {
  170. // Create sender wallet and fund it
  171. let sender_wallet = create_test_multi_mint_wallet().await;
  172. let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
  173. sender_wallet
  174. .add_mint(mint_url.clone())
  175. .await
  176. .expect("failed to add mint");
  177. let funded_amount = fund_multi_mint_wallet(&sender_wallet, &mint_url, 100.into()).await;
  178. assert_eq!(funded_amount, 100.into());
  179. // Create a token to send
  180. let send_options = SendOptions::default();
  181. let prepared_send = sender_wallet
  182. .prepare_send(mint_url.clone(), 50.into(), send_options)
  183. .await
  184. .unwrap();
  185. let token = prepared_send.confirm(None).await.unwrap();
  186. let token_string = token.to_string();
  187. // Create receiver wallet
  188. let receiver_wallet = create_test_multi_mint_wallet().await;
  189. // Add the same mint as trusted
  190. receiver_wallet
  191. .add_mint(mint_url.clone())
  192. .await
  193. .expect("failed to add mint");
  194. // Receive the token
  195. let receive_options = MultiMintReceiveOptions::default();
  196. let received_amount = receiver_wallet
  197. .receive(&token_string, receive_options)
  198. .await
  199. .unwrap();
  200. // Note: received amount may be slightly less due to fees
  201. assert!(
  202. received_amount > Amount::ZERO,
  203. "Should receive some amount, got {:?}",
  204. received_amount
  205. );
  206. // Verify receiver balance
  207. let receiver_balance = receiver_wallet.total_balance().await.unwrap();
  208. assert!(
  209. receiver_balance > Amount::ZERO,
  210. "Receiver should have balance"
  211. );
  212. // Verify sender balance decreased
  213. let sender_balance = sender_wallet.total_balance().await.unwrap();
  214. assert!(
  215. sender_balance < 100.into(),
  216. "Sender balance should be less than 100 after send"
  217. );
  218. }
  219. /// Test the receive() function with allow_untrusted option
  220. ///
  221. /// This test verifies:
  222. /// 1. Create a token from a known mint
  223. /// 2. Receive with a wallet that doesn't have the mint added
  224. /// 3. With allow_untrusted=true, the mint should be added automatically
  225. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  226. async fn test_multi_mint_wallet_receive_untrusted() {
  227. // Create sender wallet and fund it
  228. let sender_wallet = create_test_multi_mint_wallet().await;
  229. let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
  230. sender_wallet
  231. .add_mint(mint_url.clone())
  232. .await
  233. .expect("failed to add mint");
  234. let funded_amount = fund_multi_mint_wallet(&sender_wallet, &mint_url, 100.into()).await;
  235. assert_eq!(funded_amount, 100.into());
  236. // Create a token to send
  237. let send_options = SendOptions::default();
  238. let prepared_send = sender_wallet
  239. .prepare_send(mint_url.clone(), 50.into(), send_options)
  240. .await
  241. .unwrap();
  242. let token = prepared_send.confirm(None).await.unwrap();
  243. let token_string = token.to_string();
  244. // Create receiver wallet WITHOUT adding the mint
  245. let receiver_wallet = create_test_multi_mint_wallet().await;
  246. // First, verify that receiving without allow_untrusted fails
  247. let receive_options = MultiMintReceiveOptions::default();
  248. let result = receiver_wallet
  249. .receive(&token_string, receive_options)
  250. .await;
  251. assert!(result.is_err(), "Should fail without allow_untrusted");
  252. // Now receive with allow_untrusted=true
  253. let receive_options = MultiMintReceiveOptions::default().allow_untrusted(true);
  254. let received_amount = receiver_wallet
  255. .receive(&token_string, receive_options)
  256. .await
  257. .unwrap();
  258. assert!(received_amount > Amount::ZERO, "Should receive some amount");
  259. // Verify the mint was added to the wallet
  260. assert!(
  261. receiver_wallet.has_mint(&mint_url).await,
  262. "Mint should be added to wallet"
  263. );
  264. }
  265. /// Test prepare_send() happy path
  266. ///
  267. /// This test verifies:
  268. /// 1. Fund wallet
  269. /// 2. Call prepare_send() successfully
  270. /// 3. Confirm the send and get a token
  271. /// 4. Verify the token is valid
  272. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  273. async fn test_multi_mint_wallet_prepare_send_happy_path() {
  274. let multi_mint_wallet = create_test_multi_mint_wallet().await;
  275. let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
  276. multi_mint_wallet
  277. .add_mint(mint_url.clone())
  278. .await
  279. .expect("failed to add mint");
  280. // Fund the wallet
  281. let funded_amount = fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
  282. assert_eq!(funded_amount, 100.into());
  283. // Prepare send
  284. let send_options = SendOptions::default();
  285. let prepared_send = multi_mint_wallet
  286. .prepare_send(mint_url.clone(), 50.into(), send_options)
  287. .await
  288. .unwrap();
  289. // Get the token
  290. let token = prepared_send.confirm(None).await.unwrap();
  291. let token_string = token.to_string();
  292. // Verify the token can be parsed back
  293. let parsed_token = Token::from_str(&token_string).unwrap();
  294. let token_mint_url = parsed_token.mint_url().unwrap();
  295. assert_eq!(token_mint_url, mint_url, "Token mint URL should match");
  296. // Get token data to verify value
  297. let token_data = multi_mint_wallet
  298. .get_token_data(&parsed_token)
  299. .await
  300. .unwrap();
  301. assert_eq!(token_data.value, 50.into(), "Token value should be 50 sats");
  302. // Verify wallet balance decreased
  303. let balance = multi_mint_wallet.total_balance().await.unwrap();
  304. assert_eq!(balance, 50.into(), "Remaining balance should be 50 sats");
  305. }
  306. /// Test get_balances() across multiple operations
  307. ///
  308. /// This test verifies:
  309. /// 1. Empty wallet has zero balances
  310. /// 2. After minting, balance is updated
  311. /// 3. get_balances() returns per-mint breakdown
  312. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  313. async fn test_multi_mint_wallet_get_balances() {
  314. let multi_mint_wallet = create_test_multi_mint_wallet().await;
  315. let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
  316. multi_mint_wallet
  317. .add_mint(mint_url.clone())
  318. .await
  319. .expect("failed to add mint");
  320. // Check initial balances
  321. let balances = multi_mint_wallet.get_balances().await.unwrap();
  322. let initial_balance = balances.get(&mint_url).cloned().unwrap_or(Amount::ZERO);
  323. assert_eq!(initial_balance, Amount::ZERO, "Initial balance should be 0");
  324. // Fund the wallet
  325. fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
  326. // Check balances again
  327. let balances = multi_mint_wallet.get_balances().await.unwrap();
  328. let balance = balances.get(&mint_url).cloned().unwrap_or(Amount::ZERO);
  329. assert_eq!(balance, 100.into(), "Balance should be 100 sats");
  330. // Verify total_balance matches
  331. let total = multi_mint_wallet.total_balance().await.unwrap();
  332. assert_eq!(total, 100.into(), "Total balance should match");
  333. }
  334. /// Test list_proofs() function
  335. ///
  336. /// This test verifies:
  337. /// 1. Empty wallet has no proofs
  338. /// 2. After minting, proofs are listed correctly
  339. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  340. async fn test_multi_mint_wallet_list_proofs() {
  341. let multi_mint_wallet = create_test_multi_mint_wallet().await;
  342. let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
  343. multi_mint_wallet
  344. .add_mint(mint_url.clone())
  345. .await
  346. .expect("failed to add mint");
  347. // Check initial proofs
  348. let proofs = multi_mint_wallet.list_proofs().await.unwrap();
  349. let mint_proofs = proofs.get(&mint_url).cloned().unwrap_or_default();
  350. assert!(mint_proofs.is_empty(), "Should have no proofs initially");
  351. // Fund the wallet
  352. fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
  353. // Check proofs again
  354. let proofs = multi_mint_wallet.list_proofs().await.unwrap();
  355. let mint_proofs = proofs.get(&mint_url).cloned().unwrap_or_default();
  356. assert!(!mint_proofs.is_empty(), "Should have proofs after minting");
  357. // Verify proof total matches balance
  358. let proof_total: Amount = mint_proofs.total_amount().unwrap();
  359. assert_eq!(proof_total, 100.into(), "Proof total should be 100 sats");
  360. }
  361. /// Test mint management functions (add_mint, remove_mint, has_mint)
  362. ///
  363. /// This test verifies:
  364. /// 1. has_mint returns false for unknown mints
  365. /// 2. add_mint adds the mint
  366. /// 3. has_mint returns true after adding
  367. /// 4. remove_mint removes the mint
  368. /// 5. has_mint returns false after removal
  369. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  370. async fn test_multi_mint_wallet_mint_management() {
  371. let multi_mint_wallet = create_test_multi_mint_wallet().await;
  372. let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
  373. // Initially mint should not be in wallet
  374. assert!(
  375. !multi_mint_wallet.has_mint(&mint_url).await,
  376. "Mint should not be in wallet initially"
  377. );
  378. // Add the mint
  379. multi_mint_wallet
  380. .add_mint(mint_url.clone())
  381. .await
  382. .expect("failed to add mint");
  383. // Now mint should be in wallet
  384. assert!(
  385. multi_mint_wallet.has_mint(&mint_url).await,
  386. "Mint should be in wallet after adding"
  387. );
  388. // Get wallets should include this mint
  389. let wallets = multi_mint_wallet.get_wallets().await;
  390. assert!(!wallets.is_empty(), "Should have at least one wallet");
  391. // Get specific wallet
  392. let wallet = multi_mint_wallet.get_wallet(&mint_url).await;
  393. assert!(wallet.is_some(), "Should be able to get wallet for mint");
  394. // Remove the mint
  395. multi_mint_wallet.remove_mint(&mint_url).await;
  396. // Now mint should not be in wallet
  397. assert!(
  398. !multi_mint_wallet.has_mint(&mint_url).await,
  399. "Mint should not be in wallet after removal"
  400. );
  401. }
  402. /// Test check_all_mint_quotes() function
  403. ///
  404. /// This test verifies:
  405. /// 1. Create a mint quote
  406. /// 2. Pay the quote
  407. /// 3. Poll until quote is paid (like a real wallet would)
  408. /// 4. check_all_mint_quotes() processes paid quotes
  409. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  410. async fn test_multi_mint_wallet_check_all_mint_quotes() {
  411. let multi_mint_wallet = create_test_multi_mint_wallet().await;
  412. let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
  413. multi_mint_wallet
  414. .add_mint(mint_url.clone())
  415. .await
  416. .expect("failed to add mint");
  417. // Create a mint quote
  418. let mint_quote = multi_mint_wallet
  419. .mint_quote(
  420. &mint_url,
  421. PaymentMethod::BOLT11,
  422. Some(100.into()),
  423. None,
  424. None,
  425. )
  426. .await
  427. .unwrap();
  428. // Pay the invoice (in regtest mode) - for fake wallet, payment is simulated automatically
  429. let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
  430. pay_if_regtest(&get_test_temp_dir(), &invoice)
  431. .await
  432. .unwrap();
  433. // Poll for quote to be paid (like a real wallet would)
  434. let mut quote_status = multi_mint_wallet
  435. .refresh_mint_quote(&mint_url, &mint_quote.id)
  436. .await
  437. .unwrap();
  438. let timeout = tokio::time::Duration::from_secs(30);
  439. let start = tokio::time::Instant::now();
  440. while quote_status.state != MintQuoteState::Paid {
  441. if start.elapsed() > timeout {
  442. panic!(
  443. "Timeout waiting for quote to be paid, state: {:?}",
  444. quote_status.state
  445. );
  446. }
  447. quote_status = multi_mint_wallet
  448. .refresh_mint_quote(&mint_url, &mint_quote.id)
  449. .await
  450. .unwrap();
  451. }
  452. // Check all mint quotes - this should find the paid quote and mint
  453. let minted_amount = multi_mint_wallet
  454. .mint_unissued_quotes(Some(mint_url.clone()))
  455. .await
  456. .unwrap();
  457. assert_eq!(
  458. minted_amount,
  459. 100.into(),
  460. "Should mint 100 sats from paid quote"
  461. );
  462. // Verify balance
  463. let balance = multi_mint_wallet.total_balance().await.unwrap();
  464. assert_eq!(balance, 100.into(), "Balance should be 100 sats");
  465. }
  466. /// Test restore() function
  467. ///
  468. /// This test verifies:
  469. /// 1. Create and fund a wallet with a specific seed
  470. /// 2. Create a new wallet with the same seed
  471. /// 3. Call restore() to recover the proofs
  472. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  473. async fn test_multi_mint_wallet_restore() {
  474. let seed = Mnemonic::generate(12).unwrap().to_seed_normalized("");
  475. let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
  476. // Create first wallet and fund it
  477. {
  478. let localstore = Arc::new(memory::empty().await.unwrap());
  479. let wallet1 = MultiMintWallet::new(localstore, seed, CurrencyUnit::Sat)
  480. .await
  481. .expect("failed to create wallet");
  482. wallet1
  483. .add_mint(mint_url.clone())
  484. .await
  485. .expect("failed to add mint");
  486. let funded = fund_multi_mint_wallet(&wallet1, &mint_url, 100.into()).await;
  487. assert_eq!(funded, 100.into());
  488. }
  489. // wallet1 goes out of scope
  490. // Create second wallet with same seed but fresh storage
  491. let localstore2 = Arc::new(memory::empty().await.unwrap());
  492. let wallet2 = MultiMintWallet::new(localstore2, seed, CurrencyUnit::Sat)
  493. .await
  494. .expect("failed to create wallet");
  495. wallet2
  496. .add_mint(mint_url.clone())
  497. .await
  498. .expect("failed to add mint");
  499. // Initially should have no balance
  500. let balance_before = wallet2.total_balance().await.unwrap();
  501. assert_eq!(balance_before, Amount::ZERO, "Should start with no balance");
  502. // Restore from mint
  503. let restored = wallet2.restore(&mint_url).await.unwrap();
  504. assert_eq!(restored.unspent, 100.into(), "Should restore 100 sats");
  505. }
  506. /// Test melt_with_mint() with explicit mint selection
  507. ///
  508. /// This test verifies:
  509. /// 1. Fund wallet
  510. /// 2. Create melt quote at specific mint
  511. /// 3. Execute melt_with_mint()
  512. /// 4. Verify payment succeeded
  513. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  514. async fn test_multi_mint_wallet_melt_with_mint() {
  515. let multi_mint_wallet = create_test_multi_mint_wallet().await;
  516. let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
  517. multi_mint_wallet
  518. .add_mint(mint_url.clone())
  519. .await
  520. .expect("failed to add mint");
  521. // Fund the wallet
  522. fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
  523. // Create an invoice to pay
  524. let invoice = create_invoice_for_env(Some(50)).await.unwrap();
  525. // Create melt quote at specific mint
  526. let melt_quote = multi_mint_wallet
  527. .melt_quote(&mint_url, PaymentMethod::BOLT11, invoice, None, None)
  528. .await
  529. .unwrap();
  530. // Execute melt with specific mint
  531. let melt_result = multi_mint_wallet
  532. .melt_with_mint(&mint_url, &melt_quote.id)
  533. .await
  534. .unwrap();
  535. assert_eq!(
  536. melt_result.state(),
  537. MeltQuoteState::Paid,
  538. "Melt should be paid"
  539. );
  540. // Check melt quote status
  541. let quote_status = multi_mint_wallet
  542. .check_melt_quote(&mint_url, &melt_quote.id)
  543. .await
  544. .unwrap();
  545. assert_eq!(
  546. quote_status.state,
  547. MeltQuoteState::Paid,
  548. "Quote status should be paid"
  549. );
  550. }
  551. /// Test unit() function returns correct currency unit
  552. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  553. async fn test_multi_mint_wallet_unit() {
  554. let seed = Mnemonic::generate(12).unwrap().to_seed_normalized("");
  555. let localstore = Arc::new(memory::empty().await.unwrap());
  556. let wallet = MultiMintWallet::new(localstore, seed, CurrencyUnit::Sat)
  557. .await
  558. .expect("failed to create wallet");
  559. assert_eq!(wallet.unit(), &CurrencyUnit::Sat, "Unit should be Sat");
  560. }
  561. /// Test list_transactions() function
  562. ///
  563. /// This test verifies:
  564. /// 1. Initially no transactions
  565. /// 2. After minting, transaction is recorded
  566. /// 3. After melting, transaction is recorded
  567. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  568. async fn test_multi_mint_wallet_list_transactions() {
  569. let multi_mint_wallet = create_test_multi_mint_wallet().await;
  570. let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
  571. multi_mint_wallet
  572. .add_mint(mint_url.clone())
  573. .await
  574. .expect("failed to add mint");
  575. // Fund the wallet (this creates a mint transaction)
  576. fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
  577. // List all transactions
  578. let transactions = multi_mint_wallet.list_transactions(None).await.unwrap();
  579. assert!(
  580. !transactions.is_empty(),
  581. "Should have at least one transaction after minting"
  582. );
  583. // Create an invoice and melt (this creates a melt transaction)
  584. let invoice = create_invoice_for_env(Some(50)).await.unwrap();
  585. let melt_quote = multi_mint_wallet
  586. .melt_quote(&mint_url, PaymentMethod::BOLT11, invoice, None, None)
  587. .await
  588. .unwrap();
  589. multi_mint_wallet
  590. .melt_with_mint(&mint_url, &melt_quote.id)
  591. .await
  592. .unwrap();
  593. // List transactions again
  594. let transactions_after = multi_mint_wallet.list_transactions(None).await.unwrap();
  595. assert!(
  596. transactions_after.len() > transactions.len(),
  597. "Should have more transactions after melt"
  598. );
  599. }
  600. /// Test send revocation via MultiMintWallet
  601. ///
  602. /// This test verifies:
  603. /// 1. Create and confirm a send
  604. /// 2. Verify it appears in pending sends
  605. /// 3. Verify status is "not claimed"
  606. /// 4. Revoke the send
  607. /// 5. Verify balance is restored and pending send is gone
  608. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  609. async fn test_multi_mint_wallet_revoke_send() {
  610. let multi_mint_wallet = create_test_multi_mint_wallet().await;
  611. let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
  612. multi_mint_wallet
  613. .add_mint(mint_url.clone())
  614. .await
  615. .expect("failed to add mint");
  616. // Fund the wallet
  617. fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
  618. // Create a send
  619. let send_options = SendOptions::default();
  620. let prepared_send = multi_mint_wallet
  621. .prepare_send(mint_url.clone(), 50.into(), send_options)
  622. .await
  623. .unwrap();
  624. let operation_id = prepared_send.operation_id();
  625. let _token = prepared_send.confirm(None).await.unwrap();
  626. // Verify it appears in pending sends
  627. let pending = multi_mint_wallet.get_pending_sends().await.unwrap();
  628. assert_eq!(pending.len(), 1, "Should have 1 pending send");
  629. assert_eq!(pending[0].0, mint_url, "Mint URL should match");
  630. assert_eq!(pending[0].1, operation_id, "Operation ID should match");
  631. // Verify status
  632. let claimed = multi_mint_wallet
  633. .check_send_status(mint_url.clone(), operation_id)
  634. .await
  635. .unwrap();
  636. assert!(!claimed, "Token should not be claimed yet");
  637. // Revoke the send
  638. let restored_amount = multi_mint_wallet
  639. .revoke_send(mint_url.clone(), operation_id)
  640. .await
  641. .unwrap();
  642. assert_eq!(restored_amount, 50.into(), "Should restore 50 sats");
  643. // Verify pending send is gone
  644. let pending_after = multi_mint_wallet.get_pending_sends().await.unwrap();
  645. assert!(pending_after.is_empty(), "Should have no pending sends");
  646. // Verify balance is back to 100
  647. let balance = multi_mint_wallet.total_balance().await.unwrap();
  648. assert_eq!(balance, 100.into(), "Balance should be fully restored");
  649. }