bolt12.rs 14 KB

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