test_swap_flow.rs 57 KB

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