test_swap_flow.rs 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271
  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_integration_tests::init_pure_tests::*;
  20. /// Helper to get the active keyset ID from a mint
  21. async fn get_keyset_id(mint: &Mint) -> Id {
  22. let keys = mint.pubkeys().keysets.first().unwrap().clone();
  23. keys.verify_id()
  24. .expect("Keyset ID generation is successful");
  25. keys.id
  26. }
  27. /// Tests the complete happy path of a swap operation:
  28. /// 1. Wallet is funded with tokens
  29. /// 2. Blinded messages are added to database
  30. /// 3. Outputs are signed by mint
  31. /// 4. Input proofs are verified
  32. /// 5. Transaction is balanced
  33. /// 6. Proofs are added and marked as spent
  34. /// 7. Blind signatures are saved
  35. /// All steps should succeed and database should be in consistent state.
  36. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  37. async fn test_swap_happy_path() {
  38. setup_tracing();
  39. let mint = create_and_start_test_mint()
  40. .await
  41. .expect("Failed to create test mint");
  42. let wallet = create_test_wallet_for_mint(mint.clone())
  43. .await
  44. .expect("Failed to create test wallet");
  45. // Fund wallet with 100 sats
  46. fund_wallet(wallet.clone(), 100, None)
  47. .await
  48. .expect("Failed to fund wallet");
  49. let proofs = wallet
  50. .get_unspent_proofs()
  51. .await
  52. .expect("Could not get proofs");
  53. let keyset_id = get_keyset_id(&mint).await;
  54. // Check initial amounts after minting
  55. let total_issued = mint.total_issued().await.unwrap();
  56. let total_redeemed = mint.total_redeemed().await.unwrap();
  57. let initial_issued = total_issued
  58. .get(&keyset_id)
  59. .copied()
  60. .unwrap_or(Amount::ZERO);
  61. let initial_redeemed = total_redeemed
  62. .get(&keyset_id)
  63. .copied()
  64. .unwrap_or(Amount::ZERO);
  65. assert_eq!(
  66. initial_issued,
  67. Amount::from(100),
  68. "Should have issued 100 sats"
  69. );
  70. assert_eq!(
  71. initial_redeemed,
  72. Amount::ZERO,
  73. "Should have redeemed 0 sats initially"
  74. );
  75. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  76. // Create swap request for same amount (100 sats)
  77. let preswap = PreMintSecrets::random(
  78. keyset_id,
  79. 100.into(),
  80. &SplitTarget::default(),
  81. &fee_and_amounts,
  82. )
  83. .expect("Failed to create preswap");
  84. let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
  85. // Execute swap
  86. let swap_response = mint
  87. .process_swap_request(swap_request)
  88. .await
  89. .expect("Swap should succeed");
  90. // Verify response contains correct number of signatures
  91. assert_eq!(
  92. swap_response.signatures.len(),
  93. preswap.blinded_messages().len(),
  94. "Should receive signature for each blinded message"
  95. );
  96. // Verify input proofs are marked as spent
  97. let states = mint
  98. .localstore()
  99. .get_proofs_states(&proofs.iter().map(|p| p.y().unwrap()).collect::<Vec<_>>())
  100. .await
  101. .expect("Failed to get proof states");
  102. for state in states {
  103. assert_eq!(
  104. State::Spent,
  105. state.expect("State should be known"),
  106. "All input proofs should be marked as spent"
  107. );
  108. }
  109. // Verify blind signatures were saved
  110. let saved_signatures = mint
  111. .localstore()
  112. .get_blind_signatures(
  113. &preswap
  114. .blinded_messages()
  115. .iter()
  116. .map(|bm| bm.blinded_secret)
  117. .collect::<Vec<_>>(),
  118. )
  119. .await
  120. .expect("Failed to get blind signatures");
  121. assert_eq!(
  122. saved_signatures.len(),
  123. swap_response.signatures.len(),
  124. "All signatures should be saved"
  125. );
  126. // Check keyset amounts after swap
  127. // Swap redeems old proofs (100 sats) and issues new proofs (100 sats)
  128. let total_issued = mint.total_issued().await.unwrap();
  129. let total_redeemed = mint.total_redeemed().await.unwrap();
  130. let after_issued = total_issued
  131. .get(&keyset_id)
  132. .copied()
  133. .unwrap_or(Amount::ZERO);
  134. let after_redeemed = total_redeemed
  135. .get(&keyset_id)
  136. .copied()
  137. .unwrap_or(Amount::ZERO);
  138. assert_eq!(
  139. after_issued,
  140. Amount::from(200),
  141. "Should have issued 200 sats total (initial 100 + swap 100)"
  142. );
  143. assert_eq!(
  144. after_redeemed,
  145. Amount::from(100),
  146. "Should have redeemed 100 sats from the swap"
  147. );
  148. }
  149. /// Tests that duplicate blinded messages are rejected:
  150. /// 1. First swap with blinded messages succeeds
  151. /// 2. Second swap attempt with same blinded messages fails
  152. /// 3. BlindedMessageWriter should prevent reuse
  153. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  154. async fn test_swap_duplicate_blinded_messages() {
  155. setup_tracing();
  156. let mint = create_and_start_test_mint()
  157. .await
  158. .expect("Failed to create test mint");
  159. let wallet = create_test_wallet_for_mint(mint.clone())
  160. .await
  161. .expect("Failed to create test wallet");
  162. // Fund wallet with 200 sats (enough for two swaps)
  163. fund_wallet(wallet.clone(), 200, None)
  164. .await
  165. .expect("Failed to fund wallet");
  166. let all_proofs = wallet
  167. .get_unspent_proofs()
  168. .await
  169. .expect("Could not get proofs");
  170. // Split proofs into two sets
  171. let mid = all_proofs.len() / 2;
  172. let proofs1: Vec<_> = all_proofs.iter().take(mid).cloned().collect();
  173. let proofs2: Vec<_> = all_proofs.iter().skip(mid).cloned().collect();
  174. let keyset_id = get_keyset_id(&mint).await;
  175. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  176. // Create blinded messages for first swap
  177. let preswap = PreMintSecrets::random(
  178. keyset_id,
  179. proofs1.total_amount().unwrap(),
  180. &SplitTarget::default(),
  181. &fee_and_amounts,
  182. )
  183. .expect("Failed to create preswap");
  184. let blinded_messages = preswap.blinded_messages();
  185. // First swap should succeed
  186. let swap_request1 = SwapRequest::new(proofs1, blinded_messages.clone());
  187. mint.process_swap_request(swap_request1)
  188. .await
  189. .expect("First swap should succeed");
  190. // Second swap with SAME blinded messages should fail
  191. let swap_request2 = SwapRequest::new(proofs2, blinded_messages.clone());
  192. let result = mint.process_swap_request(swap_request2).await;
  193. assert!(
  194. result.is_err(),
  195. "Second swap with duplicate blinded messages should fail"
  196. );
  197. }
  198. /// Tests that swap correctly rejects double-spending attempts:
  199. /// 1. First swap with proofs succeeds
  200. /// 2. Second swap with same proofs fails with TokenAlreadySpent
  201. /// 3. ProofWriter should detect already-spent proofs
  202. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  203. async fn test_swap_double_spend_detection() {
  204. setup_tracing();
  205. let mint = create_and_start_test_mint()
  206. .await
  207. .expect("Failed to create test mint");
  208. let wallet = create_test_wallet_for_mint(mint.clone())
  209. .await
  210. .expect("Failed to create test wallet");
  211. // Fund wallet with 100 sats
  212. fund_wallet(wallet.clone(), 100, None)
  213. .await
  214. .expect("Failed to fund wallet");
  215. let proofs = wallet
  216. .get_unspent_proofs()
  217. .await
  218. .expect("Could not get proofs");
  219. let keyset_id = get_keyset_id(&mint).await;
  220. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  221. // First swap
  222. let preswap1 = PreMintSecrets::random(
  223. keyset_id,
  224. 100.into(),
  225. &SplitTarget::default(),
  226. &fee_and_amounts,
  227. )
  228. .expect("Failed to create preswap");
  229. let swap_request1 = SwapRequest::new(proofs.clone(), preswap1.blinded_messages());
  230. mint.process_swap_request(swap_request1)
  231. .await
  232. .expect("First swap should succeed");
  233. // Second swap with same proofs should fail
  234. let preswap2 = PreMintSecrets::random(
  235. keyset_id,
  236. 100.into(),
  237. &SplitTarget::default(),
  238. &fee_and_amounts,
  239. )
  240. .expect("Failed to create preswap");
  241. let swap_request2 = SwapRequest::new(proofs.clone(), preswap2.blinded_messages());
  242. let result = mint.process_swap_request(swap_request2).await;
  243. match result {
  244. Err(cdk::Error::TokenAlreadySpent) => {
  245. // Expected error
  246. }
  247. Err(err) => panic!("Wrong error type: {:?}", err),
  248. Ok(_) => panic!("Double spend should not succeed"),
  249. }
  250. }
  251. /// Tests that unbalanced swap requests are rejected:
  252. /// Case 1: Output amount < Input amount (trying to steal from mint)
  253. /// Case 2: Output amount > Input amount (trying to create tokens)
  254. /// Both should fail with TransactionUnbalanced error.
  255. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  256. async fn test_swap_unbalanced_transaction_detection() {
  257. setup_tracing();
  258. let mint = create_and_start_test_mint()
  259. .await
  260. .expect("Failed to create test mint");
  261. let wallet = create_test_wallet_for_mint(mint.clone())
  262. .await
  263. .expect("Failed to create test wallet");
  264. // Fund wallet with 100 sats
  265. fund_wallet(wallet.clone(), 100, None)
  266. .await
  267. .expect("Failed to fund wallet");
  268. let proofs = wallet
  269. .get_unspent_proofs()
  270. .await
  271. .expect("Could not get proofs");
  272. let keyset_id = get_keyset_id(&mint).await;
  273. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  274. // Case 1: Try to swap for LESS (95 < 100) - underpaying
  275. let preswap_less = PreMintSecrets::random(
  276. keyset_id,
  277. 95.into(),
  278. &SplitTarget::default(),
  279. &fee_and_amounts,
  280. )
  281. .expect("Failed to create preswap");
  282. let swap_request_less = SwapRequest::new(proofs.clone(), preswap_less.blinded_messages());
  283. match mint.process_swap_request(swap_request_less).await {
  284. Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  285. // Expected error
  286. }
  287. Err(err) => panic!("Wrong error type for underpay: {:?}", err),
  288. Ok(_) => panic!("Unbalanced swap (underpay) should not succeed"),
  289. }
  290. // Case 2: Try to swap for MORE (105 > 100) - overpaying/creating tokens
  291. let preswap_more = PreMintSecrets::random(
  292. keyset_id,
  293. 105.into(),
  294. &SplitTarget::default(),
  295. &fee_and_amounts,
  296. )
  297. .expect("Failed to create preswap");
  298. let swap_request_more = SwapRequest::new(proofs.clone(), preswap_more.blinded_messages());
  299. match mint.process_swap_request(swap_request_more).await {
  300. Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  301. // Expected error
  302. }
  303. Err(err) => panic!("Wrong error type for overpay: {:?}", err),
  304. Ok(_) => panic!("Unbalanced swap (overpay) should not succeed"),
  305. }
  306. }
  307. /// Tests that swap requests with empty inputs or outputs are rejected:
  308. /// Case 1: Empty outputs (inputs without outputs)
  309. /// Case 2: Empty inputs (outputs without inputs)
  310. /// Both should fail. Currently returns UnitMismatch (11010) instead of
  311. /// TransactionUnbalanced (11002) because there are no keyset IDs to determine units.
  312. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  313. async fn test_swap_empty_inputs_or_outputs() {
  314. setup_tracing();
  315. let mint = create_and_start_test_mint()
  316. .await
  317. .expect("Failed to create test mint");
  318. let wallet = create_test_wallet_for_mint(mint.clone())
  319. .await
  320. .expect("Failed to create test wallet");
  321. // Fund wallet with 100 sats
  322. fund_wallet(wallet.clone(), 100, None)
  323. .await
  324. .expect("Failed to fund wallet");
  325. let proofs = wallet
  326. .get_unspent_proofs()
  327. .await
  328. .expect("Could not get proofs");
  329. // Case 1: Swap request with inputs but empty outputs
  330. // This represents trying to destroy tokens (inputs with no outputs)
  331. let swap_request_empty_outputs = SwapRequest::new(proofs.clone(), vec![]);
  332. match mint.process_swap_request(swap_request_empty_outputs).await {
  333. Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  334. // This would be the more appropriate error
  335. }
  336. Err(err) => panic!("Wrong error type for empty outputs: {:?}", err),
  337. Ok(_) => panic!("Swap with empty outputs should not succeed"),
  338. }
  339. // Case 2: Swap request with empty inputs but with outputs
  340. // This represents trying to create tokens from nothing
  341. let keyset_id = get_keyset_id(&mint).await;
  342. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  343. let preswap = PreMintSecrets::random(
  344. keyset_id,
  345. 100.into(),
  346. &SplitTarget::default(),
  347. &fee_and_amounts,
  348. )
  349. .expect("Failed to create preswap");
  350. let swap_request_empty_inputs = SwapRequest::new(vec![], preswap.blinded_messages());
  351. match mint.process_swap_request(swap_request_empty_inputs).await {
  352. Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  353. // This would be the more appropriate error
  354. }
  355. Err(err) => panic!("Wrong error type for empty inputs: {:?}", err),
  356. Ok(_) => panic!("Swap with empty inputs should not succeed"),
  357. }
  358. }
  359. /// Tests P2PK (Pay-to-Public-Key) spending conditions:
  360. /// 1. Create proofs locked to a public key
  361. /// 2. Attempt swap without signature - should fail
  362. /// 3. Attempt swap with valid signature - should succeed
  363. /// Validates NUT-11 signature enforcement.
  364. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  365. async fn test_swap_p2pk_signature_validation() {
  366. setup_tracing();
  367. let mint = create_and_start_test_mint()
  368. .await
  369. .expect("Failed to create test mint");
  370. let wallet = create_test_wallet_for_mint(mint.clone())
  371. .await
  372. .expect("Failed to create test wallet");
  373. // Fund wallet with 100 sats
  374. fund_wallet(wallet.clone(), 100, None)
  375. .await
  376. .expect("Failed to fund wallet");
  377. let input_proofs = wallet
  378. .get_unspent_proofs()
  379. .await
  380. .expect("Could not get proofs");
  381. let keyset_id = get_keyset_id(&mint).await;
  382. let secret_key = SecretKey::generate();
  383. // Create P2PK locked outputs
  384. let spending_conditions = SpendingConditions::new_p2pk(secret_key.public_key(), None);
  385. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  386. let pre_swap = PreMintSecrets::with_conditions(
  387. keyset_id,
  388. 100.into(),
  389. &SplitTarget::default(),
  390. &spending_conditions,
  391. &fee_and_amounts,
  392. )
  393. .expect("Failed to create P2PK preswap");
  394. let swap_request = SwapRequest::new(input_proofs.clone(), pre_swap.blinded_messages());
  395. // First swap to get P2PK locked proofs
  396. let keys = mint.pubkeys().keysets.first().cloned().unwrap().keys;
  397. let post_swap = mint
  398. .process_swap_request(swap_request)
  399. .await
  400. .expect("Initial swap should succeed");
  401. // Construct proofs from swap response
  402. let mut p2pk_proofs = construct_proofs(
  403. post_swap.signatures,
  404. pre_swap.rs(),
  405. pre_swap.secrets(),
  406. &keys,
  407. )
  408. .expect("Failed to construct proofs");
  409. // Try to spend P2PK proofs WITHOUT signature - should fail
  410. let preswap_unsigned = PreMintSecrets::random(
  411. keyset_id,
  412. 100.into(),
  413. &SplitTarget::default(),
  414. &fee_and_amounts,
  415. )
  416. .expect("Failed to create preswap");
  417. let swap_request_unsigned =
  418. SwapRequest::new(p2pk_proofs.clone(), preswap_unsigned.blinded_messages());
  419. match mint.process_swap_request(swap_request_unsigned).await {
  420. Err(cdk::Error::NUT11(cdk::nuts::nut11::Error::SignaturesNotProvided)) => {
  421. // Expected error
  422. }
  423. Err(err) => panic!("Wrong error type: {:?}", err),
  424. Ok(_) => panic!("Unsigned P2PK spend should fail"),
  425. }
  426. // Sign the proofs with correct key
  427. for proof in &mut p2pk_proofs {
  428. proof
  429. .sign_p2pk(secret_key.clone())
  430. .expect("Failed to sign proof");
  431. }
  432. // Try again WITH signature - should succeed
  433. let preswap_signed = PreMintSecrets::random(
  434. keyset_id,
  435. 100.into(),
  436. &SplitTarget::default(),
  437. &fee_and_amounts,
  438. )
  439. .expect("Failed to create preswap");
  440. let swap_request_signed = SwapRequest::new(p2pk_proofs, preswap_signed.blinded_messages());
  441. mint.process_swap_request(swap_request_signed)
  442. .await
  443. .expect("Signed P2PK spend should succeed");
  444. }
  445. /// Tests rollback behavior when duplicate blinded messages are used:
  446. /// This validates that the BlindedMessageWriter prevents reuse of blinded messages.
  447. /// 1. First swap with blinded messages succeeds
  448. /// 2. Second swap with same blinded messages fails
  449. /// 3. The failure should happen early (during blinded message addition)
  450. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  451. async fn test_swap_rollback_on_duplicate_blinded_message() {
  452. setup_tracing();
  453. let mint = create_and_start_test_mint()
  454. .await
  455. .expect("Failed to create test mint");
  456. let wallet = create_test_wallet_for_mint(mint.clone())
  457. .await
  458. .expect("Failed to create test wallet");
  459. // Fund with enough for multiple swaps
  460. fund_wallet(wallet.clone(), 200, None)
  461. .await
  462. .expect("Failed to fund wallet");
  463. let all_proofs = wallet
  464. .get_unspent_proofs()
  465. .await
  466. .expect("Could not get proofs");
  467. let mid = all_proofs.len() / 2;
  468. let proofs1: Vec<_> = all_proofs.iter().take(mid).cloned().collect();
  469. let proofs2: Vec<_> = all_proofs.iter().skip(mid).cloned().collect();
  470. let keyset_id = get_keyset_id(&mint).await;
  471. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  472. // Create shared blinded messages
  473. let preswap = PreMintSecrets::random(
  474. keyset_id,
  475. proofs1.total_amount().unwrap(),
  476. &SplitTarget::default(),
  477. &fee_and_amounts,
  478. )
  479. .expect("Failed to create preswap");
  480. let blinded_messages = preswap.blinded_messages();
  481. // Extract proof2 ys before moving proofs2
  482. let proof2_ys: Vec<_> = proofs2.iter().map(|p| p.y().unwrap()).collect();
  483. // First swap succeeds
  484. let swap1 = SwapRequest::new(proofs1, blinded_messages.clone());
  485. mint.process_swap_request(swap1)
  486. .await
  487. .expect("First swap should succeed");
  488. // Second swap with duplicate blinded messages should fail early
  489. // The BlindedMessageWriter should detect duplicate and prevent the swap
  490. let swap2 = SwapRequest::new(proofs2, blinded_messages.clone());
  491. let result = mint.process_swap_request(swap2).await;
  492. assert!(
  493. result.is_err(),
  494. "Duplicate blinded messages should cause failure"
  495. );
  496. // Verify the second set of proofs are NOT marked as spent
  497. // (since the swap failed before processing them)
  498. let states = mint
  499. .localstore()
  500. .get_proofs_states(&proof2_ys)
  501. .await
  502. .expect("Failed to get proof states");
  503. for state in states {
  504. assert!(
  505. state.is_none(),
  506. "Proofs from failed swap should not be marked as spent"
  507. );
  508. }
  509. }
  510. /// Tests concurrent swap attempts with same proofs:
  511. /// Spawns 3 concurrent tasks trying to swap the same proofs.
  512. /// Only one should succeed, others should fail with TokenAlreadySpent or TokenPending.
  513. /// Validates that concurrent access is properly handled.
  514. #[tokio::test(flavor = "multi_thread", worker_threads = 3)]
  515. async fn test_swap_concurrent_double_spend_prevention() {
  516. setup_tracing();
  517. let mint = create_and_start_test_mint()
  518. .await
  519. .expect("Failed to create test mint");
  520. let wallet = create_test_wallet_for_mint(mint.clone())
  521. .await
  522. .expect("Failed to create test wallet");
  523. // Fund wallet
  524. fund_wallet(wallet.clone(), 100, None)
  525. .await
  526. .expect("Failed to fund wallet");
  527. let proofs = wallet
  528. .get_unspent_proofs()
  529. .await
  530. .expect("Could not get proofs");
  531. let keyset_id = get_keyset_id(&mint).await;
  532. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  533. // Create 3 different swap requests with SAME proofs but different outputs
  534. let preswap1 = PreMintSecrets::random(
  535. keyset_id,
  536. 100.into(),
  537. &SplitTarget::default(),
  538. &fee_and_amounts,
  539. )
  540. .expect("Failed to create preswap 1");
  541. let preswap2 = PreMintSecrets::random(
  542. keyset_id,
  543. 100.into(),
  544. &SplitTarget::default(),
  545. &fee_and_amounts,
  546. )
  547. .expect("Failed to create preswap 2");
  548. let preswap3 = PreMintSecrets::random(
  549. keyset_id,
  550. 100.into(),
  551. &SplitTarget::default(),
  552. &fee_and_amounts,
  553. )
  554. .expect("Failed to create preswap 3");
  555. let swap_request1 = SwapRequest::new(proofs.clone(), preswap1.blinded_messages());
  556. let swap_request2 = SwapRequest::new(proofs.clone(), preswap2.blinded_messages());
  557. let swap_request3 = SwapRequest::new(proofs.clone(), preswap3.blinded_messages());
  558. // Spawn concurrent tasks
  559. let mint1 = mint.clone();
  560. let mint2 = mint.clone();
  561. let mint3 = mint.clone();
  562. let task1 = tokio::spawn(async move { mint1.process_swap_request(swap_request1).await });
  563. let task2 = tokio::spawn(async move { mint2.process_swap_request(swap_request2).await });
  564. let task3 = tokio::spawn(async move { mint3.process_swap_request(swap_request3).await });
  565. // Wait for all tasks
  566. let results = tokio::try_join!(task1, task2, task3).expect("Tasks should complete");
  567. // Count successes and failures
  568. let mut success_count = 0;
  569. let mut failure_count = 0;
  570. for result in [results.0, results.1, results.2] {
  571. match result {
  572. Ok(_) => success_count += 1,
  573. Err(cdk::Error::TokenAlreadySpent) | Err(cdk::Error::TokenPending) => {
  574. failure_count += 1
  575. }
  576. Err(err) => panic!("Unexpected error: {:?}", err),
  577. }
  578. }
  579. assert_eq!(
  580. success_count, 1,
  581. "Exactly one swap should succeed in concurrent scenario"
  582. );
  583. assert_eq!(
  584. failure_count, 2,
  585. "Exactly two swaps should fail in concurrent scenario"
  586. );
  587. // Verify all proofs are marked as spent
  588. let states = mint
  589. .localstore()
  590. .get_proofs_states(&proofs.iter().map(|p| p.y().unwrap()).collect::<Vec<_>>())
  591. .await
  592. .expect("Failed to get proof states");
  593. for state in states {
  594. assert_eq!(
  595. State::Spent,
  596. state.expect("State should be known"),
  597. "All proofs should be marked as spent after concurrent attempts"
  598. );
  599. }
  600. }
  601. /// Tests swap with fees enabled:
  602. /// 1. Create mint with keyset that has fees (1 sat per proof)
  603. /// 2. Fund wallet with many small proofs
  604. /// 3. Attempt swap without paying fee - should fail
  605. /// 4. Attempt swap with correct fee deduction - should succeed
  606. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  607. async fn test_swap_with_fees() {
  608. setup_tracing();
  609. let mint = create_and_start_test_mint()
  610. .await
  611. .expect("Failed to create test mint");
  612. let wallet = create_test_wallet_for_mint(mint.clone())
  613. .await
  614. .expect("Failed to create test wallet");
  615. // Rotate to keyset with 1 sat per proof fee
  616. mint.rotate_keyset(
  617. CurrencyUnit::Sat,
  618. cdk_integration_tests::standard_keyset_amounts(32),
  619. 1,
  620. )
  621. .await
  622. .expect("Failed to rotate keyset");
  623. // Fund with 1000 sats as individual 1-sat proofs using the fee-based keyset
  624. // Wait a bit for keyset to be available
  625. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  626. fund_wallet(wallet.clone(), 1000, Some(SplitTarget::Value(Amount::ONE)))
  627. .await
  628. .expect("Failed to fund wallet");
  629. let proofs = wallet
  630. .get_unspent_proofs()
  631. .await
  632. .expect("Could not get proofs");
  633. // Take 100 proofs (100 sats total, will need to pay fee)
  634. let hundred_proofs: Vec<_> = proofs.iter().take(100).cloned().collect();
  635. // Get the keyset ID from the proofs (which will be the fee-based keyset)
  636. let keyset_id = hundred_proofs[0].keyset_id;
  637. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  638. // Try to swap for 100 outputs (same as input) - should fail due to unpaid fee
  639. let preswap_no_fee = PreMintSecrets::random(
  640. keyset_id,
  641. 100.into(),
  642. &SplitTarget::default(),
  643. &fee_and_amounts,
  644. )
  645. .expect("Failed to create preswap");
  646. let swap_no_fee = SwapRequest::new(hundred_proofs.clone(), preswap_no_fee.blinded_messages());
  647. match mint.process_swap_request(swap_no_fee).await {
  648. Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  649. // Expected - didn't pay the fee
  650. }
  651. Err(err) => panic!("Wrong error type: {:?}", err),
  652. Ok(_) => panic!("Should fail when fee not paid"),
  653. }
  654. // Calculate correct fee (1 sat per input proof in this keyset)
  655. let fee = hundred_proofs.len() as u64; // 1 sat per proof = 100 sats fee
  656. let output_amount = 100 - fee;
  657. // Swap with correct fee deduction - should succeed if output_amount > 0
  658. if output_amount > 0 {
  659. let preswap_with_fee = PreMintSecrets::random(
  660. keyset_id,
  661. output_amount.into(),
  662. &SplitTarget::default(),
  663. &fee_and_amounts,
  664. )
  665. .expect("Failed to create preswap with fee");
  666. let swap_with_fee =
  667. SwapRequest::new(hundred_proofs.clone(), preswap_with_fee.blinded_messages());
  668. mint.process_swap_request(swap_with_fee)
  669. .await
  670. .expect("Swap with correct fee should succeed");
  671. }
  672. }
  673. /// Tests that swap correctly handles amount overflow:
  674. /// Attempts to create outputs that would overflow u64 when summed.
  675. /// This should be rejected before any database operations occur.
  676. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  677. async fn test_swap_amount_overflow_protection() {
  678. setup_tracing();
  679. let mint = create_and_start_test_mint()
  680. .await
  681. .expect("Failed to create test mint");
  682. let wallet = create_test_wallet_for_mint(mint.clone())
  683. .await
  684. .expect("Failed to create test wallet");
  685. // Fund wallet
  686. fund_wallet(wallet.clone(), 100, None)
  687. .await
  688. .expect("Failed to fund wallet");
  689. let proofs = wallet
  690. .get_unspent_proofs()
  691. .await
  692. .expect("Could not get proofs");
  693. let keyset_id = get_keyset_id(&mint).await;
  694. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  695. // Try to create outputs that would overflow
  696. // 2^63 + 2^63 + small amount would overflow u64
  697. let large_amount = 2_u64.pow(63);
  698. let pre_mint1 = PreMintSecrets::random(
  699. keyset_id,
  700. large_amount.into(),
  701. &SplitTarget::default(),
  702. &fee_and_amounts,
  703. )
  704. .expect("Failed to create pre_mint1");
  705. let pre_mint2 = PreMintSecrets::random(
  706. keyset_id,
  707. large_amount.into(),
  708. &SplitTarget::default(),
  709. &fee_and_amounts,
  710. )
  711. .expect("Failed to create pre_mint2");
  712. let mut combined_pre_mint = PreMintSecrets::random(
  713. keyset_id,
  714. 1.into(),
  715. &SplitTarget::default(),
  716. &fee_and_amounts,
  717. )
  718. .expect("Failed to create combined_pre_mint");
  719. combined_pre_mint.combine(pre_mint1);
  720. combined_pre_mint.combine(pre_mint2);
  721. let swap_request = SwapRequest::new(proofs, combined_pre_mint.blinded_messages());
  722. // Should fail with overflow/amount error
  723. match mint.process_swap_request(swap_request).await {
  724. Err(cdk::Error::NUT03(cdk::nuts::nut03::Error::Amount(_)))
  725. | Err(cdk::Error::AmountOverflow)
  726. | Err(cdk::Error::AmountError(_))
  727. | Err(cdk::Error::TransactionUnbalanced(_, _, _)) => {
  728. // Any of these errors are acceptable for overflow
  729. }
  730. Err(err) => panic!("Unexpected error type: {:?}", err),
  731. Ok(_) => panic!("Overflow swap should not succeed"),
  732. }
  733. }
  734. /// Tests swap state transitions through pubsub notifications:
  735. /// 1. Subscribe to proof state changes
  736. /// 2. Execute swap
  737. /// 3. Verify Pending then Spent state transitions are received
  738. /// Validates NUT-17 notification behavior.
  739. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  740. async fn test_swap_state_transition_notifications() {
  741. setup_tracing();
  742. let mint = create_and_start_test_mint()
  743. .await
  744. .expect("Failed to create test mint");
  745. let wallet = create_test_wallet_for_mint(mint.clone())
  746. .await
  747. .expect("Failed to create test wallet");
  748. // Fund wallet
  749. fund_wallet(wallet.clone(), 100, None)
  750. .await
  751. .expect("Failed to fund wallet");
  752. let proofs = wallet
  753. .get_unspent_proofs()
  754. .await
  755. .expect("Could not get proofs");
  756. let keyset_id = get_keyset_id(&mint).await;
  757. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  758. let preswap = PreMintSecrets::random(
  759. keyset_id,
  760. 100.into(),
  761. &SplitTarget::default(),
  762. &fee_and_amounts,
  763. )
  764. .expect("Failed to create preswap");
  765. let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
  766. // Subscribe to proof state changes
  767. let proof_ys: Vec<String> = proofs.iter().map(|p| p.y().unwrap().to_string()).collect();
  768. let mut listener = mint
  769. .pubsub_manager()
  770. .subscribe(cdk::subscription::Params {
  771. kind: cdk::nuts::nut17::Kind::ProofState,
  772. filters: proof_ys.clone(),
  773. id: Arc::new("test_swap_notifications".into()),
  774. })
  775. .expect("Should subscribe successfully");
  776. // Execute swap
  777. mint.process_swap_request(swap_request)
  778. .await
  779. .expect("Swap should succeed");
  780. // Give pubsub time to deliver messages
  781. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  782. // Collect all state transition notifications
  783. let mut state_transitions: HashMap<String, Vec<State>> = HashMap::new();
  784. while let Some(msg) = listener.try_recv() {
  785. match msg.into_inner() {
  786. cashu::NotificationPayload::ProofState(cashu::ProofState { y, state, .. }) => {
  787. state_transitions
  788. .entry(y.to_string())
  789. .or_default()
  790. .push(state);
  791. }
  792. _ => panic!("Unexpected notification type"),
  793. }
  794. }
  795. // Verify each proof went through Pending -> Spent transition
  796. for y in proof_ys {
  797. let transitions = state_transitions
  798. .get(&y)
  799. .expect("Should have transitions for proof");
  800. assert_eq!(
  801. transitions,
  802. &vec![State::Pending, State::Spent],
  803. "Proof should transition from Pending to Spent"
  804. );
  805. }
  806. }
  807. /// Tests that swap fails gracefully when proof states cannot be updated:
  808. /// This would test the rollback path where proofs are added but state update fails.
  809. /// In the current implementation, this should trigger rollback of both proofs and blinded messages.
  810. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  811. async fn test_swap_proof_state_consistency() {
  812. setup_tracing();
  813. let mint = create_and_start_test_mint()
  814. .await
  815. .expect("Failed to create test mint");
  816. let wallet = create_test_wallet_for_mint(mint.clone())
  817. .await
  818. .expect("Failed to create test wallet");
  819. // Fund wallet
  820. fund_wallet(wallet.clone(), 100, None)
  821. .await
  822. .expect("Failed to fund wallet");
  823. let proofs = wallet
  824. .get_unspent_proofs()
  825. .await
  826. .expect("Could not get proofs");
  827. let keyset_id = get_keyset_id(&mint).await;
  828. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  829. // Execute successful swap
  830. let preswap = PreMintSecrets::random(
  831. keyset_id,
  832. 100.into(),
  833. &SplitTarget::default(),
  834. &fee_and_amounts,
  835. )
  836. .expect("Failed to create preswap");
  837. let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
  838. mint.process_swap_request(swap_request)
  839. .await
  840. .expect("Swap should succeed");
  841. // Verify all proofs have consistent state (Spent)
  842. let proof_ys: Vec<_> = proofs.iter().map(|p| p.y().unwrap()).collect();
  843. let states = mint
  844. .localstore()
  845. .get_proofs_states(&proof_ys)
  846. .await
  847. .expect("Failed to get proof states");
  848. // All states should be Some(Spent) - none should be None or Pending
  849. for (i, state) in states.iter().enumerate() {
  850. match state {
  851. Some(State::Spent) => {
  852. // Expected state
  853. }
  854. Some(other_state) => {
  855. panic!("Proof {} in unexpected state: {:?}", i, other_state)
  856. }
  857. None => {
  858. panic!("Proof {} has no state (should be Spent)", i)
  859. }
  860. }
  861. }
  862. }
  863. /// Tests that wallet correctly increments keyset counters when receiving proofs
  864. /// from multiple keysets and then performing operations with them.
  865. ///
  866. /// This test validates:
  867. /// 1. Wallet can receive proofs from multiple different keysets
  868. /// 2. Counter is correctly incremented for the target keyset during swap
  869. /// 3. Database maintains separate counters for each keyset
  870. #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
  871. async fn test_wallet_multi_keyset_counter_updates() {
  872. setup_tracing();
  873. let mint = create_and_start_test_mint()
  874. .await
  875. .expect("Failed to create test mint");
  876. let wallet = create_test_wallet_for_mint(mint.clone())
  877. .await
  878. .expect("Failed to create test wallet");
  879. // Fund wallet with initial 100 sats using first keyset
  880. fund_wallet(wallet.clone(), 100, None)
  881. .await
  882. .expect("Failed to fund wallet");
  883. let first_keyset_id = get_keyset_id(&mint).await;
  884. // Rotate to a second keyset
  885. mint.rotate_keyset(
  886. CurrencyUnit::Sat,
  887. cdk_integration_tests::standard_keyset_amounts(32),
  888. 0,
  889. )
  890. .await
  891. .expect("Failed to rotate keyset");
  892. // Wait for keyset rotation to propagate
  893. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  894. // Refresh wallet keysets to know about the new keyset
  895. wallet
  896. .refresh_keysets()
  897. .await
  898. .expect("Failed to refresh wallet keysets");
  899. // Fund wallet again with 100 sats using second keyset
  900. fund_wallet(wallet.clone(), 100, None)
  901. .await
  902. .expect("Failed to fund wallet with second keyset");
  903. let second_keyset_id = mint
  904. .pubkeys()
  905. .keysets
  906. .iter()
  907. .find(|k| k.id != first_keyset_id)
  908. .expect("Should have second keyset")
  909. .id;
  910. // Verify we now have proofs from two different keysets
  911. let all_proofs = wallet
  912. .get_unspent_proofs()
  913. .await
  914. .expect("Could not get proofs");
  915. let keysets_in_use: std::collections::HashSet<_> =
  916. all_proofs.iter().map(|p| p.keyset_id).collect();
  917. assert_eq!(
  918. keysets_in_use.len(),
  919. 2,
  920. "Should have proofs from 2 different keysets"
  921. );
  922. assert!(
  923. keysets_in_use.contains(&first_keyset_id),
  924. "Should have proofs from first keyset"
  925. );
  926. assert!(
  927. keysets_in_use.contains(&second_keyset_id),
  928. "Should have proofs from second keyset"
  929. );
  930. // Get initial total issued and redeemed for both keysets before swap
  931. let total_issued_before = mint.total_issued().await.unwrap();
  932. let total_redeemed_before = mint.total_redeemed().await.unwrap();
  933. let first_keyset_issued_before = total_issued_before
  934. .get(&first_keyset_id)
  935. .copied()
  936. .unwrap_or(Amount::ZERO);
  937. let first_keyset_redeemed_before = total_redeemed_before
  938. .get(&first_keyset_id)
  939. .copied()
  940. .unwrap_or(Amount::ZERO);
  941. let second_keyset_issued_before = total_issued_before
  942. .get(&second_keyset_id)
  943. .copied()
  944. .unwrap_or(Amount::ZERO);
  945. let second_keyset_redeemed_before = total_redeemed_before
  946. .get(&second_keyset_id)
  947. .copied()
  948. .unwrap_or(Amount::ZERO);
  949. tracing::info!(
  950. "Before swap - First keyset: issued={}, redeemed={}",
  951. first_keyset_issued_before,
  952. first_keyset_redeemed_before
  953. );
  954. tracing::info!(
  955. "Before swap - Second keyset: issued={}, redeemed={}",
  956. second_keyset_issued_before,
  957. second_keyset_redeemed_before
  958. );
  959. // Both keysets should have issued 100 sats
  960. assert_eq!(
  961. first_keyset_issued_before,
  962. Amount::from(100),
  963. "First keyset should have issued 100 sats"
  964. );
  965. assert_eq!(
  966. second_keyset_issued_before,
  967. Amount::from(100),
  968. "Second keyset should have issued 100 sats"
  969. );
  970. // Neither should have redeemed anything yet
  971. assert_eq!(
  972. first_keyset_redeemed_before,
  973. Amount::ZERO,
  974. "First keyset should have redeemed 0 sats before swap"
  975. );
  976. assert_eq!(
  977. second_keyset_redeemed_before,
  978. Amount::ZERO,
  979. "Second keyset should have redeemed 0 sats before swap"
  980. );
  981. // Now perform a swap with all proofs - this should only increment the counter
  982. // for the active (second) keyset, not for the first keyset
  983. let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
  984. let total_amount = all_proofs.total_amount().expect("Should get total amount");
  985. // Create swap using the active (second) keyset
  986. let preswap = PreMintSecrets::random(
  987. second_keyset_id,
  988. total_amount,
  989. &SplitTarget::default(),
  990. &fee_and_amounts,
  991. )
  992. .expect("Failed to create preswap");
  993. let swap_request = SwapRequest::new(all_proofs.clone(), preswap.blinded_messages());
  994. // Execute the swap
  995. let swap_response = mint
  996. .process_swap_request(swap_request)
  997. .await
  998. .expect("Swap should succeed");
  999. // Verify response
  1000. assert_eq!(
  1001. swap_response.signatures.len(),
  1002. preswap.blinded_messages().len(),
  1003. "Should receive signature for each blinded message"
  1004. );
  1005. // All the new proofs should be from the second (active) keyset
  1006. let keys = mint
  1007. .pubkeys()
  1008. .keysets
  1009. .iter()
  1010. .find(|k| k.id == second_keyset_id)
  1011. .expect("Should find second keyset")
  1012. .keys
  1013. .clone();
  1014. let new_proofs = construct_proofs(
  1015. swap_response.signatures,
  1016. preswap.rs(),
  1017. preswap.secrets(),
  1018. &keys,
  1019. )
  1020. .expect("Failed to construct proofs");
  1021. // Verify all new proofs use the second keyset
  1022. for proof in &new_proofs {
  1023. assert_eq!(
  1024. proof.keyset_id, second_keyset_id,
  1025. "All new proofs should use the active (second) keyset"
  1026. );
  1027. }
  1028. // Verify total issued and redeemed after swap
  1029. let total_issued_after = mint.total_issued().await.unwrap();
  1030. let total_redeemed_after = mint.total_redeemed().await.unwrap();
  1031. let first_keyset_issued_after = total_issued_after
  1032. .get(&first_keyset_id)
  1033. .copied()
  1034. .unwrap_or(Amount::ZERO);
  1035. let first_keyset_redeemed_after = total_redeemed_after
  1036. .get(&first_keyset_id)
  1037. .copied()
  1038. .unwrap_or(Amount::ZERO);
  1039. let second_keyset_issued_after = total_issued_after
  1040. .get(&second_keyset_id)
  1041. .copied()
  1042. .unwrap_or(Amount::ZERO);
  1043. let second_keyset_redeemed_after = total_redeemed_after
  1044. .get(&second_keyset_id)
  1045. .copied()
  1046. .unwrap_or(Amount::ZERO);
  1047. tracing::info!(
  1048. "After swap - First keyset: issued={}, redeemed={}",
  1049. first_keyset_issued_after,
  1050. first_keyset_redeemed_after
  1051. );
  1052. tracing::info!(
  1053. "After swap - Second keyset: issued={}, redeemed={}",
  1054. second_keyset_issued_after,
  1055. second_keyset_redeemed_after
  1056. );
  1057. // After swap:
  1058. // - First keyset: issued stays 100, redeemed increases by 100 (all its proofs were spent in swap)
  1059. // - Second keyset: issued increases by 200 (original 100 + new 100 from swap output),
  1060. // redeemed increases by 100 (its proofs from first funding were spent)
  1061. assert_eq!(
  1062. first_keyset_issued_after,
  1063. Amount::from(100),
  1064. "First keyset issued should stay 100 sats (no new issuance)"
  1065. );
  1066. assert_eq!(
  1067. first_keyset_redeemed_after,
  1068. Amount::from(100),
  1069. "First keyset should have redeemed 100 sats (all its proofs spent in swap)"
  1070. );
  1071. assert_eq!(
  1072. second_keyset_issued_after,
  1073. Amount::from(300),
  1074. "Second keyset should have issued 300 sats total (100 initial + 100 the second funding + 100 from swap output from the old keyset)"
  1075. );
  1076. assert_eq!(
  1077. second_keyset_redeemed_after,
  1078. Amount::from(100),
  1079. "Second keyset should have redeemed 100 sats (its proofs from initial funding spent in swap)"
  1080. );
  1081. // The test verifies that:
  1082. // 1. We can have proofs from multiple keysets in a wallet
  1083. // 2. Swap operation processes inputs from any keyset but creates outputs using active keyset
  1084. // 3. The keyset_counter table correctly handles counters for different keysets independently
  1085. // 4. The database upsert logic in increment_keyset_counter works for multiple keysets
  1086. // 5. Total issued and redeemed are tracked correctly per keyset during multi-keyset swaps
  1087. }