test_swap_flow.rs 57 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739
  1. //! Comprehensive tests for the current swap flow
  2. //!
  3. //! These tests validate the swap operation's behavior including:
  4. //! - Happy path: successful token swaps
  5. //! - Error handling: validation failures, rollback scenarios
  6. //! - Edge cases: concurrent operations, double-spending
  7. //! - State management: proof states, blinded message tracking
  8. //!
  9. //! The tests focus on the current implementation using ProofWriter and BlindedMessageWriter
  10. //! patterns to ensure proper cleanup and rollback behavior.
  11. use std::collections::HashMap;
  12. use std::sync::Arc;
  13. use cashu::amount::SplitTarget;
  14. use cashu::dhke::construct_proofs;
  15. use cashu::{
  16. CurrencyUnit, Id, PaymentMethod, PreMintSecrets, SecretKey, SpendingConditions, State,
  17. SwapRequest,
  18. };
  19. use cdk::mint::Mint;
  20. use cdk::nuts::nut00::ProofsMethods;
  21. use cdk::Amount;
  22. use cdk_fake_wallet::create_fake_invoice;
  23. use cdk_integration_tests::init_pure_tests::*;
  24. /// Helper to get the active keyset ID from a mint
  25. async fn get_keyset_id(mint: &Mint) -> Id {
  26. let keys = mint.pubkeys().keysets.first().unwrap().clone();
  27. keys.verify_id()
  28. .expect("Keyset ID generation is successful");
  29. keys.id
  30. }
  31. /// Tests the complete happy path of a swap operation:
  32. /// 1. Wallet is funded with tokens
  33. /// 2. Blinded messages are added to database
  34. /// 3. Outputs are signed by mint
  35. /// 4. Input proofs are verified
  36. /// 5. Transaction is balanced
  37. /// 6. Proofs are added and marked as spent
  38. /// 7. Blind signatures are saved
  39. /// All steps should succeed and database should be in consistent state.
  40. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  41. async fn test_swap_happy_path() {
  42. setup_tracing();
  43. let mint = create_and_start_test_mint()
  44. .await
  45. .expect("Failed to create test mint");
  46. let wallet = create_test_wallet_for_mint(mint.clone())
  47. .await
  48. .expect("Failed to create test wallet");
  49. // Fund wallet with 100 sats
  50. fund_wallet(wallet.clone(), 100, None)
  51. .await
  52. .expect("Failed to fund wallet");
  53. let proofs = wallet
  54. .get_unspent_proofs()
  55. .await
  56. .expect("Could not get proofs");
  57. let keyset_id = get_keyset_id(&mint).await;
  58. // Check initial amounts after minting
  59. let total_issued = mint.total_issued().await.unwrap();
  60. let total_redeemed = mint.total_redeemed().await.unwrap();
  61. let initial_issued = total_issued
  62. .get(&keyset_id)
  63. .copied()
  64. .unwrap_or(Amount::ZERO);
  65. let initial_redeemed = total_redeemed
  66. .get(&keyset_id)
  67. .copied()
  68. .unwrap_or(Amount::ZERO);
  69. assert_eq!(
  70. initial_issued,
  71. Amount::from(100),
  72. "Should have issued 100 sats"
  73. );
  74. assert_eq!(
  75. initial_redeemed,
  76. Amount::ZERO,
  77. "Should have redeemed 0 sats initially"
  78. );
  79. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  80. // Create swap request for same amount (100 sats)
  81. let preswap = PreMintSecrets::random(
  82. keyset_id,
  83. 100.into(),
  84. &SplitTarget::default(),
  85. &fee_and_amounts,
  86. )
  87. .expect("Failed to create preswap");
  88. let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
  89. // Execute swap
  90. let swap_response = mint
  91. .process_swap_request(swap_request)
  92. .await
  93. .expect("Swap should succeed");
  94. // Verify response contains correct number of signatures
  95. assert_eq!(
  96. swap_response.signatures.len(),
  97. preswap.blinded_messages().len(),
  98. "Should receive signature for each blinded message"
  99. );
  100. // Verify input proofs are marked as spent
  101. let states = mint
  102. .localstore()
  103. .get_proofs_states(&proofs.iter().map(|p| p.y().unwrap()).collect::<Vec<_>>())
  104. .await
  105. .expect("Failed to get proof states");
  106. for state in states {
  107. assert_eq!(
  108. State::Spent,
  109. state.expect("State should be known"),
  110. "All input proofs should be marked as spent"
  111. );
  112. }
  113. // Verify blind signatures were saved
  114. let saved_signatures = mint
  115. .localstore()
  116. .get_blind_signatures(
  117. &preswap
  118. .blinded_messages()
  119. .iter()
  120. .map(|bm| bm.blinded_secret)
  121. .collect::<Vec<_>>(),
  122. )
  123. .await
  124. .expect("Failed to get blind signatures");
  125. assert_eq!(
  126. saved_signatures.len(),
  127. swap_response.signatures.len(),
  128. "All signatures should be saved"
  129. );
  130. // Check keyset amounts after swap
  131. // Swap redeems old proofs (100 sats) and issues new proofs (100 sats)
  132. let total_issued = mint.total_issued().await.unwrap();
  133. let total_redeemed = mint.total_redeemed().await.unwrap();
  134. let after_issued = total_issued
  135. .get(&keyset_id)
  136. .copied()
  137. .unwrap_or(Amount::ZERO);
  138. let after_redeemed = total_redeemed
  139. .get(&keyset_id)
  140. .copied()
  141. .unwrap_or(Amount::ZERO);
  142. assert_eq!(
  143. after_issued,
  144. Amount::from(200),
  145. "Should have issued 200 sats total (initial 100 + swap 100)"
  146. );
  147. assert_eq!(
  148. after_redeemed,
  149. Amount::from(100),
  150. "Should have redeemed 100 sats from the swap"
  151. );
  152. }
  153. /// Tests that duplicate blinded messages are rejected:
  154. /// 1. First swap with blinded messages succeeds
  155. /// 2. Second swap attempt with same blinded messages fails
  156. /// 3. BlindedMessageWriter should prevent reuse
  157. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  158. async fn test_swap_duplicate_blinded_messages() {
  159. setup_tracing();
  160. let mint = create_and_start_test_mint()
  161. .await
  162. .expect("Failed to create test mint");
  163. let wallet = create_test_wallet_for_mint(mint.clone())
  164. .await
  165. .expect("Failed to create test wallet");
  166. // Fund wallet with 200 sats (enough for two swaps)
  167. fund_wallet(wallet.clone(), 200, None)
  168. .await
  169. .expect("Failed to fund wallet");
  170. let all_proofs = wallet
  171. .get_unspent_proofs()
  172. .await
  173. .expect("Could not get proofs");
  174. // Split proofs into two sets
  175. let mid = all_proofs.len() / 2;
  176. let proofs1: Vec<_> = all_proofs.iter().take(mid).cloned().collect();
  177. let proofs2: Vec<_> = all_proofs.iter().skip(mid).cloned().collect();
  178. let keyset_id = get_keyset_id(&mint).await;
  179. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  180. // Create blinded messages for first swap
  181. let preswap = PreMintSecrets::random(
  182. keyset_id,
  183. proofs1.total_amount().unwrap(),
  184. &SplitTarget::default(),
  185. &fee_and_amounts,
  186. )
  187. .expect("Failed to create preswap");
  188. let blinded_messages = preswap.blinded_messages();
  189. // First swap should succeed
  190. let swap_request1 = SwapRequest::new(proofs1, blinded_messages.clone());
  191. mint.process_swap_request(swap_request1)
  192. .await
  193. .expect("First swap should succeed");
  194. // Second swap with SAME blinded messages should fail
  195. let swap_request2 = SwapRequest::new(proofs2, blinded_messages.clone());
  196. let result = mint.process_swap_request(swap_request2).await;
  197. assert!(
  198. result.is_err(),
  199. "Second swap with duplicate blinded messages should fail"
  200. );
  201. }
  202. /// Tests that swap correctly rejects double-spending attempts:
  203. /// 1. First swap with proofs succeeds
  204. /// 2. Second swap with same proofs fails with TokenAlreadySpent
  205. /// 3. ProofWriter should detect already-spent proofs
  206. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  207. async fn test_swap_double_spend_detection() {
  208. setup_tracing();
  209. let mint = create_and_start_test_mint()
  210. .await
  211. .expect("Failed to create test mint");
  212. let wallet = create_test_wallet_for_mint(mint.clone())
  213. .await
  214. .expect("Failed to create test wallet");
  215. // Fund wallet with 100 sats
  216. fund_wallet(wallet.clone(), 100, None)
  217. .await
  218. .expect("Failed to fund wallet");
  219. let proofs = wallet
  220. .get_unspent_proofs()
  221. .await
  222. .expect("Could not get proofs");
  223. let keyset_id = get_keyset_id(&mint).await;
  224. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  225. // First swap
  226. let preswap1 = PreMintSecrets::random(
  227. keyset_id,
  228. 100.into(),
  229. &SplitTarget::default(),
  230. &fee_and_amounts,
  231. )
  232. .expect("Failed to create preswap");
  233. let swap_request1 = SwapRequest::new(proofs.clone(), preswap1.blinded_messages());
  234. mint.process_swap_request(swap_request1)
  235. .await
  236. .expect("First swap should succeed");
  237. // Second swap with same proofs should fail
  238. let preswap2 = PreMintSecrets::random(
  239. keyset_id,
  240. 100.into(),
  241. &SplitTarget::default(),
  242. &fee_and_amounts,
  243. )
  244. .expect("Failed to create preswap");
  245. let swap_request2 = SwapRequest::new(proofs.clone(), preswap2.blinded_messages());
  246. let result = mint.process_swap_request(swap_request2).await;
  247. match result {
  248. Err(cdk::Error::TokenAlreadySpent) => {
  249. // Expected error
  250. }
  251. Err(err) => panic!("Wrong error type: {:?}", err),
  252. Ok(_) => panic!("Double spend should not succeed"),
  253. }
  254. }
  255. /// Tests that unbalanced swap requests are rejected:
  256. /// Case 1: Output amount < Input amount (trying to steal from mint)
  257. /// Case 2: Output amount > Input amount (trying to create tokens)
  258. /// Both should fail with TransactionUnbalanced error.
  259. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  260. async fn test_swap_unbalanced_transaction_detection() {
  261. setup_tracing();
  262. let mint = create_and_start_test_mint()
  263. .await
  264. .expect("Failed to create test mint");
  265. let wallet = create_test_wallet_for_mint(mint.clone())
  266. .await
  267. .expect("Failed to create test wallet");
  268. // Fund wallet with 100 sats
  269. fund_wallet(wallet.clone(), 100, None)
  270. .await
  271. .expect("Failed to fund wallet");
  272. let proofs = wallet
  273. .get_unspent_proofs()
  274. .await
  275. .expect("Could not get proofs");
  276. let keyset_id = get_keyset_id(&mint).await;
  277. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  278. // Case 1: Try to swap for LESS (95 < 100) - underpaying
  279. let preswap_less = PreMintSecrets::random(
  280. keyset_id,
  281. 95.into(),
  282. &SplitTarget::default(),
  283. &fee_and_amounts,
  284. )
  285. .expect("Failed to create preswap");
  286. let swap_request_less = SwapRequest::new(proofs.clone(), preswap_less.blinded_messages());
  287. match mint.process_swap_request(swap_request_less).await {
  288. Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  289. // Expected error
  290. }
  291. Err(err) => panic!("Wrong error type for underpay: {:?}", err),
  292. Ok(_) => panic!("Unbalanced swap (underpay) should not succeed"),
  293. }
  294. // Case 2: Try to swap for MORE (105 > 100) - overpaying/creating tokens
  295. let preswap_more = PreMintSecrets::random(
  296. keyset_id,
  297. 105.into(),
  298. &SplitTarget::default(),
  299. &fee_and_amounts,
  300. )
  301. .expect("Failed to create preswap");
  302. let swap_request_more = SwapRequest::new(proofs.clone(), preswap_more.blinded_messages());
  303. match mint.process_swap_request(swap_request_more).await {
  304. Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  305. // Expected error
  306. }
  307. Err(err) => panic!("Wrong error type for overpay: {:?}", err),
  308. Ok(_) => panic!("Unbalanced swap (overpay) should not succeed"),
  309. }
  310. }
  311. /// Tests that swap requests with empty inputs or outputs are rejected:
  312. /// Case 1: Empty outputs (inputs without outputs)
  313. /// Case 2: Empty inputs (outputs without inputs)
  314. /// Both should fail. Currently returns UnitMismatch (11010) instead of
  315. /// TransactionUnbalanced (11002) because there are no keyset IDs to determine units.
  316. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  317. async fn test_swap_empty_inputs_or_outputs() {
  318. setup_tracing();
  319. let mint = create_and_start_test_mint()
  320. .await
  321. .expect("Failed to create test mint");
  322. let wallet = create_test_wallet_for_mint(mint.clone())
  323. .await
  324. .expect("Failed to create test wallet");
  325. // Fund wallet with 100 sats
  326. fund_wallet(wallet.clone(), 100, None)
  327. .await
  328. .expect("Failed to fund wallet");
  329. let proofs = wallet
  330. .get_unspent_proofs()
  331. .await
  332. .expect("Could not get proofs");
  333. // Case 1: Swap request with inputs but empty outputs
  334. // This represents trying to destroy tokens (inputs with no outputs)
  335. let swap_request_empty_outputs = SwapRequest::new(proofs.clone(), vec![]);
  336. match mint.process_swap_request(swap_request_empty_outputs).await {
  337. Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  338. // This would be the more appropriate error
  339. }
  340. Err(err) => panic!("Wrong error type for empty outputs: {:?}", err),
  341. Ok(_) => panic!("Swap with empty outputs should not succeed"),
  342. }
  343. // Case 2: Swap request with empty inputs but with outputs
  344. // This represents trying to create tokens from nothing
  345. let keyset_id = get_keyset_id(&mint).await;
  346. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  347. let preswap = PreMintSecrets::random(
  348. keyset_id,
  349. 100.into(),
  350. &SplitTarget::default(),
  351. &fee_and_amounts,
  352. )
  353. .expect("Failed to create preswap");
  354. let swap_request_empty_inputs = SwapRequest::new(vec![], preswap.blinded_messages());
  355. match mint.process_swap_request(swap_request_empty_inputs).await {
  356. Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  357. // This would be the more appropriate error
  358. }
  359. Err(err) => panic!("Wrong error type for empty inputs: {:?}", err),
  360. Ok(_) => panic!("Swap with empty inputs should not succeed"),
  361. }
  362. }
  363. /// Tests P2PK (Pay-to-Public-Key) spending conditions:
  364. /// 1. Create proofs locked to a public key
  365. /// 2. Attempt swap without signature - should fail
  366. /// 3. Attempt swap with valid signature - should succeed
  367. /// Validates NUT-11 signature enforcement.
  368. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  369. async fn test_swap_p2pk_signature_validation() {
  370. setup_tracing();
  371. let mint = create_and_start_test_mint()
  372. .await
  373. .expect("Failed to create test mint");
  374. let wallet = create_test_wallet_for_mint(mint.clone())
  375. .await
  376. .expect("Failed to create test wallet");
  377. // Fund wallet with 100 sats
  378. fund_wallet(wallet.clone(), 100, None)
  379. .await
  380. .expect("Failed to fund wallet");
  381. let input_proofs = wallet
  382. .get_unspent_proofs()
  383. .await
  384. .expect("Could not get proofs");
  385. let keyset_id = get_keyset_id(&mint).await;
  386. let secret_key = SecretKey::generate();
  387. // Create P2PK locked outputs
  388. let spending_conditions = SpendingConditions::new_p2pk(secret_key.public_key(), None);
  389. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  390. let pre_swap = PreMintSecrets::with_conditions(
  391. keyset_id,
  392. 100.into(),
  393. &SplitTarget::default(),
  394. &spending_conditions,
  395. &fee_and_amounts,
  396. )
  397. .expect("Failed to create P2PK preswap");
  398. let swap_request = SwapRequest::new(input_proofs.clone(), pre_swap.blinded_messages());
  399. // First swap to get P2PK locked proofs
  400. let keys = mint.pubkeys().keysets.first().cloned().unwrap().keys;
  401. let post_swap = mint
  402. .process_swap_request(swap_request)
  403. .await
  404. .expect("Initial swap should succeed");
  405. // Construct proofs from swap response
  406. let mut p2pk_proofs = construct_proofs(
  407. post_swap.signatures,
  408. pre_swap.rs(),
  409. pre_swap.secrets(),
  410. &keys,
  411. )
  412. .expect("Failed to construct proofs");
  413. // Try to spend P2PK proofs WITHOUT signature - should fail
  414. let preswap_unsigned = PreMintSecrets::random(
  415. keyset_id,
  416. 100.into(),
  417. &SplitTarget::default(),
  418. &fee_and_amounts,
  419. )
  420. .expect("Failed to create preswap");
  421. let swap_request_unsigned =
  422. SwapRequest::new(p2pk_proofs.clone(), preswap_unsigned.blinded_messages());
  423. match mint.process_swap_request(swap_request_unsigned).await {
  424. Err(cdk::Error::NUT11(cdk::nuts::nut11::Error::SignaturesNotProvided)) => {
  425. // Expected error
  426. }
  427. Err(err) => panic!("Wrong error type: {:?}", err),
  428. Ok(_) => panic!("Unsigned P2PK spend should fail"),
  429. }
  430. // Sign the proofs with correct key
  431. for proof in &mut p2pk_proofs {
  432. proof
  433. .sign_p2pk(secret_key.clone())
  434. .expect("Failed to sign proof");
  435. }
  436. // Try again WITH signature - should succeed
  437. let preswap_signed = PreMintSecrets::random(
  438. keyset_id,
  439. 100.into(),
  440. &SplitTarget::default(),
  441. &fee_and_amounts,
  442. )
  443. .expect("Failed to create preswap");
  444. let swap_request_signed = SwapRequest::new(p2pk_proofs, preswap_signed.blinded_messages());
  445. mint.process_swap_request(swap_request_signed)
  446. .await
  447. .expect("Signed P2PK spend should succeed");
  448. }
  449. /// Tests rollback behavior when duplicate blinded messages are used:
  450. /// This validates that the BlindedMessageWriter prevents reuse of blinded messages.
  451. /// 1. First swap with blinded messages succeeds
  452. /// 2. Second swap with same blinded messages fails
  453. /// 3. The failure should happen early (during blinded message addition)
  454. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  455. async fn test_swap_rollback_on_duplicate_blinded_message() {
  456. setup_tracing();
  457. let mint = create_and_start_test_mint()
  458. .await
  459. .expect("Failed to create test mint");
  460. let wallet = create_test_wallet_for_mint(mint.clone())
  461. .await
  462. .expect("Failed to create test wallet");
  463. // Fund with enough for multiple swaps
  464. fund_wallet(wallet.clone(), 200, None)
  465. .await
  466. .expect("Failed to fund wallet");
  467. let all_proofs = wallet
  468. .get_unspent_proofs()
  469. .await
  470. .expect("Could not get proofs");
  471. let mid = all_proofs.len() / 2;
  472. let proofs1: Vec<_> = all_proofs.iter().take(mid).cloned().collect();
  473. let proofs2: Vec<_> = all_proofs.iter().skip(mid).cloned().collect();
  474. let keyset_id = get_keyset_id(&mint).await;
  475. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  476. // Create shared blinded messages
  477. let preswap = PreMintSecrets::random(
  478. keyset_id,
  479. proofs1.total_amount().unwrap(),
  480. &SplitTarget::default(),
  481. &fee_and_amounts,
  482. )
  483. .expect("Failed to create preswap");
  484. let blinded_messages = preswap.blinded_messages();
  485. // Extract proof2 ys before moving proofs2
  486. let proof2_ys: Vec<_> = proofs2.iter().map(|p| p.y().unwrap()).collect();
  487. // First swap succeeds
  488. let swap1 = SwapRequest::new(proofs1, blinded_messages.clone());
  489. mint.process_swap_request(swap1)
  490. .await
  491. .expect("First swap should succeed");
  492. // Second swap with duplicate blinded messages should fail early
  493. // The BlindedMessageWriter should detect duplicate and prevent the swap
  494. let swap2 = SwapRequest::new(proofs2, blinded_messages.clone());
  495. let result = mint.process_swap_request(swap2).await;
  496. assert!(
  497. result.is_err(),
  498. "Duplicate blinded messages should cause failure"
  499. );
  500. // Verify the second set of proofs are NOT marked as spent
  501. // (since the swap failed before processing them)
  502. let states = mint
  503. .localstore()
  504. .get_proofs_states(&proof2_ys)
  505. .await
  506. .expect("Failed to get proof states");
  507. for state in states {
  508. assert!(
  509. state.is_none(),
  510. "Proofs from failed swap should not be marked as spent"
  511. );
  512. }
  513. }
  514. /// Tests concurrent swap attempts with same proofs:
  515. /// Spawns 3 concurrent tasks trying to swap the same proofs.
  516. /// Only one should succeed, others should fail with TokenAlreadySpent or TokenPending.
  517. /// Validates that concurrent access is properly handled.
  518. #[tokio::test(flavor = "multi_thread", worker_threads = 3)]
  519. async fn test_swap_concurrent_double_spend_prevention() {
  520. setup_tracing();
  521. let mint = create_and_start_test_mint()
  522. .await
  523. .expect("Failed to create test mint");
  524. let wallet = create_test_wallet_for_mint(mint.clone())
  525. .await
  526. .expect("Failed to create test wallet");
  527. // Fund wallet
  528. fund_wallet(wallet.clone(), 100, None)
  529. .await
  530. .expect("Failed to fund wallet");
  531. let proofs = wallet
  532. .get_unspent_proofs()
  533. .await
  534. .expect("Could not get proofs");
  535. let keyset_id = get_keyset_id(&mint).await;
  536. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  537. // Create 3 different swap requests with SAME proofs but different outputs
  538. let preswap1 = PreMintSecrets::random(
  539. keyset_id,
  540. 100.into(),
  541. &SplitTarget::default(),
  542. &fee_and_amounts,
  543. )
  544. .expect("Failed to create preswap 1");
  545. let preswap2 = PreMintSecrets::random(
  546. keyset_id,
  547. 100.into(),
  548. &SplitTarget::default(),
  549. &fee_and_amounts,
  550. )
  551. .expect("Failed to create preswap 2");
  552. let preswap3 = PreMintSecrets::random(
  553. keyset_id,
  554. 100.into(),
  555. &SplitTarget::default(),
  556. &fee_and_amounts,
  557. )
  558. .expect("Failed to create preswap 3");
  559. let swap_request1 = SwapRequest::new(proofs.clone(), preswap1.blinded_messages());
  560. let swap_request2 = SwapRequest::new(proofs.clone(), preswap2.blinded_messages());
  561. let swap_request3 = SwapRequest::new(proofs.clone(), preswap3.blinded_messages());
  562. // Spawn concurrent tasks
  563. let mint1 = mint.clone();
  564. let mint2 = mint.clone();
  565. let mint3 = mint.clone();
  566. let task1 = tokio::spawn(async move { mint1.process_swap_request(swap_request1).await });
  567. let task2 = tokio::spawn(async move { mint2.process_swap_request(swap_request2).await });
  568. let task3 = tokio::spawn(async move { mint3.process_swap_request(swap_request3).await });
  569. // Wait for all tasks
  570. let results = tokio::try_join!(task1, task2, task3).expect("Tasks should complete");
  571. // Count successes and failures
  572. let mut success_count = 0;
  573. let mut failure_count = 0;
  574. for result in [results.0, results.1, results.2] {
  575. match result {
  576. Ok(_) => success_count += 1,
  577. Err(cdk::Error::TokenAlreadySpent) | Err(cdk::Error::TokenPending) => {
  578. failure_count += 1
  579. }
  580. Err(err) => panic!("Unexpected error: {:?}", err),
  581. }
  582. }
  583. assert_eq!(
  584. success_count, 1,
  585. "Exactly one swap should succeed in concurrent scenario"
  586. );
  587. assert_eq!(
  588. failure_count, 2,
  589. "Exactly two swaps should fail in concurrent scenario"
  590. );
  591. // Verify all proofs are marked as spent
  592. let states = mint
  593. .localstore()
  594. .get_proofs_states(&proofs.iter().map(|p| p.y().unwrap()).collect::<Vec<_>>())
  595. .await
  596. .expect("Failed to get proof states");
  597. for state in states {
  598. assert_eq!(
  599. State::Spent,
  600. state.expect("State should be known"),
  601. "All proofs should be marked as spent after concurrent attempts"
  602. );
  603. }
  604. }
  605. /// Tests swap with fees enabled:
  606. /// 1. Create mint with keyset that has fees (1 sat per proof)
  607. /// 2. Fund wallet with many small proofs
  608. /// 3. Attempt swap without paying fee - should fail
  609. /// 4. Attempt swap with correct fee deduction - should succeed
  610. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  611. async fn test_swap_with_fees() {
  612. setup_tracing();
  613. let mint = create_and_start_test_mint()
  614. .await
  615. .expect("Failed to create test mint");
  616. let wallet = create_test_wallet_for_mint(mint.clone())
  617. .await
  618. .expect("Failed to create test wallet");
  619. // Rotate to keyset with 1 sat per proof fee
  620. mint.rotate_keyset(
  621. CurrencyUnit::Sat,
  622. cdk_integration_tests::standard_keyset_amounts(32),
  623. 100,
  624. true,
  625. )
  626. .await
  627. .expect("Failed to rotate keyset");
  628. // Fund with 1000 sats as individual 1-sat proofs using the fee-based keyset
  629. // Wait a bit for keyset to be available
  630. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  631. fund_wallet(wallet.clone(), 1000, Some(SplitTarget::Value(Amount::ONE)))
  632. .await
  633. .expect("Failed to fund wallet");
  634. let proofs = wallet
  635. .get_unspent_proofs()
  636. .await
  637. .expect("Could not get proofs");
  638. // Take 100 proofs (100 sats total, will need to pay fee)
  639. let hundred_proofs: Vec<_> = proofs.iter().take(100).cloned().collect();
  640. // Get the keyset ID from the proofs (which will be the fee-based keyset)
  641. let keyset_id = hundred_proofs[0].keyset_id;
  642. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  643. // Try to swap for 100 outputs (same as input) - should fail due to unpaid fee
  644. let preswap_no_fee = PreMintSecrets::random(
  645. keyset_id,
  646. 100.into(),
  647. &SplitTarget::default(),
  648. &fee_and_amounts,
  649. )
  650. .expect("Failed to create preswap");
  651. let swap_no_fee = SwapRequest::new(hundred_proofs.clone(), preswap_no_fee.blinded_messages());
  652. match mint.process_swap_request(swap_no_fee).await {
  653. Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  654. // Expected - didn't pay the fee
  655. }
  656. Err(err) => panic!("Wrong error type: {:?}", err),
  657. Ok(_) => panic!("Should fail when fee not paid"),
  658. }
  659. // Calculate correct fee (1 sat per input proof in this keyset)
  660. let fee = hundred_proofs.len() as u64; // 1 sat per proof = 100 sats fee
  661. let output_amount = 100 - fee;
  662. // Swap with correct fee deduction - should succeed if output_amount > 0
  663. if output_amount > 0 {
  664. let preswap_with_fee = PreMintSecrets::random(
  665. keyset_id,
  666. output_amount.into(),
  667. &SplitTarget::default(),
  668. &fee_and_amounts,
  669. )
  670. .expect("Failed to create preswap with fee");
  671. let swap_with_fee =
  672. SwapRequest::new(hundred_proofs.clone(), preswap_with_fee.blinded_messages());
  673. mint.process_swap_request(swap_with_fee)
  674. .await
  675. .expect("Swap with correct fee should succeed");
  676. }
  677. }
  678. /// Tests melt with fees enabled and swap-before-melt optimization:
  679. /// 1. Create mint with keyset that has fees (1000 ppk = 1 sat per proof)
  680. /// 2. Fund wallet with proofs using default split (optimal denominations)
  681. /// 3. Call melt() - should automatically swap if proofs don't match exactly
  682. /// 4. Verify fee calculations are reasonable
  683. ///
  684. /// Fee calculation:
  685. /// - Initial: 4096 sats in optimal denominations
  686. /// - Melt: 1000 sats, fee_reserve = 20 sats (2%)
  687. /// - inputs_needed = 1020 sats
  688. /// - Target split for 1020: [512, 256, 128, 64, 32, 16, 8, 4] = 8 proofs
  689. /// - target_fee = 8 sats
  690. /// - inputs_total_needed = 1028 sats
  691. ///
  692. /// The wallet uses two-step selection:
  693. /// - Step 1: Try to find exact proofs for inputs_needed (no swap fee)
  694. /// - Step 2: If not exact, select proofs for inputs_total_needed and swap
  695. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  696. async fn test_melt_with_fees_swap_before_melt() {
  697. setup_tracing();
  698. let mint = create_and_start_test_mint()
  699. .await
  700. .expect("Failed to create test mint");
  701. let wallet = create_test_wallet_for_mint(mint.clone())
  702. .await
  703. .expect("Failed to create test wallet");
  704. // Rotate to keyset with 1000 ppk = 1 sat per proof fee
  705. mint.rotate_keyset(
  706. CurrencyUnit::Sat,
  707. cdk_integration_tests::standard_keyset_amounts(32),
  708. 1000, // 1 sat per proof
  709. true,
  710. )
  711. .await
  712. .expect("Failed to rotate keyset");
  713. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  714. // Fund with default split target to get optimal denominations
  715. // Use larger amount to ensure enough margin for swap fees
  716. let initial_amount = 4096u64;
  717. fund_wallet(wallet.clone(), initial_amount, None)
  718. .await
  719. .expect("Failed to fund wallet");
  720. let initial_balance: u64 = wallet.total_balance().await.unwrap().into();
  721. assert_eq!(initial_balance, initial_amount);
  722. let proofs = wallet.get_unspent_proofs().await.unwrap();
  723. let proof_amounts: Vec<u64> = proofs.iter().map(|p| u64::from(p.amount)).collect();
  724. tracing::info!("Proofs after funding: {:?}", proof_amounts);
  725. let proofs_total: u64 = proof_amounts.iter().sum();
  726. assert_eq!(
  727. proofs_total, initial_amount,
  728. "Total proofs should equal funded amount"
  729. );
  730. // Create melt quote for 1000 sats (1_000_000 msats)
  731. // Fake wallet: fee_reserve = max(1, amount * 2%) = 20 sats
  732. let invoice = create_fake_invoice(1_000_000, "".to_string()); // 1000 sats in msats
  733. let melt_quote = wallet
  734. .melt_quote(PaymentMethod::BOLT11, invoice.to_string(), None, None)
  735. .await
  736. .unwrap();
  737. let quote_amount: u64 = melt_quote.amount.into();
  738. let fee_reserve: u64 = melt_quote.fee_reserve.into();
  739. tracing::info!(
  740. "Melt quote: amount={}, fee_reserve={}",
  741. quote_amount,
  742. fee_reserve
  743. );
  744. let initial_proof_count = proofs.len();
  745. tracing::info!(
  746. "Initial state: {} proofs, {} sats",
  747. initial_proof_count,
  748. proofs_total
  749. );
  750. // Perform melt
  751. let prepared = wallet
  752. .prepare_melt(&melt_quote.id, std::collections::HashMap::new())
  753. .await
  754. .unwrap();
  755. let melted = prepared.confirm().await.unwrap();
  756. let melt_amount: u64 = melted.amount().into();
  757. let ln_fee_paid: u64 = melted.fee_paid().into();
  758. tracing::info!(
  759. "Melt completed: amount={}, ln_fee_paid={}",
  760. melt_amount,
  761. ln_fee_paid
  762. );
  763. assert_eq!(melt_amount, quote_amount, "Melt amount should match quote");
  764. // Get final balance and calculate fees
  765. let final_balance: u64 = wallet.total_balance().await.unwrap().into();
  766. let total_spent = initial_amount - final_balance;
  767. let total_fees = total_spent - melt_amount;
  768. tracing::info!(
  769. "Balance: initial={}, final={}, total_spent={}, melt_amount={}, total_fees={}",
  770. initial_amount,
  771. final_balance,
  772. total_spent,
  773. melt_amount,
  774. total_fees
  775. );
  776. // Calculate input fees (swap + melt)
  777. let input_fees = total_fees - ln_fee_paid;
  778. tracing::info!(
  779. "Fee breakdown: total_fees={}, ln_fee={}, input_fees (swap+melt)={}",
  780. total_fees,
  781. ln_fee_paid,
  782. input_fees
  783. );
  784. // Verify input fees are reasonable
  785. // With swap-before-melt optimization, we use fewer proofs for the melt
  786. // Melt uses ~8 proofs for optimal split of 1028, so input_fee ~= 8
  787. // Swap (if any) also has fees, but the optimization minimizes total fees
  788. assert!(
  789. input_fees > 0,
  790. "Should have some input fees with fee-enabled keyset"
  791. );
  792. assert!(
  793. input_fees <= 20,
  794. "Input fees {} should be reasonable (not too high)",
  795. input_fees
  796. );
  797. // Verify we have change remaining
  798. assert!(final_balance > 0, "Should have change remaining after melt");
  799. tracing::info!(
  800. "Test passed: spent {} sats, fees {} (ln={}, input={}), remaining {}",
  801. total_spent,
  802. total_fees,
  803. ln_fee_paid,
  804. input_fees,
  805. final_balance
  806. );
  807. }
  808. /// Tests the "exact match" early return path in melt_with_metadata.
  809. /// When proofs already exactly match inputs_needed_amount, no swap is required.
  810. ///
  811. /// This tests Step 1 of the two-step selection:
  812. /// - Select proofs for inputs_needed_amount
  813. /// - If exact match, use proofs directly without swap
  814. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  815. async fn test_melt_exact_match_no_swap() {
  816. setup_tracing();
  817. let mint = create_and_start_test_mint()
  818. .await
  819. .expect("Failed to create test mint");
  820. let wallet = create_test_wallet_for_mint(mint.clone())
  821. .await
  822. .expect("Failed to create test wallet");
  823. // Use keyset WITHOUT fees to make exact match easier
  824. // (default keyset has no fees)
  825. // Fund with exactly inputs_needed_amount to trigger the exact match path
  826. // For a 1000 sat melt, fee_reserve = max(1, 1000 * 2%) = 20 sats
  827. // inputs_needed = 1000 + 20 = 1020 sats
  828. let initial_amount = 1020u64;
  829. fund_wallet(wallet.clone(), initial_amount, None)
  830. .await
  831. .expect("Failed to fund wallet");
  832. let initial_balance: u64 = wallet.total_balance().await.unwrap().into();
  833. assert_eq!(initial_balance, initial_amount);
  834. let proofs_before = wallet.get_unspent_proofs().await.unwrap();
  835. tracing::info!(
  836. "Proofs before melt: {:?}",
  837. proofs_before
  838. .iter()
  839. .map(|p| u64::from(p.amount))
  840. .collect::<Vec<_>>()
  841. );
  842. // Create melt quote for 1000 sats
  843. // fee_reserve = max(1, 1000 * 2%) = 20 sats
  844. // inputs_needed = 1000 + 20 = 1020 sats = our exact balance
  845. let invoice = create_fake_invoice(1_000_000, "".to_string());
  846. let melt_quote = wallet
  847. .melt_quote(PaymentMethod::BOLT11, invoice.to_string(), None, None)
  848. .await
  849. .unwrap();
  850. let quote_amount: u64 = melt_quote.amount.into();
  851. let fee_reserve: u64 = melt_quote.fee_reserve.into();
  852. let inputs_needed = quote_amount + fee_reserve;
  853. tracing::info!(
  854. "Melt quote: amount={}, fee_reserve={}, inputs_needed={}",
  855. quote_amount,
  856. fee_reserve,
  857. inputs_needed
  858. );
  859. // Perform melt
  860. let prepared = wallet
  861. .prepare_melt(&melt_quote.id, std::collections::HashMap::new())
  862. .await
  863. .unwrap();
  864. let melted = prepared.confirm().await.unwrap();
  865. let melt_amount: u64 = melted.amount().into();
  866. let ln_fee_paid: u64 = melted.fee_paid().into();
  867. tracing::info!(
  868. "Melt completed: amount={}, ln_fee_paid={}",
  869. melt_amount,
  870. ln_fee_paid
  871. );
  872. assert_eq!(melt_amount, quote_amount, "Melt amount should match quote");
  873. // Get final balance
  874. let final_balance: u64 = wallet.total_balance().await.unwrap().into();
  875. let total_spent = initial_amount - final_balance;
  876. let total_fees = total_spent - melt_amount;
  877. tracing::info!(
  878. "Balance: initial={}, final={}, total_spent={}, total_fees={}",
  879. initial_amount,
  880. final_balance,
  881. total_spent,
  882. total_fees
  883. );
  884. // With no keyset fees and no swap needed, total fees should just be ln_fee
  885. // (no input fees since default keyset has 0 ppk)
  886. assert_eq!(
  887. total_fees, ln_fee_paid,
  888. "Total fees should equal LN fee (no swap or input fees with 0 ppk keyset)"
  889. );
  890. tracing::info!("Test passed: exact match path used, no swap needed");
  891. }
  892. /// Tests melt with small amounts where swap margin is too tight.
  893. /// When fees are high relative to the melt amount, the swap-before-melt
  894. /// optimization may not have enough margin to cover both input and output fees.
  895. /// In this case, the wallet should fall back to using proofs directly.
  896. ///
  897. /// Scenario:
  898. /// - Fund with 8 sats
  899. /// - Melt 5 sats (with 2% fee_reserve = 1 sat min, so inputs_needed = 6)
  900. /// - With 1 sat per proof fee, the swap margin becomes too tight
  901. /// - Should still succeed by falling back to direct melt
  902. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  903. async fn test_melt_small_amount_tight_margin() {
  904. setup_tracing();
  905. let mint = create_and_start_test_mint()
  906. .await
  907. .expect("Failed to create test mint");
  908. let wallet = create_test_wallet_for_mint(mint.clone())
  909. .await
  910. .expect("Failed to create test wallet");
  911. // Rotate to keyset with 1000 ppk = 1 sat per proof fee
  912. mint.rotate_keyset(
  913. CurrencyUnit::Sat,
  914. cdk_integration_tests::standard_keyset_amounts(32),
  915. 1000,
  916. true,
  917. )
  918. .await
  919. .expect("Failed to rotate keyset");
  920. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  921. // Fund with enough to cover melt + fees, but amounts that will trigger swap
  922. // 32 sats gives us enough margin even with 1 sat/proof fees
  923. let initial_amount = 32;
  924. fund_wallet(wallet.clone(), initial_amount, None)
  925. .await
  926. .expect("Failed to fund wallet");
  927. let initial_balance: u64 = wallet.total_balance().await.unwrap().into();
  928. assert_eq!(initial_balance, initial_amount);
  929. let proofs = wallet.get_unspent_proofs().await.unwrap();
  930. tracing::info!(
  931. "Proofs after funding: {:?}",
  932. proofs
  933. .iter()
  934. .map(|p| u64::from(p.amount))
  935. .collect::<Vec<_>>()
  936. );
  937. // Create melt quote for 5 sats
  938. // fee_reserve = max(1, 5 * 2%) = 1 sat
  939. // inputs_needed = 5 + 1 = 6 sats
  940. let invoice = create_fake_invoice(5_000, "".to_string()); // 5 sats in msats
  941. let melt_quote = wallet
  942. .melt_quote(PaymentMethod::BOLT11, invoice.to_string(), None, None)
  943. .await
  944. .unwrap();
  945. let quote_amount: u64 = melt_quote.amount.into();
  946. let fee_reserve: u64 = melt_quote.fee_reserve.into();
  947. tracing::info!(
  948. "Melt quote: amount={}, fee_reserve={}, inputs_needed={}",
  949. quote_amount,
  950. fee_reserve,
  951. quote_amount + fee_reserve
  952. );
  953. // This should succeed even with tight margins
  954. let prepared = wallet
  955. .prepare_melt(&melt_quote.id, std::collections::HashMap::new())
  956. .await
  957. .expect("Prepare melt should succeed");
  958. let melted = prepared
  959. .confirm()
  960. .await
  961. .expect("Melt should succeed even with tight swap margin");
  962. let melt_amount: u64 = melted.amount().into();
  963. assert_eq!(melt_amount, quote_amount, "Melt amount should match quote");
  964. let final_balance: u64 = wallet.total_balance().await.unwrap().into();
  965. tracing::info!(
  966. "Melt completed: amount={}, fee_paid={}, final_balance={}",
  967. melted.amount(),
  968. melted.fee_paid(),
  969. final_balance
  970. );
  971. // Verify balance decreased appropriately
  972. assert!(
  973. final_balance < initial_balance,
  974. "Balance should decrease after melt"
  975. );
  976. }
  977. /// Tests melt where swap proofs barely cover swap_amount + input_fee.
  978. ///
  979. /// This is a regression test for a bug where the swap-before-melt was called
  980. /// with include_fees=true, causing it to try to add output fees on top of
  981. /// swap_amount + input_fee. When proofs_to_swap had just barely enough value,
  982. /// this caused InsufficientFunds error.
  983. ///
  984. /// Scenario (from the bug):
  985. /// - Balance: proofs like [4, 2, 1, 1] = 8 sats
  986. /// - Melt: 5 sats + 1 fee_reserve = 6 inputs_needed
  987. /// - target_fee = 1 (for optimal output split)
  988. /// - inputs_total_needed = 7
  989. /// - proofs_to_send = [4, 2] = 6, proofs_to_swap = [1, 1] = 2
  990. /// - swap_amount = 1 sat (7 - 6)
  991. /// - swap input_fee = 1 sat (2 proofs)
  992. /// - Before fix: include_fees=true tried to add output fee, causing failure
  993. /// - After fix: include_fees=false, swap succeeds
  994. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  995. async fn test_melt_swap_tight_margin_regression() {
  996. setup_tracing();
  997. let mint = create_and_start_test_mint()
  998. .await
  999. .expect("Failed to create test mint");
  1000. let wallet = create_test_wallet_for_mint(mint.clone())
  1001. .await
  1002. .expect("Failed to create test wallet");
  1003. // Rotate to keyset with 250 ppk = 0.25 sat per proof fee (same as original bug scenario)
  1004. // This means 4 proofs = 1 sat fee
  1005. mint.rotate_keyset(
  1006. CurrencyUnit::Sat,
  1007. cdk_integration_tests::standard_keyset_amounts(32),
  1008. 250,
  1009. true,
  1010. )
  1011. .await
  1012. .expect("Failed to rotate keyset");
  1013. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  1014. // Fund with 100 sats using default split to get optimal denominations
  1015. // This should give us proofs like [64, 32, 4] or similar power-of-2 split
  1016. let initial_amount = 100;
  1017. fund_wallet(wallet.clone(), initial_amount, None)
  1018. .await
  1019. .expect("Failed to fund wallet");
  1020. let initial_balance: u64 = wallet.total_balance().await.unwrap().into();
  1021. assert_eq!(initial_balance, initial_amount);
  1022. let proofs = wallet.get_unspent_proofs().await.unwrap();
  1023. let proof_amounts: Vec<u64> = proofs.iter().map(|p| u64::from(p.amount)).collect();
  1024. tracing::info!("Proofs after funding: {:?}", proof_amounts);
  1025. // Create melt quote for 5 sats (5000 msats)
  1026. // fee_reserve = max(1, 5 * 2%) = 1 sat
  1027. // inputs_needed = 5 + 1 = 6 sats
  1028. // The optimal split for 6 sats is [4, 2] (2 proofs)
  1029. // target_fee = 1 sat (2 proofs * 0.25, rounded up)
  1030. // inputs_total_needed = 7 sats
  1031. //
  1032. // If we don't have exact [4, 2] proofs, we'll need to swap.
  1033. // The swap path is what triggered the original bug when proofs_to_swap
  1034. // had tight margins and include_fees=true was incorrectly used.
  1035. let invoice = create_fake_invoice(5_000, "".to_string());
  1036. let melt_quote = wallet
  1037. .melt_quote(PaymentMethod::BOLT11, invoice.to_string(), None, None)
  1038. .await
  1039. .unwrap();
  1040. let quote_amount: u64 = melt_quote.amount.into();
  1041. let fee_reserve: u64 = melt_quote.fee_reserve.into();
  1042. tracing::info!(
  1043. "Melt quote: amount={}, fee_reserve={}, inputs_needed={}",
  1044. quote_amount,
  1045. fee_reserve,
  1046. quote_amount + fee_reserve
  1047. );
  1048. // This is the key test: melt should succeed even when swap is needed
  1049. // Before the fix, include_fees=true in swap caused InsufficientFunds
  1050. // After the fix, include_fees=false allows the swap to succeed
  1051. let prepared = wallet
  1052. .prepare_melt(&melt_quote.id, std::collections::HashMap::new())
  1053. .await
  1054. .expect("Prepare melt should succeed");
  1055. let melted = prepared
  1056. .confirm()
  1057. .await
  1058. .expect("Melt should succeed with swap-before-melt (regression test)");
  1059. let melt_amount: u64 = melted.amount().into();
  1060. assert_eq!(melt_amount, quote_amount, "Melt amount should match quote");
  1061. let final_balance: u64 = wallet.total_balance().await.unwrap().into();
  1062. tracing::info!(
  1063. "Melt completed: amount={}, fee_paid={}, final_balance={}",
  1064. melted.amount(),
  1065. melted.fee_paid(),
  1066. final_balance
  1067. );
  1068. // Should have change remaining
  1069. assert!(
  1070. final_balance < initial_balance,
  1071. "Balance should decrease after melt"
  1072. );
  1073. assert!(final_balance > 0, "Should have change remaining");
  1074. }
  1075. /// Tests that swap correctly handles amount overflow:
  1076. /// Attempts to create outputs that would overflow u64 when summed.
  1077. /// This should be rejected before any database operations occur.
  1078. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  1079. async fn test_swap_amount_overflow_protection() {
  1080. setup_tracing();
  1081. let mint = create_and_start_test_mint()
  1082. .await
  1083. .expect("Failed to create test mint");
  1084. let wallet = create_test_wallet_for_mint(mint.clone())
  1085. .await
  1086. .expect("Failed to create test wallet");
  1087. // Fund wallet
  1088. fund_wallet(wallet.clone(), 100, None)
  1089. .await
  1090. .expect("Failed to fund wallet");
  1091. let proofs = wallet
  1092. .get_unspent_proofs()
  1093. .await
  1094. .expect("Could not get proofs");
  1095. let keyset_id = get_keyset_id(&mint).await;
  1096. // Try to create outputs that would overflow
  1097. // 2^63 + 2^63 + small amount would overflow u64
  1098. let large_amount = 2_u64.pow(63);
  1099. let pre_mint1 = PreMintSecrets::from_secrets(
  1100. keyset_id,
  1101. vec![large_amount.into()],
  1102. vec![cashu::secret::Secret::generate()],
  1103. )
  1104. .expect("Failed to create pre_mint1");
  1105. let pre_mint2 = PreMintSecrets::from_secrets(
  1106. keyset_id,
  1107. vec![large_amount.into()],
  1108. vec![cashu::secret::Secret::generate()],
  1109. )
  1110. .expect("Failed to create pre_mint2");
  1111. let mut combined_pre_mint = PreMintSecrets::from_secrets(
  1112. keyset_id,
  1113. vec![1.into()],
  1114. vec![cashu::secret::Secret::generate()],
  1115. )
  1116. .expect("Failed to create combined_pre_mint");
  1117. combined_pre_mint.combine(pre_mint1);
  1118. combined_pre_mint.combine(pre_mint2);
  1119. let swap_request = SwapRequest::new(proofs, combined_pre_mint.blinded_messages());
  1120. // Should fail with overflow/amount error
  1121. match mint.process_swap_request(swap_request).await {
  1122. Err(cdk::Error::NUT03(cdk::nuts::nut03::Error::Amount(_)))
  1123. | Err(cdk::Error::AmountOverflow)
  1124. | Err(cdk::Error::AmountError(_))
  1125. | Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  1126. // Any of these errors are acceptable for overflow
  1127. }
  1128. Err(err) => panic!("Unexpected error type: {:?}", err),
  1129. Ok(_) => panic!("Overflow swap should not succeed"),
  1130. }
  1131. }
  1132. /// Tests swap state transitions through pubsub notifications:
  1133. /// 1. Subscribe to proof state changes
  1134. /// 2. Execute swap
  1135. /// 3. Verify Pending then Spent state transitions are received
  1136. /// Validates NUT-17 notification behavior.
  1137. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  1138. async fn test_swap_state_transition_notifications() {
  1139. setup_tracing();
  1140. let mint = create_and_start_test_mint()
  1141. .await
  1142. .expect("Failed to create test mint");
  1143. let wallet = create_test_wallet_for_mint(mint.clone())
  1144. .await
  1145. .expect("Failed to create test wallet");
  1146. // Fund wallet
  1147. fund_wallet(wallet.clone(), 100, None)
  1148. .await
  1149. .expect("Failed to fund wallet");
  1150. let proofs = wallet
  1151. .get_unspent_proofs()
  1152. .await
  1153. .expect("Could not get proofs");
  1154. let keyset_id = get_keyset_id(&mint).await;
  1155. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  1156. let preswap = PreMintSecrets::random(
  1157. keyset_id,
  1158. 100.into(),
  1159. &SplitTarget::default(),
  1160. &fee_and_amounts,
  1161. )
  1162. .expect("Failed to create preswap");
  1163. let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
  1164. // Subscribe to proof state changes
  1165. let proof_ys: Vec<String> = proofs.iter().map(|p| p.y().unwrap().to_string()).collect();
  1166. let mut listener = mint
  1167. .pubsub_manager()
  1168. .subscribe(cdk::subscription::Params {
  1169. kind: cdk::nuts::nut17::Kind::ProofState,
  1170. filters: proof_ys.clone(),
  1171. id: Arc::new("test_swap_notifications".into()),
  1172. })
  1173. .expect("Should subscribe successfully");
  1174. // Execute swap
  1175. mint.process_swap_request(swap_request)
  1176. .await
  1177. .expect("Swap should succeed");
  1178. // Give pubsub time to deliver messages
  1179. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  1180. // Collect all state transition notifications
  1181. let mut state_transitions: HashMap<String, Vec<State>> = HashMap::new();
  1182. while let Some(msg) = listener.try_recv() {
  1183. match msg.into_inner() {
  1184. cashu::NotificationPayload::ProofState(cashu::ProofState { y, state, .. }) => {
  1185. state_transitions
  1186. .entry(y.to_string())
  1187. .or_default()
  1188. .push(state);
  1189. }
  1190. _ => panic!("Unexpected notification type"),
  1191. }
  1192. }
  1193. // Verify each proof went through Pending -> Spent transition
  1194. for y in proof_ys {
  1195. let transitions = state_transitions
  1196. .get(&y)
  1197. .expect("Should have transitions for proof");
  1198. assert_eq!(
  1199. transitions,
  1200. &vec![State::Pending, State::Spent],
  1201. "Proof should transition from Pending to Spent"
  1202. );
  1203. }
  1204. }
  1205. /// Tests that swap fails gracefully when proof states cannot be updated:
  1206. /// This would test the rollback path where proofs are added but state update fails.
  1207. /// In the current implementation, this should trigger rollback of both proofs and blinded messages.
  1208. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  1209. async fn test_swap_proof_state_consistency() {
  1210. setup_tracing();
  1211. let mint = create_and_start_test_mint()
  1212. .await
  1213. .expect("Failed to create test mint");
  1214. let wallet = create_test_wallet_for_mint(mint.clone())
  1215. .await
  1216. .expect("Failed to create test wallet");
  1217. // Fund wallet
  1218. fund_wallet(wallet.clone(), 100, None)
  1219. .await
  1220. .expect("Failed to fund wallet");
  1221. let proofs = wallet
  1222. .get_unspent_proofs()
  1223. .await
  1224. .expect("Could not get proofs");
  1225. let keyset_id = get_keyset_id(&mint).await;
  1226. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  1227. // Execute successful swap
  1228. let preswap = PreMintSecrets::random(
  1229. keyset_id,
  1230. 100.into(),
  1231. &SplitTarget::default(),
  1232. &fee_and_amounts,
  1233. )
  1234. .expect("Failed to create preswap");
  1235. let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
  1236. mint.process_swap_request(swap_request)
  1237. .await
  1238. .expect("Swap should succeed");
  1239. // Verify all proofs have consistent state (Spent)
  1240. let proof_ys: Vec<_> = proofs.iter().map(|p| p.y().unwrap()).collect();
  1241. let states = mint
  1242. .localstore()
  1243. .get_proofs_states(&proof_ys)
  1244. .await
  1245. .expect("Failed to get proof states");
  1246. // All states should be Some(Spent) - none should be None or Pending
  1247. for (i, state) in states.iter().enumerate() {
  1248. match state {
  1249. Some(State::Spent) => {
  1250. // Expected state
  1251. }
  1252. Some(other_state) => {
  1253. panic!("Proof {} in unexpected state: {:?}", i, other_state)
  1254. }
  1255. None => {
  1256. panic!("Proof {} has no state (should be Spent)", i)
  1257. }
  1258. }
  1259. }
  1260. }
  1261. /// Tests that wallet correctly increments keyset counters when receiving proofs
  1262. /// from multiple keysets and then performing operations with them.
  1263. ///
  1264. /// This test validates:
  1265. /// 1. Wallet can receive proofs from multiple different keysets
  1266. /// 2. Counter is correctly incremented for the target keyset during swap
  1267. /// 3. Database maintains separate counters for each keyset
  1268. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  1269. async fn test_wallet_multi_keyset_counter_updates() {
  1270. setup_tracing();
  1271. let mint = create_and_start_test_mint()
  1272. .await
  1273. .expect("Failed to create test mint");
  1274. let wallet = create_test_wallet_for_mint(mint.clone())
  1275. .await
  1276. .expect("Failed to create test wallet");
  1277. // Fund wallet with initial 100 sats using first keyset
  1278. fund_wallet(wallet.clone(), 100, None)
  1279. .await
  1280. .expect("Failed to fund wallet");
  1281. let first_keyset_id = get_keyset_id(&mint).await;
  1282. // Rotate to a second keyset
  1283. mint.rotate_keyset(
  1284. CurrencyUnit::Sat,
  1285. cdk_integration_tests::standard_keyset_amounts(32),
  1286. 0,
  1287. true,
  1288. )
  1289. .await
  1290. .expect("Failed to rotate keyset");
  1291. // Wait for keyset rotation to propagate
  1292. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  1293. // Refresh wallet keysets to know about the new keyset
  1294. wallet
  1295. .refresh_keysets()
  1296. .await
  1297. .expect("Failed to refresh wallet keysets");
  1298. // Fund wallet again with 100 sats using second keyset
  1299. fund_wallet(wallet.clone(), 100, None)
  1300. .await
  1301. .expect("Failed to fund wallet with second keyset");
  1302. let second_keyset_id = mint
  1303. .pubkeys()
  1304. .keysets
  1305. .iter()
  1306. .find(|k| k.id != first_keyset_id)
  1307. .expect("Should have second keyset")
  1308. .id;
  1309. // Verify we now have proofs from two different keysets
  1310. let all_proofs = wallet
  1311. .get_unspent_proofs()
  1312. .await
  1313. .expect("Could not get proofs");
  1314. let keysets_in_use: std::collections::HashSet<_> =
  1315. all_proofs.iter().map(|p| p.keyset_id).collect();
  1316. assert_eq!(
  1317. keysets_in_use.len(),
  1318. 2,
  1319. "Should have proofs from 2 different keysets"
  1320. );
  1321. assert!(
  1322. keysets_in_use.contains(&first_keyset_id),
  1323. "Should have proofs from first keyset"
  1324. );
  1325. assert!(
  1326. keysets_in_use.contains(&second_keyset_id),
  1327. "Should have proofs from second keyset"
  1328. );
  1329. // Get initial total issued and redeemed for both keysets before swap
  1330. let total_issued_before = mint.total_issued().await.unwrap();
  1331. let total_redeemed_before = mint.total_redeemed().await.unwrap();
  1332. let first_keyset_issued_before = total_issued_before
  1333. .get(&first_keyset_id)
  1334. .copied()
  1335. .unwrap_or(Amount::ZERO);
  1336. let first_keyset_redeemed_before = total_redeemed_before
  1337. .get(&first_keyset_id)
  1338. .copied()
  1339. .unwrap_or(Amount::ZERO);
  1340. let second_keyset_issued_before = total_issued_before
  1341. .get(&second_keyset_id)
  1342. .copied()
  1343. .unwrap_or(Amount::ZERO);
  1344. let second_keyset_redeemed_before = total_redeemed_before
  1345. .get(&second_keyset_id)
  1346. .copied()
  1347. .unwrap_or(Amount::ZERO);
  1348. tracing::info!(
  1349. "Before swap - First keyset: issued={}, redeemed={}",
  1350. first_keyset_issued_before,
  1351. first_keyset_redeemed_before
  1352. );
  1353. tracing::info!(
  1354. "Before swap - Second keyset: issued={}, redeemed={}",
  1355. second_keyset_issued_before,
  1356. second_keyset_redeemed_before
  1357. );
  1358. // Both keysets should have issued 100 sats
  1359. assert_eq!(
  1360. first_keyset_issued_before,
  1361. Amount::from(100),
  1362. "First keyset should have issued 100 sats"
  1363. );
  1364. assert_eq!(
  1365. second_keyset_issued_before,
  1366. Amount::from(100),
  1367. "Second keyset should have issued 100 sats"
  1368. );
  1369. // Neither should have redeemed anything yet
  1370. assert_eq!(
  1371. first_keyset_redeemed_before,
  1372. Amount::ZERO,
  1373. "First keyset should have redeemed 0 sats before swap"
  1374. );
  1375. assert_eq!(
  1376. second_keyset_redeemed_before,
  1377. Amount::ZERO,
  1378. "Second keyset should have redeemed 0 sats before swap"
  1379. );
  1380. // Now perform a swap with all proofs - this should only increment the counter
  1381. // for the active (second) keyset, not for the first keyset
  1382. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  1383. let total_amount = all_proofs.total_amount().expect("Should get total amount");
  1384. // Create swap using the active (second) keyset
  1385. let preswap = PreMintSecrets::random(
  1386. second_keyset_id,
  1387. total_amount,
  1388. &SplitTarget::default(),
  1389. &fee_and_amounts,
  1390. )
  1391. .expect("Failed to create preswap");
  1392. let swap_request = SwapRequest::new(all_proofs.clone(), preswap.blinded_messages());
  1393. // Execute the swap
  1394. let swap_response = mint
  1395. .process_swap_request(swap_request)
  1396. .await
  1397. .expect("Swap should succeed");
  1398. // Verify response
  1399. assert_eq!(
  1400. swap_response.signatures.len(),
  1401. preswap.blinded_messages().len(),
  1402. "Should receive signature for each blinded message"
  1403. );
  1404. // All the new proofs should be from the second (active) keyset
  1405. let keys = mint
  1406. .pubkeys()
  1407. .keysets
  1408. .iter()
  1409. .find(|k| k.id == second_keyset_id)
  1410. .expect("Should find second keyset")
  1411. .keys
  1412. .clone();
  1413. let new_proofs = construct_proofs(
  1414. swap_response.signatures,
  1415. preswap.rs(),
  1416. preswap.secrets(),
  1417. &keys,
  1418. )
  1419. .expect("Failed to construct proofs");
  1420. // Verify all new proofs use the second keyset
  1421. for proof in &new_proofs {
  1422. assert_eq!(
  1423. proof.keyset_id, second_keyset_id,
  1424. "All new proofs should use the active (second) keyset"
  1425. );
  1426. }
  1427. // Verify total issued and redeemed after swap
  1428. let total_issued_after = mint.total_issued().await.unwrap();
  1429. let total_redeemed_after = mint.total_redeemed().await.unwrap();
  1430. let first_keyset_issued_after = total_issued_after
  1431. .get(&first_keyset_id)
  1432. .copied()
  1433. .unwrap_or(Amount::ZERO);
  1434. let first_keyset_redeemed_after = total_redeemed_after
  1435. .get(&first_keyset_id)
  1436. .copied()
  1437. .unwrap_or(Amount::ZERO);
  1438. let second_keyset_issued_after = total_issued_after
  1439. .get(&second_keyset_id)
  1440. .copied()
  1441. .unwrap_or(Amount::ZERO);
  1442. let second_keyset_redeemed_after = total_redeemed_after
  1443. .get(&second_keyset_id)
  1444. .copied()
  1445. .unwrap_or(Amount::ZERO);
  1446. tracing::info!(
  1447. "After swap - First keyset: issued={}, redeemed={}",
  1448. first_keyset_issued_after,
  1449. first_keyset_redeemed_after
  1450. );
  1451. tracing::info!(
  1452. "After swap - Second keyset: issued={}, redeemed={}",
  1453. second_keyset_issued_after,
  1454. second_keyset_redeemed_after
  1455. );
  1456. // After swap:
  1457. // - First keyset: issued stays 100, redeemed increases by 100 (all its proofs were spent in swap)
  1458. // - Second keyset: issued increases by 200 (original 100 + new 100 from swap output),
  1459. // redeemed increases by 100 (its proofs from first funding were spent)
  1460. assert_eq!(
  1461. first_keyset_issued_after,
  1462. Amount::from(100),
  1463. "First keyset issued should stay 100 sats (no new issuance)"
  1464. );
  1465. assert_eq!(
  1466. first_keyset_redeemed_after,
  1467. Amount::from(100),
  1468. "First keyset should have redeemed 100 sats (all its proofs spent in swap)"
  1469. );
  1470. assert_eq!(
  1471. second_keyset_issued_after,
  1472. Amount::from(300),
  1473. "Second keyset should have issued 300 sats total (100 initial + 100 the second funding + 100 from swap output from the old keyset)"
  1474. );
  1475. assert_eq!(
  1476. second_keyset_redeemed_after,
  1477. Amount::from(100),
  1478. "Second keyset should have redeemed 100 sats (its proofs from initial funding spent in swap)"
  1479. );
  1480. // The test verifies that:
  1481. // 1. We can have proofs from multiple keysets in a wallet
  1482. // 2. Swap operation processes inputs from any keyset but creates outputs using active keyset
  1483. // 3. The keyset_counter table correctly handles counters for different keysets independently
  1484. // 4. The database upsert logic in increment_keyset_counter works for multiple keysets
  1485. // 5. Total issued and redeemed are tracked correctly per keyset during multi-keyset swaps
  1486. }