lib.rs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. //! Integration Test Library
  2. //!
  3. //! This crate provides shared functionality for CDK integration tests.
  4. //! It includes utilities for setting up test environments, funding wallets,
  5. //! and common test operations across different test scenarios.
  6. //!
  7. //! Test Categories Supported:
  8. //! - Pure in-memory tests (no external dependencies)
  9. //! - Regtest environment tests (with actual Lightning nodes)
  10. //! - Authenticated mint tests
  11. //! - Multi-mint scenarios
  12. //!
  13. //! Key Components:
  14. //! - Test environment initialization
  15. //! - Wallet funding utilities
  16. //! - Lightning Network client helpers
  17. //! - Proof state management utilities
  18. use std::env;
  19. use std::path::Path;
  20. use std::sync::Arc;
  21. use anyhow::{anyhow, bail, Result};
  22. use cashu::{Bolt11Invoice, PaymentMethod};
  23. use cdk::amount::{Amount, SplitTarget};
  24. use cdk::nuts::{MintQuoteState, NotificationPayload, State};
  25. use cdk::wallet::WalletSubscription;
  26. use cdk::Wallet;
  27. use cdk_fake_wallet::create_fake_invoice;
  28. use init_regtest::{get_lnd_dir, LND_RPC_ADDR};
  29. use ln_regtest_rs::ln_client::{LightningClient, LndClient};
  30. use tokio::time::{sleep, timeout, Duration};
  31. pub mod cli;
  32. pub mod init_auth_mint;
  33. pub mod init_pure_tests;
  34. pub mod init_regtest;
  35. pub mod shared;
  36. pub async fn fund_wallet(wallet: Arc<Wallet>, amount: Amount) {
  37. let quote = wallet
  38. .mint_quote(amount, None)
  39. .await
  40. .expect("Could not get mint quote");
  41. wait_for_mint_to_be_paid(&wallet, &quote.id, 60)
  42. .await
  43. .expect("Waiting for mint failed");
  44. let _proofs = wallet
  45. .mint(&quote.id, SplitTarget::default(), None)
  46. .await
  47. .expect("Could not mint");
  48. }
  49. pub fn get_mint_url_from_env() -> String {
  50. match env::var("CDK_TEST_MINT_URL") {
  51. Ok(url) => url,
  52. Err(_) => panic!("Mint url not set"),
  53. }
  54. }
  55. pub fn get_second_mint_url_from_env() -> String {
  56. match env::var("CDK_TEST_MINT_URL_2") {
  57. Ok(url) => url,
  58. Err(_) => panic!("Mint url not set"),
  59. }
  60. }
  61. // Get all pending from wallet and attempt to swap
  62. // Will panic if there are no pending
  63. // Will return Ok if swap fails as expected
  64. pub async fn attempt_to_swap_pending(wallet: &Wallet) -> Result<()> {
  65. let pending = wallet
  66. .localstore
  67. .get_proofs(None, None, Some(vec![State::Pending]), None)
  68. .await?;
  69. assert!(!pending.is_empty());
  70. let swap = wallet
  71. .swap(
  72. None,
  73. SplitTarget::None,
  74. pending.into_iter().map(|p| p.proof).collect(),
  75. None,
  76. false,
  77. )
  78. .await;
  79. match swap {
  80. Ok(_swap) => {
  81. bail!("These proofs should be pending")
  82. }
  83. Err(err) => match err {
  84. cdk::error::Error::TokenPending => (),
  85. _ => {
  86. println!("{err:?}");
  87. bail!("Wrong error")
  88. }
  89. },
  90. }
  91. Ok(())
  92. }
  93. pub async fn wait_for_mint_to_be_paid(
  94. wallet: &Wallet,
  95. mint_quote_id: &str,
  96. timeout_secs: u64,
  97. ) -> Result<()> {
  98. let mut subscription = wallet
  99. .subscribe(WalletSubscription::Bolt11MintQuoteState(vec![
  100. mint_quote_id.to_owned(),
  101. ]))
  102. .await;
  103. // Create the timeout future
  104. let wait_future = async {
  105. while let Some(msg) = subscription.recv().await {
  106. if let NotificationPayload::MintQuoteBolt11Response(response) = msg {
  107. if response.state == MintQuoteState::Paid {
  108. return Ok(());
  109. }
  110. } else if let NotificationPayload::MintQuoteBolt12Response(response) = msg {
  111. if response.amount_paid > Amount::ZERO {
  112. return Ok(());
  113. }
  114. }
  115. }
  116. Err(anyhow!("Subscription ended without quote being paid"))
  117. };
  118. let timeout_future = timeout(Duration::from_secs(timeout_secs), wait_future);
  119. let check_interval = Duration::from_secs(5);
  120. let method = wallet
  121. .localstore
  122. .get_mint_quote(mint_quote_id)
  123. .await?
  124. .map(|q| q.payment_method)
  125. .unwrap_or_default();
  126. let periodic_task = async {
  127. loop {
  128. match method {
  129. PaymentMethod::Bolt11 => match wallet.mint_quote_state(mint_quote_id).await {
  130. Ok(result) => {
  131. if result.state == MintQuoteState::Paid {
  132. tracing::info!("mint quote paid via poll");
  133. return Ok(());
  134. }
  135. }
  136. Err(e) => {
  137. tracing::error!("Could not check mint quote status: {:?}", e);
  138. }
  139. },
  140. PaymentMethod::Bolt12 => {
  141. match wallet.mint_bolt12_quote_state(mint_quote_id).await {
  142. Ok(result) => {
  143. if result.amount_paid > Amount::ZERO {
  144. return Ok(());
  145. }
  146. }
  147. Err(e) => {
  148. tracing::error!("Could not check mint quote status: {:?}", e);
  149. }
  150. }
  151. }
  152. PaymentMethod::Custom(_) => (),
  153. }
  154. sleep(check_interval).await;
  155. }
  156. };
  157. tokio::select! {
  158. result = timeout_future => {
  159. match result {
  160. Ok(payment_result) => payment_result,
  161. Err(_) => Err(anyhow!("Timeout waiting for mint quote ({}) to be paid", mint_quote_id)),
  162. }
  163. }
  164. result = periodic_task => {
  165. result // Now propagates the result from periodic checks
  166. }
  167. }
  168. }
  169. // This is the ln wallet we use to send/receive ln payements as the wallet
  170. pub async fn init_lnd_client(work_dir: &Path) -> LndClient {
  171. let lnd_dir = get_lnd_dir(work_dir, "one");
  172. let cert_file = lnd_dir.join("tls.cert");
  173. let macaroon_file = lnd_dir.join("data/chain/bitcoin/regtest/admin.macaroon");
  174. LndClient::new(format!("https://{LND_RPC_ADDR}"), cert_file, macaroon_file)
  175. .await
  176. .unwrap()
  177. }
  178. /// Pays a Bolt11Invoice if it's on the regtest network, otherwise returns Ok
  179. ///
  180. /// This is useful for tests that need to pay invoices in regtest mode but
  181. /// should be skipped in other environments.
  182. pub async fn pay_if_regtest(work_dir: &Path, invoice: &Bolt11Invoice) -> Result<()> {
  183. // Check if the invoice is for the regtest network
  184. if invoice.network() == bitcoin::Network::Regtest {
  185. let lnd_client = init_lnd_client(work_dir).await;
  186. lnd_client.pay_invoice(invoice.to_string()).await?;
  187. Ok(())
  188. } else {
  189. // Not a regtest invoice, just return Ok
  190. Ok(())
  191. }
  192. }
  193. /// Determines if we're running in regtest mode based on environment variable
  194. ///
  195. /// Checks the CDK_TEST_REGTEST environment variable:
  196. /// - If set to "1", "true", or "yes" (case insensitive), returns true
  197. /// - Otherwise returns false
  198. pub fn is_regtest_env() -> bool {
  199. match env::var("CDK_TEST_REGTEST") {
  200. Ok(val) => {
  201. let val = val.to_lowercase();
  202. val == "1" || val == "true" || val == "yes"
  203. }
  204. Err(_) => false,
  205. }
  206. }
  207. /// Creates a real invoice if in regtest mode, otherwise returns a fake invoice
  208. ///
  209. /// Uses the is_regtest_env() function to determine whether to
  210. /// create a real regtest invoice or a fake one for testing.
  211. pub async fn create_invoice_for_env(work_dir: &Path, amount_sat: Option<u64>) -> Result<String> {
  212. if is_regtest_env() {
  213. // In regtest mode, create a real invoice
  214. let lnd_client = init_lnd_client(work_dir).await;
  215. lnd_client
  216. .create_invoice(amount_sat)
  217. .await
  218. .map_err(|e| anyhow!("Failed to create regtest invoice: {}", e))
  219. } else {
  220. // Not in regtest mode, create a fake invoice
  221. let fake_invoice = create_fake_invoice(
  222. amount_sat.expect("Amount must be defined") * 1_000,
  223. "".to_string(),
  224. );
  225. Ok(fake_invoice.to_string())
  226. }
  227. }