bolt12.rs 14 KB

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