bolt12.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. use std::env;
  2. use std::path::PathBuf;
  3. use std::sync::Arc;
  4. use std::time::Duration;
  5. use anyhow::{bail, Result};
  6. use bip39::Mnemonic;
  7. use cashu::amount::SplitTarget;
  8. use cashu::nut23::Amountless;
  9. use cashu::{Amount, CurrencyUnit, MintRequest, PreMintSecrets, ProofsMethods};
  10. use cdk::wallet::{HttpClient, MintConnector, Wallet};
  11. use cdk::StreamExt;
  12. use cdk_integration_tests::get_mint_url_from_env;
  13. use cdk_integration_tests::init_regtest::{get_cln_dir, get_temp_dir};
  14. use cdk_sqlite::wallet::memory;
  15. use ln_regtest_rs::ln_client::ClnClient;
  16. // Helper function to get temp directory from environment or fallback
  17. fn get_test_temp_dir() -> PathBuf {
  18. match env::var("CDK_ITESTS_DIR") {
  19. Ok(dir) => PathBuf::from(dir),
  20. Err(_) => get_temp_dir(), // fallback to default
  21. }
  22. }
  23. // Helper function to create CLN client with retries
  24. async fn create_cln_client_with_retry(cln_dir: PathBuf) -> Result<ClnClient> {
  25. let mut retries = 0;
  26. let max_retries = 10;
  27. loop {
  28. match ClnClient::new(cln_dir.clone(), None).await {
  29. Ok(client) => return Ok(client),
  30. Err(e) => {
  31. retries += 1;
  32. if retries >= max_retries {
  33. bail!(
  34. "Could not connect to CLN client after {} retries: {}",
  35. max_retries,
  36. e
  37. );
  38. }
  39. println!(
  40. "Failed to connect to CLN (attempt {}/{}): {}. Retrying in 7 seconds...",
  41. retries, max_retries, e
  42. );
  43. tokio::time::sleep(tokio::time::Duration::from_secs(7)).await;
  44. }
  45. }
  46. }
  47. }
  48. /// Tests basic BOLT12 minting functionality:
  49. /// - Creates a wallet
  50. /// - Gets a BOLT12 quote for a specific amount (100 sats)
  51. /// - Pays the quote using Core Lightning
  52. /// - Mints tokens and verifies the correct amount is received
  53. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  54. async fn test_regtest_bolt12_mint() {
  55. let wallet = Wallet::new(
  56. &get_mint_url_from_env(),
  57. CurrencyUnit::Sat,
  58. Arc::new(memory::empty().await.unwrap()),
  59. Mnemonic::generate(12).unwrap().to_seed_normalized(""),
  60. None,
  61. )
  62. .unwrap();
  63. let mint_amount = Amount::from(100);
  64. let mint_quote = wallet
  65. .mint_bolt12_quote(Some(mint_amount), None)
  66. .await
  67. .unwrap();
  68. assert_eq!(mint_quote.amount, Some(mint_amount));
  69. let work_dir = get_test_temp_dir();
  70. let cln_one_dir = get_cln_dir(&work_dir, "one");
  71. let cln_client = create_cln_client_with_retry(cln_one_dir.clone())
  72. .await
  73. .unwrap();
  74. cln_client
  75. .pay_bolt12_offer(None, mint_quote.request)
  76. .await
  77. .unwrap();
  78. let proofs = wallet
  79. .mint_bolt12(&mint_quote.id, None, SplitTarget::default(), None)
  80. .await
  81. .unwrap();
  82. assert_eq!(proofs.total_amount().unwrap(), 100.into());
  83. }
  84. /// Tests multiple payments to a single BOLT12 quote:
  85. /// - Creates a wallet and gets a BOLT12 quote without specifying amount
  86. /// - Makes two separate payments (10,000 sats and 11,000 sats) to the same quote
  87. /// - Verifies that each payment can be minted separately and correctly
  88. /// - Tests the functionality of reusing a quote for multiple payments
  89. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  90. async fn test_regtest_bolt12_mint_multiple() -> Result<()> {
  91. let wallet = Wallet::new(
  92. &get_mint_url_from_env(),
  93. CurrencyUnit::Sat,
  94. Arc::new(memory::empty().await?),
  95. Mnemonic::generate(12)?.to_seed_normalized(""),
  96. None,
  97. )?;
  98. let mint_quote = wallet.mint_bolt12_quote(None, None).await?;
  99. let work_dir = get_test_temp_dir();
  100. let cln_one_dir = get_cln_dir(&work_dir, "one");
  101. let cln_client = create_cln_client_with_retry(cln_one_dir.clone()).await?;
  102. cln_client
  103. .pay_bolt12_offer(Some(10000), mint_quote.request.clone())
  104. .await
  105. .unwrap();
  106. let mut proof_streams = wallet.proof_stream(
  107. mint_quote.clone(),
  108. SplitTarget::default(),
  109. None,
  110. Duration::from_secs(60),
  111. );
  112. let proofs = proof_streams.next().await.expect("payment")?;
  113. assert_eq!(proofs.total_amount().unwrap(), 10.into());
  114. cln_client
  115. .pay_bolt12_offer(Some(11_000), mint_quote.request.clone())
  116. .await
  117. .unwrap();
  118. let proofs = proof_streams.next().await.expect("payment")?;
  119. assert_eq!(proofs.total_amount().unwrap(), 11.into());
  120. Ok(())
  121. }
  122. /// Tests that multiple wallets can pay the same BOLT12 offer:
  123. /// - Creates a BOLT12 offer through CLN that both wallets will pay
  124. /// - Creates two separate wallets with different minting amounts
  125. /// - Has each wallet get their own quote and make payments
  126. /// - Verifies both wallets can successfully mint their tokens
  127. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  128. async fn test_regtest_bolt12_multiple_wallets() -> Result<()> {
  129. // Create first wallet
  130. let wallet_one = Wallet::new(
  131. &get_mint_url_from_env(),
  132. CurrencyUnit::Sat,
  133. Arc::new(memory::empty().await?),
  134. Mnemonic::generate(12)?.to_seed_normalized(""),
  135. None,
  136. )?;
  137. // Create second wallet
  138. let wallet_two = Wallet::new(
  139. &get_mint_url_from_env(),
  140. CurrencyUnit::Sat,
  141. Arc::new(memory::empty().await?),
  142. Mnemonic::generate(12)?.to_seed_normalized(""),
  143. None,
  144. )?;
  145. // Create a BOLT12 offer that both wallets will use
  146. let work_dir = get_test_temp_dir();
  147. let cln_one_dir = get_cln_dir(&work_dir, "one");
  148. let cln_client = create_cln_client_with_retry(cln_one_dir.clone()).await?;
  149. // First wallet payment
  150. let quote_one = wallet_one
  151. .mint_bolt12_quote(Some(10_000.into()), None)
  152. .await?;
  153. cln_client
  154. .pay_bolt12_offer(None, quote_one.request.clone())
  155. .await?;
  156. let mut proof_streams_one = wallet_one.proof_stream(
  157. quote_one.clone(),
  158. SplitTarget::default(),
  159. None,
  160. Duration::from_secs(60),
  161. );
  162. let proofs_one = proof_streams_one.next().await.expect("payment")?;
  163. assert_eq!(proofs_one.total_amount()?, 10_000.into());
  164. // Second wallet payment
  165. let quote_two = wallet_two
  166. .mint_bolt12_quote(Some(15_000.into()), None)
  167. .await?;
  168. cln_client
  169. .pay_bolt12_offer(None, quote_two.request.clone())
  170. .await?;
  171. let mut proof_streams_two = wallet_two.proof_stream(
  172. quote_two.clone(),
  173. SplitTarget::default(),
  174. None,
  175. Duration::from_secs(60),
  176. );
  177. let proofs_two = proof_streams_two.next().await.expect("payment")?;
  178. assert_eq!(proofs_two.total_amount()?, 15_000.into());
  179. let offer = cln_client
  180. .get_bolt12_offer(None, false, "test_multiple_wallets".to_string())
  181. .await?;
  182. let wallet_one_melt_quote = wallet_one
  183. .melt_bolt12_quote(
  184. offer.to_string(),
  185. Some(cashu::MeltOptions::Amountless {
  186. amountless: Amountless {
  187. amount_msat: 1500.into(),
  188. },
  189. }),
  190. )
  191. .await?;
  192. let wallet_two_melt_quote = wallet_two
  193. .melt_bolt12_quote(
  194. offer.to_string(),
  195. Some(cashu::MeltOptions::Amountless {
  196. amountless: Amountless {
  197. amount_msat: 1000.into(),
  198. },
  199. }),
  200. )
  201. .await?;
  202. let melted = wallet_one.melt(&wallet_one_melt_quote.id).await?;
  203. assert!(melted.preimage.is_some());
  204. let melted_two = wallet_two.melt(&wallet_two_melt_quote.id).await?;
  205. assert!(melted_two.preimage.is_some());
  206. Ok(())
  207. }
  208. /// Tests the BOLT12 melting (spending) functionality:
  209. /// - Creates a wallet and mints 20,000 sats using BOLT12
  210. /// - Creates a BOLT12 offer for 10,000 sats
  211. /// - Tests melting (spending) tokens using the BOLT12 offer
  212. /// - Verifies the correct amount is melted
  213. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  214. async fn test_regtest_bolt12_melt() -> Result<()> {
  215. let wallet = Wallet::new(
  216. &get_mint_url_from_env(),
  217. CurrencyUnit::Sat,
  218. Arc::new(memory::empty().await?),
  219. Mnemonic::generate(12)?.to_seed_normalized(""),
  220. None,
  221. )?;
  222. let mint_amount = Amount::from(20_000);
  223. // Create a single-use BOLT12 quote
  224. let mint_quote = wallet.mint_bolt12_quote(Some(mint_amount), None).await?;
  225. assert_eq!(mint_quote.amount, Some(mint_amount));
  226. // Pay the quote
  227. let work_dir = get_test_temp_dir();
  228. let cln_one_dir = get_cln_dir(&work_dir, "one");
  229. let cln_client = create_cln_client_with_retry(cln_one_dir.clone()).await?;
  230. cln_client
  231. .pay_bolt12_offer(None, mint_quote.request.clone())
  232. .await?;
  233. // Wait for payment to be processed
  234. let mut proof_streams = wallet.proof_stream(
  235. mint_quote.clone(),
  236. SplitTarget::default(),
  237. None,
  238. Duration::from_secs(60),
  239. );
  240. let offer = cln_client
  241. .get_bolt12_offer(Some(10_000), true, "hhhhhhhh".to_string())
  242. .await?;
  243. let _proofs = proof_streams.next().await.expect("payment")?;
  244. let quote = wallet.melt_bolt12_quote(offer.to_string(), None).await?;
  245. let melt = wallet.melt(&quote.id).await?;
  246. assert_eq!(melt.amount, 10.into());
  247. Ok(())
  248. }
  249. /// Tests security validation for BOLT12 minting to prevent overspending:
  250. /// - Creates a wallet and gets an open-ended BOLT12 quote
  251. /// - Makes a payment of 10,000 millisats
  252. /// - Attempts to mint more tokens (500 sats) than were actually paid for
  253. /// - Verifies that the mint correctly rejects the oversized mint request
  254. /// - Ensures proper error handling with TransactionUnbalanced error
  255. /// This test is crucial for ensuring the economic security of the minting process
  256. /// by preventing users from minting more tokens than they have paid for.
  257. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  258. async fn test_regtest_bolt12_mint_extra() -> Result<()> {
  259. let wallet = Wallet::new(
  260. &get_mint_url_from_env(),
  261. CurrencyUnit::Sat,
  262. Arc::new(memory::empty().await?),
  263. Mnemonic::generate(12)?.to_seed_normalized(""),
  264. None,
  265. )?;
  266. // Create a single-use BOLT12 quote
  267. let mint_quote = wallet.mint_bolt12_quote(None, None).await?;
  268. let state = wallet.mint_bolt12_quote_state(&mint_quote.id).await?;
  269. assert_eq!(state.amount_paid, Amount::ZERO);
  270. assert_eq!(state.amount_issued, Amount::ZERO);
  271. let active_keyset_id = wallet.fetch_active_keyset().await?.id;
  272. let pay_amount_msats = 10_000;
  273. let work_dir = get_test_temp_dir();
  274. let cln_one_dir = get_cln_dir(&work_dir, "one");
  275. let cln_client = create_cln_client_with_retry(cln_one_dir.clone()).await?;
  276. cln_client
  277. .pay_bolt12_offer(Some(pay_amount_msats), mint_quote.request.clone())
  278. .await?;
  279. let mut payment_streams = wallet.payment_stream(&mint_quote, Duration::from_secs(60));
  280. let _ = payment_streams.next().await;
  281. let state = wallet.mint_bolt12_quote_state(&mint_quote.id).await?;
  282. assert_eq!(state.amount_paid, (pay_amount_msats / 1_000).into());
  283. assert_eq!(state.amount_issued, Amount::ZERO);
  284. let pre_mint = PreMintSecrets::random(active_keyset_id, 500.into(), &SplitTarget::None)?;
  285. let quote_info = wallet
  286. .localstore
  287. .get_mint_quote(&mint_quote.id)
  288. .await?
  289. .expect("there is a quote");
  290. let mut mint_request = MintRequest {
  291. quote: mint_quote.id,
  292. outputs: pre_mint.blinded_messages(),
  293. signature: None,
  294. };
  295. if let Some(secret_key) = quote_info.secret_key {
  296. mint_request.sign(secret_key)?;
  297. }
  298. let http_client = HttpClient::new(get_mint_url_from_env().parse().unwrap(), None);
  299. let response = http_client.post_mint(mint_request.clone()).await;
  300. match response {
  301. Err(err) => match err {
  302. cdk::Error::TransactionUnbalanced(_, _, _) => (),
  303. err => {
  304. bail!("Wrong mint error returned: {}", err.to_string());
  305. }
  306. },
  307. Ok(_) => {
  308. bail!("Should not have allowed second payment");
  309. }
  310. }
  311. Ok(())
  312. }