bolt12.rs 11 KB

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