test_swap_flow.rs 54 KB

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