wallet_repository.rs 27 KB

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