bolt12.rs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. use std::env;
  2. use std::path::PathBuf;
  3. use std::str::FromStr;
  4. use std::sync::Arc;
  5. use anyhow::{bail, Result};
  6. use bip39::Mnemonic;
  7. use cashu::amount::SplitTarget;
  8. use cashu::nut00::KnownMethod;
  9. use cashu::nut23::Amountless;
  10. use cashu::{
  11. Amount, CurrencyUnit, MintRequest, MintUrl, PaymentMethod, PreMintSecrets, ProofsMethods,
  12. };
  13. use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletBuilder};
  14. use cdk_integration_tests::get_mint_url_from_env;
  15. use cdk_integration_tests::init_regtest::{get_cln_dir, get_temp_dir};
  16. use cdk_sqlite::wallet::memory;
  17. use ln_regtest_rs::ln_client::ClnClient;
  18. // Helper function to get temp directory from environment or fallback
  19. fn get_test_temp_dir() -> PathBuf {
  20. match env::var("CDK_ITESTS_DIR") {
  21. Ok(dir) => PathBuf::from(dir),
  22. Err(_) => get_temp_dir(), // fallback to default
  23. }
  24. }
  25. // Helper function to create CLN client with retries
  26. async fn create_cln_client_with_retry(cln_dir: PathBuf) -> Result<ClnClient> {
  27. let mut retries = 0;
  28. let max_retries = 10;
  29. loop {
  30. match ClnClient::new(cln_dir.clone(), None).await {
  31. Ok(client) => return Ok(client),
  32. Err(e) => {
  33. retries += 1;
  34. if retries >= max_retries {
  35. bail!(
  36. "Could not connect to CLN client after {} retries: {}",
  37. max_retries,
  38. e
  39. );
  40. }
  41. println!(
  42. "Failed to connect to CLN (attempt {}/{}): {}. Retrying in 7 seconds...",
  43. retries, max_retries, e
  44. );
  45. tokio::time::sleep(tokio::time::Duration::from_secs(7)).await;
  46. }
  47. }
  48. }
  49. }
  50. /// Tests basic BOLT12 minting functionality:
  51. /// - Creates a wallet
  52. /// - Gets a BOLT12 quote for a specific amount (100 sats)
  53. /// - Pays the quote using Core Lightning
  54. /// - Mints tokens and verifies the correct amount is received
  55. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  56. async fn test_regtest_bolt12_mint() {
  57. let wallet = Wallet::new(
  58. &get_mint_url_from_env(),
  59. CurrencyUnit::Sat,
  60. Arc::new(memory::empty().await.unwrap()),
  61. Mnemonic::generate(12).unwrap().to_seed_normalized(""),
  62. None,
  63. )
  64. .unwrap();
  65. let mint_amount = Amount::from(100);
  66. let mint_quote = wallet
  67. .mint_bolt12_quote(Some(mint_amount), None)
  68. .await
  69. .unwrap();
  70. assert_eq!(mint_quote.amount, Some(mint_amount));
  71. let work_dir = get_test_temp_dir();
  72. let cln_one_dir = get_cln_dir(&work_dir, "one");
  73. let cln_client = create_cln_client_with_retry(cln_one_dir.clone())
  74. .await
  75. .unwrap();
  76. cln_client
  77. .pay_bolt12_offer(None, mint_quote.request.clone())
  78. .await
  79. .unwrap();
  80. let proofs = wallet
  81. .wait_and_mint_quote(
  82. mint_quote.clone(),
  83. SplitTarget::default(),
  84. None,
  85. tokio::time::Duration::from_secs(60),
  86. )
  87. .await
  88. .unwrap();
  89. assert_eq!(proofs.total_amount().unwrap(), 100.into());
  90. }
  91. /// Tests multiple payments to a single BOLT12 quote:
  92. /// - Creates a wallet and gets a BOLT12 quote without specifying amount
  93. /// - Makes two separate payments (10,000 sats and 11,000 sats) to the same quote
  94. /// - Verifies that each payment can be minted separately and correctly
  95. /// - Tests the functionality of reusing a quote for multiple payments
  96. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  97. async fn test_regtest_bolt12_mint_multiple() -> Result<()> {
  98. let mint_url = MintUrl::from_str(&get_mint_url_from_env())?;
  99. let wallet = WalletBuilder::new()
  100. .mint_url(mint_url)
  101. .unit(CurrencyUnit::Sat)
  102. .localstore(Arc::new(memory::empty().await?))
  103. .seed(Mnemonic::generate(12)?.to_seed_normalized(""))
  104. .target_proof_count(3)
  105. .use_http_subscription()
  106. .build()?;
  107. let mint_quote = wallet.mint_bolt12_quote(None, None).await?;
  108. let work_dir = get_test_temp_dir();
  109. let cln_one_dir = get_cln_dir(&work_dir, "one");
  110. let cln_client = create_cln_client_with_retry(cln_one_dir.clone()).await?;
  111. cln_client
  112. .pay_bolt12_offer(Some(10000), mint_quote.request.clone())
  113. .await
  114. .unwrap();
  115. let proofs = wallet
  116. .wait_and_mint_quote(
  117. mint_quote.clone(),
  118. SplitTarget::default(),
  119. None,
  120. tokio::time::Duration::from_secs(60),
  121. )
  122. .await?;
  123. assert_eq!(proofs.total_amount().unwrap(), 10.into());
  124. cln_client
  125. .pay_bolt12_offer(Some(11_000), mint_quote.request.clone())
  126. .await
  127. .unwrap();
  128. let proofs = wallet
  129. .wait_and_mint_quote(
  130. mint_quote.clone(),
  131. SplitTarget::default(),
  132. None,
  133. tokio::time::Duration::from_secs(60),
  134. )
  135. .await?;
  136. assert_eq!(proofs.total_amount().unwrap(), 11.into());
  137. Ok(())
  138. }
  139. /// Tests that multiple wallets can pay the same BOLT12 offer:
  140. /// - Creates a BOLT12 offer through CLN that both wallets will pay
  141. /// - Creates two separate wallets with different minting amounts
  142. /// - Has each wallet get their own quote and make payments
  143. /// - Verifies both wallets can successfully mint their tokens
  144. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  145. async fn test_regtest_bolt12_multiple_wallets() -> Result<()> {
  146. // Create first wallet
  147. let wallet_one = Wallet::new(
  148. &get_mint_url_from_env(),
  149. CurrencyUnit::Sat,
  150. Arc::new(memory::empty().await?),
  151. Mnemonic::generate(12)?.to_seed_normalized(""),
  152. None,
  153. )?;
  154. // Create second wallet
  155. let wallet_two = Wallet::new(
  156. &get_mint_url_from_env(),
  157. CurrencyUnit::Sat,
  158. Arc::new(memory::empty().await?),
  159. Mnemonic::generate(12)?.to_seed_normalized(""),
  160. None,
  161. )?;
  162. // Create a BOLT12 offer that both wallets will use
  163. let work_dir = get_test_temp_dir();
  164. let cln_one_dir = get_cln_dir(&work_dir, "one");
  165. let cln_client = create_cln_client_with_retry(cln_one_dir.clone()).await?;
  166. // First wallet payment
  167. let quote_one = wallet_one
  168. .mint_bolt12_quote(Some(10_000.into()), None)
  169. .await?;
  170. cln_client
  171. .pay_bolt12_offer(None, quote_one.request.clone())
  172. .await?;
  173. let proofs_one = wallet_one
  174. .wait_and_mint_quote(
  175. quote_one.clone(),
  176. SplitTarget::default(),
  177. None,
  178. tokio::time::Duration::from_secs(60),
  179. )
  180. .await?;
  181. assert_eq!(proofs_one.total_amount()?, 10_000.into());
  182. // Second wallet payment
  183. let quote_two = wallet_two
  184. .mint_bolt12_quote(Some(15_000.into()), None)
  185. .await?;
  186. cln_client
  187. .pay_bolt12_offer(None, quote_two.request.clone())
  188. .await?;
  189. let proofs_two = wallet_two
  190. .wait_and_mint_quote(
  191. quote_two.clone(),
  192. SplitTarget::default(),
  193. None,
  194. tokio::time::Duration::from_secs(60),
  195. )
  196. .await?;
  197. assert_eq!(proofs_two.total_amount()?, 15_000.into());
  198. let offer = cln_client
  199. .get_bolt12_offer(None, false, "test_multiple_wallets".to_string())
  200. .await?;
  201. let wallet_one_melt_quote = wallet_one
  202. .melt_bolt12_quote(
  203. offer.to_string(),
  204. Some(cashu::MeltOptions::Amountless {
  205. amountless: Amountless {
  206. amount_msat: 1500.into(),
  207. },
  208. }),
  209. )
  210. .await?;
  211. let wallet_two_melt_quote = wallet_two
  212. .melt_bolt12_quote(
  213. offer.to_string(),
  214. Some(cashu::MeltOptions::Amountless {
  215. amountless: Amountless {
  216. amount_msat: 1000.into(),
  217. },
  218. }),
  219. )
  220. .await?;
  221. let melted = wallet_one.melt(&wallet_one_melt_quote.id).await?;
  222. assert!(melted.preimage.is_some());
  223. let melted_two = wallet_two.melt(&wallet_two_melt_quote.id).await?;
  224. assert!(melted_two.preimage.is_some());
  225. Ok(())
  226. }
  227. /// Tests the BOLT12 melting (spending) functionality:
  228. /// - Creates a wallet and mints 20,000 sats using BOLT12
  229. /// - Creates a BOLT12 offer for 10,000 sats
  230. /// - Tests melting (spending) tokens using the BOLT12 offer
  231. /// - Verifies the correct amount is melted
  232. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  233. async fn test_regtest_bolt12_melt() -> Result<()> {
  234. let wallet = Wallet::new(
  235. &get_mint_url_from_env(),
  236. CurrencyUnit::Sat,
  237. Arc::new(memory::empty().await?),
  238. Mnemonic::generate(12)?.to_seed_normalized(""),
  239. None,
  240. )?;
  241. let mint_amount = Amount::from(20_000);
  242. // Create a single-use BOLT12 quote
  243. let mint_quote = wallet.mint_bolt12_quote(Some(mint_amount), None).await?;
  244. assert_eq!(mint_quote.amount, Some(mint_amount));
  245. // Pay the quote
  246. let work_dir = get_test_temp_dir();
  247. let cln_one_dir = get_cln_dir(&work_dir, "one");
  248. let cln_client = create_cln_client_with_retry(cln_one_dir.clone()).await?;
  249. cln_client
  250. .pay_bolt12_offer(None, mint_quote.request.clone())
  251. .await?;
  252. let _proofs = wallet
  253. .wait_and_mint_quote(
  254. mint_quote.clone(),
  255. SplitTarget::default(),
  256. None,
  257. tokio::time::Duration::from_secs(60),
  258. )
  259. .await?;
  260. let offer = cln_client
  261. .get_bolt12_offer(Some(10_000), true, "hhhhhhhh".to_string())
  262. .await?;
  263. let quote = wallet.melt_bolt12_quote(offer.to_string(), None).await?;
  264. let melt = wallet.melt(&quote.id).await?;
  265. assert_eq!(melt.amount, 10.into());
  266. Ok(())
  267. }
  268. /// Tests security validation for BOLT12 minting to prevent overspending:
  269. /// - Creates a wallet and gets an open-ended BOLT12 quote
  270. /// - Makes a payment of 10,000 millisats
  271. /// - Attempts to mint more tokens (500 sats) than were actually paid for
  272. /// - Verifies that the mint correctly rejects the oversized mint request
  273. /// - Ensures proper error handling with TransactionUnbalanced error
  274. /// This test is crucial for ensuring the economic security of the minting process
  275. /// by preventing users from minting more tokens than they have paid for.
  276. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  277. async fn test_regtest_bolt12_mint_extra() -> Result<()> {
  278. let wallet = Wallet::new(
  279. &get_mint_url_from_env(),
  280. CurrencyUnit::Sat,
  281. Arc::new(memory::empty().await?),
  282. Mnemonic::generate(12)?.to_seed_normalized(""),
  283. None,
  284. )?;
  285. // Create a single-use BOLT12 quote
  286. let mint_quote = wallet.mint_bolt12_quote(None, None).await?;
  287. let state = wallet.mint_bolt12_quote_state(&mint_quote.id).await?;
  288. assert_eq!(state.amount_paid, Amount::ZERO);
  289. assert_eq!(state.amount_issued, Amount::ZERO);
  290. let active_keyset_id = wallet.fetch_active_keyset().await?.id;
  291. let pay_amount_msats = 10_000;
  292. let work_dir = get_test_temp_dir();
  293. let cln_one_dir = get_cln_dir(&work_dir, "one");
  294. let cln_client = create_cln_client_with_retry(cln_one_dir.clone()).await?;
  295. cln_client
  296. .pay_bolt12_offer(Some(pay_amount_msats), mint_quote.request.clone())
  297. .await?;
  298. let payment = wallet
  299. .wait_for_payment(&mint_quote, tokio::time::Duration::from_secs(15))
  300. .await?
  301. .unwrap();
  302. let state = wallet.mint_bolt12_quote_state(&mint_quote.id).await?;
  303. assert_eq!(payment, state.amount_paid);
  304. assert_eq!(state.amount_paid, (pay_amount_msats / 1_000).into());
  305. assert_eq!(state.amount_issued, Amount::ZERO);
  306. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  307. let pre_mint = PreMintSecrets::random(
  308. active_keyset_id,
  309. 500.into(),
  310. &SplitTarget::None,
  311. &fee_and_amounts,
  312. )?;
  313. let quote_info = wallet
  314. .localstore
  315. .get_mint_quote(&mint_quote.id)
  316. .await?
  317. .expect("there is a quote");
  318. let mut mint_request = MintRequest {
  319. quote: mint_quote.id,
  320. outputs: pre_mint.blinded_messages(),
  321. signature: None,
  322. };
  323. if let Some(secret_key) = quote_info.secret_key {
  324. mint_request.sign(secret_key)?;
  325. }
  326. let http_client = HttpClient::new(get_mint_url_from_env().parse().unwrap(), None);
  327. let response = http_client
  328. .post_mint(
  329. &PaymentMethod::Known(KnownMethod::Bolt11),
  330. mint_request.clone(),
  331. )
  332. .await;
  333. match response {
  334. Err(err) => match err {
  335. cdk::Error::TransactionUnbalanced(_, _, _) => (),
  336. err => {
  337. bail!("Wrong mint error returned: {}", err);
  338. }
  339. },
  340. Ok(_) => {
  341. bail!("Should not have allowed second payment");
  342. }
  343. }
  344. Ok(())
  345. }
  346. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  347. async fn test_attempt_to_mint_unpaid() {
  348. let wallet = Wallet::new(
  349. &get_mint_url_from_env(),
  350. CurrencyUnit::Sat,
  351. Arc::new(memory::empty().await.unwrap()),
  352. Mnemonic::generate(12).unwrap().to_seed_normalized(""),
  353. None,
  354. )
  355. .expect("failed to create new wallet");
  356. let mint_amount = Amount::from(100);
  357. let mint_quote = wallet
  358. .mint_bolt12_quote(Some(mint_amount), None)
  359. .await
  360. .unwrap();
  361. assert_eq!(mint_quote.amount, Some(mint_amount));
  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. let mint_quote = wallet
  376. .mint_bolt12_quote(Some(mint_amount), None)
  377. .await
  378. .unwrap();
  379. let state = wallet
  380. .mint_bolt12_quote_state(&mint_quote.id)
  381. .await
  382. .unwrap();
  383. assert!(state.amount_paid == Amount::ZERO);
  384. let proofs = wallet
  385. .mint_bolt12(&mint_quote.id, None, SplitTarget::default(), None)
  386. .await;
  387. match proofs {
  388. Err(err) => {
  389. if !matches!(err, cdk::Error::UnpaidQuote) {
  390. panic!("Wrong error quote should be unpaid: {}", err);
  391. }
  392. }
  393. Ok(_) => {
  394. panic!("Minting should not be allowed");
  395. }
  396. }
  397. }
  398. /// Tests the check_all_mint_quotes functionality for Bolt12 quotes
  399. ///
  400. /// This test verifies that:
  401. /// 1. Paid Bolt12 quotes are automatically minted when check_all_mint_quotes is called
  402. /// 2. The method correctly handles the Bolt12-specific logic (amount_paid > amount_issued)
  403. /// 3. Quote state is properly updated after minting
  404. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  405. async fn test_check_all_mint_quotes_bolt12() -> Result<()> {
  406. let wallet = Wallet::new(
  407. &get_mint_url_from_env(),
  408. CurrencyUnit::Sat,
  409. Arc::new(memory::empty().await?),
  410. Mnemonic::generate(12)?.to_seed_normalized(""),
  411. None,
  412. )?;
  413. let mint_amount = Amount::from(100);
  414. // Create a Bolt12 quote
  415. let mint_quote = wallet.mint_bolt12_quote(Some(mint_amount), None).await?;
  416. assert_eq!(mint_quote.amount, Some(mint_amount));
  417. // Verify the quote is in unissued quotes before payment
  418. let unissued_before = wallet.get_unissued_mint_quotes().await?;
  419. assert!(
  420. unissued_before.iter().any(|q| q.id == mint_quote.id),
  421. "Bolt12 quote should be in unissued quotes before payment"
  422. );
  423. // Pay the quote
  424. let work_dir = get_test_temp_dir();
  425. let cln_one_dir = get_cln_dir(&work_dir, "one");
  426. let cln_client = create_cln_client_with_retry(cln_one_dir.clone()).await?;
  427. cln_client
  428. .pay_bolt12_offer(None, mint_quote.request.clone())
  429. .await?;
  430. // Wait for payment to be recognized
  431. wallet
  432. .wait_for_payment(&mint_quote, tokio::time::Duration::from_secs(30))
  433. .await?;
  434. // Verify initial balance is zero
  435. assert_eq!(wallet.total_balance().await?, Amount::ZERO);
  436. // Call check_all_mint_quotes - this should mint the paid Bolt12 quote
  437. let total_minted = wallet.check_all_mint_quotes().await?;
  438. // Verify the amount minted is correct
  439. assert_eq!(
  440. total_minted, mint_amount,
  441. "check_all_mint_quotes should have minted the Bolt12 quote"
  442. );
  443. // Verify wallet balance matches
  444. assert_eq!(wallet.total_balance().await?, mint_amount);
  445. // Calling check_all_mint_quotes again should return 0 (quote already fully issued)
  446. let second_check = wallet.check_all_mint_quotes().await?;
  447. assert_eq!(
  448. second_check,
  449. Amount::ZERO,
  450. "Second check should return 0 as quote is fully issued"
  451. );
  452. Ok(())
  453. }
  454. /// Tests that Bolt12 quote state (amount_issued) is properly updated after minting
  455. ///
  456. /// This test verifies that:
  457. /// 1. amount_issued starts at 0
  458. /// 2. amount_issued is updated after minting
  459. /// 3. The quote correctly tracks issued vs paid amounts
  460. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  461. async fn test_bolt12_quote_amount_issued_tracking() -> Result<()> {
  462. let wallet = Wallet::new(
  463. &get_mint_url_from_env(),
  464. CurrencyUnit::Sat,
  465. Arc::new(memory::empty().await?),
  466. Mnemonic::generate(12)?.to_seed_normalized(""),
  467. None,
  468. )?;
  469. // Create an open-ended Bolt12 quote (no amount specified)
  470. let mint_quote = wallet.mint_bolt12_quote(None, None).await?;
  471. // Verify initial state
  472. let state_before = wallet.mint_bolt12_quote_state(&mint_quote.id).await?;
  473. assert_eq!(state_before.amount_paid, Amount::ZERO);
  474. assert_eq!(state_before.amount_issued, Amount::ZERO);
  475. // Pay the quote with a specific amount
  476. let pay_amount_msats = 50_000; // 50 sats
  477. let work_dir = get_test_temp_dir();
  478. let cln_one_dir = get_cln_dir(&work_dir, "one");
  479. let cln_client = create_cln_client_with_retry(cln_one_dir.clone()).await?;
  480. cln_client
  481. .pay_bolt12_offer(Some(pay_amount_msats), mint_quote.request.clone())
  482. .await?;
  483. // Wait for payment
  484. let payment = wallet
  485. .wait_for_payment(&mint_quote, tokio::time::Duration::from_secs(30))
  486. .await?
  487. .expect("Should receive payment notification");
  488. // Check state after payment but before minting
  489. let state_after_payment = wallet.mint_bolt12_quote_state(&mint_quote.id).await?;
  490. assert_eq!(
  491. state_after_payment.amount_paid,
  492. Amount::from(pay_amount_msats / 1000)
  493. );
  494. assert_eq!(
  495. state_after_payment.amount_issued,
  496. Amount::ZERO,
  497. "amount_issued should still be 0 before minting"
  498. );
  499. // Now mint the tokens
  500. let proofs = wallet
  501. .mint_bolt12(&mint_quote.id, None, SplitTarget::default(), None)
  502. .await?;
  503. let minted_amount = proofs.total_amount()?;
  504. assert_eq!(minted_amount, payment);
  505. // Check state after minting
  506. let state_after_mint = wallet.mint_bolt12_quote_state(&mint_quote.id).await?;
  507. assert_eq!(
  508. state_after_mint.amount_issued, minted_amount,
  509. "amount_issued should be updated after minting"
  510. );
  511. assert_eq!(
  512. state_after_mint.amount_paid, state_after_mint.amount_issued,
  513. "For a single payment, amount_paid should equal amount_issued after minting"
  514. );
  515. Ok(())
  516. }