bolt12.rs 14 KB

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