test_swap_flow.rs 57 KB

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