1
0

159 Коммитууд 18f7297d1b ... 70f20fcba8

Эзэн SHA1 Мессеж Огноо
  Cesar Rodas 70f20fcba8 Split tests 3 долоо хоног өмнө
  Cesar Rodas 28618437a1 Fix race condition 1 сар өмнө
  Cesar Rodas 6b3dbe4bcf Make sure signatory has all the keys in memory 1 сар өмнө
  Cesar Rodas 40cf8b0e84 Removed KeysDatabase Trait from MintDatabase 1 сар өмнө
  Cesar Rodas 085b912d07 Do not read keys from the DB 1 сар өмнө
  Cesar Rodas 1d402dfa71 Fixed missing default feature for signatory 1 сар өмнө
  Cesar Rodas 2660739ba3 WIP: Introduce a SignatoryManager service. 1 сар өмнө
  C 15e10c0e90 Merge pull request #730 from crodas/fix/race-condition-state-update 3 долоо хоног өмнө
  Cesar Rodas 25fad98aa8 Fix formatting 3 долоо хоног өмнө
  C 5505f0b5d7 Apply suggestions from code review 3 долоо хоног өмнө
  thesimplekid db14c1aecc Merge pull request #731 from thesimplekid/fix_p2pk_multi_sig 3 долоо хоног өмнө
  thesimplekid 570782e3b6 chore: clippy 3 долоо хоног өмнө
  thesimplekid 8379da2655 fix: only could sigs from unique pubkeys 4 долоо хоног өмнө
  thesimplekid 48fd3c652b test: multiple sigs 1 сар өмнө
  thesimplekid 204ff46dc4 Merge pull request #732 from thesimplekid/update_stable_rust 3 долоо хоног өмнө
  thesimplekid 06a3237977 chore: update stable rust to 1.86.0 3 долоо хоног өмнө
  Cesar Rodas 81a6d68ef3 Remove wallet states from `check_state_transition` 3 долоо хоног өмнө
  Cesar Rodas abdde307c6 Fix race conditions with proof state updates. 3 долоо хоног өмнө
  thesimplekid 3626dd2f6a Merge pull request #727 from thesimplekid/fix_debug_info_panic 1 сар өмнө
  thesimplekid 717742be05 fix: debug print of info 1 сар өмнө
  thesimplekid (aider) f5f3e50507 fix: Correct mnemonic hashing in Debug implementation 1 сар өмнө
  thesimplekid 607cdf23d4 feat: Add robust mnemonic hashing and debug tests for Info struct 1 сар өмнө
  thesimplekid 7715d45f3b Merge pull request #724 from thesimplekid/token_value_uniqe 1 сар өмнө
  thesimplekid 43d1d75b7e feat: mint should not enforce expiry (#723) 1 сар өмнө
  thesimplekid (aider) 3b5c8b5c5e refactor: Ensure unique proofs when calculating token value 1 сар өмнө
  thesimplekid 0d512c1d15 feat: mint should not enforce expiry 1 сар өмнө
  lollerfirst dafdf757af CORS Headers in Responses (#719) 1 сар өмнө
  timesince 5df983c388 chore: fix typo in DEVELOPMENT.md (#720) 1 сар өмнө
  thesimplekid 96179b7d14 chore: Bump CDK crates version from 0.8.1 to 0.9.0 1 сар өмнө
  thesimplekid db067a145d docs: Add README.md for cdk-mint-rpc crate (#717) 1 сар өмнө
  thesimplekid f44f79d3e0 fix: grpc set mint urls, updating description and add get quote ttl (#716) 1 сар өмнө
  thesimplekid fb613b2e21 Merge pull request #713 from thesimplekid/fix_amountless_enabled_check 1 сар өмнө
  thesimplekid 92500c27fe Merge pull request #712 from gandlafbtc/patch-2 1 сар өмнө
  thesimplekid e883d2d684 fix: amountless setting 1 сар өмнө
  gandlafbtc 017ce71bff fix typo in README.md 1 сар өмнө
  thesimplekid b1c7aed8e4 Merge pull request #711 from thesimplekid/changel 1 сар өмнө
  thesimplekid cce8dbfe7e docs: update change log 1 сар өмнө
  thesimplekid 759201bc7c Export MintDatabase traits in the cdk crate (#710) 1 сар өмнө
  David Caseria 3514f362eb Export MintDatabase traits in the cdk crate 1 сар өмнө
  thesimplekid 0b9ca1a474 Time time series (#708) 1 сар өмнө
  C 43ab1fdde1 Do not create the wallet struct directly; instead, call new. (#707) 1 сар өмнө
  thesimplekid d224cc57b5 Melt to amountless invoice (#497) 1 сар өмнө
  thesimplekid 09f339e6c6 Merge pull request #704 from thesimplekid/fix_mint_pending 1 сар өмнө
  thesimplekid 71bfe1ff9c fix: mint pending get mint info to create auth wallet 1 сар өмнө
  thesimplekid d68fdd1a0c Merge pull request #703 from thesimplekid/fix_nutshell_tests 1 сар өмнө
  thesimplekid d6d3955d50 fix: nutshell tests 1 сар өмнө
  thesimplekid 4a113ff947 Merge pull request #700 from thesimplekid/update_flake_2 1 сар өмнө
  David Caseria b1dd321f0a Add transactions to database (#686) 1 сар өмнө
  thesimplekid 7fbe55ea02 Test fees (#698) 1 сар өмнө
  thesimplekid f0766d0ae4 Merge pull request #701 from luozexuan/main 1 сар өмнө
  luozexuan 02fd849870 chore: fix some typos in comment 1 сар өмнө
  thesimplekid afbf844dbe chore: update flake 1 сар өмнө
  thesimplekid f4c857c3e7 Nutshell wallet (#695) 1 сар өмнө
  ok300 0eb5805f6f Mint example config: remove stale cln_path reference (#694) 1 сар өмнө
  thesimplekid 52bfc8c9ce feat: nutshell itests (#691) 1 сар өмнө
  ok300 240e22c96a Remove stale crate references (#692) 1 сар өмнө
  thesimplekid fa67271cca Int tests (#685) 1 сар өмнө
  thesimplekid 5484e7c33a Merge pull request #690 from thesimplekid/request_without_dleq 1 сар өмнө
  thesimplekid 4ba0b6c6ef Merge pull request #593 from BitcreditProtocol/peanut/mintdatabase_split 1 сар өмнө
  ok300 de4285bd9c Simplify `MultiMintWallet` interface (#664) 1 сар өмнө
  codingpeanut157 47903c3bfd split MintDatabase into separate narrower scoped traits 1 сар өмнө
  thesimplekid 7b4951041e Rust docs (#681) 1 сар өмнө
  thesimplekid 1e20e8fc2b Merge pull request #682 from thesimplekid/cdk_common_wallet 1 сар өмнө
  thesimplekid d1c9dbae28 refactor: cashu wallet moved to cdk-common 1 сар өмнө
  thesimplekid e86531957f Merge pull request #680 from thesimplekid/prepare_v0.8.1 1 сар өмнө
  thesimplekid f6c11173f9 chore: prepare v0.8.1 1 сар өмнө
  thesimplekid ef2b07d1e2 Merge pull request #677 from thesimplekid/cdk-redb-cli 1 сар өмнө
  thesimplekid 29452debe4 Merge pull request #679 from thesimplekid/docker_publish_ci 1 сар өмнө
  thesimplekid 1634dd6552 fix: remove arm for docker ci 1 сар өмнө
  thesimplekid 0454f6299e Merge pull request #678 from benthecarman/fix-paths 1 сар өмнө
  benthecarman b8fbd83772 fix: Fix MintUrls with a path 1 сар өмнө
  thesimplekid 4224ebdf19 Merge pull request #676 from thesimplekid/proto_exp 1 сар өмнө
  thesimplekid (aider) 7a9faec984 feat: Add optional redb feature flag for database support 1 сар өмнө
  thesimplekid 9b87a65940 docs: changelog 1 сар өмнө
  thesimplekid e260a12e4c fix: exp for mint-rpc 1 сар өмнө
  thesimplekid ad14f64f36 Merge pull request #671 from optout21/melt-example 1 сар өмнө
  thesimplekid ff654ab4b1 Merge pull request #673 from davidcaseria/export-mint-keyset-info 1 сар өмнө
  thesimplekid abbe1682e4 docs: changelog 1 сар өмнө
  David Caseria cd3a54e03b Export MintKeySetInfo 1 сар өмнө
  thesimplekid c63fc02a5a Prepare v0.8.0 (#672) 1 сар өмнө
  optout d5104a94eb sample: Add example program for melt 1 сар өмнө
  thesimplekid b3ae76d6c7 Fix dleq logging (#670) 1 сар өмнө
  thesimplekid be93ff2384 Clear and Blind Auth (#510) 1 сар өмнө
  thesimplekid cd71cd47d9 Merge pull request #669 from ok300/ok300-simplify-fee-reserve 1 сар өмнө
  ok300 13475be580 Simplify fee calculation 1 сар өмнө
  thesimplekid 27636c86b7 chore: zip version (#668) 1 сар өмнө
  thesimplekid e3570c3e98 Wallet dleq (#667) 1 сар өмнө
  thesimplekid 5ba2699eb7 Merge pull request #665 from thesimplekid/mint_mod_types 1 сар өмнө
  thesimplekid c48b5202f0 refactor: move Mint and Melt quote to cdk common 1 сар өмнө
  thesimplekid bcc9871656 Merge pull request #663 from davidcaseria/protoc-fix 1 сар өмнө
  David Caseria 24a9446581 Fix protoc build error 1 сар өмнө
  thesimplekid fe06b93db4 docs: changelog 1 сар өмнө
  David Caseria db1db86509 Prepared Send (#596) 1 сар өмнө
  thesimplekid c4488ce436 Merge pull request #662 from ok300/ok300-fix-sk-serde 1 сар өмнө
  ok300 558024d7fe Ser/Deserialize SecretKey either as bytes or string 1 сар өмнө
  thesimplekid 1cfb51a4c3 Merge pull request #659 from thesimplekid/half_msrv 2 сар өмнө
  thesimplekid 619a89060c chore: msrv of half 2 сар өмнө
  thesimplekid 4c447cf046 Merge pull request #656 from thesimplekid/payment_request_builder 2 сар өмнө
  thesimplekid 0155962d11 Update crates/cashu/src/nuts/nut18.rs 2 сар өмнө
  thesimplekid be1e048f2c Update crates/cashu/src/nuts/nut18.rs 2 сар өмнө
  thesimplekid eb5899843a feat: export transport type 2 сар өмнө
  thesimplekid (aider) 32ded596cd feat: payments request builder 2 сар өмнө
  thesimplekid 60367cdd65 Merge pull request #655 from thesimplekid/fix_output_verification 2 сар өмнө
  thesimplekid cf9cacaff4 fix: verification of melt quote with empty outputs 2 сар өмнө
  thesimplekid 0731f9e809 docs: changelog 2 сар өмнө
  thesimplekid 110245e6c8 fix: Improve spending conditions validation in ProofInfo (#654) 2 сар өмнө
  thesimplekid 8d1b35f52e fix: Improve spending conditions validation in ProofInfo 2 сар өмнө
  ok300 72dff95322 Merge pull request #653 from ok300/ok300-fix-update-mint-url 2 сар өмнө
  thesimplekid 158e321e0e Merge pull request #652 from ok300/ok300-fix-integration-test-loop 2 сар өмнө
  ok300 3ba3449c81 Integration tests: fix wait_for_mint_to_be_paid loop 2 сар өмнө
  thesimplekid 7a97510ec1 Merge pull request #651 from benthecarman/update-sqlx 2 сар өмнө
  benthecarman 8cd4ea301a chore: Update sqlx to 0.7.4 2 сар өмнө
  thesimplekid 40e1b5dda4 Merge pull request #649 from thesimplekid/ci_clean_up 2 сар өмнө
  thesimplekid (aider) 9170fbe86c ci: Make all jobs depend on pre-commit-checks passing 2 сар өмнө
  thesimplekid a691df7916 Merge pull request #650 from benthecarman/rm-clone 2 сар өмнө
  benthecarman 080f4ef2bf fix: remove unnecessary clone 2 сар өмнө
  thesimplekid 3bf908d89d Merge pull request #648 from TechVest/main 2 сар өмнө
  thesimplekid a7c7a4caea Merge pull request #597 from thesimplekid/refactor_payment_processor 2 сар өмнө
  thesimplekid 162507c492 feat: payment processor 3 сар өмнө
  TechVest c5284b889f chore: remove redundant words in comment 2 сар өмнө
  ok300 1131711d91 Drop `nostr_last_checked` table, remove references (#647) 2 сар өмнө
  thesimplekid 1d4245549b Merge pull request #646 from thesimplekid/flake_update 2 сар өмнө
  thesimplekid 4c4bde0fe4 chore: update flake 2 сар өмнө
  thesimplekid 379d5590db docs: update changelog 2 сар өмнө
  thesimplekid 3b2f31e844 Merge pull request #645 from thesimplekid/remove_nostr_from_db 2 сар өмнө
  thesimplekid (aider) cb87fefacd refactor: Remove nostr last checked methods from database trait and implementations 2 сар өмнө
  thesimplekid 214b75ac31 Merge pull request #640 from benthecarman/sqlcipher 2 сар өмнө
  benthecarman 40c53e83df feat: Add support for sqlcipher 2 сар өмнө
  thesimplekid b787951dbc feat: Add feature gates for CLN, LND, fakewallet and LNbits backends (#638) 2 сар өмнө
  thesimplekid a3993c3e4c Merge pull request #642 from thesimplekid/remove_unused_supported 2 сар өмнө
  thesimplekid 93f7979a70 refactor: remove unused ln_backends in cdk-mintd 2 сар өмнө
  thesimplekid 467cc0a027 feat: Add migration for keyset_id as foreign key in SQLite database (#634) 2 сар өмнө
  thesimplekid 39a7b15221 Check tls certs exist for grpc management serve (#637) 2 сар өмнө
  thesimplekid 22beade553 Amount and unit nut04/05 (#635) 2 сар өмнө
  thesimplekid b7380dc858 feat: Add tos_url to mintd config (#636) 2 сар өмнө
  NodlAndHodl fcf2e9d603 feat: adding tos to mint (#604) 2 сар өмнө
  ok300 5a7362c09f Simplify `process_swap_request` (#631) 2 сар өмнө
  thesimplekid 393c95e115 Merge pull request #630 from thesimplekid/db_check_on_delete_proofs 2 сар өмнө
  thesimplekid a394145fba Merge pull request #629 from ok300/ok300-ensure-cdk 2 сар өмнө
  thesimplekid (aider) d41d3a7c94 refactor: Add state check before deleting proofs to prevent removing spent proofs 2 сар өмнө
  ok300 813794353a Add ensure_cdk macro 6 сар өмнө
  thesimplekid c6200331cf Merge pull request #626 from thesimplekid/remove_phd 2 сар өмнө
  thesimplekid f5be0ceeb6 chore: remove phd 2 сар өмнө
  thesimplekid ca1fca2825 chore: update CHANGELOG 2 сар өмнө
  thesimplekid e84d6ea7ab chore: Update rust-version (MSRV) to 1.75.0 (#623) 2 сар өмнө
  thesimplekid 0cd1d6a194 style: Clean up CHANGELOG.md formatting and consistency (#625) 2 сар өмнө
  C f7d9a1b5db Drop the in-memory database (#613) 2 сар өмнө
  thesimplekid (aider) 49ca9ff5d1 style: Clean up CHANGELOG.md formatting and consistency 2 сар өмнө
  thesimplekid d6336b2da8 Merge pull request #624 from thesimplekid/improve_loggind 2 сар өмнө
  thesimplekid d615897ef0 chore: improve logging in verification 2 сар өмнө
  thesimplekid a82e3eb314 fix: attempt to swap after a failed transaction (#622) 2 сар өмнө
  C 63393056a0 Do not use `INSERT OR REPLACE` in SQLite. (#620) 2 сар өмнө
  thesimplekid 062d7be58f chore: pin base64ct as newer version is unsuported on stable 2 сар өмнө
  thesimplekid 827e4aebde Refactor mintd used in itest (#616) 2 сар өмнө
  lollerfirst 692e13ff16 Refactor Keyset Initialization (#615) 2 сар өмнө
  C d5df413d6b Drop AmountStr (#612) 2 сар өмнө
  Cesar Rodas 18f7297d1b Add more commands 3 сар өмнө
  Cesar Rodas 3db93e501c Remove the shared Mint instance between tests 3 сар өмнө
  Cesar Rodas 792b7a3180 Introduce a SignatoryManager service. 4 сар өмнө
100 өөрчлөгдсөн 6781 нэмэгдсэн , 1764 устгасан
  1. 169 38
      .github/workflows/ci.yml
  2. 60 0
      .github/workflows/docker-publish.yml
  3. 43 0
      .github/workflows/nutshell_itest.yml
  4. 3 0
      .gitignore
  5. 4 1
      .typos.toml
  6. 117 72
      CHANGELOG.md
  7. 96 1
      Cargo.toml
  8. 1 1
      DEVELOPMENT.md
  9. 21 3
      README.md
  10. 26 26
      crates/cashu/Cargo.toml
  11. 102 0
      crates/cashu/README.md
  12. 70 49
      crates/cashu/src/amount.rs
  13. 15 5
      crates/cashu/src/lib.rs
  14. 41 5
      crates/cashu/src/mint_url.rs
  15. 8 0
      crates/cashu/src/nuts/auth/mod.rs
  16. 410 0
      crates/cashu/src/nuts/auth/nut21.rs
  17. 380 0
      crates/cashu/src/nuts/auth/nut22.rs
  18. 12 1
      crates/cashu/src/nuts/mod.rs
  19. 100 6
      crates/cashu/src/nuts/nut00/mod.rs
  20. 90 38
      crates/cashu/src/nuts/nut00/token.rs
  21. 55 10
      crates/cashu/src/nuts/nut01/mod.rs
  22. 9 2
      crates/cashu/src/nuts/nut01/public_key.rs
  23. 37 11
      crates/cashu/src/nuts/nut01/secret_key.rs
  24. 24 12
      crates/cashu/src/nuts/nut02.rs
  25. 26 7
      crates/cashu/src/nuts/nut03.rs
  26. 10 13
      crates/cashu/src/nuts/nut04.rs
  27. 93 40
      crates/cashu/src/nuts/nut05.rs
  28. 126 52
      crates/cashu/src/nuts/nut06.rs
  29. 6 2
      crates/cashu/src/nuts/nut07.rs
  30. 1 1
      crates/cashu/src/nuts/nut08.rs
  31. 42 16
      crates/cashu/src/nuts/nut11/mod.rs
  32. 4 4
      crates/cashu/src/nuts/nut14/mod.rs
  33. 2 0
      crates/cashu/src/nuts/nut17/mod.rs
  34. 238 2
      crates/cashu/src/nuts/nut18.rs
  35. 3 0
      crates/cashu/src/nuts/nut19.rs
  36. 0 6
      crates/cashu/src/secret.rs
  37. 3 3
      crates/cashu/src/util/hex.rs
  38. 2 0
      crates/cashu/src/util/mod.rs
  39. 0 87
      crates/cashu/src/wallet.rs
  40. 29 25
      crates/cdk-axum/Cargo.toml
  41. 31 0
      crates/cdk-axum/README.md
  42. 194 0
      crates/cdk-axum/src/auth.rs
  43. 2 2
      crates/cdk-axum/src/cache/mod.rs
  44. 64 10
      crates/cdk-axum/src/lib.rs
  45. 172 27
      crates/cdk-axum/src/router_handlers.rs
  46. 2 3
      crates/cdk-axum/src/ws/mod.rs
  47. 27 26
      crates/cdk-cli/Cargo.toml
  48. 67 37
      crates/cdk-cli/src/main.rs
  49. 37 0
      crates/cdk-cli/src/nostr_storage.rs
  50. 194 0
      crates/cdk-cli/src/sub_commands/cat_device_login.rs
  51. 138 0
      crates/cdk-cli/src/sub_commands/cat_login.rs
  52. 2 2
      crates/cdk-cli/src/sub_commands/create_request.rs
  53. 33 20
      crates/cdk-cli/src/sub_commands/melt.rs
  54. 5 9
      crates/cdk-cli/src/sub_commands/mint.rs
  55. 204 0
      crates/cdk-cli/src/sub_commands/mint_blind_auth.rs
  56. 2 2
      crates/cdk-cli/src/sub_commands/mint_info.rs
  57. 3 0
      crates/cdk-cli/src/sub_commands/mod.rs
  58. 9 11
      crates/cdk-cli/src/sub_commands/pay_request.rs
  59. 31 27
      crates/cdk-cli/src/sub_commands/receive.rs
  60. 4 9
      crates/cdk-cli/src/sub_commands/restore.rs
  61. 14 9
      crates/cdk-cli/src/sub_commands/send.rs
  62. 62 0
      crates/cdk-cli/src/token_storage.rs
  63. 15 18
      crates/cdk-cln/Cargo.toml
  64. 1 1
      crates/cdk-cln/src/error.rs
  65. 47 38
      crates/cdk-cln/src/lib.rs
  66. 26 28
      crates/cdk-common/Cargo.toml
  67. 41 0
      crates/cdk-common/README.md
  68. 108 7
      crates/cdk-common/src/common.rs
  69. 72 0
      crates/cdk-common/src/database/mint/auth/mod.rs
  70. 51 16
      crates/cdk-common/src/database/mint/mod.rs
  71. 83 0
      crates/cdk-common/src/database/mint/test.rs
  72. 38 2
      crates/cdk-common/src/database/mod.rs
  73. 19 19
      crates/cdk-common/src/database/wallet.rs
  74. 117 4
      crates/cdk-common/src/error.rs
  75. 12 11
      crates/cdk-common/src/lib.rs
  76. 70 2
      crates/cdk-common/src/mint.rs
  77. 57 26
      crates/cdk-common/src/payment.rs
  78. 4 0
      crates/cdk-common/src/pub_sub/index.rs
  79. 1 1
      crates/cdk-common/src/pub_sub/mod.rs
  80. 0 74
      crates/cdk-common/src/signatory.rs
  81. 39 0
      crates/cdk-common/src/state.rs
  82. 13 1
      crates/cdk-common/src/subscription.rs
  83. 294 0
      crates/cdk-common/src/wallet.rs
  84. 23 0
      crates/cdk-common/src/ws.rs
  85. 17 16
      crates/cdk-fake-wallet/Cargo.toml
  86. 35 0
      crates/cdk-fake-wallet/README.md
  87. 1 1
      crates/cdk-fake-wallet/src/error.rs
  88. 62 44
      crates/cdk-fake-wallet/src/lib.rs
  89. 39 45
      crates/cdk-integration-tests/Cargo.toml
  90. 0 33
      crates/cdk-integration-tests/src/bin/fake_wallet.rs
  91. 0 209
      crates/cdk-integration-tests/src/bin/regtest_mint.rs
  92. 63 0
      crates/cdk-integration-tests/src/bin/start_regtest.rs
  93. 106 0
      crates/cdk-integration-tests/src/init_auth_mint.rs
  94. 0 83
      crates/cdk-integration-tests/src/init_fake_wallet.rs
  95. 0 37
      crates/cdk-integration-tests/src/init_mint.rs
  96. 155 41
      crates/cdk-integration-tests/src/init_pure_tests.rs
  97. 130 62
      crates/cdk-integration-tests/src/init_regtest.rs
  98. 145 112
      crates/cdk-integration-tests/src/lib.rs
  99. 863 0
      crates/cdk-integration-tests/tests/fake_auth.rs
  100. 259 100
      crates/cdk-integration-tests/tests/fake_wallet.rs

+ 169 - 38
.github/workflows/ci.yml

@@ -5,6 +5,8 @@ on:
     branches: [main]
   pull_request:
     branches: [main]
+  release:
+    types: [created]
 
 env:
   CARGO_TERM_COLOR: always
@@ -13,6 +15,7 @@ jobs:
   self-care:
     name: Flake self-check
     runs-on: ubuntu-latest
+    timeout-minutes: 15
     steps:
       - uses: actions/checkout@v4
       - name: Check Nix flake inputs
@@ -23,6 +26,7 @@ jobs:
   pre-commit-checks:
     name: "Cargo fmt, typos"
     runs-on: ubuntu-latest
+    timeout-minutes: 15
     steps:
       - name: checkout
         uses: actions/checkout@v4
@@ -45,11 +49,14 @@ jobs:
   examples:
     name: "Run examples"
     runs-on: ubuntu-latest
+    timeout-minutes: 15
+    needs: [pre-commit-checks, clippy]
     strategy:
       matrix:
         build-args:
           [
             mint-token,
+            melt-token,
             p2pk,
             proof-selection,
             wallet
@@ -69,6 +76,8 @@ jobs:
   clippy:
     name: "Stable build, clippy and test"
     runs-on: ubuntu-latest
+    timeout-minutes: 15
+    needs: pre-commit-checks
     strategy:
       matrix:
         build-args:
@@ -78,35 +87,63 @@ jobs:
             -p cashu --no-default-features --features wallet,
             -p cashu --no-default-features --features mint,
             -p cashu --no-default-features --features "mint swagger",
+            -p cashu --no-default-features --features auth,
+            -p cashu --no-default-features --features "mint auth",
+            -p cashu --no-default-features --features "wallet auth",
             -p cdk-common,
             -p cdk-common --no-default-features,
             -p cdk-common --no-default-features --features wallet,
             -p cdk-common --no-default-features --features mint,
             -p cdk-common --no-default-features --features "mint swagger",
+            -p cdk-common --no-default-features --features "auth",
+            -p cdk-common --no-default-features --features "mint auth",
+            -p cdk-common --no-default-features --features "wallet auth",
             -p cdk,
             -p cdk --no-default-features,
             -p cdk --no-default-features --features wallet,
             -p cdk --no-default-features --features mint,
             -p cdk --no-default-features --features "mint swagger",
+            -p cdk --no-default-features --features auth,
+            -p cdk --features auth,
+            -p cdk --no-default-features --features "auth mint",
+            -p cdk --no-default-features --features "auth wallet",
             -p cdk-redb,
             -p cdk-sqlite,
+            -p cdk-sqlite --features sqlcipher,
             -p cdk-axum --no-default-features,
             -p cdk-axum --no-default-features --features swagger,
             -p cdk-axum --no-default-features --features redis,
             -p cdk-axum --no-default-features --features "redis swagger",
+            -p cdk-axum --no-default-features --features "auth redis",
             -p cdk-axum,
             -p cdk-cln,
             -p cdk-lnd,
-            -p cdk-phoenixd,
-            -p cdk-strike,
             -p cdk-lnbits,
             -p cdk-fake-wallet,
+            -p cdk-payment-processor,
             --bin cdk-cli,
+            --bin cdk-cli --features sqlcipher,
+            --bin cdk-cli --features redb,
+            --bin cdk-cli --features "sqlcipher redb",
             --bin cdk-mintd,
-            --bin cdk-mintd --no-default-features --features swagger,
-            --bin cdk-mintd --no-default-features --features redis,
-            --bin cdk-mintd --no-default-features --features "redis swagger",
-            --bin cdk-mintd --no-default-features --features management-rpc,
+            --bin cdk-mintd --features redis,
+            --bin cdk-mintd --features redb,
+            --bin cdk-mintd --features "redis swagger redb",
+            --bin cdk-mintd --features sqlcipher,
+            --bin cdk-mintd --no-default-features --features lnd,
+            --bin cdk-mintd --no-default-features --features cln,
+            --bin cdk-mintd --no-default-features --features lnbits,
+            --bin cdk-mintd --no-default-features --features fakewallet,
+            --bin cdk-mintd --no-default-features --features grpc-processor,
+            --bin cdk-mintd --no-default-features --features "management-rpc lnd",
+            --bin cdk-mintd --no-default-features --features "management-rpc cln",
+            --bin cdk-mintd --no-default-features --features "management-rpc lnbits",
+            --bin cdk-mintd --no-default-features --features "management-rpc grpc-processor",
+            --bin cdk-mintd --no-default-features --features "swagger lnd",
+            --bin cdk-mintd --no-default-features --features "swagger cln",
+            --bin cdk-mintd --no-default-features --features "swagger lnbits",
+            --bin cdk-mintd --no-default-features --features "auth lnd",
+            --bin cdk-mintd --no-default-features --features "auth cln",
             --bin cdk-mint-cli,
           ]
     steps:
@@ -118,16 +155,16 @@ jobs:
         uses: DeterminateSystems/magic-nix-cache-action@v6
       - name: Rust Cache
         uses: Swatinem/rust-cache@v2
-      - name: Build
-        run: nix develop -i -L .#stable --command cargo build ${{ matrix.build-args }}
       - name: Clippy
         run: nix develop -i -L .#stable --command cargo clippy ${{ matrix.build-args }} -- -D warnings
       - name: Test
         run: nix develop -i -L .#stable --command cargo test ${{ matrix.build-args }}
 
-  itest:
+  regtest-itest:
     name: "Integration regtest tests"
     runs-on: ubuntu-latest
+    timeout-minutes: 15
+    needs: [pre-commit-checks, clippy, pure-itest, fake-mint-itest]
     strategy:
       matrix:
         build-args:
@@ -138,7 +175,6 @@ jobs:
           [
             REDB,
             SQLITE,
-            MEMORY,
           ]
     steps:
       - name: checkout
@@ -149,14 +185,14 @@ jobs:
         uses: DeterminateSystems/magic-nix-cache-action@v6
       - name: Rust Cache
         uses: Swatinem/rust-cache@v2
-      - name: Clippy
-        run: nix develop -i -L .#stable --command cargo clippy ${{ matrix.build-args }} -- -D warnings
       - name: Test
         run: nix develop -i -L .#stable --command just itest ${{ matrix.database }}
           
-  fake-wallet-itest:
-    name: "Integration fake wallet tests"
+  fake-mint-itest:
+    name: "Integration fake mint tests"
     runs-on: ubuntu-latest
+    timeout-minutes: 15
+    needs: [pre-commit-checks, clippy]
     strategy:
       matrix:
         build-args:
@@ -167,7 +203,6 @@ jobs:
           [
           REDB,
           SQLITE,
-          MEMORY,
           ]
     steps:
       - name: checkout
@@ -179,13 +214,23 @@ jobs:
       - name: Rust Cache
         uses: Swatinem/rust-cache@v2
       - name: Clippy
-        run: nix develop -i -L .#stable --command cargo clippy ${{ matrix.build-args }} -- -D warnings
-      - name: Test fake mint
+        run: nix develop -i -L .#stable --command cargo clippy -- -D warnings
+      - name: Test fake auth mint
         run: nix develop -i -L .#stable --command just fake-mint-itest ${{ matrix.database }}
                 
   pure-itest:
     name: "Integration fake wallet tests"
     runs-on: ubuntu-latest
+    timeout-minutes: 15
+    needs: [pre-commit-checks, clippy]
+    strategy:
+      matrix:
+        database: 
+          [
+          memory,
+          sqlite,
+          redb
+          ]
     steps:
       - name: checkout
         uses: actions/checkout@v4
@@ -196,26 +241,23 @@ jobs:
       - name: Rust Cache
         uses: Swatinem/rust-cache@v2
       - name: Test fake mint
+        run: nix develop -i -L .#stable --command just test-pure ${{ matrix.database }}
+      - name: Test mint
         run: nix develop -i -L .#stable --command just test
 
-  msrv-build:
-    name: "MSRV build"
+
+  payment-processor-itests:
+    name: "Payment processor tests"
     runs-on: ubuntu-latest
+    timeout-minutes: 15
+    needs: [pre-commit-checks, clippy, pure-itest, fake-mint-itest, regtest-itest]
     strategy:
       matrix:
-        build-args:
+        ln: 
           [
-            -p cashu --no-default-features --features "wallet mint",
-            -p cdk-common --no-default-features --features "wallet mint",
-            -p cdk --no-default-features --features "mint mint",
-            -p cdk-axum,
-            -p cdk-axum --no-default-features --features redis,
-            -p cdk-strike,
-            -p cdk-lnbits,
-            -p cdk-phoenixd,
-            -p cdk-fake-wallet,
-            -p cdk-cln,
-            -p cdk-mint-rpc,
+          FAKEWALLET,
+          CLN,
+          LND
           ]
     steps:
       - name: checkout
@@ -226,19 +268,34 @@ jobs:
         uses: DeterminateSystems/magic-nix-cache-action@v6
       - name: Rust Cache
         uses: Swatinem/rust-cache@v2
-      - name: Build
-        run: nix develop -i -L .#msrv --command cargo build ${{ matrix.build-args }}
+      - name: Test
+        run: nix develop -i -L .#stable --command just itest-payment-processor ${{matrix.ln}}
 
-  
-  db-msrv-build:
-    name: "DB MSRV build"
+  msrv-build:
+    name: "MSRV build"
     runs-on: ubuntu-latest
+    timeout-minutes: 15
+    needs: [pre-commit-checks, clippy, pure-itest]
     strategy:
       matrix:
         build-args:
           [
+            -p cashu --no-default-features --features "wallet mint",
+            -p cdk-common --no-default-features --features "wallet mint",
+            -p cdk,
+            -p cdk --no-default-features --features "mint auth",
+            -p cdk --no-default-features --features "wallet auth",
+            -p cdk --no-default-features --features "http_subscription",
+            -p cdk-axum,
+            -p cdk-axum --no-default-features --features redis,
+            -p cdk-lnbits,
+            -p cdk-fake-wallet,
+            -p cdk-cln,
+            -p cdk-lnd,
+            -p cdk-mint-rpc,
             -p cdk-sqlite,
-            -p cdk-redb,
+            -p cdk-mintd,
+            -p cdk-payment-processor --no-default-features,
           ]
     steps:
       - name: checkout
@@ -250,11 +307,14 @@ jobs:
       - name: Rust Cache
         uses: Swatinem/rust-cache@v2
       - name: Build
-        run: nix develop -i -L .#db_shell --command cargo build ${{ matrix.build-args }}
+        run: nix develop -i -L .#msrv --command cargo build ${{ matrix.build-args }}
 
+  
   check-wasm:
     name: Check WASM
     runs-on: ubuntu-latest
+    timeout-minutes: 15
+    needs: [pre-commit-checks, clippy, pure-itest]
     strategy:
       matrix:
         rust:
@@ -283,6 +343,8 @@ jobs:
   check-wasm-msrv:
     name: Check WASM
     runs-on: ubuntu-latest
+    timeout-minutes: 15
+    needs: [pre-commit-checks, clippy, msrv-build]
     strategy:
       matrix:
         rust:
@@ -306,3 +368,72 @@ jobs:
         uses: Swatinem/rust-cache@v2
       - name: Build cdk wasm
         run: nix develop -i -L ".#${{ matrix.rust }}" --command cargo build ${{ matrix.build-args }} --target ${{ matrix.target }}
+
+  fake-mint-auth-itest:
+    name: "Integration fake mint auth tests"
+    runs-on: ubuntu-latest
+    timeout-minutes: 15
+    needs: [pre-commit-checks, clippy, pure-itest, fake-mint-itest]
+    strategy:
+      matrix:
+        database: 
+          [
+          REDB,
+          SQLITE,
+          ]
+    steps:
+      - name: checkout
+        uses: actions/checkout@v4
+      - name: Install Nix
+        uses: DeterminateSystems/nix-installer-action@v11
+      - name: Nix Cache
+        uses: DeterminateSystems/magic-nix-cache-action@v6
+      - name: Rust Cache
+        uses: Swatinem/rust-cache@v2
+      - name: Start Keycloak with Backup
+        run: |
+          docker compose -f misc/keycloak/docker-compose-recover.yml up -d
+          until docker logs $(docker ps -q --filter "ancestor=quay.io/keycloak/keycloak:25.0.6") | grep "Keycloak 25.0.6 on JVM (powered by Quarkus 3.8.5) started"; do sleep 1; done
+
+      - name: Verify Keycloak Import
+        run: |
+          docker logs $(docker ps -q --filter "ancestor=quay.io/keycloak/keycloak:25.0.6") | grep "Imported"
+      - name: Test fake auth mint
+        run: nix develop -i -L .#stable --command just fake-auth-mint-itest ${{ matrix.database }} http://127.0.0.1:8080/realms/cdk-test-realm/.well-known/openid-configuration
+      - name: Stop and clean up Docker Compose
+        run: |
+          docker compose -f misc/keycloak/docker-compose-recover.yml down
+          
+  doc-tests:
+    name: "Documentation Tests"
+    runs-on: ubuntu-latest
+    timeout-minutes: 15
+    needs: pre-commit-checks
+    steps:
+      - name: checkout
+        uses: actions/checkout@v4
+      - name: Install Nix
+        uses: DeterminateSystems/nix-installer-action@v11
+      - name: Nix Cache
+        uses: DeterminateSystems/magic-nix-cache-action@v6
+      - name: Rust Cache
+        uses: Swatinem/rust-cache@v2
+      - name: Run doc tests
+        run: nix develop -i -L .#stable --command cargo test --doc
+        
+  strict-docs:
+    name: "Strict Documentation Check"
+    runs-on: ubuntu-latest
+    timeout-minutes: 15
+    needs: doc-tests
+    steps:
+      - name: checkout
+        uses: actions/checkout@v4
+      - name: Install Nix
+        uses: DeterminateSystems/nix-installer-action@v11
+      - name: Nix Cache
+        uses: DeterminateSystems/magic-nix-cache-action@v6
+      - name: Rust Cache
+        uses: Swatinem/rust-cache@v2
+      - name: Check docs with strict warnings
+        run: nix develop -i -L .#stable --command just docs-strict

+ 60 - 0
.github/workflows/docker-publish.yml

@@ -0,0 +1,60 @@
+name: Publish Docker Image
+
+on:
+  release:
+    types: [published]
+  workflow_dispatch:
+    inputs:
+      tag:
+        description: 'Tag to build and publish'
+        required: true
+        default: 'latest'
+
+env:
+  REGISTRY: docker.io
+  IMAGE_NAME: thesimplekid/cdk-mintd
+
+jobs:
+  build-and-push:
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      packages: write
+
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+
+      - name: Login to Docker Hub
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+      - name: Extract metadata (tags, labels) for Docker
+        id: meta
+        uses: docker/metadata-action@v5
+        with:
+          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+          tags: |
+            type=raw,value=latest,enable=${{ github.event_name == 'release' }}
+            type=semver,pattern={{version}}
+            type=semver,pattern={{major}}.{{minor}}
+            type=ref,event=branch
+            type=ref,event=pr
+            type=sha
+            ${{ github.event.inputs.tag != '' && github.event.inputs.tag || '' }}
+
+      - name: Build and push Docker image
+        uses: docker/build-push-action@v5
+        with:
+          context: .
+          push: true
+          platforms: linux/amd64
+          tags: ${{ steps.meta.outputs.tags }}
+          labels: ${{ steps.meta.outputs.labels }}
+          cache-from: type=gha
+          cache-to: type=gha,mode=max

+ 43 - 0
.github/workflows/nutshell_itest.yml

@@ -0,0 +1,43 @@
+name: Nutshell integration
+
+on: [push, pull_request]
+
+jobs:
+  nutshell-integration-tests:
+    name: Nutshell Mint Integration Tests
+    runs-on: ubuntu-latest
+    steps:
+      - name: checkout
+        uses: actions/checkout@v4
+      - name: Install Nix
+        uses: DeterminateSystems/nix-installer-action@v11
+      - name: Nix Cache
+        uses: DeterminateSystems/magic-nix-cache-action@v6
+      - name: Rust Cache
+        uses: Swatinem/rust-cache@v2
+      - name: Test Nutshell
+        run: nix develop -i -L .#integration --command just test-nutshell
+      - name: Show logs if tests fail
+        if: failure()
+        run: docker logs nutshell
+
+  nutshell-wallet-integration-tests:
+    name: Nutshell Wallet Integration Tests
+    runs-on: ubuntu-latest
+    steps:
+      - name: checkout
+        uses: actions/checkout@v4
+      - name: Pull Nutshell Docker image
+        run: docker pull cashubtc/nutshell:latest
+      - name: Install Nix
+        uses: DeterminateSystems/nix-installer-action@v11
+      - name: Nix Cache
+        uses: DeterminateSystems/magic-nix-cache-action@v6
+      - name: Rust Cache
+        uses: Swatinem/rust-cache@v2
+      - name: Test Nutshell Wallet
+        run: |
+          nix develop -i -L .#integration --command just nutshell-wallet-itest
+      - name: Show Docker logs if tests fail
+        if: failure()
+        run: docker logs nutshell-wallet || true

+ 3 - 0
.gitignore

@@ -9,3 +9,6 @@ config.toml
 .pre-commit-config.yaml
 result
 Cargo.lock
+.aider*
+**/postgres_data/
+**/.env

+ 4 - 1
.typos.toml

@@ -3,5 +3,8 @@ extend-ignore-re = [
     # Ignore cashu tokens
     "cashuA[A-Za-z0-9-_]+",
     "cashuB[A-Za-z0-9-_]+",
-    "casshuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9"
+    "casshuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9",
+    "autheticator",
+    "Gam",
+    "flate2"
 ]

+ 117 - 72
CHANGELOG.md

@@ -1,97 +1,152 @@
 # Changelog
 
 <!-- All notable changes to this project will be documented in this file. -->
-
 <!-- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -->
 <!-- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -->
 
-<!-- Template
+## [0.9.0](https://github.com/cashubtc/cdk/releases/tag/v0.9.0)
+### Added
+- Amountless invoices [NUT](https://github.com/cashubtc/nuts/pull/173) [PR](https://github.com/cashubtc/cdk/pull/497) ([thesimplekid]).
+- `create_time`, `paid_time` to mint and melt quotes [PR](https://github.com/cashubtc/cdk/pull/708) ([thesimplekid]).
+- cdk-mint-rpc: Added get mint and melt quotes ttl [PR](https://github.com/cashubtc/cdk/pull/716) ([thesimplekid]).
 
-## [Unreleased]
+### Changed
+- cashu: Move wallet mod to cdk-common ([thesimplekid]).
+- Export Mint DB Traits [PR](https://github.com/cashubtc/cdk/pull/710) ([davidcaseria]).
+- Move Mint and Melt quote to `cdk` commit from `cashu` [PR](https://github.com/cashubtc/cdk/pull/665) ([thesimplekid]).
 
-### Summary
+### Fixed
+- Creation of memory sqlite db [PR](https://github.com/cashubtc/cdk/pull/707) ([crodas]).
+- cdk-cli: Ensure auth wallet is created before attempting to mint pending [PR](https://github.com/cashubtc/cdk/pull/704) ([thesimplekid]).
+- cdk-mint-rpc: Adding mint urls was not updating correctly [PR](https://github.com/cashubtc/cdk/pull/716) ([thesimplekid]).
+- cdk-mint-rpc: Fixed setting long description [PR](https://github.com/cashubtc/cdk/pull/716) ([thesimplekid]).
 
-### Changed
 
-### Added
+## [v0.8.1](https://github.com/cashubtc/cdk/releases/tag/v0.8.1)
+### Fixed
+- cashu: Handle url with paths [PR](https://github.com/cashubtc/cdk/pull/678) ([benthecarman]).
 
+### Changed
+- cdk: Export `MintKeySetInfo` [PR](https://github.com/cashubtc/cdk/pull/673) ([davidcaseria]).
+
+## [v0.8.0](https://github.com/cashubtc/cdk/releases/tag/v0.8.0)
 ### Fixed
+- cdk: Proof matches conditions was not matching payment conditions correctly ([thesimplekid]).
+- cdk: Updating mint_url would remove proofs when we want to keep them ([ok300]).
+- Wallet: Fix ability to receive cashu tokens that include DLEQ proofs ([ok300]).
+- cdk-sqlite: Wallet was not storing dleq proofs ([thesimplekid]).
+
+### Changed
+- Updated MSRV to 1.75.0 ([thesimplekid]).
+- cdk-sqlite: Do not use `UPDATE OR REPLACE` ([crodas]).
+- cdk: Refactor keyset init ([lollerfirst]).
+- Feature-gated lightning backends (CLN, LND, LNbits, FakeWallet) for selective compilation ([thesimplekid]).
+- cdk-sqlite: Update sqlx to 0.7.4 ([benthecarman]).
+- Unifies and optimizes the proof selection algorithm to use Wallet::select_proofs ([davidcaseria]).
+- Wallet::send now requires a PreparedSend ([davidcaseria]).
+- WalletDatabase proof state update functions have been consolidated into update_proofs_state ([davidcaseria]).
+- Moved `MintQuote` and `MeltQuote` from `cashu` to `cdk-common` ([thesimplekid]).
+
+### Added
+- Added redb feature to mintd in order to meet MSRV target ([thesimplekid]).
+- cdk-sqlite: In memory sqlite database ([crodas]).
+- Add `tos_url` to `MintInfo` ([nodlAndHodl]).
+- cdk: Add tos_url setter to `MintBuilder` ([thesimplekid]).
+- Added optional "request" and "unit" fields to MeltQuoteBolt11Response [NUT Change](https://github.com/cashubtc/nuts/pull/235) ([thesimplekid]).
+- Added optional "amount" and "unit" fields to MintQuoteBolt11Response [NUT Change](https://github.com/cashubtc/nuts/pull/235) ([thesimplekid]).
+- Compile-time error when no lightning backend features are enabled ([thesimplekid]).
+- Add support for sqlcipher ([benthecarman]).
+- Payment processor ([thesimplekid]).
+- Payment request builder ([thesimplekid]).
+- Sends should be initiated by calling Wallet::prepare_send ([davidcaseria]).
+- A SendOptions struct controls optional functionality for sends ([davidcaseria]).
+- Allow Amount splitting to target a fee rate amount ([davidcaseria]).
+- Utility functions for Proofs ([davidcaseria]).
+- Utility functions for SendKind ([davidcaseria]).
+- Completed checked arithmetic operations for Amount (i.e., checked_mul and checked_div) ([davidcaseria]).
 
 ### Removed
+- Remove support for Memory Database in cdk ([crodas]).
+- Remove `AmountStr` ([crodas]).
+- Remove `get_nostr_last_checked` from `WalletDatabase` ([thesimplekid]).
+- Remove `add_nostr_last_checked` from `WalletDatabase` ([thesimplekid]).
 
--->
+## [cdk-mintd:v0.7.4](https://github.com/cashubtc/cdk/releases/tag/cdk-mintd-v0.7.4)
+### Changed
+- cdk-mintd: Update to cdk v0.7.2 ([thesimplekid]).
 
-#[Unrelased]
+## [cdk:v0.7.2](https://github.com/cashubtc/cdk/releases/tag/cdk-v0.7.2)
+### Fixed
+- cdk: Ordering of swap verification checks ([thesimplekid]).
 
-#[cdk-mintd:v0.7.2]
+## [cdk-mintd-v0.7.2](https://github.com/cashubtc/cdk/releases/tag/cdk-mintd-v0.7.2)
 ### Fixed
--cdk-mintd: Fixed mint and melt error on mint initialized with RPC interface disabled ([ok300]).
+- cdk-mintd: Fixed mint and melt error on mint initialized with RPC interface disabled ([ok300]).
 
 
-#[v0.7.1]
+## [v0.7.1](https://github.com/cashubtc/cdk/releases/tag/v0.7.1)
 ### Changed
 - cdk: Debug print of `Id` is hex ([thesimplekid]).
 - cdk: Debug print of mint secret is the hash ([thesimplekid]).
-- cdk: Use check_incoming payment on attempted mint or check mint qutoe ([thesimplekid]).
+- cdk: Use check_incoming payment on attempted mint or check mint quote ([thesimplekid]).
 - cdk-cln: Use `call_typed` for cln rpc calls ([daywalker90]).
 
 ### Added
 - cdk: Mint builder add ability to set custom derivation paths ([thesimplekid]).
 
 ### Fixed
-- cdk-cln: return error on stream error ([thesimplekid]).
+- cdk-cln: Return error on stream error ([thesimplekid]).
 
 
-#[v0.7.0]
+## [v0.7.0](https://github.com/cashubtc/cdk/releases/tag/v0.7.0)
 ### Changed
-* Moved db traits to `cdk-common` ([crodas]).
-* Moved other commin types to `cdk-common` ([crodas]).
-* `Wallet::mint` returns the minted `Proofs` and not just the amount ([dacidcaseria]).
+- Moved db traits to `cdk-common` ([crodas]).
+- Moved other common types to `cdk-common` ([crodas]).
+- `Wallet::mint` returns the minted `Proofs` and not just the amount ([davidcaseria]).
 
 ### Added
-* `Token::to_raw_bytes` serializes generic token to raw bytes ([lollerfirst]).
-* `Token::try_from` for `Vec<u8>` constructs a generic token from raw bytes ([lollerfirst]).
-* `TokenV4::to_raw_bytes()` serializes a TokenV4 to raw bytes following the spec ([lollerfirst]).
-* `Wallet::receive_raw` which receives raw binary tokens ([lollerfirst]).
-* cdk-mint-rpc: Mint management gRPC client and server ([thesimpekid]).
-* cdk-common: cdk specific types and traits ([crodas]).
-* cashu: Core types and functions defined in NUTs ([crodas]).
+- `Token::to_raw_bytes` serializes generic token to raw bytes ([lollerfirst]).
+- `Token::try_from` for `Vec<u8>` constructs a generic token from raw bytes ([lollerfirst]).
+- `TokenV4::to_raw_bytes()` serializes a TokenV4 to raw bytes following the spec ([lollerfirst]).
+- `Wallet::receive_raw` which receives raw binary tokens ([lollerfirst]).
+- cdk-mint-rpc: Mint management gRPC client and server ([thesimplekid]).
+- cdk-common: cdk specific types and traits ([crodas]).
+- cashu: Core types and functions defined in NUTs ([crodas]).
 
 ### Fixed
-* Multimint unit check when wallet receiving token ([thesimplekid]).
-* Mint start up with most recent keyset after a rotation ([thesimplekid]).
+- Multimint unit check when wallet receiving token ([thesimplekid]).
+- Mint start up with most recent keyset after a rotation ([thesimplekid]).
 
 
-#[cdk:v0.6.1,cdk-mintd:v0.6.2]
+## [cdk-v0.6.1, cdk-mintd-v0.6.2](https://github.com/cashubtc/cdk/releases/tag/cdk-mintd-v0.6.1)
 ### Fixed
-cdk: Missing check on mint that outputs equals the quote amount ([thesimplekid]).
-cdk: Reset mint quote status if in state that cannot continue ([thesimplekid]).
+- cdk: Missing check on mint that outputs equals the quote amount ([thesimplekid]).
+- cdk: Reset mint quote status if in state that cannot continue ([thesimplekid]).
 
-#[0.6.1]
+## [v0.6.1](https://github.com/cashubtc/cdk/releases/tag/cdk-v0.6.1)
 ### Added
-cdk-mintd: Get work-dir from env var ([thesimplekid])
-
-#[0.6.0]
+- cdk-mintd: Get work-dir from env var ([thesimplekid]).
 
+## [v0.6.0](https://github.com/cashubtc/cdk/releases/tag/v0.6.0)
 ### Changed
-cdk: Enforce `quote_id` to uuid type in mint ([tdelabro]).
-cdk: Refactor wallet mint connector ([ok300]).
+- cdk: Enforce `quote_id` to uuid type in mint ([tdelabro]).
+- cdk: Refactor wallet mint connector ([ok300]).
 
 ### Added
-cdk: `NUT19` Settings in `NUT06` info ([thesimplekid]).
-cdk: `NUT17` Websocket support for wallet ([crodas]).
-cdk-axum: Redis cache backend ([crodas]).
-cdk-mints: Get mint settings from env vars ([thesimplekid]).
-cdk-axum: HTTP compression support ([ok300]).
+- cdk: `NUT19` Settings in `NUT06` info ([thesimplekid]).
+- cdk: `NUT17` Websocket support for wallet ([crodas]).
+- cdk-axum: Redis cache backend ([crodas]).
+- cdk-mints: Get mint settings from env vars ([thesimplekid]).
+- cdk-axum: HTTP compression support ([ok300]).
 
 ### Fixed
-cdk-sqlite: keyset counter was overwritten when keyset was fetched from mint ([thesimplekid]).
-cdk-cli: on `mint` use `unit` from cli args ([thesimplekid]).
-cdk-cli: on `restore` create `wallet` if it does not exist ([thesimplekid]).
-cdk: Signaling support for optional nuts ([thesimpekid]).
-cdk-phd: Check payment has valid uuis ([thesimplekid]).
+- cdk-sqlite: Keyset counter was overwritten when keyset was fetched from mint ([thesimplekid]).
+- cdk-cli: On `mint` use `unit` from cli args ([thesimplekid]).
+- cdk-cli: On `restore` create `wallet` if it does not exist ([thesimplekid]).
+- cdk: Signaling support for optional nuts ([thesimplekid]).
+- cdk-phd: Check payment has valid uuid ([thesimplekid]).
 
-#[0.5.0]
+## [v0.5.0](https://github.com/cashubtc/cdk/releases/tag/v0.5.0)
 ### Changed
 - cdk: Bump `bitcoin` to `0.32.2` ([prusnak]).
 - cdk: Bump `lightning-invoice` to `0.32.2` ([prusnak]).
@@ -107,20 +162,20 @@ cdk-phd: Check payment has valid uuis ([thesimplekid]).
 - cdk: Use `MintUrl` directly in wallet client ([ok300]).
 - cdk-cli: Change cdk-cli pay command to melt ([mubarak23]).
 - cdk: Rename `Wallet::get_proofs` to `Wallet::get_unspent_proofs` ([ok300]).
-- cdk: `Id` to `u32` changed from `TryFrom` to `From` ([vnrpc]). 
+- cdk: `Id` to `u32` changed from `TryFrom` to `From` ([vnprc]).
 
 
 ### Added
 - cdk: Added description to `MintQuoteBolt11Request` ([lollerfirst]).
 - cdk(wallet): Added description to `mint_quote` ([lollerfirst]).
 - cdk: Add `amount` and `fee_paid` to `Melted` ([davidcaseria]).
-- cdk: Add `from_proofs` on `Melted` ([davidcaseria]). 
+- cdk: Add `from_proofs` on `Melted` ([davidcaseria]).
 - cdk: Add unit on `PaymentResponse` ([thesimplekid]).
 - cdk: Add description for mint quote ([lollerfirst]).
 - cdk-axum: Add cache to some endpoints ([lollerfirst]).
 - cdk: Add Proofs trait ([ok300]).
 - cdk: Wallet verifies keyset id when first fetching keys ([thesimplekid]).
-- cdk-mind: Add swagger docs ([ok300]).
+- cdk-mintd: Add swagger docs ([ok300]).
 - cdk: NUT18 payment request support ([thesimplekid]).
 - cdk: Add `Wallet::get_proofs_with` ([ok300]).
 - cdk: Mint NUT-17 Websocket support ([crodas]).
@@ -137,9 +192,7 @@ cdk-phd: Check payment has valid uuis ([thesimplekid]).
 
 
 
-#[0.4.0]
-### Summary
-
+## [v0.4.0](https://github.com/cashubtc/cdk/releases/tag/v0.4.0)
 ### Changed
 - cdk: Reduce MSRV to 1.63.0 ([thesimplekid]).
 - cdk-axum: Reduce MSRV to 1.63.0 ([thesimplekid]).
@@ -158,19 +211,15 @@ cdk-phd: Check payment has valid uuis ([thesimplekid]).
 ### Added
 - cdk: Multiple error types ([thesimplekid]).
 
-
 ### Fixed
-- cdk(mint): use checked addition on amount to ensure there is no overflow ([thesimplekid]).
+- cdk(mint): Use checked addition on amount to ensure there is no overflow ([thesimplekid]).
 
 ### Removed
 - cdk(wallet): Removed CDK wallet error ([thesimplekid]).
 - cdk(mint): Removed CDK mint error ([thesimplekid]).
 
 
-## [0.3.0]
-
-### Summary
-
+## [v0.3.0](https://github.com/cashubtc/cdk/releases/tag/v0.3.0)
 ### Changed
 - cdk(wallet): `fn send` returns `Token` so the user can use the struct of convert it to a v3 or v4 string ([thesimplekid]).
 - cdk(wallet): Publicly export `MultiMintWallet` ([thesimplekid]).
@@ -181,7 +230,7 @@ cdk-phd: Check payment has valid uuis ([thesimplekid]).
 - cdk-cli: Receive will add wallet when receiving if mint is unknown ([thesimplekid]).
 - cdk(cdk-database/mint): Rename `get_blinded_signatures` to `get_blind_signatures` ([thesimplekid]).
 - cdk(cdk-database/mint): Rename `get_blinded_signatures_for_keyset` to `get_blind_signatures_for_keyset` ([thesimplekid]).
-- cdk(mint): typo rename `total_redeame` to `total_redeemed` ([vnprc])
+- cdk(mint): Typo rename `total_redeame` to `total_redeemed` ([vnprc]).
 - cdk(mint): Refactored `MintKeySet::generate_from_xpriv` and `MintKeySet::generate_from_seed` methods to accept max_order, currency_unit, and derivation_path parameters directly ([vnprc]).
 - cdk(wallet): Return WalletKey for UnknownWallet error ([davidcaseria]).
 - cdk(cdk-lightning): `CreateInvoiceResponse` added expiry time to better support backends where it cannot be set ([thesimplekid]).
@@ -200,7 +249,7 @@ cdk-phd: Check payment has valid uuis ([thesimplekid]).
 - cdk(mint): Add `total_issued` and `total_redeamed` ([thesimplekid]).
 - cdk(cdk-database/mint) Add `get_proofs_by_keyset_id` ([thesimplekid]).
 - cdk(wallet/mint): Add `mint_icon_url` ([cjbeery24]).
-- cdk: Add `MintUrl` that sanatizes mint url by removing trailing `/` ([cjbeery24]).
+- cdk: Add `MintUrl` that sanitizes mint url by removing trailing `/` ([cjbeery24]).
 - cdk(cdk-database/mint): Add `update_proofs` that both adds new `ProofInfo`s to the db and deletes ([davidcaseria]).
 - cdk(cdk-database/mint): Add `set_pending_proofs`, `reserve_proofs`, and `set_unspent_proofs` ([davidcaseria]).
 
@@ -218,8 +267,7 @@ cdk-phd: Check payment has valid uuis ([thesimplekid]).
 - cdk: Remove `UncheckedUrl` in favor of `MintUrl` ([cjbeery24]).
 - cdk(cdk-database/mint): Remove `set_proof_state`, `remove_proofs` and `add_proofs` ([davidcaseria]).
 
-## [v0.2.0]
-
+## [v0.2.0](https://github.com/cashubtc/cdk/releases/tag/v0.2.0)
 ### Summary
 This release introduces TokenV4, which uses CBOR encoding as the default token format. It also includes fee support for both wallet and mint operations.
 
@@ -228,11 +276,11 @@ When sending, the sender can choose to include the necessary fee to ensure that
 Additionally, this release introduces a Mint binary cdk-mintd that uses the cdk-axum crate as a web server to create a full Cashu mint. When paired with a Lightning backend, currently implemented as Core Lightning, it is included in this release as cdk-cln.
 
 ### Changed
-- cdk(wallet): `wallet:receive` will not claim `proofs` from a mint other then the wallet's mint ([thesimplekid]).
+- cdk(wallet): `wallet:receive` will not claim `proofs` from a mint other than the wallet's mint ([thesimplekid]).
 - cdk(NUT00): `Token` is changed from a `struct` to `enum` of either `TokenV4` or `Tokenv3` ([thesimplekid]).
 - cdk(NUT00): Rename `MintProofs` to `TokenV3Token` ([thesimplekid]).
 - cdk(wallet): Additional arguments in `send` `send_kind` and `include_fees` for control of how to handle fees in a send ([thesimplekid]).
-- cdk(wallet): Additional arguments in `create_swap` `include_fees` for control of if fees to redeam the send proofs are included in send amount ([thesimplekid]).
+- cdk(wallet): Additional arguments in `create_swap` `include_fees` for control of if fees to redeem the send proofs are included in send amount ([thesimplekid]).
 
 ### Added
 - cdk: TokenV4 CBOR ([davidcaseria]/[thesimplekid]).
@@ -247,19 +295,15 @@ Additionally, this release introduces a Mint binary cdk-mintd that uses the cdk-
 - cdk: NUT02 Support fees ([thesimplekid]).
 
 ### Fixed
-- cdk: NUT06 deseralize `MintInfo` ([thesimplekid]).
+- cdk: NUT06 deserialize `MintInfo` ([thesimplekid]).
 
 
-## [v0.1.1]
-
-### Summary
-
+## [v0.1.1](https://github.com/cashubtc/cdk/releases/tag/v0.1.1)
 ### Changed
 - cdk(wallet): `wallet::total_pending_balance` does not include reserved proofs ([thesimplekid]).
 
-
 ### Added
-- cdk(wallet): Added get reserved proofs [thesimplekid](https://github.com/thesimplekid).
+- cdk(wallet): Added get reserved proofs ([thesimplekid]).
 
 <!-- Contributors -->
 [thesimplekid]: https://github.com/thesimplekid
@@ -271,7 +315,8 @@ Additionally, this release introduces a Mint binary cdk-mintd that uses the cdk-
 [lollerfirst]: https://github.com/lollerfirst
 [prusnak]: https://github.com/prusnak
 [mubarak23]: https://github.com/mubarak23
-[vnprc]: https://github.com/vnprc
 [crodas]: https://github.com/crodas
 [tdelabro]: https://github.com/tdelabro
 [daywalker90]: https://github.com/daywalker90
+[nodlAndHodl]: https://github.com/nodlAndHodl
+[benthecarman]: https://github.com/benthecarman

+ 96 - 1
Cargo.toml

@@ -4,14 +4,109 @@ members = [
 ]
 resolver = "2"
 
+[workspace.lints.rust]
+unsafe_code = "forbid"
+unreachable_pub = "warn"
+missing_debug_implementations = "warn"
+large_enum_variant = "warn"
+
+[workspace.lints.clippy]
+pedantic = "warn"
+unwrap_used = "warn"
+clone_on_ref_ptr = "warn"
+missing_errors_doc = "warn"
+missing_panics_doc = "warn"
+missing_safety_doc = "warn"
+nursery = "warn"
+redundant_else = "warn"
+redundant_closure_for_method_calls = "warn"
+unneeded_field_pattern = "warn"
+use_debug = "warn"
+
+[workspace.lints.rustdoc]
+missing_docs = "warn"
+bare_urls = "warn"
+
 [workspace.package]
+edition = "2021"
+rust-version = "1.75.0"
 license = "MIT"
 homepage = "https://github.com/cashubtc/cdk"
 repository = "https://github.com/cashubtc/cdk.git"
+version = "0.9.0"
+readme = "README.md"
+
+[workspace.dependencies]
+anyhow = "1"
+async-trait = "0.1"
+axum = { version = "0.8.1", features = ["ws"] }
+bitcoin = { version = "0.32.2", features = ["base64", "serde", "rand", "rand-std"] }
+bip39 = { version = "2.0", features = ["rand"] }
+jsonwebtoken = "9.2.0"
+cashu = { path = "./crates/cashu", version = "=0.9.0" }
+cdk = { path = "./crates/cdk", default-features = false, version = "=0.9.0" }
+cdk-common = { path = "./crates/cdk-common", default-features = false, version = "=0.9.0" }
+cdk-axum = { path = "./crates/cdk-axum", default-features = false, version = "=0.9.0" }
+cdk-cln = { path = "./crates/cdk-cln", version = "=0.9.0" }
+cdk-lnbits = { path = "./crates/cdk-lnbits", version = "=0.9.0" }
+cdk-lnd = { path = "./crates/cdk-lnd", version = "=0.9.0" }
+cdk-fake-wallet = { path = "./crates/cdk-fake-wallet", version = "=0.9.0" }
+cdk-payment-processor = { path = "./crates/cdk-payment-processor", default-features = true, version = "=0.9.0" }
+cdk-mint-rpc = { path = "./crates/cdk-mint-rpc", version = "=0.9.0" }
+cdk-redb = { path = "./crates/cdk-redb", default-features = true, version = "=0.9.0" }
+cdk-sqlite = { path = "./crates/cdk-sqlite", default-features = true, version = "=0.9.0" }
+cdk-signatory = { path = "./crates/cdk-signatory", version = "=0.9.0" }
+clap = { version = "4.5.31", features = ["derive"] }
+ciborium = { version = "0.2.2", default-features = false, features = ["std"] }
+cbor-diag = "0.1.12"
+futures = { version = "0.3.28", default-features = false, features = ["async-await"] }
+lightning-invoice = { version = "0.32.0", features = ["serde", "std"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+thiserror = { version = "1" }
+tokio = { version = "1", default-features = false, features = ["rt", "macros", "test-util"] }
+tokio-util = { version = "0.7.11", default-features = false }
+tower-http = { version = "0.6.1", features = ["compression-full", "decompression-full", "cors", "trace"] }
+tokio-tungstenite = { version = "0.26.0", default-features = false }
+tokio-stream = "0.1.15"
+tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
+tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
+url = "2.3"
+uuid = { version = "=1.12.1", features = ["v4", "serde"] }
+utoipa = { version = "5.3.1", features = [
+    "preserve_order",
+    "preserve_path_order",
+]}
+serde_with = "3"
+reqwest = { version = "0.12", default-features = false, features = [
+    "json",
+    "rustls-tls",
+    "rustls-tls-native-roots",
+    "socks",
+    "zstd",
+    "brotli",
+    "gzip",
+    "deflate",
+]}
+once_cell = "1.20.2"
+instant = { version = "0.1", default-features = false }
+rand = "0.8.5"
+regex = "1"
+home = "0.5.5"
+tonic = { version = "0.12.3", features = [
+    "channel",
+    "tls",
+    "tls-webpki-roots",
+] }
+prost = "0.13.1"
+tonic-build = "0.12"
+strum = "0.27.1"
+strum_macros = "0.27.1"
+
+
 
 [workspace.metadata]
 authors = ["CDK Developers"]
-edition = "2021"
 description = "Cashu Development Kit"
 readme = "README.md"
 repository = "https://github.com/cashubtc/cdk"

+ 1 - 1
DEVELOPMENT.md

@@ -83,7 +83,7 @@ just test
 
 ### Running Integration Tests
 ```bash
-just itest REDB/SQLITE/MEMEORY
+just itest REDB/SQLITE/MEMORY
 ```
 
 NOTE: if this command fails on macos change the nix channel to unstable (in the `flake.nix` file modify `nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";` to `nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";`)

+ 21 - 3
README.md

@@ -22,21 +22,35 @@ The project is split up into several crates in the `crates/` directory:
     * [**cdk-axum**](./crates/cdk-axum/): Axum webserver for mint.
     * [**cdk-cln**](./crates/cdk-cln/): CLN Lightning backend for mint.
     * [**cdk-lnd**](./crates/cdk-lnd/): Lnd Lightning backend for mint.
-    * [**cdk-strike**](./crates/cdk-strike/): Strike Lightning backend for mint.
     * [**cdk-lnbits**](./crates/cdk-lnbits/): [LNbits](https://lnbits.com/) Lightning backend for mint.
-    * [**cdk-phoenixd**](./crates/cdk-phoenixd/): Phoenixd Lightning backend for mint.
     * [**cdk-fake-wallet**](./crates/cdk-fake-wallet/): Fake Lightning backend for mint. To be used only for testing, quotes are automatically filled.
     * [**cdk-mint-rpc**](./crates/cdk-mint-rpc/): Mint management gRPC server and cli.
 * Binaries:
     * [**cdk-cli**](./crates/cdk-cli/): Cashu wallet CLI.
     * [**cdk-mintd**](./crates/cdk-mintd/): Cashu Mint Binary.
-    * [**cdk-mint-cli**](./crates/cdk-mint-rpc/): Cashu Mint managemtn gRPC client cli.
+    * [**cdk-mint-cli**](./crates/cdk-mint-rpc/): Cashu Mint management gRPC client cli.
 
 
 ## Development 
 
 For a guide to settings up a development environment see [DEVELOPMENT.md](./DEVELOPMENT.md)
 
+### Code Style Guidelines
+
+- **Large Enum Variants**: When an enum variant contains a large type (>100 bytes), box it using `Box<T>` to reduce the overall enum size. This improves memory efficiency, especially for error types.
+
+  ```rust
+  // Instead of this:
+  enum Error {
+      SomeLargeError(LargeType),  // LargeType is >100 bytes
+  }
+  
+  // Do this:
+  enum Error {
+      SomeLargeError(Box<LargeType>),
+  }
+  ```
+
 ## Implemented [NUTs](https://github.com/cashubtc/nuts/):
 
 ### Mandatory
@@ -69,6 +83,8 @@ For a guide to settings up a development environment see [DEVELOPMENT.md](./DEVE
 | [18][18] | Payment Requests  | :heavy_check_mark: |
 | [19][19] | Cached responses  | :heavy_check_mark: |
 | [20][20] | Signature on Mint Quote  | :heavy_check_mark: |
+| [21][21] | Clear Authentication | :heavy_check_mark: |
+| [22][22] | Blind Authentication  | :heavy_check_mark: |
 
 
 ## Bindings
@@ -109,3 +125,5 @@ Please see the [development guide](DEVELOPMENT.md).
 [18]: https://github.com/cashubtc/nuts/blob/main/18.md
 [19]: https://github.com/cashubtc/nuts/blob/main/19.md
 [20]: https://github.com/cashubtc/nuts/blob/main/20.md
+[20]: https://github.com/cashubtc/nuts/blob/main/21.md
+[20]: https://github.com/cashubtc/nuts/blob/main/22.md

+ 26 - 26
crates/cashu/Cargo.toml

@@ -1,44 +1,44 @@
 [package]
 name = "cashu"
-version = "0.7.1"
-edition = "2021"
+version.workspace = true
+edition.workspace = true
 authors = ["CDK Developers"]
 description = "Cashu shared types and crypto utilities, used as the foundation for the CDK and their crates"
 homepage = "https://github.com/cashubtc/cdk"
 repository = "https://github.com/cashubtc/cdk.git"
-rust-version = "1.63.0"                                                                                      # MSRV
-license = "MIT"
+rust-version.workspace = true # MSRV
+license.workspace = true
+readme = "README.md"
 
 [features]
-default = ["mint", "wallet"]
+default = ["mint", "wallet", "auth"]
 swagger = ["dep:utoipa"]
 mint = ["dep:uuid"]
 wallet = []
+auth = ["dep:strum", "dep:strum_macros", "dep:regex"]
 bench = []
 
 [dependencies]
-uuid = { version = "=1.12.1", features = ["v4", "serde"], optional = true }
-bitcoin = { version = "0.32.2", features = [
-    "base64",
-    "serde",
-    "rand",
-    "rand-std",
-] }
-cbor-diag = "0.1.12"
-ciborium = { version = "0.2.2", default-features = false, features = ["std"] }
-once_cell = "1.20.2"
-serde = { version = "1", features = ["derive"] }
-lightning-invoice = { version = "0.32.0", features = ["serde", "std"] }
-thiserror = "2"
-tracing = "0.1"
-url = "2.3"
-utoipa = { version = "4", optional = true }
-serde_json = "1"
-serde_with = "3"
+uuid = { workspace = true, optional = true }
+bitcoin.workspace = true
+cbor-diag.workspace = true
+ciborium.workspace = true
+once_cell.workspace = true
+serde.workspace = true
+lightning-invoice.workspace = true
+thiserror.workspace = true
+tracing.workspace = true
+url.workspace = true
+utoipa = { workspace = true, optional = true }
+serde_json.workspace = true
+serde_with.workspace = true
+regex = { workspace = true, optional = true }
+strum = { workspace = true, optional = true }
+strum_macros = { workspace = true, optional = true }
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
-instant = { version = "0.1", features = ["wasm-bindgen", "inaccurate"] }
+instant = { workspace = true, features = ["wasm-bindgen", "inaccurate"] }
 
 [dev-dependencies]
-bip39 = "2.0"
-uuid = { version = "=1.12.1", features = ["v4", "serde"] }
+bip39.workspace = true
+uuid.workspace = true

+ 102 - 0
crates/cashu/README.md

@@ -0,0 +1,102 @@
+# Cashu
+
+[![crates.io](https://img.shields.io/crates/v/cashu.svg)](https://crates.io/crates/cashu)
+[![Documentation](https://docs.rs/cashu/badge.svg)](https://docs.rs/cashu)
+[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/cashubtc/cdk/blob/main/LICENSE)
+
+A Rust implementation of the [Cashu](https://github.com/cashubtc) protocol, providing the core functionality for Cashu e-cash operations.
+
+## Overview
+
+ This crate implements the core Cashu protocol as defined in the [Cashu NUTs (Notation, Usage, and Terminology)](https://github.com/cashubtc/nuts/).
+
+## Features
+
+- **Cryptographic Operations**: Secure blind signatures and verification
+- **Token Management**: Creation, validation, and manipulation of Cashu tokens
+- **NUTs Implementation**: Support for the core Cashu protocol specifications
+- **Type-safe API**: Strongly-typed interfaces for working with Cashu primitives
+
+## Usage
+
+Add this to your `Cargo.toml`:
+
+```toml
+[dependencies]
+cashu = "*"
+```
+
+### Basic Example
+
+```rust
+use cashu::amount::Amount;
+use cashu::nuts::nut00::Token;
+use std::str::FromStr;
+
+// Parse a Cashu token from a string
+let token_str = "cashuBo2FteCJodHRwczovL25vZmVlcy50ZXN0bnV0LmNhc2h1LnNwYWNlYXVjc2F0YXSBomFpSAC0zSfYhhpEYXCCpGFhAmFzeEAzYzNlOWRhMDU3ZjQzNmExOTc2MmRhOWYyYTBjMzc5YzE5N2RlNDMzZDY5MWU1NDI0ZmRjODcxNjZjMmNlMjZmYWNYIQKKtwESLR-yn5rqNAL3_8_H5BtpwjSPs7uOJ18kPn2mV2Fko2FlWCCsMAK1xoLlwVRxpv8hfsxKYXlXTOomiVt3JCbzNgQpUmFzWCD9MfRUr0asiF_jUJMSylphLvKUd2SLz9oSpcvuLCXPp2FyWCA_1toQ_l158xW0zorqTBXvh76o-_D3e-Ru1Ea-51UrFaRhYQhhc3hAMTM5YWRjZDJlY2Q5MWQyNjNjMDhhMzdhNjBmODZjNDVkYWE3NjNmNjM4NTY0NzEyMmFiZjhlMDM3OGQ0NjA5OGFjWCECHZh5Qx9o-8PaY6t0d5hRTbWeez1dh3md7ehfE25f2N5hZKNhZVgg5MLkVzIw2tDzdUpYwFe-MLhIPJ4hkCpPGL0X7RxpPIRhc1ggyEtcsq3FX8wZOGpwTXOP7BsqfdYdMhGG1X8jVjncDcVhclggyLVOc2xy4m1_YeYGef2HQ8WyJX7LjZq403CS9Dt_eME=";
+let token = Token::from_str(token_str).expect("Valid token");
+
+// Get the total amount
+let amount: Amount = token.value().expect("Value");
+println!("Token amount: {}", amount);
+```
+
+## Implemented NUTs
+
+### Mandatory
+
+| NUT #    | Description                       |
+|----------|-----------------------------------|
+| [00][00] | Cryptography and Models           |
+| [01][01] | Mint public keys                  |
+| [02][02] | Keysets and fees                  |
+| [03][03] | Swapping tokens                   |
+| [04][04] | Minting tokens                    |
+| [05][05] | Melting tokens                    |
+| [06][06] | Mint info                         |
+
+### Optional
+
+| # | Description | Status
+| --- | --- | --- |
+| [07][07] | Token state check | Implemented |
+| [08][08] | Overpaid Lightning fees | Implemented |
+| [09][09] | Signature restore | Implemented |
+| [10][10] | Spending conditions | Implemented |
+| [11][11] | Pay-To-Pubkey (P2PK) | Implemented |
+| [12][12] | DLEQ proofs | Implemented |
+| [13][13] | Deterministic secrets | Implemented |
+| [14][14] | Hashed Timelock Contracts (HTLCs) | Implemented |
+| [15][15] | Partial multi-path payments (MPP) | Implemented |
+| [16][16] | Animated QR codes | Not implemented |
+| [17][17] | WebSocket subscriptions  | Implemented |
+| [18][18] | Payment Requests  | Implemented |
+| [19][19] | Cached responses  | Implemented |
+| [20][20] | Signature on Mint Quote  | Implemented |
+
+## License
+
+This project is licensed under the [MIT License](https://github.com/cashubtc/cdk/blob/main/LICENSE).
+
+[00]: https://github.com/cashubtc/nuts/blob/main/00.md
+[01]: https://github.com/cashubtc/nuts/blob/main/01.md
+[02]: https://github.com/cashubtc/nuts/blob/main/02.md
+[03]: https://github.com/cashubtc/nuts/blob/main/03.md
+[04]: https://github.com/cashubtc/nuts/blob/main/04.md
+[05]: https://github.com/cashubtc/nuts/blob/main/05.md
+[06]: https://github.com/cashubtc/nuts/blob/main/06.md
+[07]: https://github.com/cashubtc/nuts/blob/main/07.md
+[08]: https://github.com/cashubtc/nuts/blob/main/08.md
+[09]: https://github.com/cashubtc/nuts/blob/main/09.md
+[10]: https://github.com/cashubtc/nuts/blob/main/10.md
+[11]: https://github.com/cashubtc/nuts/blob/main/11.md
+[12]: https://github.com/cashubtc/nuts/blob/main/12.md
+[13]: https://github.com/cashubtc/nuts/blob/main/13.md
+[14]: https://github.com/cashubtc/nuts/blob/main/14.md
+[15]: https://github.com/cashubtc/nuts/blob/main/15.md
+[16]: https://github.com/cashubtc/nuts/blob/main/16.md
+[17]: https://github.com/cashubtc/nuts/blob/main/17.md
+[18]: https://github.com/cashubtc/nuts/blob/main/18.md
+[19]: https://github.com/cashubtc/nuts/blob/main/19.md
+[20]: https://github.com/cashubtc/nuts/blob/main/20.md

+ 70 - 49
crates/cashu/src/amount.rs

@@ -6,7 +6,7 @@ use std::cmp::Ordering;
 use std::fmt;
 use std::str::FromStr;
 
-use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use serde::{Deserialize, Serialize};
 use thiserror::Error;
 
 use crate::nuts::CurrencyUnit;
@@ -23,6 +23,9 @@ pub enum Error {
     /// Cannot convert units
     #[error("Cannot convert units")]
     CannotConvertUnits,
+    /// Invalid amount
+    #[error("Invalid Amount: {0}")]
+    InvalidAmount(String),
 }
 
 /// Amount can be any unit
@@ -31,10 +34,24 @@ pub enum Error {
 #[serde(transparent)]
 pub struct Amount(u64);
 
+impl FromStr for Amount {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let value = s
+            .parse::<u64>()
+            .map_err(|_| Error::InvalidAmount(s.to_owned()))?;
+        Ok(Amount(value))
+    }
+}
+
 impl Amount {
     /// Amount zero
     pub const ZERO: Amount = Amount(0);
 
+    /// Amount one
+    pub const ONE: Amount = Amount(1);
+
     /// Split into parts that are powers of two
     pub fn split(&self) -> Vec<Self> {
         let sats = self.0;
@@ -105,6 +122,27 @@ impl Amount {
         Ok(parts)
     }
 
+    /// Splits amount into powers of two while accounting for the swap fee
+    pub fn split_with_fee(&self, fee_ppk: u64) -> Result<Vec<Self>, Error> {
+        let without_fee_amounts = self.split();
+        let fee_ppk = fee_ppk * without_fee_amounts.len() as u64;
+        let fee = Amount::from(fee_ppk.div_ceil(1000));
+        let new_amount = self.checked_add(fee).ok_or(Error::AmountOverflow)?;
+
+        let split = new_amount.split();
+        let split_fee_ppk = split.len() as u64 * fee_ppk;
+        let split_fee = Amount::from(split_fee_ppk.div_ceil(1000));
+
+        if let Some(net_amount) = new_amount.checked_sub(split_fee) {
+            if net_amount >= *self {
+                return Ok(split);
+            }
+        }
+        self.checked_add(Amount::ONE)
+            .ok_or(Error::AmountOverflow)?
+            .split_with_fee(fee_ppk)
+    }
+
     /// Checked addition for Amount. Returns None if overflow occurs.
     pub fn checked_add(self, other: Amount) -> Option<Amount> {
         self.0.checked_add(other.0).map(Amount)
@@ -115,6 +153,16 @@ impl Amount {
         self.0.checked_sub(other.0).map(Amount)
     }
 
+    /// Checked multiplication for Amount. Returns None if overflow occurs.
+    pub fn checked_mul(self, other: Amount) -> Option<Amount> {
+        self.0.checked_mul(other.0).map(Amount)
+    }
+
+    /// Checked division for Amount. Returns None if overflow occurs.
+    pub fn checked_div(self, other: Amount) -> Option<Amount> {
+        self.0.checked_div(other.0).map(Amount)
+    }
+
     /// Try sum to check for overflow
     pub fn try_sum<I>(iter: I) -> Result<Self, Error>
     where
@@ -216,54 +264,6 @@ impl std::ops::Div for Amount {
     }
 }
 
-/// String wrapper for an [Amount].
-///
-/// It ser-/deserializes the inner [Amount] to a string, while at the same time using the [u64]
-/// value of the [Amount] for comparison and ordering. This helps automatically sort the keys of
-/// a [BTreeMap] when [AmountStr] is used as key.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct AmountStr(Amount);
-
-impl AmountStr {
-    pub(crate) fn from(amt: Amount) -> Self {
-        Self(amt)
-    }
-}
-
-impl PartialOrd<Self> for AmountStr {
-    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
-        Some(self.cmp(other))
-    }
-}
-
-impl Ord for AmountStr {
-    fn cmp(&self, other: &Self) -> Ordering {
-        self.0.cmp(&other.0)
-    }
-}
-
-impl<'de> Deserialize<'de> for AmountStr {
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: Deserializer<'de>,
-    {
-        let s = String::deserialize(deserializer)?;
-        u64::from_str(&s)
-            .map(Amount)
-            .map(Self)
-            .map_err(serde::de::Error::custom)
-    }
-}
-
-impl Serialize for AmountStr {
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: Serializer,
-    {
-        serializer.serialize_str(&self.0.to_string())
-    }
-}
-
 /// Kinds of targeting that are supported
 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize)]
 pub enum SplitTarget {
@@ -369,6 +369,27 @@ mod tests {
     }
 
     #[test]
+    fn test_split_with_fee() {
+        let amount = Amount(2);
+        let fee_ppk = 1;
+
+        let split = amount.split_with_fee(fee_ppk).unwrap();
+        assert_eq!(split, vec![Amount(2), Amount(1)]);
+
+        let amount = Amount(3);
+        let fee_ppk = 1;
+
+        let split = amount.split_with_fee(fee_ppk).unwrap();
+        assert_eq!(split, vec![Amount(4)]);
+
+        let amount = Amount(3);
+        let fee_ppk = 1000;
+
+        let split = amount.split_with_fee(fee_ppk).unwrap();
+        assert_eq!(split, vec![Amount(32)]);
+    }
+
+    #[test]
     fn test_split_values() {
         let amount = Amount(10);
 

+ 15 - 5
crates/cashu/src/lib.rs

@@ -1,17 +1,27 @@
-//! CDK common types and traits
+#![doc = include_str!("../README.md")]
+#![warn(missing_docs)]
+#![warn(rustdoc::bare_urls)]
+
 pub mod amount;
 pub mod dhke;
-#[cfg(feature = "mint")]
-pub mod mint;
 pub mod mint_url;
 pub mod nuts;
 pub mod secret;
 pub mod util;
-#[cfg(feature = "wallet")]
-pub mod wallet;
 
 pub use lightning_invoice::{self, Bolt11Invoice};
 
 pub use self::amount::Amount;
+pub use self::mint_url::MintUrl;
 pub use self::nuts::*;
 pub use self::util::SECP256K1;
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! ensure_cdk {
+    ($cond:expr, $err:expr) => {
+        if !$cond {
+            return Err($err);
+        }
+    };
+}

+ 41 - 5
crates/cashu/src/mint_url.rs

@@ -10,6 +10,8 @@ use serde::{Deserialize, Serialize};
 use thiserror::Error;
 use url::{ParseError, Url};
 
+use crate::ensure_cdk;
+
 /// Url Error
 #[derive(Debug, Error, PartialEq, Eq)]
 pub enum Error {
@@ -27,9 +29,8 @@ pub struct MintUrl(String);
 
 impl MintUrl {
     fn format_url(url: &str) -> Result<String, Error> {
-        if url.is_empty() {
-            return Err(Error::InvalidUrl);
-        }
+        ensure_cdk!(!url.is_empty(), Error::InvalidUrl);
+
         let url = url.trim_end_matches('/');
         // https://URL.com/path/TO/resource -> https://url.com/path/TO/resource
         let protocol = url
@@ -55,7 +56,7 @@ impl MintUrl {
             .join("/");
         let mut formatted_url = format!("{}://{}", protocol, host);
         if !path.is_empty() {
-            formatted_url.push_str(&format!("/{}", path));
+            formatted_url.push_str(&format!("/{}/", path));
         }
         Ok(formatted_url)
     }
@@ -120,7 +121,7 @@ mod tests {
         assert_eq!(correct_cased_url, cased_url_formatted.to_string());
 
         let wrong_cased_url_with_path = "http://URL-to-check.com/PATH/to/check";
-        let correct_cased_url_with_path = "http://url-to-check.com/PATH/to/check";
+        let correct_cased_url_with_path = "http://url-to-check.com/PATH/to/check/";
 
         let cased_url_with_path_formatted = MintUrl::from_str(wrong_cased_url_with_path).unwrap();
         assert_eq!(
@@ -128,4 +129,39 @@ mod tests {
             cased_url_with_path_formatted.to_string()
         );
     }
+
+    #[test]
+    fn test_join_paths() {
+        let url_no_path = "http://url-to-check.com";
+
+        let url = MintUrl::from_str(url_no_path).unwrap();
+        assert_eq!(
+            format!("{url_no_path}/hello/world"),
+            url.join_paths(&["hello", "world"]).unwrap().to_string()
+        );
+
+        let url_no_path_with_slash = "http://url-to-check.com/";
+
+        let url = MintUrl::from_str(url_no_path_with_slash).unwrap();
+        assert_eq!(
+            format!("{url_no_path_with_slash}hello/world"),
+            url.join_paths(&["hello", "world"]).unwrap().to_string()
+        );
+
+        let url_with_path = "http://url-to-check.com/my/path";
+
+        let url = MintUrl::from_str(url_with_path).unwrap();
+        assert_eq!(
+            format!("{url_with_path}/hello/world"),
+            url.join_paths(&["hello", "world"]).unwrap().to_string()
+        );
+
+        let url_with_path_with_slash = "http://url-to-check.com/my/path/";
+
+        let url = MintUrl::from_str(url_with_path_with_slash).unwrap();
+        assert_eq!(
+            format!("{url_with_path_with_slash}hello/world"),
+            url.join_paths(&["hello", "world"]).unwrap().to_string()
+        );
+    }
 }

+ 8 - 0
crates/cashu/src/nuts/auth/mod.rs

@@ -0,0 +1,8 @@
+pub mod nut21;
+pub mod nut22;
+
+pub use nut21::{Method, ProtectedEndpoint, RoutePath, Settings as ClearAuthSettings};
+pub use nut22::{
+    AuthProof, AuthRequired, AuthToken, BlindAuthToken, MintAuthRequest,
+    Settings as BlindAuthSettings,
+};

+ 410 - 0
crates/cashu/src/nuts/auth/nut21.rs

@@ -0,0 +1,410 @@
+//! 21 Clear Auth
+
+use std::collections::HashSet;
+use std::str::FromStr;
+
+use regex::Regex;
+use serde::{Deserialize, Serialize};
+use strum::IntoEnumIterator;
+use strum_macros::EnumIter;
+use thiserror::Error;
+
+/// NUT21 Error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Invalid regex pattern
+    #[error("Invalid regex pattern: {0}")]
+    InvalidRegex(#[from] regex::Error),
+}
+
+/// Clear Auth Settings
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct Settings {
+    /// Openid discovery
+    pub openid_discovery: String,
+    /// Client ID
+    pub client_id: String,
+    /// Protected endpoints
+    pub protected_endpoints: Vec<ProtectedEndpoint>,
+}
+
+impl Settings {
+    /// Create new [`Settings`]
+    pub fn new(
+        openid_discovery: String,
+        client_id: String,
+        protected_endpoints: Vec<ProtectedEndpoint>,
+    ) -> Self {
+        Self {
+            openid_discovery,
+            client_id,
+            protected_endpoints,
+        }
+    }
+}
+
+// Custom deserializer for Settings to expand regex patterns in protected endpoints
+impl<'de> Deserialize<'de> for Settings {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        // Define a temporary struct to deserialize the raw data
+        #[derive(Deserialize)]
+        struct RawSettings {
+            openid_discovery: String,
+            client_id: String,
+            protected_endpoints: Vec<RawProtectedEndpoint>,
+        }
+
+        #[derive(Deserialize)]
+        struct RawProtectedEndpoint {
+            method: Method,
+            path: String,
+        }
+
+        // Deserialize into the temporary struct
+        let raw = RawSettings::deserialize(deserializer)?;
+
+        // Process protected endpoints, expanding regex patterns if present
+        let mut protected_endpoints = HashSet::new();
+
+        for raw_endpoint in raw.protected_endpoints {
+            let expanded_paths = matching_route_paths(&raw_endpoint.path).map_err(|e| {
+                serde::de::Error::custom(format!(
+                    "Invalid regex pattern '{}': {}",
+                    raw_endpoint.path, e
+                ))
+            })?;
+
+            for path in expanded_paths {
+                protected_endpoints.insert(ProtectedEndpoint::new(raw_endpoint.method, path));
+            }
+        }
+
+        // Create the final Settings struct
+        Ok(Settings {
+            openid_discovery: raw.openid_discovery,
+            client_id: raw.client_id,
+            protected_endpoints: protected_endpoints.into_iter().collect(),
+        })
+    }
+}
+
+/// List of the methods and paths that are protected
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct ProtectedEndpoint {
+    /// HTTP Method
+    pub method: Method,
+    /// Route path
+    pub path: RoutePath,
+}
+
+impl ProtectedEndpoint {
+    /// Create [`ProtectedEndpoint`]
+    pub fn new(method: Method, path: RoutePath) -> Self {
+        Self { method, path }
+    }
+}
+
+/// HTTP method
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[serde(rename_all = "UPPERCASE")]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub enum Method {
+    /// Get
+    Get,
+    /// POST
+    Post,
+}
+
+/// Route path
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, EnumIter)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+#[serde(rename_all = "snake_case")]
+pub enum RoutePath {
+    /// Bolt11 Mint Quote
+    #[serde(rename = "/v1/mint/quote/bolt11")]
+    MintQuoteBolt11,
+    /// Bolt11 Mint
+    #[serde(rename = "/v1/mint/bolt11")]
+    MintBolt11,
+    /// Bolt11 Melt Quote
+    #[serde(rename = "/v1/melt/quote/bolt11")]
+    MeltQuoteBolt11,
+    /// Bolt11 Melt
+    #[serde(rename = "/v1/melt/bolt11")]
+    MeltBolt11,
+    /// Swap
+    #[serde(rename = "/v1/swap")]
+    Swap,
+    /// Checkstate
+    #[serde(rename = "/v1/checkstate")]
+    Checkstate,
+    /// Restore
+    #[serde(rename = "/v1/restore")]
+    Restore,
+    /// Mint Blind Auth
+    #[serde(rename = "/v1/auth/blind/mint")]
+    MintBlindAuth,
+}
+
+/// Returns [`RoutePath`]s that match regex
+pub fn matching_route_paths(pattern: &str) -> Result<Vec<RoutePath>, Error> {
+    let regex = Regex::from_str(pattern)?;
+
+    Ok(RoutePath::iter()
+        .filter(|path| regex.is_match(&path.to_string()))
+        .collect())
+}
+
+impl std::fmt::Display for RoutePath {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        // Use serde to serialize to a JSON string, then extract the value without quotes
+        let json_str = match serde_json::to_string(self) {
+            Ok(s) => s,
+            Err(_) => return write!(f, "<error>"),
+        };
+        // Remove the quotes from the JSON string
+        let path = json_str.trim_matches('"');
+        write!(f, "{}", path)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+
+    use super::*;
+
+    #[test]
+    fn test_matching_route_paths_all() {
+        // Regex that matches all paths
+        let paths = matching_route_paths(".*").unwrap();
+
+        // Should match all variants
+        assert_eq!(paths.len(), RoutePath::iter().count());
+
+        // Verify all variants are included
+        assert!(paths.contains(&RoutePath::MintQuoteBolt11));
+        assert!(paths.contains(&RoutePath::MintBolt11));
+        assert!(paths.contains(&RoutePath::MeltQuoteBolt11));
+        assert!(paths.contains(&RoutePath::MeltBolt11));
+        assert!(paths.contains(&RoutePath::Swap));
+        assert!(paths.contains(&RoutePath::Checkstate));
+        assert!(paths.contains(&RoutePath::Restore));
+        assert!(paths.contains(&RoutePath::MintBlindAuth));
+    }
+
+    #[test]
+    fn test_matching_route_paths_mint_only() {
+        // Regex that matches only mint paths
+        let paths = matching_route_paths("^/v1/mint/.*").unwrap();
+
+        // Should match only mint paths
+        assert_eq!(paths.len(), 2);
+        assert!(paths.contains(&RoutePath::MintQuoteBolt11));
+        assert!(paths.contains(&RoutePath::MintBolt11));
+
+        // Should not match other paths
+        assert!(!paths.contains(&RoutePath::MeltQuoteBolt11));
+        assert!(!paths.contains(&RoutePath::MeltBolt11));
+        assert!(!paths.contains(&RoutePath::Swap));
+    }
+
+    #[test]
+    fn test_matching_route_paths_quote_only() {
+        // Regex that matches only quote paths
+        let paths = matching_route_paths(".*/quote/.*").unwrap();
+
+        // Should match only quote paths
+        assert_eq!(paths.len(), 2);
+        assert!(paths.contains(&RoutePath::MintQuoteBolt11));
+        assert!(paths.contains(&RoutePath::MeltQuoteBolt11));
+
+        // Should not match non-quote paths
+        assert!(!paths.contains(&RoutePath::MintBolt11));
+        assert!(!paths.contains(&RoutePath::MeltBolt11));
+    }
+
+    #[test]
+    fn test_matching_route_paths_no_match() {
+        // Regex that matches nothing
+        let paths = matching_route_paths("/nonexistent/path").unwrap();
+
+        // Should match nothing
+        assert!(paths.is_empty());
+    }
+
+    #[test]
+    fn test_matching_route_paths_quote_bolt11_only() {
+        // Regex that matches only quote paths
+        let paths = matching_route_paths("/v1/mint/quote/bolt11").unwrap();
+
+        // Should match only quote paths
+        assert_eq!(paths.len(), 1);
+        assert!(paths.contains(&RoutePath::MintQuoteBolt11));
+    }
+
+    #[test]
+    fn test_matching_route_paths_invalid_regex() {
+        // Invalid regex pattern
+        let result = matching_route_paths("(unclosed parenthesis");
+
+        // Should return an error for invalid regex
+        assert!(result.is_err());
+        assert!(matches!(result.unwrap_err(), Error::InvalidRegex(_)));
+    }
+
+    #[test]
+    fn test_route_path_to_string() {
+        // Test that to_string() returns the correct path strings
+        assert_eq!(
+            RoutePath::MintQuoteBolt11.to_string(),
+            "/v1/mint/quote/bolt11"
+        );
+        assert_eq!(RoutePath::MintBolt11.to_string(), "/v1/mint/bolt11");
+        assert_eq!(
+            RoutePath::MeltQuoteBolt11.to_string(),
+            "/v1/melt/quote/bolt11"
+        );
+        assert_eq!(RoutePath::MeltBolt11.to_string(), "/v1/melt/bolt11");
+        assert_eq!(RoutePath::Swap.to_string(), "/v1/swap");
+        assert_eq!(RoutePath::Checkstate.to_string(), "/v1/checkstate");
+        assert_eq!(RoutePath::Restore.to_string(), "/v1/restore");
+        assert_eq!(RoutePath::MintBlindAuth.to_string(), "/v1/auth/blind/mint");
+    }
+
+    #[test]
+    fn test_settings_deserialize_direct_paths() {
+        let json = r#"{
+            "openid_discovery": "https://example.com/.well-known/openid-configuration",
+            "client_id": "client123",
+            "protected_endpoints": [
+                {
+                    "method": "GET",
+                    "path": "/v1/mint/bolt11"
+                },
+                {
+                    "method": "POST",
+                    "path": "/v1/swap"
+                }
+            ]
+        }"#;
+
+        let settings: Settings = serde_json::from_str(json).unwrap();
+
+        assert_eq!(
+            settings.openid_discovery,
+            "https://example.com/.well-known/openid-configuration"
+        );
+        assert_eq!(settings.client_id, "client123");
+        assert_eq!(settings.protected_endpoints.len(), 2);
+
+        // Check that both paths are included
+        let paths = settings
+            .protected_endpoints
+            .iter()
+            .map(|ep| (ep.method, ep.path))
+            .collect::<Vec<_>>();
+        assert!(paths.contains(&(Method::Get, RoutePath::MintBolt11)));
+        assert!(paths.contains(&(Method::Post, RoutePath::Swap)));
+    }
+
+    #[test]
+    fn test_settings_deserialize_with_regex() {
+        let json = r#"{
+            "openid_discovery": "https://example.com/.well-known/openid-configuration",
+            "client_id": "client123",
+            "protected_endpoints": [
+                {
+                    "method": "GET",
+                    "path": "^/v1/mint/.*"
+                },
+                {
+                    "method": "POST",
+                    "path": "/v1/swap"
+                }
+            ]
+        }"#;
+
+        let settings: Settings = serde_json::from_str(json).unwrap();
+
+        assert_eq!(
+            settings.openid_discovery,
+            "https://example.com/.well-known/openid-configuration"
+        );
+        assert_eq!(settings.client_id, "client123");
+        assert_eq!(settings.protected_endpoints.len(), 3); // 2 mint paths + 1 swap path
+
+        let expected_protected: HashSet<ProtectedEndpoint> = HashSet::from_iter(vec![
+            ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
+            ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt11),
+            ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
+        ]);
+
+        let deserlized_protected = settings.protected_endpoints.into_iter().collect();
+
+        assert_eq!(expected_protected, deserlized_protected);
+    }
+
+    #[test]
+    fn test_settings_deserialize_invalid_regex() {
+        let json = r#"{
+            "openid_discovery": "https://example.com/.well-known/openid-configuration",
+            "client_id": "client123",
+            "protected_endpoints": [
+                {
+                    "method": "GET",
+                    "path": "(unclosed parenthesis"
+                }
+            ]
+        }"#;
+
+        let result = serde_json::from_str::<Settings>(json);
+        assert!(result.is_err());
+    }
+
+    #[test]
+    fn test_settings_deserialize_exact_path_match() {
+        let json = r#"{
+            "openid_discovery": "https://example.com/.well-known/openid-configuration",
+            "client_id": "client123",
+            "protected_endpoints": [
+                {
+                    "method": "GET",
+                    "path": "/v1/mint/quote/bolt11"
+                }
+            ]
+        }"#;
+
+        let settings: Settings = serde_json::from_str(json).unwrap();
+        assert_eq!(settings.protected_endpoints.len(), 1);
+        assert_eq!(settings.protected_endpoints[0].method, Method::Get);
+        assert_eq!(
+            settings.protected_endpoints[0].path,
+            RoutePath::MintQuoteBolt11
+        );
+    }
+
+    #[test]
+    fn test_settings_deserialize_all_paths() {
+        let json = r#"{
+            "openid_discovery": "https://example.com/.well-known/openid-configuration",
+            "client_id": "client123",
+            "protected_endpoints": [
+                {
+                    "method": "GET",
+                    "path": ".*"
+                }
+            ]
+        }"#;
+
+        let settings: Settings = serde_json::from_str(json).unwrap();
+        assert_eq!(
+            settings.protected_endpoints.len(),
+            RoutePath::iter().count()
+        );
+    }
+}

+ 380 - 0
crates/cashu/src/nuts/auth/nut22.rs

@@ -0,0 +1,380 @@
+//! 22 Blind Auth
+
+use std::fmt;
+
+use bitcoin::base64::engine::general_purpose;
+use bitcoin::base64::Engine;
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+
+use super::nut21::ProtectedEndpoint;
+use crate::dhke::hash_to_curve;
+use crate::secret::Secret;
+use crate::util::hex;
+use crate::{BlindedMessage, Id, Proof, ProofDleq, PublicKey};
+
+/// NUT22 Error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Invalid Prefix
+    #[error("Invalid prefix")]
+    InvalidPrefix,
+    /// Dleq proof not included
+    #[error("Dleq Proof not included for auth proof")]
+    DleqProofNotIncluded,
+    /// Hex Error
+    #[error(transparent)]
+    HexError(#[from] hex::Error),
+    /// Base64 error
+    #[error(transparent)]
+    Base64Error(#[from] bitcoin::base64::DecodeError),
+    /// Serde Json error
+    #[error(transparent)]
+    SerdeJsonError(#[from] serde_json::Error),
+    /// Utf8 parse error
+    #[error(transparent)]
+    Utf8ParseError(#[from] std::string::FromUtf8Error),
+    /// DHKE error
+    #[error(transparent)]
+    DHKE(#[from] crate::dhke::Error),
+}
+
+/// Blind auth settings
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct Settings {
+    /// Max number of blind auth tokens that can be minted per request
+    pub bat_max_mint: u64,
+    /// Protected endpoints
+    pub protected_endpoints: Vec<ProtectedEndpoint>,
+}
+
+impl Settings {
+    /// Create new [`Settings`]
+    pub fn new(bat_max_mint: u64, protected_endpoints: Vec<ProtectedEndpoint>) -> Self {
+        Self {
+            bat_max_mint,
+            protected_endpoints,
+        }
+    }
+}
+
+// Custom deserializer for Settings to expand regex patterns in protected endpoints
+impl<'de> Deserialize<'de> for Settings {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        use std::collections::HashSet;
+
+        use super::nut21::matching_route_paths;
+
+        // Define a temporary struct to deserialize the raw data
+        #[derive(Deserialize)]
+        struct RawSettings {
+            bat_max_mint: u64,
+            protected_endpoints: Vec<RawProtectedEndpoint>,
+        }
+
+        #[derive(Deserialize)]
+        struct RawProtectedEndpoint {
+            method: super::nut21::Method,
+            path: String,
+        }
+
+        // Deserialize into the temporary struct
+        let raw = RawSettings::deserialize(deserializer)?;
+
+        // Process protected endpoints, expanding regex patterns if present
+        let mut protected_endpoints = HashSet::new();
+
+        for raw_endpoint in raw.protected_endpoints {
+            let expanded_paths = matching_route_paths(&raw_endpoint.path).map_err(|e| {
+                serde::de::Error::custom(format!(
+                    "Invalid regex pattern '{}': {}",
+                    raw_endpoint.path, e
+                ))
+            })?;
+
+            for path in expanded_paths {
+                protected_endpoints.insert(super::nut21::ProtectedEndpoint::new(
+                    raw_endpoint.method,
+                    path,
+                ));
+            }
+        }
+
+        // Create the final Settings struct
+        Ok(Settings {
+            bat_max_mint: raw.bat_max_mint,
+            protected_endpoints: protected_endpoints.into_iter().collect(),
+        })
+    }
+}
+
+/// Auth Token
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum AuthToken {
+    /// Clear Auth token
+    ClearAuth(String),
+    /// Blind Auth token
+    BlindAuth(BlindAuthToken),
+}
+
+impl fmt::Display for AuthToken {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::ClearAuth(cat) => cat.fmt(f),
+            Self::BlindAuth(bat) => bat.fmt(f),
+        }
+    }
+}
+
+impl AuthToken {
+    /// Header key for auth token type
+    pub fn header_key(&self) -> String {
+        match self {
+            Self::ClearAuth(_) => "Clear-auth".to_string(),
+            Self::BlindAuth(_) => "Blind-auth".to_string(),
+        }
+    }
+}
+
+/// Required Auth
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum AuthRequired {
+    /// Clear Auth token
+    Clear,
+    /// Blind Auth token
+    Blind,
+}
+
+/// Auth Proofs
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct AuthProof {
+    /// `Keyset id`
+    #[serde(rename = "id")]
+    pub keyset_id: Id,
+    /// Secret message
+    #[cfg_attr(feature = "swagger", schema(value_type = String))]
+    pub secret: Secret,
+    /// Unblinded signature
+    #[serde(rename = "C")]
+    #[cfg_attr(feature = "swagger", schema(value_type = String))]
+    pub c: PublicKey,
+    /// Auth Proof Dleq
+    pub dleq: Option<ProofDleq>,
+}
+
+impl AuthProof {
+    /// Y of AuthProof
+    pub fn y(&self) -> Result<PublicKey, Error> {
+        Ok(hash_to_curve(self.secret.as_bytes())?)
+    }
+}
+
+impl From<AuthProof> for Proof {
+    fn from(value: AuthProof) -> Self {
+        Self {
+            amount: 1.into(),
+            keyset_id: value.keyset_id,
+            secret: value.secret,
+            c: value.c,
+            witness: None,
+            dleq: value.dleq,
+        }
+    }
+}
+
+impl TryFrom<Proof> for AuthProof {
+    type Error = Error;
+    fn try_from(value: Proof) -> Result<Self, Self::Error> {
+        Ok(Self {
+            keyset_id: value.keyset_id,
+            secret: value.secret,
+            c: value.c,
+            dleq: value.dleq,
+        })
+    }
+}
+
+/// Blind Auth Token
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct BlindAuthToken {
+    /// [AuthProof]
+    pub auth_proof: AuthProof,
+}
+
+impl BlindAuthToken {
+    /// Create new [ `BlindAuthToken`]
+    pub fn new(auth_proof: AuthProof) -> Self {
+        BlindAuthToken { auth_proof }
+    }
+
+    /// Remove DLEQ
+    ///
+    /// We do not send the DLEQ to the mint as it links redemption and creation
+    pub fn without_dleq(&self) -> Self {
+        Self {
+            auth_proof: AuthProof {
+                keyset_id: self.auth_proof.keyset_id,
+                secret: self.auth_proof.secret.clone(),
+                c: self.auth_proof.c,
+                dleq: None,
+            },
+        }
+    }
+}
+
+impl fmt::Display for BlindAuthToken {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let json_string = serde_json::to_string(&self.auth_proof).map_err(|_| fmt::Error)?;
+        let encoded = general_purpose::URL_SAFE.encode(json_string);
+        write!(f, "authA{}", encoded)
+    }
+}
+
+impl std::str::FromStr for BlindAuthToken {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        // Check prefix and extract the base64 encoded part in one step
+        let encoded = s.strip_prefix("authA").ok_or(Error::InvalidPrefix)?;
+
+        // Decode the base64 URL-safe string
+        let json_string = general_purpose::URL_SAFE.decode(encoded)?;
+
+        // Convert bytes to UTF-8 string
+        let json_str = String::from_utf8(json_string)?;
+
+        // Deserialize the JSON string into AuthProof
+        let auth_proof: AuthProof = serde_json::from_str(&json_str)?;
+
+        Ok(BlindAuthToken { auth_proof })
+    }
+}
+
+/// Mint auth request [NUT-XX]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct MintAuthRequest {
+    /// Outputs
+    #[cfg_attr(feature = "swagger", schema(max_items = 1_000))]
+    pub outputs: Vec<BlindedMessage>,
+}
+
+impl MintAuthRequest {
+    /// Count of tokens
+    pub fn amount(&self) -> u64 {
+        self.outputs.len() as u64
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::collections::HashSet;
+
+    use strum::IntoEnumIterator;
+
+    use super::super::nut21::{Method, RoutePath};
+    use super::*;
+
+    #[test]
+    fn test_settings_deserialize_direct_paths() {
+        let json = r#"{
+            "bat_max_mint": 10,
+            "protected_endpoints": [
+                {
+                    "method": "GET",
+                    "path": "/v1/mint/bolt11"
+                },
+                {
+                    "method": "POST",
+                    "path": "/v1/swap"
+                }
+            ]
+        }"#;
+
+        let settings: Settings = serde_json::from_str(json).unwrap();
+
+        assert_eq!(settings.bat_max_mint, 10);
+        assert_eq!(settings.protected_endpoints.len(), 2);
+
+        // Check that both paths are included
+        let paths = settings
+            .protected_endpoints
+            .iter()
+            .map(|ep| (ep.method, ep.path))
+            .collect::<Vec<_>>();
+        assert!(paths.contains(&(Method::Get, RoutePath::MintBolt11)));
+        assert!(paths.contains(&(Method::Post, RoutePath::Swap)));
+    }
+
+    #[test]
+    fn test_settings_deserialize_with_regex() {
+        let json = r#"{
+            "bat_max_mint": 5,
+            "protected_endpoints": [
+                {
+                    "method": "GET",
+                    "path": "^/v1/mint/.*"
+                },
+                {
+                    "method": "POST",
+                    "path": "/v1/swap"
+                }
+            ]
+        }"#;
+
+        let settings: Settings = serde_json::from_str(json).unwrap();
+
+        assert_eq!(settings.bat_max_mint, 5);
+        assert_eq!(settings.protected_endpoints.len(), 3); // 2 mint paths + 1 swap path
+
+        let expected_protected: HashSet<ProtectedEndpoint> = HashSet::from_iter(vec![
+            ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
+            ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt11),
+            ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
+        ]);
+
+        let deserialized_protected = settings.protected_endpoints.into_iter().collect();
+
+        assert_eq!(expected_protected, deserialized_protected);
+    }
+
+    #[test]
+    fn test_settings_deserialize_invalid_regex() {
+        let json = r#"{
+            "bat_max_mint": 5,
+            "protected_endpoints": [
+                {
+                    "method": "GET",
+                    "path": "(unclosed parenthesis"
+                }
+            ]
+        }"#;
+
+        let result = serde_json::from_str::<Settings>(json);
+        assert!(result.is_err());
+    }
+
+    #[test]
+    fn test_settings_deserialize_all_paths() {
+        let json = r#"{
+            "bat_max_mint": 5,
+            "protected_endpoints": [
+                {
+                    "method": "GET",
+                    "path": ".*"
+                }
+            ]
+        }"#;
+
+        let settings: Settings = serde_json::from_str(json).unwrap();
+        assert_eq!(
+            settings.protected_endpoints.len(),
+            RoutePath::iter().count()
+        );
+    }
+}

+ 12 - 1
crates/cashu/src/nuts/mod.rs

@@ -24,6 +24,14 @@ pub mod nut18;
 pub mod nut19;
 pub mod nut20;
 
+#[cfg(feature = "auth")]
+mod auth;
+
+#[cfg(feature = "auth")]
+pub use auth::{
+    nut21, nut22, AuthProof, AuthRequired, AuthToken, BlindAuthSettings, BlindAuthToken,
+    ClearAuthSettings, Method, MintAuthRequest, ProtectedEndpoint, RoutePath,
+};
 pub use nut00::{
     BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proof, Proofs, ProofsMethods,
     Token, TokenV3, TokenV4, Witness,
@@ -54,4 +62,7 @@ pub use nut12::{BlindSignatureDleq, ProofDleq};
 pub use nut14::HTLCWitness;
 pub use nut15::{Mpp, MppMethodSettings, Settings as NUT15Settings};
 pub use nut17::NotificationPayload;
-pub use nut18::{PaymentRequest, PaymentRequestPayload, Transport};
+pub use nut18::{
+    PaymentRequest, PaymentRequestBuilder, PaymentRequestPayload, Transport, TransportBuilder,
+    TransportType,
+};

+ 100 - 6
crates/cashu/src/nuts/nut00/mod.rs

@@ -3,6 +3,7 @@
 //! <https://github.com/cashubtc/nuts/blob/main/00.md>
 
 use std::cmp::Ordering;
+use std::collections::{HashMap, HashSet};
 use std::fmt;
 use std::hash::{Hash, Hasher};
 use std::str::FromStr;
@@ -38,25 +39,102 @@ pub type Proofs = Vec<Proof>;
 
 /// Utility methods for [Proofs]
 pub trait ProofsMethods {
+    /// Count proofs by keyset
+    fn count_by_keyset(&self) -> HashMap<Id, u64>;
+
+    /// Sum proofs by keyset
+    fn sum_by_keyset(&self) -> HashMap<Id, Amount>;
+
     /// Try to sum up the amounts of all [Proof]s
     fn total_amount(&self) -> Result<Amount, Error>;
 
     /// Try to fetch the pubkeys of all [Proof]s
     fn ys(&self) -> Result<Vec<PublicKey>, Error>;
+
+    /// Create a copy of proofs without dleqs
+    fn without_dleqs(&self) -> Proofs;
 }
 
 impl ProofsMethods for Proofs {
+    fn count_by_keyset(&self) -> HashMap<Id, u64> {
+        count_by_keyset(self.iter())
+    }
+
+    fn sum_by_keyset(&self) -> HashMap<Id, Amount> {
+        sum_by_keyset(self.iter())
+    }
+
     fn total_amount(&self) -> Result<Amount, Error> {
-        Amount::try_sum(self.iter().map(|p| p.amount)).map_err(Into::into)
+        total_amount(self.iter())
     }
 
     fn ys(&self) -> Result<Vec<PublicKey>, Error> {
+        ys(self.iter())
+    }
+
+    fn without_dleqs(&self) -> Proofs {
         self.iter()
-            .map(|p| p.y())
-            .collect::<Result<Vec<PublicKey>, _>>()
+            .map(|p| {
+                let mut p = p.clone();
+                p.dleq = None;
+                p
+            })
+            .collect()
     }
 }
 
+impl ProofsMethods for HashSet<Proof> {
+    fn count_by_keyset(&self) -> HashMap<Id, u64> {
+        count_by_keyset(self.iter())
+    }
+
+    fn sum_by_keyset(&self) -> HashMap<Id, Amount> {
+        sum_by_keyset(self.iter())
+    }
+
+    fn total_amount(&self) -> Result<Amount, Error> {
+        total_amount(self.iter())
+    }
+
+    fn ys(&self) -> Result<Vec<PublicKey>, Error> {
+        ys(self.iter())
+    }
+
+    fn without_dleqs(&self) -> Proofs {
+        self.iter()
+            .map(|p| {
+                let mut p = p.clone();
+                p.dleq = None;
+                p
+            })
+            .collect()
+    }
+}
+
+fn count_by_keyset<'a, I: Iterator<Item = &'a Proof>>(proofs: I) -> HashMap<Id, u64> {
+    let mut counts = HashMap::new();
+    for proof in proofs {
+        *counts.entry(proof.keyset_id).or_insert(0) += 1;
+    }
+    counts
+}
+
+fn sum_by_keyset<'a, I: Iterator<Item = &'a Proof>>(proofs: I) -> HashMap<Id, Amount> {
+    let mut sums = HashMap::new();
+    for proof in proofs {
+        *sums.entry(proof.keyset_id).or_insert(Amount::ZERO) += proof.amount;
+    }
+    sums
+}
+
+fn total_amount<'a, I: Iterator<Item = &'a Proof>>(proofs: I) -> Result<Amount, Error> {
+    Amount::try_sum(proofs.map(|p| p.amount)).map_err(Into::into)
+}
+
+fn ys<'a, I: Iterator<Item = &'a Proof>>(proofs: I) -> Result<Vec<PublicKey>, Error> {
+    proofs.map(|p| p.y()).collect::<Result<Vec<PublicKey>, _>>()
+}
+
 /// NUT00 Error
 #[derive(Debug, Error)]
 pub enum Error {
@@ -72,6 +150,9 @@ pub enum Error {
     /// Unsupported token
     #[error("Unsupported payment method")]
     UnsupportedPaymentMethod,
+    /// Duplicate proofs in token
+    #[error("Duplicate proofs in token")]
+    DuplicateProofs,
     /// Serde Json error
     #[error(transparent)]
     SerdeJsonError(#[from] serde_json::Error),
@@ -284,6 +365,11 @@ impl Proof {
         }
     }
 
+    /// Check if proof is in active keyset `Id`s
+    pub fn is_active(&self, active_keyset_ids: &[Id]) -> bool {
+        active_keyset_ids.contains(&self.keyset_id)
+    }
+
     /// Get y from proof
     ///
     /// Where y is `hash_to_curve(secret)`
@@ -397,6 +483,8 @@ pub enum CurrencyUnit {
     Usd,
     /// Euro
     Eur,
+    /// Auth
+    Auth,
     /// Custom currency unit
     Custom(String),
 }
@@ -410,6 +498,7 @@ impl CurrencyUnit {
             Self::Msat => Some(1),
             Self::Usd => Some(2),
             Self::Eur => Some(3),
+            Self::Auth => Some(4),
             _ => None,
         }
     }
@@ -424,6 +513,7 @@ impl FromStr for CurrencyUnit {
             "MSAT" => Ok(Self::Msat),
             "USD" => Ok(Self::Usd),
             "EUR" => Ok(Self::Eur),
+            "AUTH" => Ok(Self::Auth),
             c => Ok(Self::Custom(c.to_string())),
         }
     }
@@ -436,6 +526,7 @@ impl fmt::Display for CurrencyUnit {
             CurrencyUnit::Msat => "MSAT",
             CurrencyUnit::Usd => "USD",
             CurrencyUnit::Eur => "EUR",
+            CurrencyUnit::Auth => "AUTH",
             CurrencyUnit::Custom(unit) => unit,
         };
         if let Some(width) = f.width() {
@@ -467,20 +558,22 @@ impl<'de> Deserialize<'de> for CurrencyUnit {
 
 /// Payment Method
 #[non_exhaustive]
-#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub enum PaymentMethod {
     /// Bolt11 payment type
     #[default]
     Bolt11,
+    /// Custom
+    Custom(String),
 }
 
 impl FromStr for PaymentMethod {
     type Err = Error;
     fn from_str(value: &str) -> Result<Self, Self::Err> {
-        match value {
+        match value.to_lowercase().as_str() {
             "bolt11" => Ok(Self::Bolt11),
-            _ => Err(Error::UnsupportedPaymentMethod),
+            c => Ok(Self::Custom(c.to_string())),
         }
     }
 }
@@ -489,6 +582,7 @@ impl fmt::Display for PaymentMethod {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
             PaymentMethod::Bolt11 => write!(f, "bolt11"),
+            PaymentMethod::Custom(p) => write!(f, "{}", p),
         }
     }
 }

+ 90 - 38
crates/cashu/src/nuts/nut00/token.rs

@@ -14,7 +14,7 @@ use super::{Error, Proof, ProofV4, Proofs};
 use crate::mint_url::MintUrl;
 use crate::nuts::nut00::ProofsMethods;
 use crate::nuts::{CurrencyUnit, Id};
-use crate::Amount;
+use crate::{ensure_cdk, Amount};
 
 /// Token Enum
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -103,11 +103,9 @@ impl Token {
             Self::TokenV3(token) => {
                 let mint_urls = token.mint_urls();
 
-                if mint_urls.len() != 1 {
-                    return Err(Error::UnsupportedToken);
-                }
+                ensure_cdk!(mint_urls.len() == 1, Error::UnsupportedToken);
 
-                Ok(mint_urls.first().expect("Length is checked above").clone())
+                mint_urls.first().ok_or(Error::UnsupportedToken).cloned()
             }
             Self::TokenV4(token) => Ok(token.mint_url.clone()),
         }
@@ -164,9 +162,7 @@ impl TryFrom<&Vec<u8>> for Token {
     type Error = Error;
 
     fn try_from(bytes: &Vec<u8>) -> Result<Self, Self::Error> {
-        if bytes.len() < 5 {
-            return Err(Error::UnsupportedToken);
-        }
+        ensure_cdk!(bytes.len() >= 5, Error::UnsupportedToken);
 
         let prefix = String::from_utf8(bytes[..5].to_vec())?;
 
@@ -220,9 +216,7 @@ impl TokenV3 {
         memo: Option<String>,
         unit: Option<CurrencyUnit>,
     ) -> Result<Self, Error> {
-        if proofs.is_empty() {
-            return Err(Error::ProofsRequired);
-        }
+        ensure_cdk!(!proofs.is_empty(), Error::ProofsRequired);
 
         Ok(Self {
             token: vec![TokenV3Token::new(mint_url, proofs)],
@@ -239,15 +233,21 @@ impl TokenV3 {
             .collect()
     }
 
-    /// Value
+    /// Value - errors if duplicate proofs are found
     #[inline]
     pub fn value(&self) -> Result<Amount, Error> {
-        Ok(Amount::try_sum(
-            self.token
-                .iter()
-                .map(|t| t.proofs.total_amount())
-                .collect::<Result<Vec<Amount>, _>>()?,
-        )?)
+        let proofs = self.proofs();
+        let unique_count = proofs
+            .iter()
+            .collect::<std::collections::HashSet<_>>()
+            .len();
+
+        // Check if there are any duplicate proofs
+        if unique_count != proofs.len() {
+            return Err(Error::DuplicateProofs);
+        }
+
+        proofs.total_amount()
     }
 
     /// Memo
@@ -273,6 +273,9 @@ impl TokenV3 {
         mint_urls
     }
 
+    /// Checks if a token has multiple mints
+    ///
+    /// These tokens are not supported by this crate
     pub fn is_multi_mint(&self) -> bool {
         self.token.len() > 1
     }
@@ -339,15 +342,21 @@ impl TokenV4 {
             .collect()
     }
 
-    /// Value
+    /// Value - errors if duplicate proofs are found
     #[inline]
     pub fn value(&self) -> Result<Amount, Error> {
-        Ok(Amount::try_sum(
-            self.token
-                .iter()
-                .map(|t| Amount::try_sum(t.proofs.iter().map(|p| p.amount)))
-                .collect::<Result<Vec<Amount>, _>>()?,
-        )?)
+        let proofs = self.proofs();
+        let unique_count = proofs
+            .iter()
+            .collect::<std::collections::HashSet<_>>()
+            .len();
+
+        // Check if there are any duplicate proofs
+        if unique_count != proofs.len() {
+            return Err(Error::DuplicateProofs);
+        }
+
+        proofs.total_amount()
     }
 
     /// Memo
@@ -400,18 +409,12 @@ impl TryFrom<&Vec<u8>> for TokenV4 {
     type Error = Error;
 
     fn try_from(bytes: &Vec<u8>) -> Result<Self, Self::Error> {
-        if bytes.len() < 5 {
-            return Err(Error::UnsupportedToken);
-        }
+        ensure_cdk!(bytes.len() >= 5, Error::UnsupportedToken);
 
         let prefix = String::from_utf8(bytes[..5].to_vec())?;
+        ensure_cdk!(prefix.as_str() == "crawB", Error::UnsupportedToken);
 
-        if prefix.as_str() == "crawB" {
-            let token: TokenV4 = ciborium::from_reader(&bytes[5..])?;
-            Ok(token)
-        } else {
-            Err(Error::UnsupportedToken)
-        }
+        Ok(ciborium::from_reader(&bytes[5..])?)
     }
 }
 
@@ -421,11 +424,9 @@ impl TryFrom<TokenV3> for TokenV4 {
         let proofs = token.proofs();
         let mint_urls = token.mint_urls();
 
-        if mint_urls.len() != 1 {
-            return Err(Error::UnsupportedToken);
-        }
+        ensure_cdk!(mint_urls.len() == 1, Error::UnsupportedToken);
 
-        let mint_url = mint_urls.first().expect("Len is checked");
+        let mint_url = mint_urls.first().ok_or(Error::UnsupportedToken)?;
 
         let proofs = proofs
             .iter()
@@ -494,6 +495,7 @@ mod tests {
 
     use super::*;
     use crate::mint_url::MintUrl;
+    use crate::secret::Secret;
     use crate::util::hex;
 
     #[test]
@@ -632,4 +634,54 @@ mod tests {
         let tokenv4_bytes_ = tokenv4_.to_raw_bytes().expect("Serialization error");
         assert!(tokenv4_bytes_ == tokenv4_bytes);
     }
+
+    #[test]
+    fn test_token_with_duplicate_proofs() {
+        // Create a token with duplicate proofs
+        let mint_url = MintUrl::from_str("https://example.com").unwrap();
+        let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
+
+        let secret = Secret::generate();
+        // Create two identical proofs
+        let proof1 = Proof {
+            amount: Amount::from(10),
+            keyset_id,
+            secret: secret.clone(),
+            c: "02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"
+                .parse()
+                .unwrap(),
+            witness: None,
+            dleq: None,
+        };
+
+        let proof2 = proof1.clone(); // Duplicate proof
+
+        // Create a token with the duplicate proofs
+        let proofs = vec![proof1.clone(), proof2].into_iter().collect();
+        let token = Token::new(mint_url.clone(), proofs, None, CurrencyUnit::Sat);
+
+        // Verify that value() returns an error
+        let result = token.value();
+        assert!(result.is_err());
+
+        // Create a token with unique proofs
+        let proof3 = Proof {
+            amount: Amount::from(10),
+            keyset_id,
+            secret: Secret::generate(),
+            c: "03bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"
+                .parse()
+                .unwrap(), // Different C value
+            witness: None,
+            dleq: None,
+        };
+
+        let proofs = vec![proof1, proof3].into_iter().collect();
+        let token = Token::new(mint_url, proofs, None, CurrencyUnit::Sat);
+
+        // Verify that value() succeeds with unique proofs
+        let result = token.value();
+        assert!(result.is_ok());
+        assert_eq!(result.unwrap(), Amount::from(20));
+    }
 }

+ 55 - 10
crates/cashu/src/nuts/nut01/mod.rs

@@ -3,10 +3,12 @@
 //! <https://github.com/cashubtc/nuts/blob/main/01.md>
 
 use std::collections::BTreeMap;
+use std::fmt;
 use std::ops::{Deref, DerefMut};
 
 use bitcoin::secp256k1;
-use serde::{Deserialize, Serialize};
+use serde::de::{self, MapAccess, Visitor};
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
 use serde_with::{serde_as, VecSkipError};
 use thiserror::Error;
 
@@ -16,7 +18,7 @@ mod secret_key;
 pub use self::public_key::PublicKey;
 pub use self::secret_key::SecretKey;
 use super::nut02::KeySet;
-use crate::amount::{Amount, AmountStr};
+use crate::amount::Amount;
 
 /// Nut01 Error
 #[derive(Debug, Error)]
@@ -42,12 +44,55 @@ pub enum Error {
 /// This is a variation of [MintKeys] that only exposes the public keys.
 ///
 /// See [NUT-01]
-#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
-pub struct Keys(BTreeMap<AmountStr, PublicKey>);
+pub struct Keys(BTreeMap<Amount, PublicKey>);
+
+impl Serialize for Keys {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let map: BTreeMap<String, _> = self.0.iter().map(|(k, v)| (k.to_string(), v)).collect();
+        map.serialize(serializer)
+    }
+}
+
+impl<'de> Deserialize<'de> for Keys {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        struct KeysVisitor;
+
+        impl<'de> Visitor<'de> for KeysVisitor {
+            type Value = Keys;
+
+            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+                formatter.write_str("a map with string keys representing u64 values")
+            }
+
+            fn visit_map<M>(self, mut map: M) -> Result<Keys, M::Error>
+            where
+                M: MapAccess<'de>,
+            {
+                let mut btree_map = BTreeMap::new();
+
+                while let Some((key, value)) = map.next_entry::<String, _>()? {
+                    let parsed_key = key.parse::<Amount>().map_err(de::Error::custom)?;
+                    btree_map.insert(parsed_key, value);
+                }
+
+                Ok(Keys(btree_map))
+            }
+        }
+
+        deserializer.deserialize_map(KeysVisitor)
+    }
+}
 
 impl Deref for Keys {
-    type Target = BTreeMap<AmountStr, PublicKey>;
+    type Target = BTreeMap<Amount, PublicKey>;
 
     fn deref(&self) -> &Self::Target {
         &self.0
@@ -59,7 +104,7 @@ impl From<MintKeys> for Keys {
         Self(
             keys.0
                 .into_iter()
-                .map(|(amount, keypair)| (AmountStr::from(amount), keypair.public_key))
+                .map(|(amount, keypair)| (amount, keypair.public_key))
                 .collect(),
         )
     }
@@ -68,25 +113,25 @@ impl From<MintKeys> for Keys {
 impl Keys {
     /// Create new [`Keys`]
     #[inline]
-    pub fn new(keys: BTreeMap<AmountStr, PublicKey>) -> Self {
+    pub fn new(keys: BTreeMap<Amount, PublicKey>) -> Self {
         Self(keys)
     }
 
     /// Get [`Keys`]
     #[inline]
-    pub fn keys(&self) -> &BTreeMap<AmountStr, PublicKey> {
+    pub fn keys(&self) -> &BTreeMap<Amount, PublicKey> {
         &self.0
     }
 
     /// Get [`PublicKey`] for [`Amount`]
     #[inline]
     pub fn amount_key(&self, amount: Amount) -> Option<PublicKey> {
-        self.0.get(&AmountStr::from(amount)).copied()
+        self.0.get(&amount).copied()
     }
 
     /// Iterate through the (`Amount`, `PublicKey`) entries in the Map
     #[inline]
-    pub fn iter(&self) -> impl Iterator<Item = (&AmountStr, &PublicKey)> {
+    pub fn iter(&self) -> impl Iterator<Item = (&Amount, &PublicKey)> {
         self.0.iter()
     }
 }

+ 9 - 2
crates/cashu/src/nuts/nut01/public_key.rs

@@ -12,13 +12,19 @@ use super::Error;
 use crate::SECP256K1;
 
 /// PublicKey
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub struct PublicKey {
     #[cfg_attr(feature = "swagger", schema(value_type = String))]
     inner: secp256k1::PublicKey,
 }
 
+impl fmt::Debug for PublicKey {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "PublicKey({})", self.to_hex())
+    }
+}
+
 impl Deref for PublicKey {
     type Target = secp256k1::PublicKey;
 
@@ -152,8 +158,9 @@ mod tests {
     }
 }
 
-#[cfg(feature = "bench")]
+#[cfg(all(feature = "bench", test))]
 mod benches {
+    extern crate test;
     use test::{black_box, Bencher};
 
     use super::*;

+ 37 - 11
crates/cashu/src/nuts/nut01/secret_key.rs

@@ -8,6 +8,7 @@ use bitcoin::secp256k1;
 use bitcoin::secp256k1::rand::rngs::OsRng;
 use bitcoin::secp256k1::schnorr::Signature;
 use bitcoin::secp256k1::{Keypair, Message, Scalar};
+use serde::de::Visitor;
 use serde::{Deserialize, Deserializer, Serialize};
 
 use super::{Error, PublicKey};
@@ -115,21 +116,46 @@ impl FromStr for SecretKey {
 }
 
 impl Serialize for SecretKey {
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: serde::Serializer,
-    {
-        serializer.serialize_str(&self.to_secret_hex())
+    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        match serializer.is_human_readable() {
+            // For human-readable formats like JSON, serialize as hex string
+            true => serializer.serialize_str(&self.to_secret_hex()),
+            // For binary formats like CBOR, use the bytes serialization
+            false => serializer.serialize_bytes(self.as_secret_bytes()),
+        }
     }
 }
 
 impl<'de> Deserialize<'de> for SecretKey {
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: Deserializer<'de>,
-    {
-        let secret_key: String = String::deserialize(deserializer)?;
-        Self::from_hex(secret_key).map_err(serde::de::Error::custom)
+    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+        match deserializer.is_human_readable() {
+            // For human-readable formats like JSON, deserialize from hex string
+            true => {
+                let secret_key: String = String::deserialize(deserializer)?;
+                SecretKey::from_hex(secret_key).map_err(serde::de::Error::custom)
+            }
+            // For binary formats like CBOR, use the bytes deserialization
+            false => {
+                struct SecretKeyVisitor;
+
+                impl Visitor<'_> for SecretKeyVisitor {
+                    type Value = SecretKey;
+
+                    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+                        formatter.write_str("a byte array")
+                    }
+
+                    fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
+                    where
+                        E: serde::de::Error,
+                    {
+                        SecretKey::from_slice(value).map_err(serde::de::Error::custom)
+                    }
+                }
+
+                deserializer.deserialize_bytes(SecretKeyVisitor)
+            }
+        }
     }
 }
 

+ 24 - 12
crates/cashu/src/nuts/nut02.rs

@@ -16,18 +16,16 @@ use bitcoin::hashes::Hash;
 use bitcoin::key::Secp256k1;
 #[cfg(feature = "mint")]
 use bitcoin::secp256k1;
-use serde::{Deserialize, Serialize};
+use serde::{Deserialize, Deserializer, Serialize};
 use serde_with::{serde_as, VecSkipError};
 use thiserror::Error;
 
 use super::nut01::Keys;
 #[cfg(feature = "mint")]
 use super::nut01::{MintKeyPair, MintKeys};
-use crate::amount::AmountStr;
 use crate::nuts::nut00::CurrencyUnit;
 use crate::util::hex;
-#[cfg(feature = "mint")]
-use crate::Amount;
+use crate::{ensure_cdk, Amount};
 
 /// NUT02 Error
 #[derive(Debug, Error)]
@@ -51,6 +49,7 @@ pub enum Error {
 
 /// Keyset version
 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub enum KeySetVersion {
     /// Current Version 00
     Version00,
@@ -87,6 +86,7 @@ impl fmt::Display for KeySetVersion {
 /// which mint or keyset it was generated from.
 #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
 #[serde(into = "String", try_from = "String")]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub struct Id {
     version: KeySetVersion,
     id: [u8; Self::BYTELEN],
@@ -146,9 +146,7 @@ impl TryFrom<String> for Id {
     type Error = Error;
 
     fn try_from(s: String) -> Result<Self, Self::Error> {
-        if s.len() != 16 {
-            return Err(Error::Length);
-        }
+        ensure_cdk!(s.len() == 16, Error::Length);
 
         Ok(Self {
             version: KeySetVersion::from_byte(&hex::decode(&s[..2])?[0])?,
@@ -181,7 +179,7 @@ impl From<&Keys> for Id {
     ///   4. take the first 14 characters of the hex-encoded hash
     ///   5. prefix it with a keyset ID version byte
     fn from(map: &Keys) -> Self {
-        let mut keys: Vec<(&AmountStr, &super::PublicKey)> = map.iter().collect();
+        let mut keys: Vec<(&Amount, &super::PublicKey)> = map.iter().collect();
         keys.sort_by_key(|(amt, _v)| *amt);
 
         let pubkeys_concat: Vec<u8> = keys
@@ -232,9 +230,7 @@ impl KeySet {
     pub fn verify_id(&self) -> Result<(), Error> {
         let keys_id: Id = (&self.keys).into();
 
-        if keys_id != self.id {
-            return Err(Error::IncorrectKeysetId);
-        }
+        ensure_cdk!(keys_id == self.id, Error::IncorrectKeysetId);
 
         Ok(())
     }
@@ -264,10 +260,22 @@ pub struct KeySetInfo {
     /// Mint will only sign from an active keyset
     pub active: bool,
     /// Input Fee PPK
-    #[serde(default = "default_input_fee_ppk")]
+    #[serde(
+        deserialize_with = "deserialize_input_fee_ppk",
+        default = "default_input_fee_ppk"
+    )]
     pub input_fee_ppk: u64,
 }
 
+fn deserialize_input_fee_ppk<'de, D>(deserializer: D) -> Result<u64, D::Error>
+where
+    D: Deserializer<'de>,
+{
+    // This will either give us a u64 or null (which becomes None)
+    let opt = Option::<u64>::deserialize(deserializer)?;
+    Ok(opt.unwrap_or_else(default_input_fee_ppk))
+}
+
 fn default_input_fee_ppk() -> u64 {
     0
 }
@@ -490,6 +498,10 @@ mod test {
         let h = r#"{"id":"009a1f293253e41e","unit":"sat","active":true}"#;
 
         let _keyset_response: KeySetInfo = serde_json::from_str(h).unwrap();
+
+        let h = r#"{"id":"009a1f293253e41e","unit":"sat","active":true, "input_fee_ppk":null}"#;
+
+        let _keyset_response: KeySetInfo = serde_json::from_str(h).unwrap();
     }
 
     #[test]

+ 26 - 7
crates/cashu/src/nuts/nut03.rs

@@ -8,6 +8,7 @@ use thiserror::Error;
 #[cfg(feature = "wallet")]
 use super::nut00::PreMintSecrets;
 use super::nut00::{BlindSignature, BlindedMessage, Proofs};
+use super::ProofsMethods;
 use crate::Amount;
 
 /// NUT03 Error
@@ -40,16 +41,34 @@ pub struct PreSwap {
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub struct SwapRequest {
     /// Proofs that are to be spent in a `Swap`
-    #[cfg_attr(feature = "swagger", schema(value_type = Vec<Proof>))]
-    pub inputs: Proofs,
+    #[cfg_attr(feature = "swagger", schema(value_type = Vec<crate::Proof>))]
+    inputs: Proofs,
     /// Blinded Messages for Mint to sign
-    pub outputs: Vec<BlindedMessage>,
+    outputs: Vec<BlindedMessage>,
 }
 
 impl SwapRequest {
     /// Create new [`SwapRequest`]
     pub fn new(inputs: Proofs, outputs: Vec<BlindedMessage>) -> Self {
-        Self { inputs, outputs }
+        Self {
+            inputs: inputs.without_dleqs(),
+            outputs,
+        }
+    }
+
+    /// Get inputs (proofs)
+    pub fn inputs(&self) -> &Proofs {
+        &self.inputs
+    }
+
+    /// Get outputs (blinded messages)
+    pub fn outputs(&self) -> &Vec<BlindedMessage> {
+        &self.outputs
+    }
+
+    /// Get mutable reference to outputs (blinded messages)
+    pub fn outputs_mut(&mut self) -> &mut Vec<BlindedMessage> {
+        &mut self.outputs
     }
 
     /// Total value of proofs in [`SwapRequest`]
@@ -76,9 +95,9 @@ pub struct SwapResponse {
 }
 
 impl SwapResponse {
-    /// Create new [`SwapRequest`]
-    pub fn new(promises: Vec<BlindSignature>) -> SwapResponse {
-        SwapResponse {
+    /// Create new [`SwapResponse`]
+    pub fn new(promises: Vec<BlindSignature>) -> Self {
+        Self {
             signatures: promises,
         }
     }

+ 10 - 13
crates/cashu/src/nuts/nut04.rs

@@ -94,6 +94,12 @@ pub struct MintQuoteBolt11Response<Q> {
     pub quote: Q,
     /// Payment request to fulfil
     pub request: String,
+    /// Amount
+    // REVIEW: This is now required in the spec, we should remove the option once all mints update
+    pub amount: Option<Amount>,
+    /// Unit
+    // REVIEW: This is now required in the spec, we should remove the option once all mints update
+    pub unit: Option<CurrencyUnit>,
     /// Quote State
     pub state: MintQuoteState,
     /// Unix timestamp until the quote is valid
@@ -112,6 +118,8 @@ impl<Q: ToString> MintQuoteBolt11Response<Q> {
             state: self.state,
             expiry: self.expiry,
             pubkey: self.pubkey,
+            amount: self.amount,
+            unit: self.unit.clone(),
         }
     }
 }
@@ -125,19 +133,8 @@ impl From<MintQuoteBolt11Response<Uuid>> for MintQuoteBolt11Response<String> {
             state: value.state,
             expiry: value.expiry,
             pubkey: value.pubkey,
-        }
-    }
-}
-
-#[cfg(feature = "mint")]
-impl From<crate::mint::MintQuote> for MintQuoteBolt11Response<Uuid> {
-    fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt11Response<Uuid> {
-        MintQuoteBolt11Response {
-            quote: mint_quote.id,
-            request: mint_quote.request,
-            state: mint_quote.state,
-            expiry: Some(mint_quote.expiry),
-            pubkey: mint_quote.pubkey,
+            amount: value.amount,
+            unit: value.unit.clone(),
         }
     }
 }

+ 93 - 40
crates/cashu/src/nuts/nut05.rs

@@ -14,8 +14,7 @@ use uuid::Uuid;
 
 use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
 use super::nut15::Mpp;
-#[cfg(feature = "mint")]
-use crate::mint::{self, MeltQuote};
+use super::ProofsMethods;
 use crate::nuts::MeltQuoteState;
 use crate::{Amount, Bolt11Invoice};
 
@@ -59,10 +58,15 @@ pub enum MeltOptions {
         /// MPP
         mpp: Mpp,
     },
+    /// Amountless options
+    Amountless {
+        /// Amountless
+        amountless: Amountless,
+    },
 }
 
 impl MeltOptions {
-    /// Create new [`Options::Mpp`]
+    /// Create new [`MeltOptions::Mpp`]
     pub fn new_mpp<A>(amount: A) -> Self
     where
         A: Into<Amount>,
@@ -74,14 +78,35 @@ impl MeltOptions {
         }
     }
 
+    /// Create new [`MeltOptions::Amountless`]
+    pub fn new_amountless<A>(amount_msat: A) -> Self
+    where
+        A: Into<Amount>,
+    {
+        Self::Amountless {
+            amountless: Amountless {
+                amount_msat: amount_msat.into(),
+            },
+        }
+    }
+
     /// Payment amount
     pub fn amount_msat(&self) -> Amount {
         match self {
             Self::Mpp { mpp } => mpp.amount,
+            Self::Amountless { amountless } => amountless.amount_msat,
         }
     }
 }
 
+/// Amountless payment
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct Amountless {
+    /// Amount to pay in msat
+    pub amount_msat: Amount,
+}
+
 impl MeltQuoteBolt11Request {
     /// Amount from [`MeltQuoteBolt11Request`]
     ///
@@ -101,6 +126,15 @@ impl MeltQuoteBolt11Request {
                 .ok_or(Error::InvalidAmountRequest)?
                 .into()),
             Some(MeltOptions::Mpp { mpp }) => Ok(mpp.amount),
+            Some(MeltOptions::Amountless { amountless }) => {
+                let amount = amountless.amount_msat;
+                if let Some(amount_msat) = request.amount_milli_satoshis() {
+                    if amount != amount_msat.into() {
+                        return Err(Error::InvalidAmountRequest);
+                    }
+                }
+                Ok(amount)
+            }
         }
     }
 }
@@ -175,6 +209,14 @@ pub struct MeltQuoteBolt11Response<Q> {
     /// Change
     #[serde(skip_serializing_if = "Option::is_none")]
     pub change: Option<Vec<BlindSignature>>,
+    /// Payment request to fulfill
+    // REVIEW: This is now required in the spec, we should remove the option once all mints update
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub request: Option<String>,
+    /// Unit
+    // REVIEW: This is now required in the spec, we should remove the option once all mints update
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub unit: Option<CurrencyUnit>,
 }
 
 impl<Q: ToString> MeltQuoteBolt11Response<Q> {
@@ -190,6 +232,8 @@ impl<Q: ToString> MeltQuoteBolt11Response<Q> {
             expiry: self.expiry,
             payment_preimage: self.payment_preimage,
             change: self.change,
+            request: self.request,
+            unit: self.unit,
         }
     }
 }
@@ -206,22 +250,8 @@ impl From<MeltQuoteBolt11Response<Uuid>> for MeltQuoteBolt11Response<String> {
             expiry: value.expiry,
             payment_preimage: value.payment_preimage,
             change: value.change,
-        }
-    }
-}
-
-#[cfg(feature = "mint")]
-impl From<&MeltQuote> for MeltQuoteBolt11Response<Uuid> {
-    fn from(melt_quote: &MeltQuote) -> MeltQuoteBolt11Response<Uuid> {
-        MeltQuoteBolt11Response {
-            quote: melt_quote.id,
-            payment_preimage: None,
-            change: None,
-            state: melt_quote.state,
-            paid: Some(melt_quote.state == MeltQuoteState::Paid),
-            expiry: melt_quote.expiry,
-            amount: melt_quote.amount,
-            fee_reserve: melt_quote.fee_reserve,
+            request: value.request,
+            unit: value.unit,
         }
     }
 }
@@ -297,6 +327,14 @@ impl<'de, Q: DeserializeOwned> Deserialize<'de> for MeltQuoteBolt11Response<Q> {
             .get("change")
             .and_then(|b| serde_json::from_value(b.clone()).ok());
 
+        let request: Option<String> = value
+            .get("request")
+            .and_then(|r| serde_json::from_value(r.clone()).ok());
+
+        let unit: Option<CurrencyUnit> = value
+            .get("unit")
+            .and_then(|u| serde_json::from_value(u.clone()).ok());
+
         Ok(Self {
             quote,
             amount,
@@ -306,40 +344,25 @@ impl<'de, Q: DeserializeOwned> Deserialize<'de> for MeltQuoteBolt11Response<Q> {
             expiry,
             payment_preimage,
             change,
+            request,
+            unit,
         })
     }
 }
 
-#[cfg(feature = "mint")]
-impl From<mint::MeltQuote> for MeltQuoteBolt11Response<Uuid> {
-    fn from(melt_quote: mint::MeltQuote) -> MeltQuoteBolt11Response<Uuid> {
-        let paid = melt_quote.state == QuoteState::Paid;
-        MeltQuoteBolt11Response {
-            quote: melt_quote.id,
-            amount: melt_quote.amount,
-            fee_reserve: melt_quote.fee_reserve,
-            paid: Some(paid),
-            state: melt_quote.state,
-            expiry: melt_quote.expiry,
-            payment_preimage: melt_quote.payment_preimage,
-            change: None,
-        }
-    }
-}
-
 /// Melt Bolt11 Request [NUT-05]
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 #[serde(bound = "Q: Serialize + DeserializeOwned")]
 pub struct MeltBolt11Request<Q> {
     /// Quote ID
-    pub quote: Q,
+    quote: Q,
     /// Proofs
-    #[cfg_attr(feature = "swagger", schema(value_type = Vec<Proof>))]
-    pub inputs: Proofs,
+    #[cfg_attr(feature = "swagger", schema(value_type = Vec<crate::Proof>))]
+    inputs: Proofs,
     /// Blinded Message that can be used to return change [NUT-08]
     /// Amount field of BlindedMessages `SHOULD` be set to zero
-    pub outputs: Option<Vec<BlindedMessage>>,
+    outputs: Option<Vec<BlindedMessage>>,
 }
 
 #[cfg(feature = "mint")]
@@ -355,7 +378,34 @@ impl TryFrom<MeltBolt11Request<String>> for MeltBolt11Request<Uuid> {
     }
 }
 
+// Basic implementation without trait bounds
+impl<Q> MeltBolt11Request<Q> {
+    /// Get inputs (proofs)
+    pub fn inputs(&self) -> &Proofs {
+        &self.inputs
+    }
+
+    /// Get outputs (blinded messages for change)
+    pub fn outputs(&self) -> &Option<Vec<BlindedMessage>> {
+        &self.outputs
+    }
+}
+
 impl<Q: Serialize + DeserializeOwned> MeltBolt11Request<Q> {
+    /// Create new [`MeltBolt11Request`]
+    pub fn new(quote: Q, inputs: Proofs, outputs: Option<Vec<BlindedMessage>>) -> Self {
+        Self {
+            quote,
+            inputs: inputs.without_dleqs(),
+            outputs,
+        }
+    }
+
+    /// Get quote
+    pub fn quote(&self) -> &Q {
+        &self.quote
+    }
+
     /// Total [`Amount`] of [`Proofs`]
     pub fn proofs_amount(&self) -> Result<Amount, Error> {
         Amount::try_sum(self.inputs.iter().map(|proof| proof.amount))
@@ -377,6 +427,9 @@ pub struct MeltMethodSettings {
     /// Max Amount
     #[serde(skip_serializing_if = "Option::is_none")]
     pub max_amount: Option<Amount>,
+    /// Amountless
+    #[serde(default)]
+    pub amountless: bool,
 }
 
 impl Settings {

+ 126 - 52
crates/cashu/src/nuts/nut06.rs

@@ -2,12 +2,17 @@
 //!
 //! <https://github.com/cashubtc/nuts/blob/main/06.md>
 
+#[cfg(feature = "auth")]
+use std::collections::HashMap;
+
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
 
 use super::nut01::PublicKey;
 use super::nut17::SupportedMethods;
 use super::nut19::CachedEndpoint;
 use super::{nut04, nut05, nut15, nut19, MppMethodSettings};
+#[cfg(feature = "auth")]
+use super::{AuthRequired, BlindAuthSettings, ClearAuthSettings, ProtectedEndpoint};
 
 /// Mint Version
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -59,7 +64,7 @@ impl<'de> Deserialize<'de> for MintVersion {
     }
 }
 
-/// Mint Info [NIP-06]
+/// Mint Info [NUT-06]
 #[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub struct MintInfo {
@@ -95,6 +100,9 @@ pub struct MintInfo {
     /// server unix timestamp
     #[serde(skip_serializing_if = "Option::is_none")]
     pub time: Option<u64>,
+    /// terms of url service of the mint
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub tos_url: Option<String>,
 }
 
 impl MintInfo {
@@ -197,6 +205,57 @@ impl MintInfo {
             ..self
         }
     }
+
+    /// Set tos_url
+    pub fn tos_url<S>(self, tos_url: S) -> Self
+    where
+        S: Into<String>,
+    {
+        Self {
+            tos_url: Some(tos_url.into()),
+            ..self
+        }
+    }
+
+    /// Get protected endpoints
+    #[cfg(feature = "auth")]
+    pub fn protected_endpoints(&self) -> HashMap<ProtectedEndpoint, AuthRequired> {
+        let mut protected_endpoints = HashMap::new();
+
+        if let Some(nut21_settings) = &self.nuts.nut21 {
+            for endpoint in nut21_settings.protected_endpoints.iter() {
+                protected_endpoints.insert(*endpoint, AuthRequired::Clear);
+            }
+        }
+
+        if let Some(nut22_settings) = &self.nuts.nut22 {
+            for endpoint in nut22_settings.protected_endpoints.iter() {
+                protected_endpoints.insert(*endpoint, AuthRequired::Blind);
+            }
+        }
+        protected_endpoints
+    }
+
+    /// Get Openid discovery of the mint if it is set
+    #[cfg(feature = "auth")]
+    pub fn openid_discovery(&self) -> Option<String> {
+        self.nuts
+            .nut21
+            .as_ref()
+            .map(|s| s.openid_discovery.to_string())
+    }
+
+    /// Get Openid discovery of the mint if it is set
+    #[cfg(feature = "auth")]
+    pub fn client_id(&self) -> Option<String> {
+        self.nuts.nut21.as_ref().map(|s| s.client_id.clone())
+    }
+
+    /// Max bat mint
+    #[cfg(feature = "auth")]
+    pub fn bat_max_mint(&self) -> Option<u64> {
+        self.nuts.nut22.as_ref().map(|s| s.bat_max_mint)
+    }
 }
 
 /// Supported nuts and settings
@@ -255,6 +314,16 @@ pub struct Nuts {
     #[serde(default)]
     #[serde(rename = "20")]
     pub nut20: SupportedSettings,
+    /// NUT21 Settings
+    #[serde(rename = "21")]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    #[cfg(feature = "auth")]
+    pub nut21: Option<ClearAuthSettings>,
+    /// NUT22 Settings
+    #[serde(rename = "22")]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    #[cfg(feature = "auth")]
+    pub nut22: Option<BlindAuthSettings>,
 }
 
 impl Nuts {
@@ -405,12 +474,12 @@ mod tests {
     #[test]
     fn test_des_mint_into() {
         let mint_info_str = r#"{
-    "name": "Cashu mint",
-    "pubkey": "0296d0aa13b6a31cf0cd974249f28c7b7176d7274712c95a41c7d8066d3f29d679",
-    "version": "Nutshell/0.15.3",
-    "contact": [
-        ["", ""],
-        ["", ""]
+"name": "Cashu mint",
+"pubkey": "0296d0aa13b6a31cf0cd974249f28c7b7176d7274712c95a41c7d8066d3f29d679",
+"version": "Nutshell/0.15.3",
+"contact": [
+    ["", ""],
+    ["", ""]
     ],
     "nuts": {
         "4": {
@@ -432,7 +501,8 @@ mod tests {
         "9": {"supported": true},
         "10": {"supported": true},
         "11": {"supported": true}
-    }
+    },
+"tos_url": "https://cashu.mint/tos"
 }"#;
 
         let _mint_info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
@@ -455,7 +525,8 @@ mod tests {
 
                 println!("{}", mint_info);
         */
-        let mint_info_str = r#"{
+        let mint_info_str = r#"
+{
   "name": "Bob's Cashu mint",
   "pubkey": "0283bf290884eed3a7ca2663fc0260de2e2064d6b355ea13f98dec004b7a7ead99",
   "version": "Nutshell/0.15.0",
@@ -502,51 +573,54 @@ mod tests {
     "9": {"supported": true},
     "10": {"supported": true},
     "12": {"supported": true}
-  }
+  },
+  "tos_url": "https://cashu.mint/tos"
 }"#;
         let info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
-        let mint_info_str = r#"{
-  "name": "Bob's Cashu mint",
-  "pubkey": "0283bf290884eed3a7ca2663fc0260de2e2064d6b355ea13f98dec004b7a7ead99",
-  "version": "Nutshell/0.15.0",
-  "description": "The short mint description",
-  "description_long": "A description that can be a long piece of text.",
-  "contact": [
-        ["nostr", "xxxxx"],
-        ["email", "contact@me.com"]
-  ],
-  "motd": "Message to display to users.",
-  "icon_url": "https://this-is-a-mint-icon-url.com/icon.png",
-  "nuts": {
-    "4": {
-      "methods": [
-        {
-        "method": "bolt11",
-        "unit": "sat",
-        "min_amount": 0,
-        "max_amount": 10000,
-        "description": true
-        }
-      ],
-      "disabled": false
-    },
-    "5": {
-      "methods": [
-        {
-        "method": "bolt11",
-        "unit": "sat",
-        "min_amount": 0,
-        "max_amount": 10000
-        }
-      ],
-      "disabled": false
-    },
-    "7": {"supported": true},
-    "8": {"supported": true},
-    "9": {"supported": true},
-    "10": {"supported": true},
-    "12": {"supported": true}
-  }
+        let mint_info_str = r#"
+{
+    "name": "Bob's Cashu mint",
+    "pubkey": "0283bf290884eed3a7ca2663fc0260de2e2064d6b355ea13f98dec004b7a7ead99",
+    "version": "Nutshell/0.15.0",
+    "description": "The short mint description",
+    "description_long": "A description that can be a long piece of text.",
+    "contact": [
+    ["nostr", "xxxxx"],
+    ["email", "contact@me.com"]
+        ],
+        "motd": "Message to display to users.",
+        "icon_url": "https://this-is-a-mint-icon-url.com/icon.png",
+        "nuts": {
+            "4": {
+            "methods": [
+                {
+                "method": "bolt11",
+                "unit": "sat",
+                "min_amount": 0,
+                "max_amount": 10000,
+                "description": true
+                }
+            ],
+            "disabled": false
+            },
+            "5": {
+            "methods": [
+                {
+                "method": "bolt11",
+                "unit": "sat",
+                "min_amount": 0,
+                "max_amount": 10000
+                }
+            ],
+            "disabled": false
+            },
+            "7": {"supported": true},
+            "8": {"supported": true},
+            "9": {"supported": true},
+            "10": {"supported": true},
+            "12": {"supported": true}
+        },
+        "tos_url": "https://cashu.mint/tos"
 }"#;
         let mint_info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
 

+ 6 - 2
crates/cashu/src/nuts/nut07.rs

@@ -31,10 +31,12 @@ pub enum State {
     ///
     /// Currently being used in a transaction i.e. melt in progress
     Pending,
-    /// Proof is reserved
+    /// Reserved
     ///
-    /// i.e. used to create a token
+    /// Proof is reserved for future token creation
     Reserved,
+    /// Pending spent (i.e., spent but not yet swapped by receiver)
+    PendingSpent,
 }
 
 impl fmt::Display for State {
@@ -44,6 +46,7 @@ impl fmt::Display for State {
             Self::Unspent => "UNSPENT",
             Self::Pending => "PENDING",
             Self::Reserved => "RESERVED",
+            Self::PendingSpent => "PENDING_SPENT",
         };
 
         write!(f, "{}", s)
@@ -59,6 +62,7 @@ impl FromStr for State {
             "UNSPENT" => Ok(Self::Unspent),
             "PENDING" => Ok(Self::Pending),
             "RESERVED" => Ok(Self::Reserved),
+            "PENDING_SPENT" => Ok(Self::PendingSpent),
             _ => Err(Error::UnknownState),
         }
     }

+ 1 - 1
crates/cashu/src/nuts/nut08.rs

@@ -8,7 +8,7 @@ use crate::Amount;
 impl<Q> MeltBolt11Request<Q> {
     /// Total output [`Amount`]
     pub fn output_amount(&self) -> Option<Amount> {
-        self.outputs
+        self.outputs()
             .as_ref()
             .and_then(|o| Amount::try_sum(o.iter().map(|proof| proof.amount)).ok())
     }

+ 42 - 16
crates/cashu/src/nuts/nut11/mod.rs

@@ -17,6 +17,7 @@ use thiserror::Error;
 use super::nut00::Witness;
 use super::nut01::PublicKey;
 use super::{Kind, Nut10Secret, Proof, Proofs, SecretKey};
+use crate::ensure_cdk;
 use crate::nuts::nut00::BlindedMessage;
 use crate::secret::Secret;
 use crate::util::{hex, unix_time};
@@ -59,6 +60,9 @@ pub enum Error {
     /// Witness Signatures not provided
     #[error("Witness signatures not provided")]
     SignaturesNotProvided,
+    /// Duplicate signature from same pubkey
+    #[error("Duplicate signature from the same pubkey detected")]
+    DuplicateSignature,
     /// Parse Url Error
     #[error(transparent)]
     UrlParseError(#[from] url::ParseError),
@@ -127,7 +131,7 @@ impl Proof {
             secret.secret_data.tags.unwrap_or_default().try_into()?;
         let msg: &[u8] = self.secret.as_bytes();
 
-        let mut valid_sigs = 0;
+        let mut verified_pubkeys = HashSet::new();
 
         let witness_signatures = match &self.witness {
             Some(witness) => witness.signatures(),
@@ -147,7 +151,10 @@ impl Proof {
                 let sig = Signature::from_str(signature)?;
 
                 if v.verify(msg, &sig).is_ok() {
-                    valid_sigs += 1;
+                    // If the pubkey is already verified, return a duplicate signature error
+                    if !verified_pubkeys.insert(*v) {
+                        return Err(Error::DuplicateSignature);
+                    }
                 } else {
                     tracing::debug!(
                         "Could not verify signature: {sig} on message: {}",
@@ -157,6 +164,8 @@ impl Proof {
             }
         }
 
+        let valid_sigs = verified_pubkeys.len() as u64;
+
         if valid_sigs >= spending_conditions.num_sigs.unwrap_or(1) {
             return Ok(());
         }
@@ -184,19 +193,27 @@ impl Proof {
     }
 }
 
-/// Returns count of valid signatures
-pub fn valid_signatures(msg: &[u8], pubkeys: &[PublicKey], signatures: &[Signature]) -> u64 {
-    let mut count = 0;
+/// Returns count of valid signatures (each public key is only counted once)
+/// Returns error if the same pubkey has multiple valid signatures
+pub fn valid_signatures(
+    msg: &[u8],
+    pubkeys: &[PublicKey],
+    signatures: &[Signature],
+) -> Result<u64, Error> {
+    let mut verified_pubkeys = HashSet::new();
 
     for pubkey in pubkeys {
         for signature in signatures {
             if pubkey.verify(msg, signature).is_ok() {
-                count += 1;
+                // If the pubkey is already verified, return a duplicate signature error
+                if !verified_pubkeys.insert(*pubkey) {
+                    return Err(Error::DuplicateSignature);
+                }
             }
         }
     }
 
-    count
+    Ok(verified_pubkeys.len() as u64)
 }
 
 impl BlindedMessage {
@@ -223,7 +240,7 @@ impl BlindedMessage {
 
     /// Verify P2PK conditions on [BlindedMessage]
     pub fn verify_p2pk(&self, pubkeys: &Vec<PublicKey>, required_sigs: u64) -> Result<(), Error> {
-        let mut valid_sigs = 0;
+        let mut verified_pubkeys = HashSet::new();
         if let Some(witness) = &self.witness {
             for signature in witness
                 .signatures()
@@ -235,7 +252,10 @@ impl BlindedMessage {
                     let sig = Signature::from_str(signature)?;
 
                     if v.verify(msg, &sig).is_ok() {
-                        valid_sigs += 1;
+                        // If the pubkey is already verified, return a duplicate signature error
+                        if !verified_pubkeys.insert(*v) {
+                            return Err(Error::DuplicateSignature);
+                        }
                     } else {
                         tracing::debug!(
                             "Could not verify signature: {sig} on message: {}",
@@ -246,6 +266,8 @@ impl BlindedMessage {
             }
         }
 
+        let valid_sigs = verified_pubkeys.len() as u64;
+
         if valid_sigs.ge(&required_sigs) {
             Ok(())
         } else {
@@ -422,9 +444,7 @@ impl Conditions {
         sig_flag: Option<SigFlag>,
     ) -> Result<Self, Error> {
         if let Some(locktime) = locktime {
-            if locktime.lt(&unix_time()) {
-                return Err(Error::LocktimeInPast);
-            }
+            ensure_cdk!(locktime.ge(&unix_time()), Error::LocktimeInPast);
         }
 
         Ok(Self {
@@ -704,10 +724,7 @@ where
     type Error = Error;
 
     fn try_from(tag: Vec<S>) -> Result<Self, Self::Error> {
-        let tag_kind: TagKind = match tag.first() {
-            Some(kind) => TagKind::from(kind),
-            None => return Err(Error::KindNotFound),
-        };
+        let tag_kind = tag.first().map(TagKind::from).ok_or(Error::KindNotFound)?;
 
         match tag_kind {
             TagKind::SigFlag => Ok(Tag::SigFlag(SigFlag::from_str(tag[1].as_ref())?)),
@@ -932,4 +949,13 @@ mod tests {
 
         assert!(invalid_proof.verify_p2pk().is_err());
     }
+
+    #[test]
+    fn test_duplicate_signatures_counting() {
+        let proof: Proof = serde_json::from_str(
+            r#"{"amount":1,"id":"009a1f293253e41e","secret":"[\"P2PK\",{\"nonce\":\"e434a9efbc5f65d144a620e368c9a6dc12c719d0ebc57e0c74f7341864dc449a\",\"data\":\"02a60c27104cf6023581e790970fc33994a320abe36e7ceed16771b0f8d76f0666\",\"tags\":[[\"pubkeys\",\"039c6a20a6ba354b7bb92eb9750716c1098063006362a1fa2afca7421f262d45c5\",\"0203eb2f7cd72a4f725d3327216365d2df18bb4bbc810522fd973c9af987e9b05b\"],[\"locktime\",\"1744876528\"],[\"n_sigs\",\"2\"],[\"sigflag\",\"SIG_INPUTS\"]]}]","C":"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904","witness":"{\"signatures\":[\"3e9ff9e55c9eccb9e5aa0b6c62d54500b40d0eebadb06efcc8e76f3ce38e0923f956ec1bccb9080db96a17c1e98a1b857abfd1a56bb25670037cea3db1f73d81\",\"c5e29c38e60c4db720cf3f78e590358cf1291a06b9eadf77c1108ae84d533520c2707ffda224eb6a63fddaee9abd5ecf8f2cd263d2556950550e3061a5511f65\"]}"}"#,
+        ).unwrap();
+
+        assert!(proof.verify_p2pk().is_err());
+    }
 }

+ 4 - 4
crates/cashu/src/nuts/nut14/mod.rs

@@ -14,6 +14,7 @@ use super::nut00::Witness;
 use super::nut10::Secret;
 use super::nut11::valid_signatures;
 use super::{Conditions, Proof};
+use crate::ensure_cdk;
 use crate::util::unix_time;
 
 pub mod serde_htlc_witness;
@@ -93,7 +94,7 @@ impl Proof {
                         .collect::<Result<Vec<Signature>, _>>()?;
 
                     // If secret includes refund keys check that there is a valid signature
-                    if valid_signatures(self.secret.as_bytes(), &refund_key, &signatures).ge(&1) {
+                    if valid_signatures(self.secret.as_bytes(), &refund_key, &signatures)?.ge(&1) {
                         return Ok(());
                     }
                 }
@@ -112,9 +113,8 @@ impl Proof {
                     .map(|s| Signature::from_str(s))
                     .collect::<Result<Vec<Signature>, _>>()?;
 
-                if valid_signatures(self.secret.as_bytes(), &pubkey, &signatures).lt(&req_sigs) {
-                    return Err(Error::IncorrectSecretKind);
-                }
+                let valid_sigs = valid_signatures(self.secret.as_bytes(), &pubkey, &signatures)?;
+                ensure_cdk!(valid_sigs >= req_sigs, Error::IncorrectSecretKind);
             }
         }
 

+ 2 - 0
crates/cashu/src/nuts/nut17/mod.rs

@@ -27,6 +27,7 @@ pub struct Params<I> {
 
 /// Check state Settings
 #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub struct SupportedSettings {
     /// Supported methods
     pub supported: Vec<SupportedMethods>,
@@ -34,6 +35,7 @@ pub struct SupportedSettings {
 
 /// Supported WS Methods
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub struct SupportedMethods {
     /// Payment Method
     pub method: PaymentMethod,

+ 238 - 2
crates/cashu/src/nuts/nut18.rs

@@ -49,11 +49,27 @@ impl fmt::Display for TransportType {
     }
 }
 
+impl FromStr for TransportType {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s.to_lowercase().as_str() {
+            "nostr" => Ok(Self::Nostr),
+            "post" => Ok(Self::HttpPost),
+            _ => Err(Error::InvalidPrefix),
+        }
+    }
+}
+
 impl FromStr for Transport {
-    type Err = serde_json::Error;
+    type Err = Error;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        serde_json::from_str(s)
+        let decode_config = general_purpose::GeneralPurposeConfig::new()
+            .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
+        let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
+
+        Ok(ciborium::from_reader(&decoded[..])?)
     }
 }
 
@@ -71,6 +87,65 @@ pub struct Transport {
     pub tags: Option<Vec<Vec<String>>>,
 }
 
+impl Transport {
+    /// Create a new TransportBuilder
+    pub fn builder() -> TransportBuilder {
+        TransportBuilder::default()
+    }
+}
+
+/// Builder for Transport
+#[derive(Debug, Default, Clone)]
+pub struct TransportBuilder {
+    _type: Option<TransportType>,
+    target: Option<String>,
+    tags: Option<Vec<Vec<String>>>,
+}
+
+impl TransportBuilder {
+    /// Set transport type
+    pub fn transport_type(mut self, transport_type: TransportType) -> Self {
+        self._type = Some(transport_type);
+        self
+    }
+
+    /// Set target
+    pub fn target<S: Into<String>>(mut self, target: S) -> Self {
+        self.target = Some(target.into());
+        self
+    }
+
+    /// Add a tag
+    pub fn add_tag(mut self, tag: Vec<String>) -> Self {
+        self.tags.get_or_insert_with(Vec::new).push(tag);
+        self
+    }
+
+    /// Set tags
+    pub fn tags(mut self, tags: Vec<Vec<String>>) -> Self {
+        self.tags = Some(tags);
+        self
+    }
+
+    /// Build the Transport
+    pub fn build(self) -> Result<Transport, &'static str> {
+        let _type = self._type.ok_or("Transport type is required")?;
+        let target = self.target.ok_or("Target is required")?;
+
+        Ok(Transport {
+            _type,
+            target,
+            tags: self.tags,
+        })
+    }
+}
+
+impl AsRef<String> for Transport {
+    fn as_ref(&self) -> &String {
+        &self.target
+    }
+}
+
 /// Payment Request
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct PaymentRequest {
@@ -97,6 +172,106 @@ pub struct PaymentRequest {
     pub transports: Vec<Transport>,
 }
 
+impl PaymentRequest {
+    /// Create a new PaymentRequestBuilder
+    pub fn builder() -> PaymentRequestBuilder {
+        PaymentRequestBuilder::default()
+    }
+}
+
+/// Builder for PaymentRequest
+#[derive(Debug, Default, Clone)]
+pub struct PaymentRequestBuilder {
+    payment_id: Option<String>,
+    amount: Option<Amount>,
+    unit: Option<CurrencyUnit>,
+    single_use: Option<bool>,
+    mints: Option<Vec<MintUrl>>,
+    description: Option<String>,
+    transports: Vec<Transport>,
+}
+
+impl PaymentRequestBuilder {
+    /// Set payment ID
+    pub fn payment_id<S>(mut self, payment_id: S) -> Self
+    where
+        S: Into<String>,
+    {
+        self.payment_id = Some(payment_id.into());
+        self
+    }
+
+    /// Set amount
+    pub fn amount<A>(mut self, amount: A) -> Self
+    where
+        A: Into<Amount>,
+    {
+        self.amount = Some(amount.into());
+        self
+    }
+
+    /// Set unit
+    pub fn unit(mut self, unit: CurrencyUnit) -> Self {
+        self.unit = Some(unit);
+        self
+    }
+
+    /// Set single use flag
+    pub fn single_use(mut self, single_use: bool) -> Self {
+        self.single_use = Some(single_use);
+        self
+    }
+
+    /// Add a mint URL
+    pub fn add_mint(mut self, mint_url: MintUrl) -> Self {
+        self.mints.get_or_insert_with(Vec::new).push(mint_url);
+        self
+    }
+
+    /// Set mints
+    pub fn mints(mut self, mints: Vec<MintUrl>) -> Self {
+        self.mints = Some(mints);
+        self
+    }
+
+    /// Set description
+    pub fn description<S: Into<String>>(mut self, description: S) -> Self {
+        self.description = Some(description.into());
+        self
+    }
+
+    /// Add a transport
+    pub fn add_transport(mut self, transport: Transport) -> Self {
+        self.transports.push(transport);
+        self
+    }
+
+    /// Set transports
+    pub fn transports(mut self, transports: Vec<Transport>) -> Self {
+        self.transports = transports;
+        self
+    }
+
+    /// Build the PaymentRequest
+    pub fn build(self) -> PaymentRequest {
+        PaymentRequest {
+            payment_id: self.payment_id,
+            amount: self.amount,
+            unit: self.unit,
+            single_use: self.single_use,
+            mints: self.mints,
+            description: self.description,
+            transports: self.transports,
+        }
+    }
+}
+
+impl AsRef<Option<String>> for PaymentRequest {
+    fn as_ref(&self) -> &Option<String> {
+        &self.payment_id
+    }
+}
+
 impl fmt::Display for PaymentRequest {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         use serde::ser::Error;
@@ -198,4 +373,65 @@ mod tests {
         let t = req.transports.first().unwrap();
         assert_eq!(&transport, t);
     }
+
+    #[test]
+    fn test_payment_request_builder() {
+        let transport = Transport {
+            _type: TransportType::Nostr,
+            target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), 
+            tags: Some(vec![vec!["n".to_string(), "17".to_string()]])
+        };
+
+        let mint_url =
+            MintUrl::from_str("https://nofees.testnut.cashu.space").expect("valid mint url");
+
+        // Build a payment request using the builder pattern
+        let request = PaymentRequest::builder()
+            .payment_id("b7a90176")
+            .amount(Amount::from(10))
+            .unit(CurrencyUnit::Sat)
+            .add_mint(mint_url.clone())
+            .add_transport(transport.clone())
+            .build();
+
+        // Verify the built request
+        assert_eq!(&request.payment_id.clone().unwrap(), "b7a90176");
+        assert_eq!(request.amount.unwrap(), 10.into());
+        assert_eq!(request.unit.clone().unwrap(), CurrencyUnit::Sat);
+        assert_eq!(request.mints.clone().unwrap(), vec![mint_url]);
+
+        let t = request.transports.first().unwrap();
+        assert_eq!(&transport, t);
+
+        // Test serialization and deserialization
+        let request_str = request.to_string();
+        let req = PaymentRequest::from_str(&request_str).expect("valid payment request");
+
+        assert_eq!(req.payment_id, request.payment_id);
+        assert_eq!(req.amount, request.amount);
+        assert_eq!(req.unit, request.unit);
+    }
+
+    #[test]
+    fn test_transport_builder() {
+        // Build a transport using the builder pattern
+        let transport = Transport::builder()
+            .transport_type(TransportType::Nostr)
+            .target("nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5")
+            .add_tag(vec!["n".to_string(), "17".to_string()])
+            .build()
+            .expect("Valid transport");
+
+        // Verify the built transport
+        assert_eq!(transport._type, TransportType::Nostr);
+        assert_eq!(transport.target, "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5");
+        assert_eq!(
+            transport.tags,
+            Some(vec![vec!["n".to_string(), "17".to_string()]])
+        );
+
+        // Test error case - missing required fields
+        let result = TransportBuilder::default().build();
+        assert!(result.is_err());
+    }
 }

+ 3 - 0
crates/cashu/src/nuts/nut19.rs

@@ -16,6 +16,7 @@ pub struct Settings {
 
 /// List of the methods and paths for which caching is enabled
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub struct CachedEndpoint {
     /// HTTP Method
     pub method: Method,
@@ -33,6 +34,7 @@ impl CachedEndpoint {
 /// HTTP method
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 #[serde(rename_all = "UPPERCASE")]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub enum Method {
     /// Get
     Get,
@@ -42,6 +44,7 @@ pub enum Method {
 
 /// Route path
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub enum Path {
     /// Bolt11 Mint
     #[serde(rename = "/v1/mint/bolt11")]

+ 0 - 6
crates/cashu/src/secret.rs

@@ -44,12 +44,6 @@ impl Secret {
         Self(secret.into())
     }
 
-    /// Creates a new [`Secret`] from bytes
-    pub fn from_bytes(bytes: Vec<u8>) -> Self {
-        let secret = hex::encode(bytes);
-        Self(secret)
-    }
-
     /// Create secret value
     /// Generate a new random secret as the recommended 32 byte hex
     pub fn generate() -> Self {

+ 3 - 3
crates/cashu/src/util/hex.rs

@@ -5,6 +5,8 @@
 
 use core::fmt;
 
+use crate::ensure_cdk;
+
 /// Hex error
 #[derive(Debug, PartialEq, Eq)]
 pub enum Error {
@@ -76,9 +78,7 @@ where
     let hex = hex.as_ref();
     let len = hex.len();
 
-    if len % 2 != 0 {
-        return Err(Error::OddLength);
-    }
+    ensure_cdk!(len % 2 == 0, Error::OddLength);
 
     let mut bytes: Vec<u8> = Vec::with_capacity(len / 2);
 

+ 2 - 0
crates/cashu/src/util/mod.rs

@@ -1,3 +1,5 @@
+//! Cashu utils
+
 #[cfg(not(target_arch = "wasm32"))]
 use std::time::{SystemTime, UNIX_EPOCH};
 

+ 0 - 87
crates/cashu/src/wallet.rs

@@ -1,87 +0,0 @@
-//! Wallet Types
-
-use std::fmt;
-
-use serde::{Deserialize, Serialize};
-
-use crate::mint_url::MintUrl;
-use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState, SecretKey};
-use crate::Amount;
-
-/// Wallet Key
-#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
-pub struct WalletKey {
-    /// Mint Url
-    pub mint_url: MintUrl,
-    /// Currency Unit
-    pub unit: CurrencyUnit,
-}
-
-impl fmt::Display for WalletKey {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "mint_url: {}, unit: {}", self.mint_url, self.unit,)
-    }
-}
-
-impl WalletKey {
-    /// Create new [`WalletKey`]
-    pub fn new(mint_url: MintUrl, unit: CurrencyUnit) -> Self {
-        Self { mint_url, unit }
-    }
-}
-
-/// Mint Quote Info
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct MintQuote {
-    /// Quote id
-    pub id: String,
-    /// Mint Url
-    pub mint_url: MintUrl,
-    /// Amount of quote
-    pub amount: Amount,
-    /// Unit of quote
-    pub unit: CurrencyUnit,
-    /// Quote payment request e.g. bolt11
-    pub request: String,
-    /// Quote state
-    pub state: MintQuoteState,
-    /// Expiration time of quote
-    pub expiry: u64,
-    /// Secretkey for signing mint quotes [NUT-20]
-    pub secret_key: Option<SecretKey>,
-}
-
-/// Melt Quote Info
-#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
-pub struct MeltQuote {
-    /// Quote id
-    pub id: String,
-    /// Quote unit
-    pub unit: CurrencyUnit,
-    /// Quote amount
-    pub amount: Amount,
-    /// Quote Payment request e.g. bolt11
-    pub request: String,
-    /// Quote fee reserve
-    pub fee_reserve: Amount,
-    /// Quote state
-    pub state: MeltQuoteState,
-    /// Expiration time of quote
-    pub expiry: u64,
-    /// Payment preimage
-    pub payment_preimage: Option<String>,
-}
-
-/// Send Kind
-#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
-pub enum SendKind {
-    #[default]
-    /// Allow online swap before send if wallet does not have exact amount
-    OnlineExact,
-    /// Prefer offline send if difference is less then tolerance
-    OnlineTolerance(Amount),
-    /// Wallet cannot do an online swap and selected proof must be exactly send amount
-    OfflineExact,
-    /// Wallet must remain offline but can over pay if below tolerance
-    OfflineTolerance(Amount),
-}

+ 29 - 25
crates/cdk-axum/Cargo.toml

@@ -1,40 +1,44 @@
 [package]
 name = "cdk-axum"
-version = "0.7.1"
-edition = "2021"
-license = "MIT"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
 homepage = "https://github.com/cashubtc/cdk"
 repository = "https://github.com/cashubtc/cdk.git"
-rust-version = "1.63.0"                            # MSRV
+rust-version.workspace = true                            # MSRV
 description = "Cashu CDK axum webserver"
+readme = "README.md"
+
+
+[features]
+default = ["auth"]
+redis = ["dep:redis"]
+swagger = ["cdk/swagger", "dep:utoipa"]
+auth = ["cdk/auth"]
 
 [dependencies]
-anyhow = "1"
-async-trait = "0.1.83"
-axum = { version = "0.6.20", features = ["ws"] }
-cdk = { path = "../cdk", version = "0.7.1", default-features = false, features = [
+anyhow.workspace = true
+async-trait.workspace = true
+axum = { workspace = true, features = ["ws"] }
+cdk = { workspace = true, features = [
     "mint",
-] }
-tokio = { version = "1", default-features = false, features = ["io-util"] }
-tracing = { version = "0.1", default-features = false, features = [
-    "attributes",
-    "log",
-] }
-utoipa = { version = "4", features = [
-    "preserve_order",
-    "preserve_path_order",
-], optional = true }
-futures = { version = "0.3.28", default-features = false }
+]}
+tokio.workspace = true
+tracing.workspace = true
+utoipa = { workspace = true, optional = true }
+futures.workspace = true
 moka = { version = "0.11.1", features = ["future"] }
-serde_json = "1"
+serde_json.workspace = true
 paste = "1.0.15"
-serde = { version = "1.0.210", features = ["derive"] }
-uuid = { version = "=1.12.1", features = ["v4", "serde"] }
+serde.workspace = true
+uuid.workspace = true
 sha2 = "0.10.8"
 redis = { version = "0.23.3", features = [
     "tokio-rustls-comp",
 ], optional = true }
 
-[features]
-redis = ["dep:redis"]
-swagger = ["cdk/swagger", "dep:utoipa"]
+
+[build-dependencies]
+# Dep of utopia 2.5.0 breaks so keeping here for now
+time = "=0.3.39"
+

+ 31 - 0
crates/cdk-axum/README.md

@@ -0,0 +1,31 @@
+# CDK Axum
+
+[![crates.io](https://img.shields.io/crates/v/cdk-axum.svg)](https://crates.io/crates/cdk-axum) [![Documentation](https://docs.rs/cdk-axum/badge.svg)](https://docs.rs/cdk-axum)
+
+The CDK Axum crate is a component of the [Cashu Development Kit](https://github.com/cashubtc/cdk) that provides a web server implementation for Cashu mints using the [Axum](https://github.com/tokio-rs/axum) web framework.
+
+## Overview
+
+This crate implements the HTTP API for Cashu mints, providing endpoints for all the Cashu NUTs (Notation, Usage, and Terminology) specifications. It handles routing, request validation, response formatting, and includes features like WebSocket support and HTTP caching.
+
+## Features
+
+- Complete implementation of Cashu mint HTTP API
+- WebSocket support for real-time notifications (NUT-17)
+- HTTP response caching for improved performance (NUT-19)
+- CORS support for browser-based clients
+- Compression and decompression of HTTP payloads
+- Configurable logging and tracing
+
+## Usage
+
+Add this to your `Cargo.toml`:
+
+```toml
+[dependencies]
+cdk-axum = "*"
+```
+
+## License
+
+This project is licensed under the [MIT License](https://github.com/cashubtc/cdk/blob/main/LICENSE).

+ 194 - 0
crates/cdk-axum/src/auth.rs

@@ -0,0 +1,194 @@
+use std::str::FromStr;
+
+use axum::extract::{FromRequestParts, State};
+use axum::http::request::Parts;
+use axum::http::StatusCode;
+use axum::response::Response;
+use axum::routing::{get, post};
+use axum::{Json, Router};
+#[cfg(feature = "swagger")]
+use cdk::error::ErrorResponse;
+use cdk::nuts::{
+    AuthToken, BlindAuthToken, KeysResponse, KeysetResponse, MintAuthRequest, MintBolt11Response,
+};
+use serde::{Deserialize, Serialize};
+
+#[cfg(feature = "auth")]
+use crate::{get_keyset_pubkeys, into_response, MintState};
+
+const CLEAR_AUTH_KEY: &str = "Clear-auth";
+const BLIND_AUTH_KEY: &str = "Blind-auth";
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum AuthHeader {
+    /// Clear Auth token
+    Clear(String),
+    /// Blind Auth token
+    Blind(BlindAuthToken),
+    /// No auth
+    None,
+}
+
+impl From<AuthHeader> for Option<AuthToken> {
+    fn from(value: AuthHeader) -> Option<AuthToken> {
+        match value {
+            AuthHeader::Clear(token) => Some(AuthToken::ClearAuth(token)),
+            AuthHeader::Blind(token) => Some(AuthToken::BlindAuth(token)),
+            AuthHeader::None => None,
+        }
+    }
+}
+
+impl<S> FromRequestParts<S> for AuthHeader
+where
+    S: Send + Sync,
+{
+    type Rejection = (StatusCode, String);
+
+    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
+        // Check for Blind-auth header
+        if let Some(bat) = parts.headers.get(BLIND_AUTH_KEY) {
+            let token = bat
+                .to_str()
+                .map_err(|_| {
+                    (
+                        StatusCode::BAD_REQUEST,
+                        "Invalid Blind-auth header value".to_string(),
+                    )
+                })?
+                .to_string();
+
+            let token = BlindAuthToken::from_str(&token).map_err(|_| {
+                (
+                    StatusCode::BAD_REQUEST,
+                    "Invalid Blind-auth header value".to_string(),
+                )
+            })?;
+
+            return Ok(AuthHeader::Blind(token));
+        }
+
+        // Check for Clear-auth header
+        if let Some(cat) = parts.headers.get(CLEAR_AUTH_KEY) {
+            let token = cat
+                .to_str()
+                .map_err(|_| {
+                    (
+                        StatusCode::BAD_REQUEST,
+                        "Invalid Clear-auth header value".to_string(),
+                    )
+                })?
+                .to_string();
+            return Ok(AuthHeader::Clear(token));
+        }
+
+        // No authentication headers found - this is now valid
+        Ok(AuthHeader::None)
+    }
+}
+
+#[cfg_attr(feature = "swagger", utoipa::path(
+    get,
+    context_path = "/v1/auth/blind",
+    path = "/keysets",
+    responses(
+        (status = 200, description = "Successful response", body = KeysetResponse, content_type = "application/json"),
+        (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
+    )
+))]
+/// Get all active keyset IDs of the mint
+///
+/// This endpoint returns a list of keysets that the mint currently supports and will accept tokens from.
+#[cfg(feature = "auth")]
+pub async fn get_auth_keysets(
+    State(state): State<MintState>,
+) -> Result<Json<KeysetResponse>, Response> {
+    let keysets = state.mint.auth_keysets().await.map_err(|err| {
+        tracing::error!("Could not get keysets: {}", err);
+        into_response(err)
+    })?;
+
+    Ok(Json(keysets))
+}
+
+#[cfg_attr(feature = "swagger", utoipa::path(
+    get,
+    context_path = "/v1/auth/blind",
+    path = "/keys",
+    responses(
+        (status = 200, description = "Successful response", body = KeysResponse, content_type = "application/json")
+    )
+))]
+/// Get the public keys of the newest blind auth mint keyset
+///
+/// This endpoint returns a dictionary of all supported token values of the mint and their associated public key.
+pub async fn get_blind_auth_keys(
+    State(state): State<MintState>,
+) -> Result<Json<KeysResponse>, Response> {
+    let pubkeys = state.mint.auth_pubkeys().await.map_err(|err| {
+        tracing::error!("Could not get keys: {}", err);
+        into_response(err)
+    })?;
+
+    Ok(Json(pubkeys))
+}
+
+/// Mint tokens by paying a BOLT11 Lightning invoice.
+///
+/// Requests the minting of tokens belonging to a paid payment request.
+///
+/// Call this endpoint after `POST /v1/mint/quote`.
+#[cfg_attr(feature = "swagger", utoipa::path(
+    post,
+    context_path = "/v1/auth",
+    path = "/blind/mint",
+    request_body(content = MintAuthRequest, description = "Request params", content_type = "application/json"),
+    responses(
+        (status = 200, description = "Successful response", body = MintBolt11Response, content_type = "application/json"),
+        (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
+    )
+))]
+pub async fn post_mint_auth(
+    auth: AuthHeader,
+    State(state): State<MintState>,
+    Json(payload): Json<MintAuthRequest>,
+) -> Result<Json<MintBolt11Response>, Response> {
+    let auth_token = match auth {
+        AuthHeader::Clear(cat) => {
+            if cat.is_empty() {
+                tracing::debug!("Received blind auth mint request without cat");
+                return Err(into_response(cdk::Error::ClearAuthRequired));
+            }
+
+            AuthToken::ClearAuth(cat)
+        }
+        _ => {
+            tracing::debug!("Received blind auth mint request without cat");
+            return Err(into_response(cdk::Error::ClearAuthRequired));
+        }
+    };
+
+    let res = state
+        .mint
+        .mint_blind_auth(auth_token, payload)
+        .await
+        .map_err(|err| {
+            tracing::error!("Could not process blind auth mint: {}", err);
+            into_response(err)
+        })?;
+
+    Ok(Json(res))
+}
+
+pub fn create_auth_router(state: MintState) -> Router<MintState> {
+    Router::new()
+        .nest(
+            "/auth/blind",
+            Router::new()
+                .route("/keys", get(get_blind_auth_keys))
+                .route("/keysets", get(get_auth_keysets))
+                .route("/keys/{keyset_id}", get(get_keyset_pubkeys))
+                .route("/mint", post(post_mint_auth)),
+        )
+        .with_state(state)
+}

+ 2 - 2
crates/cdk-axum/src/cache/mod.rs

@@ -69,7 +69,7 @@ const DEFAULT_TTI_SECS: u64 = 60;
 
 /// Http cache key.
 ///
-/// This type ensures no Vec<u8> is used as a key, which is error-prone.
+/// This type ensures no `Vec<u8>` is used as a key, which is error-prone.
 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
 pub struct HttpCacheKey([u8; 32]);
 
@@ -117,7 +117,7 @@ impl HttpCache {
         tti: Duration,
         storage: Option<Box<dyn HttpCacheStorage + Send + Sync + 'static>>,
     ) -> Self {
-        let mut storage = storage.unwrap_or_else(|| Box::new(InMemoryHttpCache::default()));
+        let mut storage = storage.unwrap_or_else(|| Box::<InMemoryHttpCache>::default());
         storage.set_expiration_times(ttl, tti);
 
         Self {

+ 64 - 10
crates/cdk-axum/src/lib.rs

@@ -1,17 +1,24 @@
 //! Axum server for Mint
 
+#![doc = include_str!("../README.md")]
 #![warn(missing_docs)]
 #![warn(rustdoc::bare_urls)]
 
 use std::sync::Arc;
 
 use anyhow::Result;
+#[cfg(feature = "auth")]
+use auth::create_auth_router;
+use axum::middleware::from_fn;
+use axum::response::Response;
 use axum::routing::{get, post};
 use axum::Router;
 use cache::HttpCache;
 use cdk::mint::Mint;
 use router_handlers::*;
 
+#[cfg(feature = "auth")]
+mod auth;
 pub mod cache;
 mod router_handlers;
 mod ws;
@@ -45,8 +52,6 @@ mod swagger_imports {
 
 #[cfg(feature = "swagger")]
 use swagger_imports::*;
-#[cfg(feature = "swagger")]
-use uuid::Uuid;
 
 /// CDK Mint State
 #[derive(Clone)]
@@ -75,16 +80,16 @@ pub struct MintState {
         KeysetResponse,
         KeySet,
         KeySetInfo,
-        MeltBolt11Request<Uuid>,
+        MeltBolt11Request<String>,
         MeltQuoteBolt11Request,
-        MeltQuoteBolt11Response<Uuid>,
+        MeltQuoteBolt11Response<String>,
         MeltQuoteState,
         MeltMethodSettings,
-        MintBolt11Request<Uuid>,
+        MintBolt11Request<String>,
         MintBolt11Response,
         MintInfo,
         MintQuoteBolt11Request,
-        MintQuoteBolt11Response<Uuid>,
+        MintQuoteBolt11Response<String>,
         MintQuoteState,
         MintMethodSettings,
         MintVersion,
@@ -134,6 +139,45 @@ pub async fn create_mint_router(mint: Arc<Mint>) -> Result<Router> {
     create_mint_router_with_custom_cache(mint, Default::default()).await
 }
 
+async fn cors_middleware(
+    req: axum::http::Request<axum::body::Body>,
+    next: axum::middleware::Next,
+) -> Response {
+    // Handle preflight requests
+    if req.method() == axum::http::Method::OPTIONS {
+        let mut response = Response::new("".into());
+        response
+            .headers_mut()
+            .insert("Access-Control-Allow-Origin", "*".parse().unwrap());
+        response.headers_mut().insert(
+            "Access-Control-Allow-Methods",
+            "GET, POST, OPTIONS".parse().unwrap(),
+        );
+        response.headers_mut().insert(
+            "Access-Control-Allow-Headers",
+            "Content-Type".parse().unwrap(),
+        );
+        return response;
+    }
+
+    // Call the next handler
+    let mut response = next.run(req).await;
+
+    response
+        .headers_mut()
+        .insert("Access-Control-Allow-Origin", "*".parse().unwrap());
+    response.headers_mut().insert(
+        "Access-Control-Allow-Methods",
+        "GET, POST, OPTIONS".parse().unwrap(),
+    );
+    response.headers_mut().insert(
+        "Access-Control-Allow-Headers",
+        "Content-Type".parse().unwrap(),
+    );
+
+    response
+}
+
 /// Create mint [`Router`] with required endpoints for cashu mint with a custom
 /// backend for cache
 pub async fn create_mint_router_with_custom_cache(
@@ -148,18 +192,18 @@ pub async fn create_mint_router_with_custom_cache(
     let v1_router = Router::new()
         .route("/keys", get(get_keys))
         .route("/keysets", get(get_keysets))
-        .route("/keys/:keyset_id", get(get_keyset_pubkeys))
+        .route("/keys/{keyset_id}", get(get_keyset_pubkeys))
         .route("/swap", post(cache_post_swap))
         .route("/mint/quote/bolt11", post(post_mint_bolt11_quote))
         .route(
-            "/mint/quote/bolt11/:quote_id",
+            "/mint/quote/bolt11/{quote_id}",
             get(get_check_mint_bolt11_quote),
         )
         .route("/mint/bolt11", post(cache_post_mint_bolt11))
         .route("/melt/quote/bolt11", post(post_melt_bolt11_quote))
         .route("/ws", get(ws_handler))
         .route(
-            "/melt/quote/bolt11/:quote_id",
+            "/melt/quote/bolt11/{quote_id}",
             get(get_check_melt_bolt11_quote),
         )
         .route("/melt/bolt11", post(cache_post_melt_bolt11))
@@ -167,7 +211,17 @@ pub async fn create_mint_router_with_custom_cache(
         .route("/info", get(get_mint_info))
         .route("/restore", post(post_restore));
 
-    let mint_router = Router::new().nest("/v1", v1_router).with_state(state);
+    let mint_router = Router::new()
+        .nest("/v1", v1_router)
+        .layer(from_fn(cors_middleware));
+
+    #[cfg(feature = "auth")]
+    let mint_router = {
+        let auth_router = create_auth_router(state.clone());
+        mint_router.nest("/v1", auth_router)
+    };
+
+    let mint_router = mint_router.with_state(state);
 
     Ok(mint_router)
 }

+ 172 - 27
crates/cdk-axum/src/router_handlers.rs

@@ -4,6 +4,8 @@ use axum::extract::{Json, Path, State};
 use axum::http::StatusCode;
 use axum::response::{IntoResponse, Response};
 use cdk::error::ErrorResponse;
+#[cfg(feature = "auth")]
+use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
 use cdk::nuts::{
     CheckStateRequest, CheckStateResponse, Id, KeysResponse, KeysetResponse, MeltBolt11Request,
     MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request, MintBolt11Response,
@@ -15,6 +17,8 @@ use paste::paste;
 use tracing::instrument;
 use uuid::Uuid;
 
+#[cfg(feature = "auth")]
+use crate::auth::AuthHeader;
 use crate::ws::main_websocket;
 use crate::MintState;
 
@@ -24,25 +28,29 @@ macro_rules! post_cache_wrapper {
             /// Cache wrapper function for $handler:
             /// Wrap $handler into a function that caches responses using the request as key
             pub async fn [<cache_ $handler>](
+                #[cfg(feature = "auth")] auth: AuthHeader,
                 state: State<MintState>,
                 payload: Json<$request_type>
             ) -> Result<Json<$response_type>, Response> {
                 use std::ops::Deref;
-
                 let json_extracted_payload = payload.deref();
                 let State(mint_state) = state.clone();
                 let cache_key = match mint_state.cache.calculate_key(&json_extracted_payload) {
                     Some(key) => key,
                     None => {
                         // Could not calculate key, just return the handler result
-                        return $handler(state, payload).await;
+                        #[cfg(feature = "auth")]
+                        return $handler(auth, state, payload).await;
+                        #[cfg(not(feature = "auth"))]
+                        return $handler( state, payload).await;
                     }
                 };
-
                 if let Some(cached_response) = mint_state.cache.get::<$response_type>(&cache_key).await {
                     return Ok(Json(cached_response));
                 }
-
+                #[cfg(feature = "auth")]
+                let response = $handler(auth, state, payload).await?;
+                #[cfg(not(feature = "auth"))]
                 let response = $handler(state, payload).await?;
                 mint_state.cache.set(cache_key, &response.deref()).await;
                 Ok(response)
@@ -74,7 +82,10 @@ post_cache_wrapper!(
 /// Get the public keys of the newest mint keyset
 ///
 /// This endpoint returns a dictionary of all supported token values of the mint and their associated public key.
-pub async fn get_keys(State(state): State<MintState>) -> Result<Json<KeysResponse>, Response> {
+#[instrument(skip_all)]
+pub(crate) async fn get_keys(
+    State(state): State<MintState>,
+) -> Result<Json<KeysResponse>, Response> {
     let pubkeys = state.mint.pubkeys().await.map_err(|err| {
         tracing::error!("Could not get keys: {}", err);
         into_response(err)
@@ -98,7 +109,8 @@ pub async fn get_keys(State(state): State<MintState>) -> Result<Json<KeysRespons
 /// Get the public keys of a specific keyset
 ///
 /// Get the public keys of the mint from a specific keyset ID.
-pub async fn get_keyset_pubkeys(
+#[instrument(skip_all, fields(keyset_id = ?keyset_id))]
+pub(crate) async fn get_keyset_pubkeys(
     State(state): State<MintState>,
     Path(keyset_id): Path<Id>,
 ) -> Result<Json<KeysResponse>, Response> {
@@ -122,7 +134,10 @@ pub async fn get_keyset_pubkeys(
 /// Get all active keyset IDs of the mint
 ///
 /// This endpoint returns a list of keysets that the mint currently supports and will accept tokens from.
-pub async fn get_keysets(State(state): State<MintState>) -> Result<Json<KeysetResponse>, Response> {
+#[instrument(skip_all)]
+pub(crate) async fn get_keysets(
+    State(state): State<MintState>,
+) -> Result<Json<KeysetResponse>, Response> {
     let keysets = state.mint.keysets().await.map_err(|err| {
         tracing::error!("Could not get keysets: {}", err);
         into_response(err)
@@ -137,17 +152,29 @@ pub async fn get_keysets(State(state): State<MintState>) -> Result<Json<KeysetRe
     path = "/mint/quote/bolt11",
     request_body(content = MintQuoteBolt11Request, description = "Request params", content_type = "application/json"),
     responses(
-        (status = 200, description = "Successful response", body = MintQuoteBolt11Response, content_type = "application/json"),
+        (status = 200, description = "Successful response", body = MintQuoteBolt11Response<String>, content_type = "application/json"),
         (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
     )
 ))]
 /// Request a quote for minting of new tokens
 ///
 /// Request minting of new tokens. The mint responds with a Lightning invoice. This endpoint can be used for a Lightning invoice UX flow.
-pub async fn post_mint_bolt11_quote(
+#[instrument(skip_all, fields(amount = ?payload.amount))]
+pub(crate) async fn post_mint_bolt11_quote(
+    #[cfg(feature = "auth")] auth: AuthHeader,
     State(state): State<MintState>,
     Json(payload): Json<MintQuoteBolt11Request>,
 ) -> Result<Json<MintQuoteBolt11Response<Uuid>>, Response> {
+    #[cfg(feature = "auth")]
+    state
+        .mint
+        .verify_auth(
+            auth.into(),
+            &ProtectedEndpoint::new(Method::Post, RoutePath::MintQuoteBolt11),
+        )
+        .await
+        .map_err(into_response)?;
+
     let quote = state
         .mint
         .get_mint_bolt11_quote(payload)
@@ -165,17 +192,31 @@ pub async fn post_mint_bolt11_quote(
         ("quote_id" = String, description = "The quote ID"),
     ),
     responses(
-        (status = 200, description = "Successful response", body = MintQuoteBolt11Response, content_type = "application/json"),
+        (status = 200, description = "Successful response", body = MintQuoteBolt11Response<String>, content_type = "application/json"),
         (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
     )
 ))]
 /// Get mint quote by ID
 ///
 /// Get mint quote state.
-pub async fn get_check_mint_bolt11_quote(
+#[instrument(skip_all, fields(quote_id = ?quote_id))]
+pub(crate) async fn get_check_mint_bolt11_quote(
+    #[cfg(feature = "auth")] auth: AuthHeader,
     State(state): State<MintState>,
     Path(quote_id): Path<Uuid>,
 ) -> Result<Json<MintQuoteBolt11Response<Uuid>>, Response> {
+    #[cfg(feature = "auth")]
+    {
+        state
+            .mint
+            .verify_auth(
+                auth.into(),
+                &ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
+            )
+            .await
+            .map_err(into_response)?;
+    }
+
     let quote = state
         .mint
         .check_mint_quote(&quote_id)
@@ -188,7 +229,11 @@ pub async fn get_check_mint_bolt11_quote(
     Ok(Json(quote))
 }
 
-pub async fn ws_handler(State(state): State<MintState>, ws: WebSocketUpgrade) -> impl IntoResponse {
+#[instrument(skip_all)]
+pub(crate) async fn ws_handler(
+    State(state): State<MintState>,
+    ws: WebSocketUpgrade,
+) -> impl IntoResponse {
     ws.on_upgrade(|ws| main_websocket(ws, state))
 }
 
@@ -201,16 +246,30 @@ pub async fn ws_handler(State(state): State<MintState>, ws: WebSocketUpgrade) ->
     post,
     context_path = "/v1",
     path = "/mint/bolt11",
-    request_body(content = MintBolt11Request, description = "Request params", content_type = "application/json"),
+    request_body(content = MintBolt11Request<String>, description = "Request params", content_type = "application/json"),
     responses(
         (status = 200, description = "Successful response", body = MintBolt11Response, content_type = "application/json"),
         (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
     )
 ))]
-pub async fn post_mint_bolt11(
+#[instrument(skip_all, fields(quote_id = ?payload.quote))]
+pub(crate) async fn post_mint_bolt11(
+    #[cfg(feature = "auth")] auth: AuthHeader,
     State(state): State<MintState>,
     Json(payload): Json<MintBolt11Request<Uuid>>,
 ) -> Result<Json<MintBolt11Response>, Response> {
+    #[cfg(feature = "auth")]
+    {
+        state
+            .mint
+            .verify_auth(
+                auth.into(),
+                &ProtectedEndpoint::new(Method::Post, RoutePath::MintBolt11),
+            )
+            .await
+            .map_err(into_response)?;
+    }
+
     let res = state
         .mint
         .process_mint_request(payload)
@@ -229,16 +288,29 @@ pub async fn post_mint_bolt11(
     path = "/melt/quote/bolt11",
     request_body(content = MeltQuoteBolt11Request, description = "Quote params", content_type = "application/json"),
     responses(
-        (status = 200, description = "Successful response", body = MeltQuoteBolt11Response, content_type = "application/json"),
+        (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
         (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
     )
 ))]
 #[instrument(skip_all)]
 /// Request a quote for melting tokens
-pub async fn post_melt_bolt11_quote(
+pub(crate) async fn post_melt_bolt11_quote(
+    #[cfg(feature = "auth")] auth: AuthHeader,
     State(state): State<MintState>,
     Json(payload): Json<MeltQuoteBolt11Request>,
 ) -> Result<Json<MeltQuoteBolt11Response<Uuid>>, Response> {
+    #[cfg(feature = "auth")]
+    {
+        state
+            .mint
+            .verify_auth(
+                auth.into(),
+                &ProtectedEndpoint::new(Method::Post, RoutePath::MeltQuoteBolt11),
+            )
+            .await
+            .map_err(into_response)?;
+    }
+
     let quote = state
         .mint
         .get_melt_bolt11_quote(&payload)
@@ -256,18 +328,31 @@ pub async fn post_melt_bolt11_quote(
         ("quote_id" = String, description = "The quote ID"),
     ),
     responses(
-        (status = 200, description = "Successful response", body = MeltQuoteBolt11Response, content_type = "application/json"),
+        (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
         (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
     )
 ))]
 /// Get melt quote by ID
 ///
 /// Get melt quote state.
-#[instrument(skip_all)]
-pub async fn get_check_melt_bolt11_quote(
+#[instrument(skip_all, fields(quote_id = ?quote_id))]
+pub(crate) async fn get_check_melt_bolt11_quote(
+    #[cfg(feature = "auth")] auth: AuthHeader,
     State(state): State<MintState>,
     Path(quote_id): Path<Uuid>,
 ) -> Result<Json<MeltQuoteBolt11Response<Uuid>>, Response> {
+    #[cfg(feature = "auth")]
+    {
+        state
+            .mint
+            .verify_auth(
+                auth.into(),
+                &ProtectedEndpoint::new(Method::Get, RoutePath::MeltQuoteBolt11),
+            )
+            .await
+            .map_err(into_response)?;
+    }
+
     let quote = state
         .mint
         .check_melt_quote(&quote_id)
@@ -284,9 +369,9 @@ pub async fn get_check_melt_bolt11_quote(
     post,
     context_path = "/v1",
     path = "/melt/bolt11",
-    request_body(content = MeltBolt11Request, description = "Melt params", content_type = "application/json"),
+    request_body(content = MeltBolt11Request<String>, description = "Melt params", content_type = "application/json"),
     responses(
-        (status = 200, description = "Successful response", body = MeltQuoteBolt11Response, content_type = "application/json"),
+        (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
         (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
     )
 ))]
@@ -294,10 +379,23 @@ pub async fn get_check_melt_bolt11_quote(
 ///
 /// Requests tokens to be destroyed and sent out via Lightning.
 #[instrument(skip_all)]
-pub async fn post_melt_bolt11(
+pub(crate) async fn post_melt_bolt11(
+    #[cfg(feature = "auth")] auth: AuthHeader,
     State(state): State<MintState>,
     Json(payload): Json<MeltBolt11Request<Uuid>>,
 ) -> Result<Json<MeltQuoteBolt11Response<Uuid>>, Response> {
+    #[cfg(feature = "auth")]
+    {
+        state
+            .mint
+            .verify_auth(
+                auth.into(),
+                &ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt11),
+            )
+            .await
+            .map_err(into_response)?;
+    }
+
     let res = state
         .mint
         .melt_bolt11(&payload)
@@ -320,10 +418,24 @@ pub async fn post_melt_bolt11(
 /// Check whether a proof is spent already or is pending in a transaction
 ///
 /// Check whether a secret has been spent already or not.
-pub async fn post_check(
+#[instrument(skip_all, fields(y_count = ?payload.ys.len()))]
+pub(crate) async fn post_check(
+    #[cfg(feature = "auth")] auth: AuthHeader,
     State(state): State<MintState>,
     Json(payload): Json<CheckStateRequest>,
 ) -> Result<Json<CheckStateResponse>, Response> {
+    #[cfg(feature = "auth")]
+    {
+        state
+            .mint
+            .verify_auth(
+                auth.into(),
+                &ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate),
+            )
+            .await
+            .map_err(into_response)?;
+    }
+
     let state = state.mint.check_state(&payload).await.map_err(|err| {
         tracing::error!("Could not check state of proofs");
         into_response(err)
@@ -341,7 +453,10 @@ pub async fn post_check(
     )
 ))]
 /// Mint information, operator contact information, and other info
-pub async fn get_mint_info(State(state): State<MintState>) -> Result<Json<MintInfo>, Response> {
+#[instrument(skip_all)]
+pub(crate) async fn get_mint_info(
+    State(state): State<MintState>,
+) -> Result<Json<MintInfo>, Response> {
     Ok(Json(
         state
             .mint
@@ -371,10 +486,24 @@ pub async fn get_mint_info(State(state): State<MintState>) -> Result<Json<MintIn
 /// Requests a set of Proofs to be swapped for another set of BlindSignatures.
 ///
 /// This endpoint can be used by Alice to swap a set of proofs before making a payment to Carol. It can then used by Carol to redeem the tokens for new proofs.
-pub async fn post_swap(
+#[instrument(skip_all, fields(inputs_count = ?payload.inputs().len()))]
+pub(crate) async fn post_swap(
+    #[cfg(feature = "auth")] auth: AuthHeader,
     State(state): State<MintState>,
     Json(payload): Json<SwapRequest>,
 ) -> Result<Json<SwapResponse>, Response> {
+    #[cfg(feature = "auth")]
+    {
+        state
+            .mint
+            .verify_auth(
+                auth.into(),
+                &ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
+            )
+            .await
+            .map_err(into_response)?;
+    }
+
     let swap_response = state
         .mint
         .process_swap_request(payload)
@@ -383,6 +512,7 @@ pub async fn post_swap(
             tracing::error!("Could not process swap request: {}", err);
             into_response(err)
         })?;
+
     Ok(Json(swap_response))
 }
 
@@ -397,10 +527,24 @@ pub async fn post_swap(
     )
 ))]
 /// Restores blind signature for a set of outputs.
-pub async fn post_restore(
+#[instrument(skip_all, fields(outputs_count = ?payload.outputs.len()))]
+pub(crate) async fn post_restore(
+    #[cfg(feature = "auth")] auth: AuthHeader,
     State(state): State<MintState>,
     Json(payload): Json<RestoreRequest>,
 ) -> Result<Json<RestoreResponse>, Response> {
+    #[cfg(feature = "auth")]
+    {
+        state
+            .mint
+            .verify_auth(
+                auth.into(),
+                &ProtectedEndpoint::new(Method::Post, RoutePath::Restore),
+            )
+            .await
+            .map_err(into_response)?;
+    }
+
     let restore_response = state.mint.restore(payload).await.map_err(|err| {
         tracing::error!("Could not process restore: {}", err);
         into_response(err)
@@ -409,7 +553,8 @@ pub async fn post_restore(
     Ok(Json(restore_response))
 }
 
-pub fn into_response<T>(error: T) -> Response
+#[instrument(skip_all)]
+pub(crate) fn into_response<T>(error: T) -> Response
 where
     T: Into<ErrorResponse>,
 {

+ 2 - 3
crates/cdk-axum/src/ws/mod.rs

@@ -46,7 +46,6 @@ pub struct WsContext {
 ///
 /// For simplicity sake this function will spawn tasks for each subscription and
 /// keep them in a hashmap, and will have a single subscriber for all of them.
-#[allow(clippy::incompatible_msrv)]
 pub async fn main_websocket(mut socket: WebSocket, state: MintState) {
     let (publisher, mut subscriber) = mpsc::channel(100);
     let mut context = WsContext {
@@ -75,7 +74,7 @@ pub async fn main_websocket(mut socket: WebSocket, state: MintState) {
                     }
                 };
 
-          if let Err(err)= socket.send(Message::Text(message)).await {
+          if let Err(err)= socket.send(Message::Text(message.into())).await {
                 tracing::error!("Could not send websocket message: {}", err);
                 break;
           }
@@ -92,7 +91,7 @@ pub async fn main_websocket(mut socket: WebSocket, state: MintState) {
                 match process(&mut context, request).await {
                     Ok(result) => {
                         if let Err(err) = socket
-                            .send(Message::Text(result.to_string()))
+                            .send(Message::Text(result.to_string().into()))
                             .await
                         {
                             tracing::error!("Could not send request: {}", err);

+ 27 - 26
crates/cdk-cli/Cargo.toml

@@ -1,38 +1,39 @@
 [package]
 name = "cdk-cli"
-version = "0.7.1"
-edition = "2021"
+version.workspace = true
 authors = ["CDK Developers"]
 description = "Cashu cli wallet built on CDK"
-license = "MIT"
-homepage = "https://github.com/cashubtc/cdk"
-repository = "https://github.com/cashubtc/cdk.git"
-rust-version = "1.63.0" # MSRV
+license.workspace = true
+homepage.workspace = true
+repository.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+readme = "README.md"
 
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+[features]
+sqlcipher = ["cdk-sqlite/sqlcipher"]
+# MSRV is not tracked with redb enabled
+redb = ["dep:cdk-redb"]
 
 [dependencies]
-anyhow = "1"
-bip39 = { version = "2.0", features = ["rand"] }
-cdk = { path = "../cdk", version = "0.7.1", default-features = false, features = ["wallet"]}
-cdk-redb = { path = "../cdk-redb", version = "0.7.1", default-features = false, features = ["wallet"] }
-cdk-sqlite = { path = "../cdk-sqlite", version = "0.7.1", default-features = false, features = ["wallet"] }
-clap = { version = "~4.0.32", features = ["derive"] }
-serde = { version = "1", default-features = false, features = ["derive"] }
-serde_json = "1"
-tokio = { version = "1", default-features = false }
-tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
-tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
-home = "0.5.5"
+anyhow.workspace = true
+bip39.workspace = true
+cdk = { workspace = true, default-features = false, features = ["wallet", "auth"]}
+cdk-redb = { workspace = true, features = ["wallet"], optional = true }
+cdk-sqlite = { workspace = true, features = ["wallet"] }
+clap.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+tokio.workspace = true
+tracing.workspace = true
+tracing-subscriber.workspace = true
+home.workspace = true
 nostr-sdk = { version = "0.35.0", default-features = false, features = [
     "nip04",
     "nip44",
     "nip59"
 ]}
-reqwest = { version = "0.12", default-features = false, features = [
-    "json",
-    "rustls-tls",
-    "rustls-tls-native-roots",
-    "socks",
-]}
-url = "2.3"
+reqwest.workspace = true
+url.workspace = true
+serde_with.workspace = true
+

+ 67 - 37
crates/cdk-cli/src/main.rs

@@ -8,8 +8,8 @@ use bip39::rand::{thread_rng, Rng};
 use bip39::Mnemonic;
 use cdk::cdk_database;
 use cdk::cdk_database::WalletDatabase;
-use cdk::wallet::client::HttpClient;
-use cdk::wallet::{MultiMintWallet, Wallet};
+use cdk::wallet::{HttpClient, MultiMintWallet, Wallet, WalletBuilder};
+#[cfg(feature = "redb")]
 use cdk_redb::WalletRedbDatabase;
 use cdk_sqlite::WalletSqliteDatabase;
 use clap::{Parser, Subcommand};
@@ -17,7 +17,9 @@ use tracing::Level;
 use tracing_subscriber::EnvFilter;
 use url::Url;
 
+mod nostr_storage;
 mod sub_commands;
+mod token_storage;
 
 const DEFAULT_WORK_DIR: &str = ".cdk-cli";
 
@@ -31,6 +33,10 @@ struct Cli {
     /// Database engine to use (sqlite/redb)
     #[arg(short, long, default_value = "sqlite")]
     engine: String,
+    /// Database password for sqlcipher
+    #[cfg(feature = "sqlcipher")]
+    #[arg(long)]
+    password: Option<String>,
     /// Path to working dir
     #[arg(short, long)]
     work_dir: Option<PathBuf>,
@@ -78,6 +84,12 @@ enum Commands {
     PayRequest(sub_commands::pay_request::PayRequestSubCommand),
     /// Create Payment request
     CreateRequest(sub_commands::create_request::CreateRequestSubCommand),
+    /// Mint blind auth proofs
+    MintBlindAuth(sub_commands::mint_blind_auth::MintBlindAuthSubCommand),
+    /// Cat login with username/password
+    CatLogin(sub_commands::cat_login::CatLoginSubCommand),
+    /// Cat login with device code flow
+    CatDeviceLogin(sub_commands::cat_device_login::CatDeviceLoginSubCommand),
 }
 
 #[tokio::main]
@@ -106,16 +118,28 @@ async fn main() -> Result<()> {
         match args.engine.as_str() {
             "sqlite" => {
                 let sql_path = work_dir.join("cdk-cli.sqlite");
+                #[cfg(not(feature = "sqlcipher"))]
                 let sql = WalletSqliteDatabase::new(&sql_path).await?;
-
-                sql.migrate().await;
+                #[cfg(feature = "sqlcipher")]
+                let sql = {
+                    match args.password {
+                        Some(pass) => WalletSqliteDatabase::new(&sql_path, pass).await?,
+                        None => bail!("Missing database password"),
+                    }
+                };
 
                 Arc::new(sql)
             }
             "redb" => {
-                let redb_path = work_dir.join("cdk-cli.redb");
-
-                Arc::new(WalletRedbDatabase::new(&redb_path)?)
+                #[cfg(feature = "redb")]
+                {
+                    let redb_path = work_dir.join("cdk-cli.redb");
+                    Arc::new(WalletRedbDatabase::new(&redb_path)?)
+                }
+                #[cfg(not(feature = "redb"))]
+                {
+                    bail!("redb feature not enabled");
+                }
             }
             _ => bail!("Unknown DB engine"),
         };
@@ -139,28 +163,32 @@ async fn main() -> Result<()> {
             mnemonic
         }
     };
+    let seed = mnemonic.to_seed_normalized("");
 
     let mut wallets: Vec<Wallet> = Vec::new();
 
     let mints = localstore.get_mints().await?;
 
     for (mint_url, _) in mints {
-        let mut wallet = Wallet::new(
-            &mint_url.to_string(),
-            cdk::nuts::CurrencyUnit::Sat,
-            localstore.clone(),
-            &mnemonic.to_seed_normalized(""),
-            None,
-        )?;
+        let mut builder = WalletBuilder::new()
+            .mint_url(mint_url.clone())
+            .unit(cdk::nuts::CurrencyUnit::Sat)
+            .localstore(localstore.clone())
+            .seed(&mnemonic.to_seed_normalized(""));
+
         if let Some(proxy_url) = args.proxy.as_ref() {
             let http_client = HttpClient::with_proxy(mint_url, proxy_url.clone(), None, true)?;
-            wallet.set_client(http_client);
+            builder = builder.client(http_client);
         }
 
+        let wallet = builder.build()?;
+
+        wallet.get_mint_info().await?;
+
         wallets.push(wallet);
     }
 
-    let multi_mint_wallet = MultiMintWallet::new(wallets);
+    let multi_mint_wallet = MultiMintWallet::new(localstore, Arc::new(seed), wallets);
 
     match &args.command {
         Commands::DecodeToken(sub_command_args) => {
@@ -171,13 +199,7 @@ async fn main() -> Result<()> {
             sub_commands::melt::pay(&multi_mint_wallet, sub_command_args).await
         }
         Commands::Receive(sub_command_args) => {
-            sub_commands::receive::receive(
-                &multi_mint_wallet,
-                localstore,
-                &mnemonic.to_seed_normalized(""),
-                sub_command_args,
-            )
-            .await
+            sub_commands::receive::receive(&multi_mint_wallet, sub_command_args, &work_dir).await
         }
         Commands::Send(sub_command_args) => {
             sub_commands::send::send(&multi_mint_wallet, sub_command_args).await
@@ -189,13 +211,7 @@ async fn main() -> Result<()> {
             sub_commands::mint_info::mint_info(args.proxy, sub_command_args).await
         }
         Commands::Mint(sub_command_args) => {
-            sub_commands::mint::mint(
-                &multi_mint_wallet,
-                &mnemonic.to_seed_normalized(""),
-                localstore,
-                sub_command_args,
-            )
-            .await
+            sub_commands::mint::mint(&multi_mint_wallet, sub_command_args).await
         }
         Commands::MintPending => {
             sub_commands::pending_mints::mint_pending(&multi_mint_wallet).await
@@ -204,13 +220,7 @@ async fn main() -> Result<()> {
             sub_commands::burn::burn(&multi_mint_wallet, sub_command_args).await
         }
         Commands::Restore(sub_command_args) => {
-            sub_commands::restore::restore(
-                &multi_mint_wallet,
-                &mnemonic.to_seed_normalized(""),
-                localstore,
-                sub_command_args,
-            )
-            .await
+            sub_commands::restore::restore(&multi_mint_wallet, sub_command_args).await
         }
         Commands::UpdateMintUrl(sub_command_args) => {
             sub_commands::update_mint_url::update_mint_url(&multi_mint_wallet, sub_command_args)
@@ -228,5 +238,25 @@ async fn main() -> Result<()> {
         Commands::CreateRequest(sub_command_args) => {
             sub_commands::create_request::create_request(&multi_mint_wallet, sub_command_args).await
         }
+        Commands::MintBlindAuth(sub_command_args) => {
+            sub_commands::mint_blind_auth::mint_blind_auth(
+                &multi_mint_wallet,
+                sub_command_args,
+                &work_dir,
+            )
+            .await
+        }
+        Commands::CatLogin(sub_command_args) => {
+            sub_commands::cat_login::cat_login(&multi_mint_wallet, sub_command_args, &work_dir)
+                .await
+        }
+        Commands::CatDeviceLogin(sub_command_args) => {
+            sub_commands::cat_device_login::cat_device_login(
+                &multi_mint_wallet,
+                sub_command_args,
+                &work_dir,
+            )
+            .await
+        }
     }
 }

+ 37 - 0
crates/cdk-cli/src/nostr_storage.rs

@@ -0,0 +1,37 @@
+use std::fs;
+use std::path::Path;
+
+use anyhow::Result;
+use cdk::nuts::PublicKey;
+use cdk::util::hex;
+
+/// Stores the last checked time for a nostr key in a file
+pub async fn store_nostr_last_checked(
+    work_dir: &Path,
+    verifying_key: &PublicKey,
+    last_checked: u32,
+) -> Result<()> {
+    let key_hex = hex::encode(verifying_key.to_bytes());
+    let file_path = work_dir.join(format!("nostr_last_checked_{}", key_hex));
+
+    fs::write(file_path, last_checked.to_string())?;
+
+    Ok(())
+}
+
+/// Gets the last checked time for a nostr key from a file
+pub async fn get_nostr_last_checked(
+    work_dir: &Path,
+    verifying_key: &PublicKey,
+) -> Result<Option<u32>> {
+    let key_hex = hex::encode(verifying_key.to_bytes());
+    let file_path = work_dir.join(format!("nostr_last_checked_{}", key_hex));
+
+    match fs::read_to_string(file_path) {
+        Ok(content) => {
+            let timestamp = content.trim().parse::<u32>()?;
+            Ok(Some(timestamp))
+        }
+        Err(_) => Ok(None),
+    }
+}

+ 194 - 0
crates/cdk-cli/src/sub_commands/cat_device_login.rs

@@ -0,0 +1,194 @@
+use std::path::Path;
+use std::str::FromStr;
+use std::time::Duration;
+
+use anyhow::{anyhow, Result};
+use cdk::mint_url::MintUrl;
+use cdk::nuts::{CurrencyUnit, MintInfo};
+use cdk::wallet::types::WalletKey;
+use cdk::wallet::MultiMintWallet;
+use cdk::OidcClient;
+use clap::Args;
+use serde::{Deserialize, Serialize};
+use tokio::time::sleep;
+
+use crate::token_storage;
+
+#[derive(Args, Serialize, Deserialize)]
+pub struct CatDeviceLoginSubCommand {
+    /// Mint url
+    mint_url: MintUrl,
+    /// Currency unit e.g. sat
+    #[arg(default_value = "sat")]
+    #[arg(short, long)]
+    unit: String,
+    /// Client ID for OIDC authentication
+    #[arg(default_value = "cashu-client")]
+    #[arg(long)]
+    client_id: String,
+}
+
+pub async fn cat_device_login(
+    multi_mint_wallet: &MultiMintWallet,
+    sub_command_args: &CatDeviceLoginSubCommand,
+    work_dir: &Path,
+) -> Result<()> {
+    let mint_url = sub_command_args.mint_url.clone();
+    let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
+
+    let wallet = match multi_mint_wallet
+        .get_wallet(&WalletKey::new(mint_url.clone(), unit.clone()))
+        .await
+    {
+        Some(wallet) => wallet.clone(),
+        None => {
+            multi_mint_wallet
+                .create_and_add_wallet(&mint_url.to_string(), unit, None)
+                .await?
+        }
+    };
+
+    let mint_info = wallet
+        .get_mint_info()
+        .await?
+        .ok_or(anyhow!("Mint info not found"))?;
+
+    let (access_token, refresh_token) =
+        get_device_code_token(&mint_info, &sub_command_args.client_id).await;
+
+    // Save tokens to file in work directory
+    if let Err(e) =
+        token_storage::save_tokens(work_dir, &mint_url, &access_token, &refresh_token).await
+    {
+        println!("Warning: Failed to save tokens to file: {}", e);
+    } else {
+        println!("Tokens saved to work directory");
+    }
+
+    // Print a cute ASCII cat
+    println!("\nAuthentication successful! 🎉\n");
+    println!("\nYour tokens:");
+    println!("access_token: {}", access_token);
+    println!("refresh_token: {}", refresh_token);
+
+    Ok(())
+}
+
+async fn get_device_code_token(mint_info: &MintInfo, client_id: &str) -> (String, String) {
+    let openid_discovery = mint_info
+        .nuts
+        .nut21
+        .clone()
+        .expect("Nut21 defined")
+        .openid_discovery;
+
+    let oidc_client = OidcClient::new(openid_discovery);
+
+    // Get the OIDC configuration
+    let oidc_config = oidc_client
+        .get_oidc_config()
+        .await
+        .expect("Failed to get OIDC config");
+
+    // Get the device authorization endpoint
+    let device_auth_url = oidc_config.device_authorization_endpoint;
+
+    // Make the device code request
+    let client = reqwest::Client::new();
+    let device_code_response = client
+        .post(device_auth_url)
+        .form(&[("client_id", client_id)])
+        .send()
+        .await
+        .expect("Failed to send device code request");
+
+    let device_code_data: serde_json::Value = device_code_response
+        .json()
+        .await
+        .expect("Failed to parse device code response");
+
+    let device_code = device_code_data["device_code"]
+        .as_str()
+        .expect("No device code in response");
+
+    let user_code = device_code_data["user_code"]
+        .as_str()
+        .expect("No user code in response");
+
+    let verification_uri = device_code_data["verification_uri"]
+        .as_str()
+        .expect("No verification URI in response");
+
+    let verification_uri_complete = device_code_data["verification_uri_complete"]
+        .as_str()
+        .unwrap_or(verification_uri);
+
+    let interval = device_code_data["interval"].as_u64().unwrap_or(5);
+
+    println!("\nTo login, visit: {}", verification_uri);
+    println!("And enter code: {}\n", user_code);
+
+    if verification_uri_complete != verification_uri {
+        println!(
+            "Or visit this URL directly: {}\n",
+            verification_uri_complete
+        );
+    }
+
+    // Poll for the token
+    let token_url = oidc_config.token_endpoint;
+
+    loop {
+        sleep(Duration::from_secs(interval)).await;
+
+        let token_response = client
+            .post(&token_url)
+            .form(&[
+                ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
+                ("device_code", device_code),
+                ("client_id", client_id),
+            ])
+            .send()
+            .await
+            .expect("Failed to send token request");
+
+        if token_response.status().is_success() {
+            let token_data: serde_json::Value = token_response
+                .json()
+                .await
+                .expect("Failed to parse token response");
+
+            let access_token = token_data["access_token"]
+                .as_str()
+                .expect("No access token in response")
+                .to_string();
+
+            let refresh_token = token_data["refresh_token"]
+                .as_str()
+                .expect("No refresh token in response")
+                .to_string();
+
+            return (access_token, refresh_token);
+        } else {
+            let error_data: serde_json::Value = token_response
+                .json()
+                .await
+                .expect("Failed to parse error response");
+
+            let error = error_data["error"].as_str().unwrap_or("unknown_error");
+
+            // If the user hasn't completed the flow yet, continue polling
+            if error == "authorization_pending" || error == "slow_down" {
+                if error == "slow_down" {
+                    // If we're polling too fast, slow down
+                    sleep(Duration::from_secs(interval + 5)).await;
+                }
+                println!("Waiting for user to complete authentication...");
+                continue;
+            } else {
+                // For other errors, exit with an error message
+                panic!("Authentication failed: {}", error);
+            }
+        }
+    }
+}

+ 138 - 0
crates/cdk-cli/src/sub_commands/cat_login.rs

@@ -0,0 +1,138 @@
+use std::path::Path;
+use std::str::FromStr;
+
+use anyhow::{anyhow, Result};
+use cdk::mint_url::MintUrl;
+use cdk::nuts::{CurrencyUnit, MintInfo};
+use cdk::wallet::types::WalletKey;
+use cdk::wallet::MultiMintWallet;
+use cdk::OidcClient;
+use clap::Args;
+use serde::{Deserialize, Serialize};
+
+use crate::token_storage;
+
+#[derive(Args, Serialize, Deserialize)]
+pub struct CatLoginSubCommand {
+    /// Mint url
+    mint_url: MintUrl,
+    /// Username
+    username: String,
+    /// Password
+    password: String,
+    /// Currency unit e.g. sat
+    #[arg(default_value = "sat")]
+    #[arg(short, long)]
+    unit: String,
+    /// Client ID for OIDC authentication
+    #[arg(default_value = "cashu-client")]
+    #[arg(long)]
+    client_id: String,
+}
+
+pub async fn cat_login(
+    multi_mint_wallet: &MultiMintWallet,
+    sub_command_args: &CatLoginSubCommand,
+    work_dir: &Path,
+) -> Result<()> {
+    let mint_url = sub_command_args.mint_url.clone();
+    let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
+
+    let wallet = match multi_mint_wallet
+        .get_wallet(&WalletKey::new(mint_url.clone(), unit.clone()))
+        .await
+    {
+        Some(wallet) => wallet.clone(),
+        None => {
+            multi_mint_wallet
+                .create_and_add_wallet(&mint_url.to_string(), unit, None)
+                .await?
+        }
+    };
+
+    let mint_info = wallet
+        .get_mint_info()
+        .await?
+        .ok_or(anyhow!("Mint info not found"))?;
+
+    let (access_token, refresh_token) = get_access_token(
+        &mint_info,
+        &sub_command_args.client_id,
+        &sub_command_args.username,
+        &sub_command_args.password,
+    )
+    .await;
+
+    // Save tokens to file in work directory
+    if let Err(e) =
+        token_storage::save_tokens(work_dir, &mint_url, &access_token, &refresh_token).await
+    {
+        println!("Warning: Failed to save tokens to file: {}", e);
+    } else {
+        println!("Tokens saved to work directory");
+    }
+
+    println!("\nAuthentication successful! 🎉\n");
+    println!("\nYour tokens:");
+    println!("access_token: {}", access_token);
+    println!("refresh_token: {}", refresh_token);
+
+    Ok(())
+}
+
+async fn get_access_token(
+    mint_info: &MintInfo,
+    client_id: &str,
+    user: &str,
+    password: &str,
+) -> (String, String) {
+    let openid_discovery = mint_info
+        .nuts
+        .nut21
+        .clone()
+        .expect("Nut21 defined")
+        .openid_discovery;
+
+    let oidc_client = OidcClient::new(openid_discovery);
+
+    // Get the token endpoint from the OIDC configuration
+    let token_url = oidc_client
+        .get_oidc_config()
+        .await
+        .expect("Failed to get OIDC config")
+        .token_endpoint;
+
+    // Create the request parameters
+    let params = [
+        ("grant_type", "password"),
+        ("client_id", client_id),
+        ("username", user),
+        ("password", password),
+    ];
+
+    // Make the token request directly
+    let client = reqwest::Client::new();
+    let response = client
+        .post(token_url)
+        .form(&params)
+        .send()
+        .await
+        .expect("Failed to send token request");
+
+    let token_response: serde_json::Value = response
+        .json()
+        .await
+        .expect("Failed to parse token response");
+
+    let access_token = token_response["access_token"]
+        .as_str()
+        .expect("No access token in response")
+        .to_string();
+
+    let refresh_token = token_response["refresh_token"]
+        .as_str()
+        .expect("No refresh token in response")
+        .to_string();
+
+    (access_token, refresh_token)
+}

+ 2 - 2
crates/cdk-cli/src/sub_commands/create_request.rs

@@ -1,7 +1,7 @@
 use anyhow::Result;
 use cdk::nuts::nut18::TransportType;
 use cdk::nuts::{CurrencyUnit, PaymentRequest, PaymentRequestPayload, Token, Transport};
-use cdk::wallet::MultiMintWallet;
+use cdk::wallet::{MultiMintWallet, ReceiveOptions};
 use clap::Args;
 use nostr_sdk::nips::nip19::Nip19Profile;
 use nostr_sdk::prelude::*;
@@ -83,7 +83,7 @@ pub async fn create_request(
                 let token = Token::new(payload.mint, payload.proofs, payload.memo, payload.unit);
 
                 let amount = multi_mint_wallet
-                    .receive(&token.to_string(), &[], &[])
+                    .receive(&token.to_string(), ReceiveOptions::default())
                     .await?;
 
                 println!("Received {}", amount);

+ 33 - 20
crates/cdk-cli/src/sub_commands/melt.rs

@@ -57,39 +57,52 @@ pub async fn pay(
     stdin.read_line(&mut user_input)?;
     let bolt11 = Bolt11Invoice::from_str(user_input.trim())?;
 
-    let mut options: Option<MeltOptions> = None;
+    let available_funds =
+        <cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * MSAT_IN_SAT;
+
+    // Determine payment amount and options
+    let options = if sub_command_args.mpp || bolt11.amount_milli_satoshis().is_none() {
+        // Get user input for amount
+        println!(
+            "Enter the amount you would like to pay in sats for a {} payment.",
+            if sub_command_args.mpp {
+                "MPP"
+            } else {
+                "amountless invoice"
+            }
+        );
 
-    if sub_command_args.mpp {
-        println!("Enter the amount you would like to pay in sats, for a mpp payment.");
         let mut user_input = String::new();
-        let stdin = io::stdin();
-        io::stdout().flush().unwrap();
-        stdin.read_line(&mut user_input)?;
+        io::stdout().flush()?;
+        io::stdin().read_line(&mut user_input)?;
 
-        let user_amount = user_input.trim_end().parse::<u64>()?;
+        let user_amount = user_input.trim_end().parse::<u64>()? * MSAT_IN_SAT;
 
-        if user_amount
-            .gt(&(<cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * MSAT_IN_SAT))
-        {
+        if user_amount > available_funds {
             bail!("Not enough funds");
         }
 
-        options = Some(MeltOptions::new_mpp(user_amount * MSAT_IN_SAT));
-    } else if bolt11
-        .amount_milli_satoshis()
-        .unwrap()
-        .gt(&(<cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * MSAT_IN_SAT))
-    {
-        bail!("Not enough funds");
-    }
+        Some(if sub_command_args.mpp {
+            MeltOptions::new_mpp(user_amount)
+        } else {
+            MeltOptions::new_amountless(user_amount)
+        })
+    } else {
+        // Check if invoice amount exceeds available funds
+        let invoice_amount = bolt11.amount_milli_satoshis().unwrap();
+        if invoice_amount > available_funds {
+            bail!("Not enough funds");
+        }
+        None
+    };
 
+    // Process payment
     let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
-
     println!("{:?}", quote);
 
     let melt = wallet.melt(&quote.id).await?;
-
     println!("Paid invoice: {}", melt.state);
+
     if let Some(preimage) = melt.preimage {
         println!("Payment preimage: {}", preimage);
     }

+ 5 - 9
crates/cdk-cli/src/sub_commands/mint.rs

@@ -1,14 +1,12 @@
 use std::str::FromStr;
-use std::sync::Arc;
 
 use anyhow::{anyhow, Result};
 use cdk::amount::SplitTarget;
-use cdk::cdk_database::{Error, WalletDatabase};
 use cdk::mint_url::MintUrl;
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload};
 use cdk::wallet::types::WalletKey;
-use cdk::wallet::{MultiMintWallet, Wallet, WalletSubscription};
+use cdk::wallet::{MultiMintWallet, WalletSubscription};
 use cdk::Amount;
 use clap::Args;
 use serde::{Deserialize, Serialize};
@@ -32,8 +30,6 @@ pub struct MintSubCommand {
 
 pub async fn mint(
     multi_mint_wallet: &MultiMintWallet,
-    seed: &[u8],
-    localstore: Arc<dyn WalletDatabase<Err = Error> + Sync + Send>,
     sub_command_args: &MintSubCommand,
 ) -> Result<()> {
     let mint_url = sub_command_args.mint_url.clone();
@@ -46,10 +42,10 @@ pub async fn mint(
     {
         Some(wallet) => wallet.clone(),
         None => {
-            let wallet = Wallet::new(&mint_url.to_string(), unit, localstore, seed, None)?;
-
-            multi_mint_wallet.add_wallet(wallet.clone()).await;
-            wallet
+            tracing::debug!("Wallet does not exist creating..");
+            multi_mint_wallet
+                .create_and_add_wallet(&mint_url.to_string(), unit, None)
+                .await?
         }
     };
 

+ 204 - 0
crates/cdk-cli/src/sub_commands/mint_blind_auth.rs

@@ -0,0 +1,204 @@
+use std::path::Path;
+use std::str::FromStr;
+
+use anyhow::{anyhow, Result};
+use cdk::mint_url::MintUrl;
+use cdk::nuts::{CurrencyUnit, MintInfo};
+use cdk::wallet::types::WalletKey;
+use cdk::wallet::MultiMintWallet;
+use cdk::{Amount, OidcClient};
+use clap::Args;
+use serde::{Deserialize, Serialize};
+
+use crate::token_storage;
+
+#[derive(Args, Serialize, Deserialize)]
+pub struct MintBlindAuthSubCommand {
+    /// Mint url
+    mint_url: MintUrl,
+    /// Amount
+    amount: Option<u64>,
+    /// Cat (access token)
+    #[arg(long)]
+    cat: Option<String>,
+    /// Currency unit e.g. sat
+    #[arg(default_value = "sat")]
+    #[arg(short, long)]
+    unit: String,
+}
+
+pub async fn mint_blind_auth(
+    multi_mint_wallet: &MultiMintWallet,
+    sub_command_args: &MintBlindAuthSubCommand,
+    work_dir: &Path,
+) -> Result<()> {
+    let mint_url = sub_command_args.mint_url.clone();
+    let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
+
+    let wallet = match multi_mint_wallet
+        .get_wallet(&WalletKey::new(mint_url.clone(), unit.clone()))
+        .await
+    {
+        Some(wallet) => wallet.clone(),
+        None => {
+            multi_mint_wallet
+                .create_and_add_wallet(&mint_url.to_string(), unit, None)
+                .await?
+        }
+    };
+
+    wallet.get_mint_info().await?;
+
+    // Try to get the token from the provided argument or from the stored file
+    let cat = match &sub_command_args.cat {
+        Some(token) => token.clone(),
+        None => {
+            // Try to load from file
+            match token_storage::get_token_for_mint(work_dir, &mint_url).await {
+                Ok(Some(token_data)) => {
+                    println!("Using access token from cashu_tokens.json");
+                    token_data.access_token
+                }
+                Ok(None) => {
+                    return Err(anyhow::anyhow!(
+                        "No access token provided and no token found in cashu_tokens.json for this mint"
+                    ));
+                }
+                Err(e) => {
+                    return Err(anyhow::anyhow!(
+                        "Failed to read token from cashu_tokens.json: {}",
+                        e
+                    ));
+                }
+            }
+        }
+    };
+
+    // Try to set the access token
+    if let Err(err) = wallet.set_cat(cat.clone()).await {
+        tracing::error!("Could not set cat: {}", err);
+
+        // Try to refresh the token if we have a refresh token
+        if let Ok(Some(token_data)) = token_storage::get_token_for_mint(work_dir, &mint_url).await {
+            println!("Attempting to refresh the access token...");
+
+            // Get the mint info to access OIDC configuration
+            if let Some(mint_info) = wallet.get_mint_info().await? {
+                match refresh_access_token(&mint_info, &token_data.refresh_token).await {
+                    Ok((new_access_token, new_refresh_token)) => {
+                        println!("Successfully refreshed access token");
+
+                        // Save the new tokens
+                        if let Err(e) = token_storage::save_tokens(
+                            work_dir,
+                            &mint_url,
+                            &new_access_token,
+                            &new_refresh_token,
+                        )
+                        .await
+                        {
+                            println!("Warning: Failed to save refreshed tokens: {}", e);
+                        }
+
+                        // Try setting the new access token
+                        if let Err(err) = wallet.set_cat(new_access_token).await {
+                            tracing::error!("Could not set refreshed cat: {}", err);
+                            return Err(anyhow::anyhow!(
+                                "Authentication failed even after token refresh"
+                            ));
+                        }
+
+                        // Set the refresh token
+                        wallet.set_refresh_token(new_refresh_token).await?;
+                    }
+                    Err(e) => {
+                        tracing::error!("Failed to refresh token: {}", e);
+                        return Err(anyhow::anyhow!("Failed to refresh access token: {}", e));
+                    }
+                }
+            }
+        } else {
+            return Err(anyhow::anyhow!(
+                "Authentication failed and no refresh token available"
+            ));
+        }
+    } else {
+        // If we have a refresh token, set it
+        if let Ok(Some(token_data)) = token_storage::get_token_for_mint(work_dir, &mint_url).await {
+            tracing::info!("Attempting to use refresh access token to refresh auth token");
+            wallet.set_refresh_token(token_data.refresh_token).await?;
+            wallet.refresh_access_token().await?;
+        }
+    }
+
+    println!("Attempting to mint blind auth");
+
+    let amount = match sub_command_args.amount {
+        Some(amount) => amount,
+        None => {
+            let mint_info = wallet
+                .get_mint_info()
+                .await?
+                .ok_or(anyhow!("Unknown mint info"))?;
+            mint_info
+                .bat_max_mint()
+                .ok_or(anyhow!("Unknown max bat mint"))?
+        }
+    };
+
+    let proofs = wallet.mint_blind_auth(Amount::from(amount)).await?;
+
+    println!("Received {} auth proofs for mint {mint_url}", proofs.len());
+
+    Ok(())
+}
+
+async fn refresh_access_token(
+    mint_info: &MintInfo,
+    refresh_token: &str,
+) -> Result<(String, String)> {
+    let openid_discovery = mint_info
+        .nuts
+        .nut21
+        .clone()
+        .ok_or_else(|| anyhow::anyhow!("OIDC discovery information not available"))?
+        .openid_discovery;
+
+    let oidc_client = OidcClient::new(openid_discovery);
+
+    // Get the token endpoint from the OIDC configuration
+    let token_url = oidc_client.get_oidc_config().await?.token_endpoint;
+
+    // Create the request parameters for token refresh
+    let params = [
+        ("grant_type", "refresh_token"),
+        ("refresh_token", refresh_token),
+        ("client_id", "cashu-client"), // Using default client ID
+    ];
+
+    // Make the token refresh request
+    let client = reqwest::Client::new();
+    let response = client.post(token_url).form(&params).send().await?;
+
+    if !response.status().is_success() {
+        return Err(anyhow::anyhow!(
+            "Token refresh failed with status: {}",
+            response.status()
+        ));
+    }
+
+    let token_response: serde_json::Value = response.json().await?;
+
+    let access_token = token_response["access_token"]
+        .as_str()
+        .ok_or_else(|| anyhow::anyhow!("No access token in refresh response"))?
+        .to_string();
+
+    // Get the new refresh token or use the old one if not provided
+    let new_refresh_token = token_response["refresh_token"]
+        .as_str()
+        .unwrap_or(refresh_token)
+        .to_string();
+
+    Ok((access_token, new_refresh_token))
+}

+ 2 - 2
crates/cdk-cli/src/sub_commands/mint_info.rs

@@ -1,6 +1,6 @@
 use anyhow::Result;
 use cdk::mint_url::MintUrl;
-use cdk::wallet::client::MintConnector;
+use cdk::wallet::MintConnector;
 use cdk::HttpClient;
 use clap::Args;
 use url::Url;
@@ -14,7 +14,7 @@ pub async fn mint_info(proxy: Option<Url>, sub_command_args: &MintInfoSubcommand
     let mint_url = sub_command_args.mint_url.clone();
     let client = match proxy {
         Some(proxy) => HttpClient::with_proxy(mint_url, proxy, None, true)?,
-        None => HttpClient::new(mint_url),
+        None => HttpClient::new(mint_url, None),
     };
 
     let info = client.get_mint_info().await?;

+ 3 - 0
crates/cdk-cli/src/sub_commands/mod.rs

@@ -1,5 +1,7 @@
 pub mod balance;
 pub mod burn;
+pub mod cat_device_login;
+pub mod cat_login;
 pub mod check_spent;
 pub mod create_request;
 pub mod decode_request;
@@ -7,6 +9,7 @@ pub mod decode_token;
 pub mod list_mint_proofs;
 pub mod melt;
 pub mod mint;
+pub mod mint_blind_auth;
 pub mod mint_info;
 pub mod pay_request;
 pub mod pending_mints;

+ 9 - 11
crates/cdk-cli/src/sub_commands/pay_request.rs

@@ -1,10 +1,9 @@
 use std::io::{self, Write};
 
 use anyhow::{anyhow, Result};
-use cdk::amount::SplitTarget;
 use cdk::nuts::nut18::TransportType;
 use cdk::nuts::{PaymentRequest, PaymentRequestPayload};
-use cdk::wallet::{MultiMintWallet, SendKind};
+use cdk::wallet::{MultiMintWallet, SendOptions};
 use clap::Args;
 use nostr_sdk::nips::nip19::Nip19Profile;
 use nostr_sdk::{Client as NostrClient, EventBuilder, FromBech32, Keys};
@@ -81,17 +80,16 @@ pub async fn pay_request(
         })
         .ok_or(anyhow!("No supported transport method found"))?;
 
-    let proofs = matching_wallet
-        .send(
+    let prepared_send = matching_wallet
+        .prepare_send(
             amount,
-            None,
-            None,
-            &SplitTarget::default(),
-            &SendKind::default(),
-            true,
+            SendOptions {
+                include_fee: true,
+                ..Default::default()
+            },
         )
-        .await?
-        .proofs();
+        .await?;
+    let proofs = matching_wallet.send(prepared_send, None).await?.proofs();
 
     let payload = PaymentRequestPayload {
         id: payment_request.payment_id.clone(),

+ 31 - 27
crates/cdk-cli/src/sub_commands/receive.rs

@@ -1,19 +1,20 @@
 use std::collections::HashSet;
+use std::path::Path;
 use std::str::FromStr;
-use std::sync::Arc;
 
 use anyhow::{anyhow, Result};
-use cdk::cdk_database::{self, WalletDatabase};
 use cdk::nuts::{SecretKey, Token};
 use cdk::util::unix_time;
 use cdk::wallet::multi_mint_wallet::MultiMintWallet;
 use cdk::wallet::types::WalletKey;
-use cdk::wallet::Wallet;
+use cdk::wallet::ReceiveOptions;
 use cdk::Amount;
 use clap::Args;
 use nostr_sdk::nips::nip04;
 use nostr_sdk::{Filter, Keys, Kind, Timestamp};
 
+use crate::nostr_storage;
+
 #[derive(Args)]
 pub struct ReceiveSubCommand {
     /// Cashu Token
@@ -27,7 +28,7 @@ pub struct ReceiveSubCommand {
     /// Nostr relay
     #[arg(short, long, action = clap::ArgAction::Append)]
     relay: Vec<String>,
-    /// Unix time to to query nostr from
+    /// Unix time to query nostr from
     #[arg(long)]
     since: Option<u64>,
     /// Preimage
@@ -37,9 +38,8 @@ pub struct ReceiveSubCommand {
 
 pub async fn receive(
     multi_mint_wallet: &MultiMintWallet,
-    localstore: Arc<dyn WalletDatabase<Err = cdk_database::Error> + Send + Sync>,
-    seed: &[u8],
     sub_command_args: &ReceiveSubCommand,
+    work_dir: &Path,
 ) -> Result<()> {
     let mut signing_keys = Vec::new();
 
@@ -64,8 +64,6 @@ pub async fn receive(
         Some(token_str) => {
             receive_token(
                 multi_mint_wallet,
-                localstore,
-                seed,
                 token_str,
                 &signing_keys,
                 &sub_command_args.preimage,
@@ -89,18 +87,23 @@ pub async fn receive(
             signing_keys.push(nostr_key.clone());
 
             let relays = sub_command_args.relay.clone();
-            let since = localstore
-                .get_nostr_last_checked(&nostr_key.public_key())
-                .await?;
+            let since =
+                nostr_storage::get_nostr_last_checked(work_dir, &nostr_key.public_key()).await?;
 
             let tokens = nostr_receive(relays, nostr_key.clone(), since).await?;
 
+            // Store the current time as last checked
+            nostr_storage::store_nostr_last_checked(
+                work_dir,
+                &nostr_key.public_key(),
+                unix_time() as u32,
+            )
+            .await?;
+
             let mut total_amount = Amount::ZERO;
             for token_str in &tokens {
                 match receive_token(
                     multi_mint_wallet,
-                    localstore.clone(),
-                    seed,
                     token_str,
                     &signing_keys,
                     &sub_command_args.preimage,
@@ -116,9 +119,6 @@ pub async fn receive(
                 }
             }
 
-            localstore
-                .add_nostr_last_checked(nostr_key.public_key(), unix_time() as u32)
-                .await?;
             total_amount
         }
     };
@@ -130,8 +130,6 @@ pub async fn receive(
 
 async fn receive_token(
     multi_mint_wallet: &MultiMintWallet,
-    localstore: Arc<dyn WalletDatabase<Err = cdk_database::Error> + Send + Sync>,
-    seed: &[u8],
     token_str: &str,
     signing_keys: &[SecretKey],
     preimage: &[String],
@@ -143,18 +141,24 @@ async fn receive_token(
     let wallet_key = WalletKey::new(mint_url.clone(), token.unit().unwrap_or_default());
 
     if multi_mint_wallet.get_wallet(&wallet_key).await.is_none() {
-        let wallet = Wallet::new(
-            &mint_url.to_string(),
-            token.unit().unwrap_or_default(),
-            localstore,
-            seed,
-            None,
-        )?;
-        multi_mint_wallet.add_wallet(wallet).await;
+        multi_mint_wallet
+            .create_and_add_wallet(
+                &mint_url.to_string(),
+                token.unit().unwrap_or_default(),
+                None,
+            )
+            .await?;
     }
 
     let amount = multi_mint_wallet
-        .receive(token_str, signing_keys, preimage)
+        .receive(
+            token_str,
+            ReceiveOptions {
+                p2pk_signing_keys: signing_keys.to_vec(),
+                preimages: preimage.to_vec(),
+                ..Default::default()
+            },
+        )
         .await?;
     Ok(amount)
 }

+ 4 - 9
crates/cdk-cli/src/sub_commands/restore.rs

@@ -1,12 +1,10 @@
 use std::str::FromStr;
-use std::sync::Arc;
 
 use anyhow::Result;
-use cdk::cdk_database::{Error, WalletDatabase};
 use cdk::mint_url::MintUrl;
 use cdk::nuts::CurrencyUnit;
 use cdk::wallet::types::WalletKey;
-use cdk::wallet::{MultiMintWallet, Wallet};
+use cdk::wallet::MultiMintWallet;
 use clap::Args;
 
 #[derive(Args)]
@@ -20,8 +18,6 @@ pub struct RestoreSubCommand {
 
 pub async fn restore(
     multi_mint_wallet: &MultiMintWallet,
-    seed: &[u8],
-    localstore: Arc<dyn WalletDatabase<Err = Error> + Sync + Send>,
     sub_command_args: &RestoreSubCommand,
 ) -> Result<()> {
     let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
@@ -33,10 +29,9 @@ pub async fn restore(
     {
         Some(wallet) => wallet.clone(),
         None => {
-            let wallet = Wallet::new(&mint_url.to_string(), unit, localstore, seed, None)?;
-
-            multi_mint_wallet.add_wallet(wallet.clone()).await;
-            wallet
+            multi_mint_wallet
+                .create_and_add_wallet(&mint_url.to_string(), unit, None)
+                .await?
         }
     };
 

+ 14 - 9
crates/cdk-cli/src/sub_commands/send.rs

@@ -3,10 +3,9 @@ use std::io::Write;
 use std::str::FromStr;
 
 use anyhow::{bail, Result};
-use cdk::amount::SplitTarget;
 use cdk::nuts::{Conditions, CurrencyUnit, PublicKey, SpendingConditions};
 use cdk::wallet::types::{SendKind, WalletKey};
-use cdk::wallet::MultiMintWallet;
+use cdk::wallet::{MultiMintWallet, SendMemo, SendOptions};
 use cdk::Amount;
 use clap::Args;
 
@@ -170,16 +169,22 @@ pub async fn send(
         (false, None) => SendKind::OnlineExact,
     };
 
-    let token = wallet
-        .send(
+    let prepared_send = wallet
+        .prepare_send(
             token_amount,
-            sub_command_args.memo.clone(),
-            conditions,
-            &SplitTarget::default(),
-            &send_kind,
-            sub_command_args.include_fee,
+            SendOptions {
+                memo: sub_command_args.memo.clone().map(|memo| SendMemo {
+                    memo,
+                    include_memo: true,
+                }),
+                send_kind,
+                include_fee: sub_command_args.include_fee,
+                conditions,
+                ..Default::default()
+            },
         )
         .await?;
+    let token = wallet.send(prepared_send, None).await?;
 
     match sub_command_args.v3 {
         true => {

+ 62 - 0
crates/cdk-cli/src/token_storage.rs

@@ -0,0 +1,62 @@
+use std::fs::File;
+use std::io::{Read, Write};
+use std::path::Path;
+
+use anyhow::Result;
+use cdk::mint_url::MintUrl;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct TokenData {
+    pub mint_url: String,
+    pub access_token: String,
+    pub refresh_token: String,
+}
+
+/// Stores authentication tokens in the work directory
+pub async fn save_tokens(
+    work_dir: &Path,
+    mint_url: &MintUrl,
+    access_token: &str,
+    refresh_token: &str,
+) -> Result<()> {
+    let token_data = TokenData {
+        mint_url: mint_url.to_string(),
+        access_token: access_token.to_string(),
+        refresh_token: refresh_token.to_string(),
+    };
+
+    let json = serde_json::to_string_pretty(&token_data)?;
+    let file_path = work_dir.join(format!(
+        "auth_tokens_{}",
+        mint_url.to_string().replace("/", "_")
+    ));
+    let mut file = File::create(file_path)?;
+    file.write_all(json.as_bytes())?;
+
+    Ok(())
+}
+
+/// Gets authentication tokens from the work directory
+pub async fn get_token_for_mint(work_dir: &Path, mint_url: &MintUrl) -> Result<Option<TokenData>> {
+    let file_path = work_dir.join(format!(
+        "auth_tokens_{}",
+        mint_url.to_string().replace("/", "_")
+    ));
+
+    if !file_path.exists() {
+        return Ok(None);
+    }
+
+    let mut file = File::open(file_path)?;
+    let mut contents = String::new();
+    file.read_to_string(&mut contents)?;
+
+    let token_data: TokenData = serde_json::from_str(&contents)?;
+
+    if token_data.mint_url == mint_url.to_string() {
+        Ok(Some(token_data))
+    } else {
+        Ok(None)
+    }
+}

+ 15 - 18
crates/cdk-cln/Cargo.toml

@@ -1,27 +1,24 @@
 [package]
 name = "cdk-cln"
-version = "0.7.1"
-edition = "2021"
+version.workspace = true
+edition.workspace = true
 authors = ["CDK Developers"]
-license = "MIT"
+license.workspace = true
 homepage = "https://github.com/cashubtc/cdk"
 repository = "https://github.com/cashubtc/cdk.git"
-rust-version = "1.63.0"                            # MSRV
+rust-version.workspace = true # MSRV
 description = "CDK ln backend for cln"
+readme = "README.md"
 
 [dependencies]
-async-trait = "0.1"
-bitcoin = { version = "0.32.2", default-features = false }
-cdk = { path = "../cdk", version = "0.7.1", default-features = false, features = [
-    "mint",
-] }
+async-trait.workspace = true
+bitcoin.workspace = true
+cdk = { workspace = true, features = ["mint"] }
 cln-rpc = "0.3.0"
-futures = { version = "0.3.28", default-features = false }
-tokio = { version = "1", default-features = false }
-tokio-util = { version = "0.7.11", default-features = false }
-tracing = { version = "0.1", default-features = false, features = [
-    "attributes",
-    "log",
-] }
-thiserror = "1"
-uuid = { version = "=1.12.1", features = ["v4"] }
+futures.workspace = true
+tokio.workspace = true
+tokio-util.workspace = true
+tracing.workspace = true
+thiserror.workspace = true
+uuid.workspace = true
+serde_json.workspace = true

+ 1 - 1
crates/cdk-cln/src/error.rs

@@ -28,7 +28,7 @@ pub enum Error {
     Amount(#[from] cdk::amount::Error),
 }
 
-impl From<Error> for cdk::cdk_lightning::Error {
+impl From<Error> for cdk::cdk_payment::Error {
     fn from(e: Error) -> Self {
         Self::Lightning(Box::new(e))
     }

+ 47 - 38
crates/cdk-cln/src/lib.rs

@@ -1,8 +1,10 @@
 //! CDK lightning backend for CLN
 
+#![doc = include_str!("../README.md")]
 #![warn(missing_docs)]
 #![warn(rustdoc::bare_urls)]
 
+use std::cmp::max;
 use std::path::PathBuf;
 use std::pin::Pin;
 use std::str::FromStr;
@@ -10,12 +12,13 @@ use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::Arc;
 
 use async_trait::async_trait;
-use cdk::amount::{to_unit, Amount, MSAT_IN_SAT};
-use cdk::cdk_lightning::{
-    self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings,
+use cdk::amount::{to_unit, Amount};
+use cdk::cdk_payment::{
+    self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
+    PaymentQuoteResponse,
 };
-use cdk::mint::FeeReserve;
-use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
+use cdk::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState};
+use cdk::types::FeeReserve;
 use cdk::util::{hex, unix_time};
 use cdk::{mint, Bolt11Invoice};
 use cln_rpc::model::requests::{
@@ -28,6 +31,7 @@ use cln_rpc::model::responses::{
 use cln_rpc::primitives::{Amount as CLN_Amount, AmountOrAny};
 use error::Error;
 use futures::{Stream, StreamExt};
+use serde_json::Value;
 use tokio::sync::Mutex;
 use tokio_util::sync::CancellationToken;
 use uuid::Uuid;
@@ -60,15 +64,16 @@ impl Cln {
 }
 
 #[async_trait]
-impl MintLightning for Cln {
-    type Err = cdk_lightning::Error;
+impl MintPayment for Cln {
+    type Err = cdk_payment::Error;
 
-    fn get_settings(&self) -> Settings {
-        Settings {
+    async fn get_settings(&self) -> Result<Value, Self::Err> {
+        Ok(serde_json::to_value(Bolt11Settings {
             mpp: true,
             unit: CurrencyUnit::Msat,
             invoice_description: true,
-        }
+            amountless: true,
+        })?)
     }
 
     /// Is wait invoice active
@@ -81,9 +86,7 @@ impl MintLightning for Cln {
         self.wait_invoice_cancel_token.cancel()
     }
 
-    #[allow(clippy::incompatible_msrv)]
-    // Clippy thinks select is not stable but it compiles fine on MSRV (1.63.0)
-    async fn wait_any_invoice(
+    async fn wait_any_incoming_payment(
         &self,
     ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
         let last_pay_index = self.get_last_pay_index().await?;
@@ -177,36 +180,43 @@ impl MintLightning for Cln {
 
     async fn get_payment_quote(
         &self,
-        melt_quote_request: &MeltQuoteBolt11Request,
+        request: &str,
+        unit: &CurrencyUnit,
+        options: Option<MeltOptions>,
     ) -> Result<PaymentQuoteResponse, Self::Err> {
-        let amount = melt_quote_request.amount_msat()?;
+        let bolt11 = Bolt11Invoice::from_str(request)?;
+
+        let amount_msat = match options {
+            Some(amount) => amount.amount_msat(),
+            None => bolt11
+                .amount_milli_satoshis()
+                .ok_or(Error::UnknownInvoiceAmount)?
+                .into(),
+        };
 
-        let amount = amount / MSAT_IN_SAT.into();
+        let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
 
         let relative_fee_reserve =
             (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
 
         let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
 
-        let fee = match relative_fee_reserve > absolute_fee_reserve {
-            true => relative_fee_reserve,
-            false => absolute_fee_reserve,
-        };
+        let fee = max(relative_fee_reserve, absolute_fee_reserve);
 
         Ok(PaymentQuoteResponse {
-            request_lookup_id: melt_quote_request.request.payment_hash().to_string(),
+            request_lookup_id: bolt11.payment_hash().to_string(),
             amount,
             fee: fee.into(),
             state: MeltQuoteState::Unpaid,
         })
     }
 
-    async fn pay_invoice(
+    async fn make_payment(
         &self,
         melt_quote: mint::MeltQuote,
         partial_amount: Option<Amount>,
         max_fee: Option<Amount>,
-    ) -> Result<PayInvoiceResponse, Self::Err> {
+    ) -> Result<MakePaymentResponse, Self::Err> {
         let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?;
         let pay_state = self
             .check_outgoing_payment(&bolt11.payment_hash().to_string())
@@ -273,8 +283,8 @@ impl MintLightning for Cln {
                     PayStatus::FAILED => MeltQuoteState::Failed,
                 };
 
-                PayInvoiceResponse {
-                    payment_preimage: Some(hex::encode(pay_response.payment_preimage.to_vec())),
+                MakePaymentResponse {
+                    payment_proof: Some(hex::encode(pay_response.payment_preimage.to_vec())),
                     payment_lookup_id: pay_response.payment_hash.to_string(),
                     status,
                     total_spent: to_unit(
@@ -294,15 +304,14 @@ impl MintLightning for Cln {
         Ok(response)
     }
 
-    async fn create_invoice(
+    async fn create_incoming_payment_request(
         &self,
         amount: Amount,
         unit: &CurrencyUnit,
         description: String,
-        unix_expiry: u64,
-    ) -> Result<CreateInvoiceResponse, Self::Err> {
+        unix_expiry: Option<u64>,
+    ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
         let time_now = unix_time();
-        assert!(unix_expiry > time_now);
 
         let mut cln_client = self.cln_client.lock().await;
 
@@ -316,7 +325,7 @@ impl MintLightning for Cln {
                 amount_msat,
                 description,
                 label: label.clone(),
-                expiry: Some(unix_expiry - time_now),
+                expiry: unix_expiry.map(|t| t - time_now),
                 fallbacks: None,
                 preimage: None,
                 cltv: None,
@@ -330,14 +339,14 @@ impl MintLightning for Cln {
         let expiry = request.expires_at().map(|t| t.as_secs());
         let payment_hash = request.payment_hash();
 
-        Ok(CreateInvoiceResponse {
+        Ok(CreateIncomingPaymentResponse {
             request_lookup_id: payment_hash.to_string(),
-            request,
+            request: request.to_string(),
             expiry,
         })
     }
 
-    async fn check_incoming_invoice_status(
+    async fn check_incoming_payment_status(
         &self,
         payment_hash: &str,
     ) -> Result<MintQuoteState, Self::Err> {
@@ -373,7 +382,7 @@ impl MintLightning for Cln {
     async fn check_outgoing_payment(
         &self,
         payment_hash: &str,
-    ) -> Result<PayInvoiceResponse, Self::Err> {
+    ) -> Result<MakePaymentResponse, Self::Err> {
         let mut cln_client = self.cln_client.lock().await;
 
         let listpays_response = cln_client
@@ -392,9 +401,9 @@ impl MintLightning for Cln {
             Some(pays_response) => {
                 let status = cln_pays_status_to_mint_state(pays_response.status);
 
-                Ok(PayInvoiceResponse {
+                Ok(MakePaymentResponse {
                     payment_lookup_id: pays_response.payment_hash.to_string(),
-                    payment_preimage: pays_response.preimage.map(|p| hex::encode(p.to_vec())),
+                    payment_proof: pays_response.preimage.map(|p| hex::encode(p.to_vec())),
                     status,
                     total_spent: pays_response
                         .amount_sent_msat
@@ -402,9 +411,9 @@ impl MintLightning for Cln {
                     unit: CurrencyUnit::Msat,
                 })
             }
-            None => Ok(PayInvoiceResponse {
+            None => Ok(MakePaymentResponse {
                 payment_lookup_id: payment_hash.to_string(),
-                payment_preimage: None,
+                payment_proof: None,
                 status: MeltQuoteState::Unknown,
                 total_spent: Amount::ZERO,
                 unit: CurrencyUnit::Msat,

+ 26 - 28
crates/cdk-common/Cargo.toml

@@ -1,47 +1,45 @@
 [package]
 name = "cdk-common"
-version = "0.7.1"
-edition = "2021"
+version.workspace = true
 authors = ["CDK Developers"]
 description = "CDK common types and traits"
 homepage = "https://github.com/cashubtc/cdk"
 repository = "https://github.com/cashubtc/cdk.git"
-rust-version = "1.63.0"                     # MSRV
-license = "MIT"
+edition.workspace = true
+rust-version.workspace = true # MSRV
+license.workspace = true
+readme = "README.md"
 
 [features]
 default = ["mint", "wallet"]
 swagger = ["dep:utoipa", "cashu/swagger"]
+test = []
 bench = []
 wallet = ["cashu/wallet"]
 mint = ["cashu/mint", "dep:uuid"]
+auth = ["cashu/auth"]
 
 [dependencies]
-async-trait = "0.1"
-bitcoin = { version = "0.32.2", features = [
-    "base64",
-    "serde",
-    "rand",
-    "rand-std",
-] }
-cashu = { path = "../cashu", default-features = false, version = "0.7.1" }
-cbor-diag = "0.1.12"
-ciborium = { version = "0.2.2", default-features = false, features = ["std"] }
-serde = { version = "1", features = ["derive"] }
-lightning-invoice = { version = "0.32.0", features = ["serde", "std"] }
-thiserror = "2"
-tracing = "0.1"
-url = "2.3"
-uuid = { version = "=1.12.1", features = ["v4", "serde"], optional = true }
-utoipa = { version = "4", optional = true }
-futures = "0.3.31"
-anyhow = "1.0"
-serde_json = "1"
-serde_with = "3"
+async-trait.workspace = true
+bitcoin.workspace = true
+cashu.workspace = true
+cbor-diag.workspace = true
+ciborium.workspace = true
+serde.workspace = true
+lightning-invoice.workspace = true
+thiserror.workspace = true
+tracing.workspace = true
+url.workspace = true
+uuid = { workspace = true, optional = true }
+utoipa = { workspace = true, optional = true }
+futures.workspace = true
+anyhow.workspace = true
+serde_json.workspace = true
+serde_with.workspace = true
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
-instant = { version = "0.1", features = ["wasm-bindgen", "inaccurate"] }
+instant = { workspace = true, features = ["wasm-bindgen", "inaccurate"] }
 
 [dev-dependencies]
-rand = "0.8.5"
-bip39 = "2.0"
+rand.workspace = true
+bip39.workspace = true

+ 41 - 0
crates/cdk-common/README.md

@@ -0,0 +1,41 @@
+# CDK Common
+
+[![crates.io](https://img.shields.io/crates/v/cdk-common.svg)](https://crates.io/crates/cdk-common)
+[![Documentation](https://docs.rs/cdk-common/badge.svg)](https://docs.rs/cdk-common)
+[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/cashubtc/cdk/blob/main/LICENSE)
+
+Common types, traits, and utilities for the Cashu Development Kit (CDK).
+
+## Overview
+
+The `cdk-common` crate provides shared functionality used across the CDK crates. It contains core data structures, error types, and utility functions that are essential for implementing Cashu wallets and mints.
+
+## Features
+
+- **Core Data Types**: Implementations of fundamental Cashu types like `MintUrl`, `ProofInfo`, and `Melted`
+- **Error Handling**: Comprehensive error types for Cashu operations
+- **Database Abstractions**: Traits for database operations used by wallets and mints
+- **NUT Implementations**: Common functionality for Cashu NUTs (Notation, Usage, and Terminology)
+
+## Usage
+
+Add this to your `Cargo.toml`:
+
+```toml
+[dependencies]
+cdk-common = "*"
+```
+
+## Components
+
+The crate includes several key modules:
+
+- **common**: Core data structures used throughout the CDK
+- **database**: Traits for database operations
+- **error**: Error types and handling
+- **mint_url**: Implementation of the MintUrl type
+- **nuts**: Common functionality for Cashu NUTs
+
+## License
+
+This project is licensed under the [MIT License](https://github.com/cashubtc/cdk/blob/main/LICENSE).

+ 108 - 7
crates/cdk-common/src/common.rs

@@ -43,7 +43,8 @@ impl Melted {
 
         let fee_paid = proofs_amount
             .checked_sub(amount + change_amount)
-            .ok_or(Error::AmountOverflow)?;
+            .ok_or(Error::AmountOverflow)
+            .unwrap();
 
         Ok(Self {
             state,
@@ -127,7 +128,11 @@ impl ProofInfo {
 
         if let Some(spending_conditions) = spending_conditions {
             match &self.spending_condition {
-                None => return false,
+                None => {
+                    if !spending_conditions.is_empty() {
+                        return false;
+                    }
+                }
                 Some(s) => {
                     if !spending_conditions.contains(s) {
                         return false;
@@ -143,15 +148,15 @@ impl ProofInfo {
 /// Key used in hashmap of ln backends to identify what unit and payment method
 /// it is for
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
-pub struct LnKey {
+pub struct PaymentProcessorKey {
     /// Unit of Payment backend
     pub unit: CurrencyUnit,
     /// Method of payment backend
     pub method: PaymentMethod,
 }
 
-impl LnKey {
-    /// Create new [`LnKey`]
+impl PaymentProcessorKey {
+    /// Create new [`PaymentProcessorKey`]
     pub fn new(unit: CurrencyUnit, method: PaymentMethod) -> Self {
         Self { unit, method }
     }
@@ -177,8 +182,11 @@ impl QuoteTTL {
 mod tests {
     use std::str::FromStr;
 
-    use super::Melted;
-    use crate::nuts::{Id, Proof, PublicKey};
+    use cashu::SecretKey;
+
+    use super::{Melted, ProofInfo};
+    use crate::mint_url::MintUrl;
+    use crate::nuts::{CurrencyUnit, Id, Proof, PublicKey, SpendingConditions, State};
     use crate::secret::Secret;
     use crate::Amount;
 
@@ -240,4 +248,97 @@ mod tests {
         assert_eq!(melted.fee_paid, Amount::from(1));
         assert_eq!(melted.total_amount(), Amount::from(32));
     }
+
+    #[test]
+    fn test_matches_conditions() {
+        let keyset_id = Id::from_str("00deadbeef123456").unwrap();
+        let proof = Proof::new(
+            Amount::from(64),
+            keyset_id,
+            Secret::new("test_secret"),
+            PublicKey::from_hex(
+                "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+            )
+            .unwrap(),
+        );
+
+        let mint_url = MintUrl::from_str("https://example.com").unwrap();
+        let proof_info =
+            ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
+
+        // Test matching mint_url
+        assert!(proof_info.matches_conditions(&Some(mint_url.clone()), &None, &None, &None));
+        assert!(!proof_info.matches_conditions(
+            &Some(MintUrl::from_str("https://different.com").unwrap()),
+            &None,
+            &None,
+            &None
+        ));
+
+        // Test matching unit
+        assert!(proof_info.matches_conditions(&None, &Some(CurrencyUnit::Sat), &None, &None));
+        assert!(!proof_info.matches_conditions(&None, &Some(CurrencyUnit::Msat), &None, &None));
+
+        // Test matching state
+        assert!(proof_info.matches_conditions(&None, &None, &Some(vec![State::Unspent]), &None));
+        assert!(proof_info.matches_conditions(
+            &None,
+            &None,
+            &Some(vec![State::Unspent, State::Spent]),
+            &None
+        ));
+        assert!(!proof_info.matches_conditions(&None, &None, &Some(vec![State::Spent]), &None));
+
+        // Test with no conditions (should match)
+        assert!(proof_info.matches_conditions(&None, &None, &None, &None));
+
+        // Test with multiple conditions
+        assert!(proof_info.matches_conditions(
+            &Some(mint_url),
+            &Some(CurrencyUnit::Sat),
+            &Some(vec![State::Unspent]),
+            &None
+        ));
+    }
+
+    #[test]
+    fn test_matches_conditions_with_spending_conditions() {
+        // This test would need to be expanded with actual SpendingConditions
+        // implementation, but we can test the basic case where no spending
+        // conditions are present
+
+        let keyset_id = Id::from_str("00deadbeef123456").unwrap();
+        let proof = Proof::new(
+            Amount::from(64),
+            keyset_id,
+            Secret::new("test_secret"),
+            PublicKey::from_hex(
+                "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+            )
+            .unwrap(),
+        );
+
+        let mint_url = MintUrl::from_str("https://example.com").unwrap();
+        let proof_info =
+            ProofInfo::new(proof, mint_url, State::Unspent, CurrencyUnit::Sat).unwrap();
+
+        // Test with empty spending conditions (should match when proof has none)
+        assert!(proof_info.matches_conditions(&None, &None, &None, &Some(vec![])));
+
+        // Test with non-empty spending conditions (should not match when proof has none)
+        let dummy_condition = SpendingConditions::P2PKConditions {
+            data: SecretKey::generate().public_key(),
+            conditions: None,
+        };
+        assert!(!proof_info.matches_conditions(&None, &None, &None, &Some(vec![dummy_condition])));
+    }
+}
+
+/// Mint Fee Reserve
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct FeeReserve {
+    /// Absolute expected min fee
+    pub min_fee_reserve: Amount,
+    /// Percentage expected fee
+    pub percent_fee_reserve: f32,
 }

+ 72 - 0
crates/cdk-common/src/database/mint/auth/mod.rs

@@ -0,0 +1,72 @@
+//! Mint in memory database use std::collections::HashMap;
+
+use std::collections::HashMap;
+
+use async_trait::async_trait;
+use cashu::{AuthRequired, ProtectedEndpoint};
+
+use crate::database::Error;
+use crate::mint::MintKeySetInfo;
+use crate::nuts::nut07::State;
+use crate::nuts::{AuthProof, BlindSignature, Id, PublicKey};
+
+/// Mint Database trait
+#[async_trait]
+pub trait MintAuthDatabase {
+    /// Mint Database Error
+    type Err: Into<Error> + From<Error>;
+    /// Add Active Keyset
+    async fn set_active_keyset(&self, id: Id) -> Result<(), Self::Err>;
+    /// Get Active Keyset
+    async fn get_active_keyset_id(&self) -> Result<Option<Id>, Self::Err>;
+
+    /// Add [`MintKeySetInfo`]
+    async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err>;
+    /// Get [`MintKeySetInfo`]
+    async fn get_keyset_info(&self, id: &Id) -> Result<Option<MintKeySetInfo>, Self::Err>;
+    /// Get [`MintKeySetInfo`]s
+    async fn get_keyset_infos(&self) -> Result<Vec<MintKeySetInfo>, Self::Err>;
+
+    /// Add spent [`AuthProof`]
+    async fn add_proof(&self, proof: AuthProof) -> Result<(), Self::Err>;
+    /// Get [`AuthProof`] state
+    async fn get_proofs_states(&self, ys: &[PublicKey]) -> Result<Vec<Option<State>>, Self::Err>;
+    /// Update [`AuthProof`]s state
+    async fn update_proof_state(
+        &self,
+        y: &PublicKey,
+        proofs_state: State,
+    ) -> Result<Option<State>, Self::Err>;
+
+    /// Add [`BlindSignature`]
+    async fn add_blind_signatures(
+        &self,
+        blinded_messages: &[PublicKey],
+        blind_signatures: &[BlindSignature],
+    ) -> Result<(), Self::Err>;
+    /// Get [`BlindSignature`]s
+    async fn get_blind_signatures(
+        &self,
+        blinded_messages: &[PublicKey],
+    ) -> Result<Vec<Option<BlindSignature>>, Self::Err>;
+
+    /// Add protected endpoints
+    async fn add_protected_endpoints(
+        &self,
+        protected_endpoints: HashMap<ProtectedEndpoint, AuthRequired>,
+    ) -> Result<(), Self::Err>;
+    /// Removed Protected endpoints
+    async fn remove_protected_endpoints(
+        &self,
+        protected_endpoints: Vec<ProtectedEndpoint>,
+    ) -> Result<(), Self::Err>;
+    /// Get auth for protected_endpoint
+    async fn get_auth_for_endpoint(
+        &self,
+        protected_endpoint: ProtectedEndpoint,
+    ) -> Result<Option<AuthRequired>, Self::Err>;
+    /// Get protected endpoints
+    async fn get_auth_for_endpoints(
+        &self,
+    ) -> Result<HashMap<ProtectedEndpoint, Option<AuthRequired>>, Self::Err>;
+}

+ 51 - 16
crates/cdk-common/src/database/mint.rs → crates/cdk-common/src/database/mint/mod.rs

@@ -7,17 +7,26 @@ use cashu::MintInfo;
 use uuid::Uuid;
 
 use super::Error;
-use crate::common::{LnKey, QuoteTTL};
+use crate::common::{PaymentProcessorKey, QuoteTTL};
 use crate::mint::{self, MintKeySetInfo, MintQuote as MintMintQuote};
 use crate::nuts::{
     BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintQuoteState, Proof,
     Proofs, PublicKey, State,
 };
 
-/// Mint Database trait
+#[cfg(feature = "auth")]
+mod auth;
+
+#[cfg(feature = "test")]
+pub mod test;
+
+#[cfg(feature = "auth")]
+pub use auth::MintAuthDatabase;
+
+/// Mint Keys Database trait
 #[async_trait]
-pub trait Database {
-    /// Mint Database Error
+pub trait KeysDatabase {
+    /// Mint Keys Database Error
     type Err: Into<Error> + From<Error>;
 
     /// Add Active Keyset
@@ -26,6 +35,19 @@ pub trait Database {
     async fn get_active_keyset_id(&self, unit: &CurrencyUnit) -> Result<Option<Id>, Self::Err>;
     /// Get all Active Keyset
     async fn get_active_keysets(&self) -> Result<HashMap<CurrencyUnit, Id>, Self::Err>;
+    /// Add [`MintKeySetInfo`]
+    async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err>;
+    /// Get [`MintKeySetInfo`]
+    async fn get_keyset_info(&self, id: &Id) -> Result<Option<MintKeySetInfo>, Self::Err>;
+    /// Get [`MintKeySetInfo`]s
+    async fn get_keyset_infos(&self) -> Result<Vec<MintKeySetInfo>, Self::Err>;
+}
+
+/// Mint Quote Database trait
+#[async_trait]
+pub trait QuotesDatabase {
+    /// Mint Quotes Database Error
+    type Err: Into<Error> + From<Error>;
 
     /// Add [`MintMintQuote`]
     async fn add_mint_quote(&self, quote: MintMintQuote) -> Result<(), Self::Err>;
@@ -76,20 +98,20 @@ pub trait Database {
     async fn add_melt_request(
         &self,
         melt_request: MeltBolt11Request<Uuid>,
-        ln_key: LnKey,
+        ln_key: PaymentProcessorKey,
     ) -> Result<(), Self::Err>;
     /// Get melt request
     async fn get_melt_request(
         &self,
         quote_id: &Uuid,
-    ) -> Result<Option<(MeltBolt11Request<Uuid>, LnKey)>, Self::Err>;
+    ) -> Result<Option<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>, Self::Err>;
+}
 
-    /// Add [`MintKeySetInfo`]
-    async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err>;
-    /// Get [`MintKeySetInfo`]
-    async fn get_keyset_info(&self, id: &Id) -> Result<Option<MintKeySetInfo>, Self::Err>;
-    /// Get [`MintKeySetInfo`]s
-    async fn get_keyset_infos(&self) -> Result<Vec<MintKeySetInfo>, Self::Err>;
+/// Mint Proof Database trait
+#[async_trait]
+pub trait ProofsDatabase {
+    /// Mint Proof Database Error
+    type Err: Into<Error> + From<Error>;
 
     /// Add  [`Proofs`]
     async fn add_proofs(&self, proof: Proofs, quote_id: Option<Uuid>) -> Result<(), Self::Err>;
@@ -116,6 +138,13 @@ pub trait Database {
         &self,
         keyset_id: &Id,
     ) -> Result<(Proofs, Vec<Option<State>>), Self::Err>;
+}
+
+#[async_trait]
+/// Mint Signatures Database trait
+pub trait SignaturesDatabase {
+    /// Mint Signature Database Error
+    type Err: Into<Error> + From<Error>;
 
     /// Add [`BlindSignature`]
     async fn add_blind_signatures(
@@ -139,14 +168,20 @@ pub trait Database {
         &self,
         quote_id: &Uuid,
     ) -> Result<Vec<BlindSignature>, Self::Err>;
+}
 
+/// Mint Database trait
+#[async_trait]
+pub trait Database<Error>:
+    QuotesDatabase<Err = Error> + ProofsDatabase<Err = Error> + SignaturesDatabase<Err = Error>
+{
     /// Set [`MintInfo`]
-    async fn set_mint_info(&self, mint_info: MintInfo) -> Result<(), Self::Err>;
+    async fn set_mint_info(&self, mint_info: MintInfo) -> Result<(), Error>;
     /// Get [`MintInfo`]
-    async fn get_mint_info(&self) -> Result<MintInfo, Self::Err>;
+    async fn get_mint_info(&self) -> Result<MintInfo, Error>;
 
     /// Set [`QuoteTTL`]
-    async fn set_quote_ttl(&self, quote_ttl: QuoteTTL) -> Result<(), Self::Err>;
+    async fn set_quote_ttl(&self, quote_ttl: QuoteTTL) -> Result<(), Error>;
     /// Get [`QuoteTTL`]
-    async fn get_quote_ttl(&self) -> Result<QuoteTTL, Self::Err>;
+    async fn get_quote_ttl(&self) -> Result<QuoteTTL, Error>;
 }

+ 83 - 0
crates/cdk-common/src/database/mint/test.rs

@@ -0,0 +1,83 @@
+//! Macro with default tests
+//!
+//! This set is generic and checks the default and expected behaviour for a mint database
+//! implementation
+use std::fmt::Debug;
+use std::str::FromStr;
+
+use cashu::secret::Secret;
+use cashu::{Amount, CurrencyUnit, SecretKey};
+
+use super::*;
+use crate::mint::MintKeySetInfo;
+
+#[inline]
+async fn setup_keyset<E: Debug, DB: Database<E> + KeysDatabase<Err = E>>(db: &DB) -> Id {
+    let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();
+    let keyset_info = MintKeySetInfo {
+        id: keyset_id,
+        unit: CurrencyUnit::Sat,
+        active: true,
+        valid_from: 0,
+        valid_to: None,
+        derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
+        derivation_path_index: Some(0),
+        max_order: 32,
+        input_fee_ppk: 0,
+    };
+    db.add_keyset_info(keyset_info).await.unwrap();
+    keyset_id
+}
+
+/// State transition test
+pub async fn state_transition<E: Debug, DB: Database<E> + KeysDatabase<Err = E>>(db: DB) {
+    let keyset_id = setup_keyset(&db).await;
+
+    let proofs = vec![
+        Proof {
+            amount: Amount::from(100),
+            keyset_id,
+            secret: Secret::generate(),
+            c: SecretKey::generate().public_key(),
+            witness: None,
+            dleq: None,
+        },
+        Proof {
+            amount: Amount::from(200),
+            keyset_id,
+            secret: Secret::generate(),
+            c: SecretKey::generate().public_key(),
+            witness: None,
+            dleq: None,
+        },
+    ];
+
+    // Add proofs to database
+    db.add_proofs(proofs.clone(), None).await.unwrap();
+
+    // Mark one proof as `pending`
+    assert!(db
+        .update_proofs_states(&[proofs[0].y().unwrap()], State::Pending)
+        .await
+        .is_ok());
+
+    // Attempt to select the `pending` proof, as `pending` again (which should fail)
+    assert!(db
+        .update_proofs_states(&[proofs[0].y().unwrap()], State::Pending)
+        .await
+        .is_err());
+}
+
+/// Unit test that is expected to be passed for a correct database implementation
+#[macro_export]
+macro_rules! mint_db_test {
+    ($make_db_fn:ident) => {
+        mint_db_test!(state_transition, $make_db_fn);
+    };
+    ($name:ident, $make_db_fn:ident) => {
+        #[tokio::test]
+        async fn $name() {
+            cdk_common::database::mint::test::$name($make_db_fn().await).await;
+        }
+    };
+}

+ 38 - 2
crates/cdk-common/src/database/mod.rs

@@ -1,12 +1,18 @@
 //! CDK Database
 
 #[cfg(feature = "mint")]
-mod mint;
+pub mod mint;
 #[cfg(feature = "wallet")]
 mod wallet;
 
+#[cfg(all(feature = "mint", feature = "auth"))]
+pub use mint::MintAuthDatabase;
 #[cfg(feature = "mint")]
-pub use mint::Database as MintDatabase;
+pub use mint::{
+    Database as MintDatabase, KeysDatabase as MintKeysDatabase,
+    ProofsDatabase as MintProofsDatabase, QuotesDatabase as MintQuotesDatabase,
+    SignaturesDatabase as MintSignaturesDatabase,
+};
 #[cfg(feature = "wallet")]
 pub use wallet::Database as WalletDatabase;
 
@@ -25,10 +31,40 @@ pub enum Error {
     /// NUT02 Error
     #[error(transparent)]
     NUT02(#[from] crate::nuts::nut02::Error),
+    /// NUT22 Error
+    #[error(transparent)]
+    #[cfg(feature = "auth")]
+    NUT22(#[from] crate::nuts::nut22::Error),
     /// Serde Error
     #[error(transparent)]
     Serde(#[from] serde_json::Error),
     /// Unknown Quote
     #[error("Unknown Quote")]
     UnknownQuote,
+    /// Attempt to remove spent proof
+    #[error("Attempt to remove spent proof")]
+    AttemptRemoveSpentProof,
+    /// Attempt to update state of spent proof
+    #[error("Attempt to update state of spent proof")]
+    AttemptUpdateSpentProof,
+    /// Proof not found
+    #[error("Proof not found")]
+    ProofNotFound,
+    /// Invalid keyset
+    #[error("Unknown or invalid keyset")]
+    InvalidKeysetId,
+    #[cfg(feature = "mint")]
+    /// Invalid state transition
+    #[error("Invalid state transition")]
+    InvalidStateTransition(crate::state::Error),
+}
+
+#[cfg(feature = "mint")]
+impl From<crate::state::Error> for Error {
+    fn from(state: crate::state::Error) -> Self {
+        match state {
+            crate::state::Error::AlreadySpent => Error::AttemptUpdateSpentProof,
+            _ => Error::InvalidStateTransition(state),
+        }
+    }
 }

+ 19 - 19
crates/cdk-common/src/database/wallet.rs

@@ -11,8 +11,9 @@ use crate::mint_url::MintUrl;
 use crate::nuts::{
     CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions, State,
 };
-use crate::wallet;
-use crate::wallet::MintQuote as WalletMintQuote;
+use crate::wallet::{
+    self, MintQuote as WalletMintQuote, Transaction, TransactionDirection, TransactionId,
+};
 
 /// Wallet Database trait
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -84,14 +85,6 @@ pub trait Database: Debug {
         added: Vec<ProofInfo>,
         removed_ys: Vec<PublicKey>,
     ) -> Result<(), Self::Err>;
-    /// Set proofs as pending in storage. Proofs are identified by their Y
-    /// value.
-    async fn set_pending_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err>;
-    /// Reserve proofs in storage. Proofs are identified by their Y value.
-    async fn reserve_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err>;
-    /// Set proofs as unspent in storage. Proofs are identified by their Y
-    /// value.
-    async fn set_unspent_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err>;
     /// Get proofs from storage
     async fn get_proofs(
         &self,
@@ -100,21 +93,28 @@ pub trait Database: Debug {
         state: Option<Vec<State>>,
         spending_conditions: Option<Vec<SpendingConditions>>,
     ) -> Result<Vec<ProofInfo>, Self::Err>;
+    /// Update proofs state in storage
+    async fn update_proofs_state(&self, ys: Vec<PublicKey>, state: State) -> Result<(), Self::Err>;
 
     /// Increment Keyset counter
     async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err>;
     /// Get current Keyset counter
     async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u32>, Self::Err>;
 
-    /// Get when nostr key was last checked
-    async fn get_nostr_last_checked(
+    /// Add transaction to storage
+    async fn add_transaction(&self, transaction: Transaction) -> Result<(), Self::Err>;
+    /// Get transaction from storage
+    async fn get_transaction(
         &self,
-        verifying_key: &PublicKey,
-    ) -> Result<Option<u32>, Self::Err>;
-    /// Update last checked time
-    async fn add_nostr_last_checked(
+        transaction_id: TransactionId,
+    ) -> Result<Option<Transaction>, Self::Err>;
+    /// List transactions from storage
+    async fn list_transactions(
         &self,
-        verifying_key: PublicKey,
-        last_checked: u32,
-    ) -> Result<(), Self::Err>;
+        mint_url: Option<MintUrl>,
+        direction: Option<TransactionDirection>,
+        unit: Option<CurrencyUnit>,
+    ) -> Result<Vec<Transaction>, Self::Err>;
+    /// Remove transaction from storage
+    async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), Self::Err>;
 }

+ 117 - 4
crates/cdk-common/src/error.rs

@@ -58,6 +58,39 @@ pub enum Error {
     /// Multi-Part Payment not supported for unit and method
     #[error("Multi-Part payment is not supported for unit `{0}` and method `{1}`")]
     MppUnitMethodNotSupported(CurrencyUnit, PaymentMethod),
+    /// Clear Auth Required
+    #[error("Clear Auth Required")]
+    ClearAuthRequired,
+    /// Blind Auth Required
+    #[error("Blind Auth Required")]
+    BlindAuthRequired,
+    /// Clear Auth Failed
+    #[error("Clear Auth Failed")]
+    ClearAuthFailed,
+    /// Blind Auth Failed
+    #[error("Blind Auth Failed")]
+    BlindAuthFailed,
+    /// Auth settings undefined
+    #[error("Auth settings undefined")]
+    AuthSettingsUndefined,
+    /// Mint time outside of tolerance
+    #[error("Mint time outside of tolerance")]
+    MintTimeExceedsTolerance,
+    /// Insufficient blind auth tokens
+    #[error("Insufficient blind auth tokens, must reauth")]
+    InsufficientBlindAuthTokens,
+    /// Auth localstore undefined
+    #[error("Auth localstore undefined")]
+    AuthLocalstoreUndefined,
+    /// Wallet cat not set
+    #[error("Wallet cat not set")]
+    CatNotSet,
+    /// Could not get mint info
+    #[error("Could not get mint info")]
+    CouldNotGetMintInfo,
+    /// Multi-Part Payment not supported for unit and method
+    #[error("Amountless invoices are not supported for unit `{0}` and method `{1}`")]
+    AmountlessInvoiceNotSupported(CurrencyUnit, PaymentMethod),
 
     /// Internal Error - Send error
     #[error("Internal send error: {0}")]
@@ -134,6 +167,9 @@ pub enum Error {
     /// Internal Error
     #[error("Internal Error")]
     Internal,
+    /// Oidc config not set
+    #[error("Oidc client not set")]
+    OidcNotSet,
 
     // Wallet Errors
     /// P2PK spending conditions not met
@@ -164,6 +200,9 @@ pub enum Error {
     /// Invalid DLEQ proof
     #[error("Could not verify DLEQ proof")]
     CouldNotVerifyDleq,
+    /// Dleq Proof not provided for signature
+    #[error("Dleq proof not provided for signature")]
+    DleqProofNotProvided,
     /// Incorrect Mint
     /// Token does not match wallet mint
     #[error("Token does not match wallet mint")]
@@ -177,6 +216,9 @@ pub enum Error {
     /// Insufficient Funds
     #[error("Insufficient funds")]
     InsufficientFunds,
+    /// Unexpected proof state
+    #[error("Unexpected proof state")]
+    UnexpectedProofState,
     /// No active keyset
     #[error("No active keyset")]
     NoActiveKeyset,
@@ -186,6 +228,12 @@ pub enum Error {
     /// Invoice Description not supported
     #[error("Invoice Description not supported")]
     InvoiceDescriptionUnsupported,
+    /// Invalid transaction direction
+    #[error("Invalid transaction direction")]
+    InvalidTransactionDirection,
+    /// Invalid transaction id
+    #[error("Invalid transaction id")]
+    InvalidTransactionId,
     /// Custom Error
     #[error("`{0}`")]
     Custom(String),
@@ -218,7 +266,7 @@ pub enum Error {
     /// Http transport error
     #[error("Http transport error: {0}")]
     HttpError(String),
-
+    #[cfg(feature = "wallet")]
     // Crate error conversions
     /// Cashu Url Error
     #[error(transparent)]
@@ -269,13 +317,19 @@ pub enum Error {
     /// NUT20 Error
     #[error(transparent)]
     NUT20(#[from] crate::nuts::nut20::Error),
+    /// NUTXX Error
+    #[error(transparent)]
+    NUT21(#[from] crate::nuts::nut21::Error),
+    /// NUTXX1 Error
+    #[error(transparent)]
+    NUT22(#[from] crate::nuts::nut22::Error),
     /// Database Error
     #[error(transparent)]
-    Database(#[from] crate::database::Error),
-    /// Lightning Error
+    Database(crate::database::Error),
+    /// Payment Error
     #[error(transparent)]
     #[cfg(feature = "mint")]
-    Lightning(#[from] crate::lightning::Error),
+    Payment(#[from] crate::payment::Error),
 }
 
 /// CDK Error Response
@@ -402,6 +456,26 @@ impl From<Error> for ErrorResponse {
                 error: Some(err.to_string()),
                 detail: None,
             },
+            Error::ClearAuthRequired => ErrorResponse {
+                code: ErrorCode::ClearAuthRequired,
+                error: None,
+                detail: None,
+            },
+            Error::ClearAuthFailed => ErrorResponse {
+                code: ErrorCode::ClearAuthFailed,
+                error: None,
+                detail: None,
+            },
+            Error::BlindAuthRequired => ErrorResponse {
+                code: ErrorCode::BlindAuthRequired,
+                error: None,
+                detail: None,
+            },
+            Error::BlindAuthFailed => ErrorResponse {
+                code: ErrorCode::BlindAuthFailed,
+                error: None,
+                detail: None,
+            },
             Error::NUT20(err) => ErrorResponse {
                 code: ErrorCode::WitnessMissingOrInvalid,
                 error: Some(err.to_string()),
@@ -436,6 +510,27 @@ impl From<Error> for ErrorResponse {
     }
 }
 
+#[cfg(feature = "mint")]
+impl From<crate::database::Error> for Error {
+    fn from(db_error: crate::database::Error) -> Self {
+        match db_error {
+            crate::database::Error::InvalidStateTransition(state) => match state {
+                crate::state::Error::Pending => Self::TokenPending,
+                crate::state::Error::AlreadySpent => Self::TokenAlreadySpent,
+                state => Self::Database(crate::database::Error::InvalidStateTransition(state)),
+            },
+            db_error => Self::Database(db_error),
+        }
+    }
+}
+
+#[cfg(not(feature = "mint"))]
+impl From<crate::database::Error> for Error {
+    fn from(db_error: crate::database::Error) -> Self {
+        Self::Database(db_error)
+    }
+}
+
 impl From<ErrorResponse> for Error {
     fn from(err: ErrorResponse) -> Error {
         match err.code {
@@ -461,6 +556,8 @@ impl From<ErrorResponse> for Error {
             ErrorCode::DuplicateOutputs => Self::DuplicateOutputs,
             ErrorCode::MultipleUnits => Self::MultipleUnits,
             ErrorCode::UnitMismatch => Self::UnitMismatch,
+            ErrorCode::ClearAuthRequired => Self::ClearAuthRequired,
+            ErrorCode::BlindAuthRequired => Self::BlindAuthRequired,
             _ => Self::UnknownErrorResponse(err.to_string()),
         }
     }
@@ -512,6 +609,14 @@ pub enum ErrorCode {
     MultipleUnits,
     /// Input unit does not match output
     UnitMismatch,
+    /// Clear Auth Required
+    ClearAuthRequired,
+    /// Clear Auth Failed
+    ClearAuthFailed,
+    /// Blind Auth Required
+    BlindAuthRequired,
+    /// Blind Auth Failed
+    BlindAuthFailed,
     /// Unknown error code
     Unknown(u16),
 }
@@ -541,6 +646,10 @@ impl ErrorCode {
             20006 => Self::InvoiceAlreadyPaid,
             20007 => Self::QuoteExpired,
             20008 => Self::WitnessMissingOrInvalid,
+            30001 => Self::ClearAuthRequired,
+            30002 => Self::ClearAuthFailed,
+            31001 => Self::BlindAuthRequired,
+            31002 => Self::BlindAuthFailed,
             _ => Self::Unknown(code),
         }
     }
@@ -569,6 +678,10 @@ impl ErrorCode {
             Self::InvoiceAlreadyPaid => 20006,
             Self::QuoteExpired => 20007,
             Self::WitnessMissingOrInvalid => 20008,
+            Self::ClearAuthRequired => 30001,
+            Self::ClearAuthFailed => 30002,
+            Self::BlindAuthRequired => 31001,
+            Self::BlindAuthFailed => 31002,
             Self::Unknown(code) => *code,
         }
     }

+ 12 - 11
crates/cdk-common/src/lib.rs

@@ -1,30 +1,31 @@
-//! Cashu shared types and functions.
-//!
 //! This crate is the base foundation to build things that can interact with the CDK (Cashu
 //! Development Kit) and their internal crates.
 //!
 //! This is meant to contain the shared types, traits and common functions that are used across the
 //! internal crates.
 
+#![doc = include_str!("../README.md")]
+#![warn(missing_docs)]
+#![warn(rustdoc::bare_urls)]
+
 pub mod common;
 pub mod database;
 pub mod error;
 #[cfg(feature = "mint")]
-pub mod lightning;
-pub mod pub_sub;
+pub mod mint;
 #[cfg(feature = "mint")]
-pub mod signatory;
+pub mod payment;
+pub mod pub_sub;
 #[cfg(feature = "mint")]
+pub mod state;
 pub mod subscription;
+#[cfg(feature = "wallet")]
+pub mod wallet;
 pub mod ws;
-
 // re-exporting external crates
 pub use bitcoin;
 pub use cashu::amount::{self, Amount};
 pub use cashu::lightning_invoice::{self, Bolt11Invoice};
-#[cfg(feature = "mint")]
-pub use cashu::mint;
 pub use cashu::nuts::{self, *};
-#[cfg(feature = "wallet")]
-pub use cashu::wallet;
-pub use cashu::{dhke, mint_url, secret, util, SECP256K1};
+pub use cashu::{dhke, ensure_cdk, mint_url, secret, util, SECP256K1};
+pub use error::Error;

+ 70 - 2
crates/cashu/src/mint.rs → crates/cdk-common/src/mint.rs

@@ -1,6 +1,8 @@
 //! Mint types
 
 use bitcoin::bip32::DerivationPath;
+use cashu::util::unix_time;
+use cashu::{MeltQuoteBolt11Response, MintQuoteBolt11Response};
 use serde::{Deserialize, Serialize};
 use uuid::Uuid;
 
@@ -26,6 +28,13 @@ pub struct MintQuote {
     pub request_lookup_id: String,
     /// Pubkey
     pub pubkey: Option<PublicKey>,
+    /// Unix time quote was created
+    #[serde(default)]
+    pub created_time: u64,
+    /// Unix time quote was paid
+    pub paid_time: Option<u64>,
+    /// Unix time quote was issued
+    pub issued_time: Option<u64>,
 }
 
 impl MintQuote {
@@ -49,11 +58,14 @@ impl MintQuote {
             expiry,
             request_lookup_id,
             pubkey,
+            created_time: unix_time(),
+            paid_time: None,
+            issued_time: None,
         }
     }
 }
 
-// Melt Quote Info
+/// Melt Quote Info
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct MeltQuote {
     /// Quote id
@@ -78,6 +90,11 @@ pub struct MeltQuote {
     ///
     /// Used for an amountless invoice
     pub msat_to_pay: Option<Amount>,
+    /// Unix time quote was created
+    #[serde(default)]
+    pub created_time: u64,
+    /// Unix time quote was paid
+    pub paid_time: Option<u64>,
 }
 
 impl MeltQuote {
@@ -104,6 +121,8 @@ impl MeltQuote {
             payment_preimage: None,
             request_lookup_id,
             msat_to_pay,
+            created_time: unix_time(),
+            paid_time: None,
         }
     }
 }
@@ -116,7 +135,7 @@ pub struct MintKeySetInfo {
     /// Keyset [`CurrencyUnit`]
     pub unit: CurrencyUnit,
     /// Keyset active or inactive
-    /// Mint will only issue new [`BlindSignature`] on active keysets
+    /// Mint will only issue new signatures on active keysets
     pub active: bool,
     /// Starting unix time Keyset is valid from
     pub valid_from: u64,
@@ -149,3 +168,52 @@ impl From<MintKeySetInfo> for KeySetInfo {
         }
     }
 }
+
+impl From<MintQuote> for MintQuoteBolt11Response<Uuid> {
+    fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt11Response<Uuid> {
+        MintQuoteBolt11Response {
+            quote: mint_quote.id,
+            request: mint_quote.request,
+            state: mint_quote.state,
+            expiry: Some(mint_quote.expiry),
+            pubkey: mint_quote.pubkey,
+            amount: Some(mint_quote.amount),
+            unit: Some(mint_quote.unit.clone()),
+        }
+    }
+}
+
+impl From<&MeltQuote> for MeltQuoteBolt11Response<Uuid> {
+    fn from(melt_quote: &MeltQuote) -> MeltQuoteBolt11Response<Uuid> {
+        MeltQuoteBolt11Response {
+            quote: melt_quote.id,
+            payment_preimage: None,
+            change: None,
+            state: melt_quote.state,
+            paid: Some(melt_quote.state == MeltQuoteState::Paid),
+            expiry: melt_quote.expiry,
+            amount: melt_quote.amount,
+            fee_reserve: melt_quote.fee_reserve,
+            request: None,
+            unit: Some(melt_quote.unit.clone()),
+        }
+    }
+}
+
+impl From<MeltQuote> for MeltQuoteBolt11Response<Uuid> {
+    fn from(melt_quote: MeltQuote) -> MeltQuoteBolt11Response<Uuid> {
+        let paid = melt_quote.state == MeltQuoteState::Paid;
+        MeltQuoteBolt11Response {
+            quote: melt_quote.id,
+            amount: melt_quote.amount,
+            fee_reserve: melt_quote.fee_reserve,
+            paid: Some(paid),
+            state: melt_quote.state,
+            expiry: melt_quote.expiry,
+            payment_preimage: melt_quote.payment_preimage,
+            change: None,
+            request: Some(melt_quote.request.clone()),
+            unit: Some(melt_quote.unit.clone()),
+        }
+    }
+}

+ 57 - 26
crates/cdk-common/src/lightning.rs → crates/cdk-common/src/payment.rs

@@ -3,12 +3,14 @@
 use std::pin::Pin;
 
 use async_trait::async_trait;
+use cashu::MeltOptions;
 use futures::Stream;
-use lightning_invoice::{Bolt11Invoice, ParseOrSemanticError};
+use lightning_invoice::ParseOrSemanticError;
 use serde::{Deserialize, Serialize};
+use serde_json::Value;
 use thiserror::Error;
 
-use crate::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
+use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState};
 use crate::{mint, Amount};
 
 /// CDK Lightning Error
@@ -23,6 +25,9 @@ pub enum Error {
     /// Unsupported unit
     #[error("Unsupported unit")]
     UnsupportedUnit,
+    /// Unsupported payment option
+    #[error("Unsupported payment option")]
+    UnsupportedPaymentOption,
     /// Payment state is unknown
     #[error("Payment state is unknown")]
     UnknownPaymentState,
@@ -41,47 +46,55 @@ pub enum Error {
     /// Amount Error
     #[error(transparent)]
     Amount(#[from] crate::amount::Error),
+    /// NUT04 Error
+    #[error(transparent)]
+    NUT04(#[from] crate::nuts::nut04::Error),
     /// NUT05 Error
     #[error(transparent)]
     NUT05(#[from] crate::nuts::nut05::Error),
+    /// Custom
+    #[error("`{0}`")]
+    Custom(String),
 }
 
-/// MintLighting Trait
+/// Mint payment trait
 #[async_trait]
-pub trait MintLightning {
+pub trait MintPayment {
     /// Mint Lightning Error
     type Err: Into<Error> + From<Error>;
 
-    /// Base Unit
-    fn get_settings(&self) -> Settings;
+    /// Base Settings
+    async fn get_settings(&self) -> Result<serde_json::Value, Self::Err>;
 
     /// Create a new invoice
-    async fn create_invoice(
+    async fn create_incoming_payment_request(
         &self,
         amount: Amount,
         unit: &CurrencyUnit,
         description: String,
-        unix_expiry: u64,
-    ) -> Result<CreateInvoiceResponse, Self::Err>;
+        unix_expiry: Option<u64>,
+    ) -> Result<CreateIncomingPaymentResponse, Self::Err>;
 
     /// Get payment quote
     /// Used to get fee and amount required for a payment request
     async fn get_payment_quote(
         &self,
-        melt_quote_request: &MeltQuoteBolt11Request,
+        request: &str,
+        unit: &CurrencyUnit,
+        options: Option<MeltOptions>,
     ) -> Result<PaymentQuoteResponse, Self::Err>;
 
-    /// Pay bolt11 invoice
-    async fn pay_invoice(
+    /// Pay request
+    async fn make_payment(
         &self,
         melt_quote: mint::MeltQuote,
         partial_amount: Option<Amount>,
         max_fee_amount: Option<Amount>,
-    ) -> Result<PayInvoiceResponse, Self::Err>;
+    ) -> Result<MakePaymentResponse, Self::Err>;
 
     /// Listen for invoices to be paid to the mint
     /// Returns a stream of request_lookup_id once invoices are paid
-    async fn wait_any_invoice(
+    async fn wait_any_incoming_payment(
         &self,
     ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err>;
 
@@ -92,7 +105,7 @@ pub trait MintLightning {
     fn cancel_wait_invoice(&self);
 
     /// Check the status of an incoming payment
-    async fn check_incoming_invoice_status(
+    async fn check_incoming_payment_status(
         &self,
         request_lookup_id: &str,
     ) -> Result<MintQuoteState, Self::Err>;
@@ -101,27 +114,27 @@ pub trait MintLightning {
     async fn check_outgoing_payment(
         &self,
         request_lookup_id: &str,
-    ) -> Result<PayInvoiceResponse, Self::Err>;
+    ) -> Result<MakePaymentResponse, Self::Err>;
 }
 
-/// Create invoice response
+/// Create incoming payment response
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
-pub struct CreateInvoiceResponse {
-    /// Id that is used to look up the invoice from the ln backend
+pub struct CreateIncomingPaymentResponse {
+    /// Id that is used to look up the payment from the ln backend
     pub request_lookup_id: String,
-    /// Bolt11 payment request
-    pub request: Bolt11Invoice,
+    /// Payment request
+    pub request: String,
     /// Unix Expiry of Invoice
     pub expiry: Option<u64>,
 }
 
-/// Pay invoice response
+/// Payment response
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
-pub struct PayInvoiceResponse {
+pub struct MakePaymentResponse {
     /// Payment hash
     pub payment_lookup_id: String,
-    /// Payment Preimage
-    pub payment_preimage: Option<String>,
+    /// Payment proof
+    pub payment_proof: Option<String>,
     /// Status
     pub status: MeltQuoteState,
     /// Total Amount Spent
@@ -145,11 +158,29 @@ pub struct PaymentQuoteResponse {
 
 /// Ln backend settings
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
-pub struct Settings {
+pub struct Bolt11Settings {
     /// MPP supported
     pub mpp: bool,
     /// Base unit of backend
     pub unit: CurrencyUnit,
     /// Invoice Description supported
     pub invoice_description: bool,
+    /// Paying amountless invoices supported
+    pub amountless: bool,
+}
+
+impl TryFrom<Bolt11Settings> for Value {
+    type Error = crate::error::Error;
+
+    fn try_from(value: Bolt11Settings) -> Result<Self, Self::Error> {
+        serde_json::to_value(value).map_err(|err| err.into())
+    }
+}
+
+impl TryFrom<Value> for Bolt11Settings {
+    type Error = crate::error::Error;
+
+    fn try_from(value: Value) -> Result<Self, Self::Error> {
+        serde_json::from_value(value).map_err(|err| err.into())
+    }
 }

+ 4 - 0
crates/cdk-common/src/pub_sub/index.rs

@@ -1,3 +1,7 @@
+//! WS Index
+//!
+//!
+
 use std::fmt::Debug;
 use std::ops::Deref;
 use std::sync::atomic::{AtomicUsize, Ordering};

+ 1 - 1
crates/cdk-common/src/pub_sub/mod.rs

@@ -1,7 +1,7 @@
 //! Publish–subscribe pattern.
 //!
 //! This is a generic implementation for
-//! [NUT-17(https://github.com/cashubtc/nuts/blob/main/17.md) with a type
+//! [NUT-17(<https://github.com/cashubtc/nuts/blob/main/17.md>) with a type
 //! agnostic Publish-subscribe manager.
 //!
 //! The manager has a method for subscribers to subscribe to events with a

+ 0 - 74
crates/cdk-common/src/signatory.rs

@@ -1,74 +0,0 @@
-//! Signatory mod
-//!
-//! This module abstract all the key related operations, defining an interface for the necessary
-//! operations, to be implemented by the different signatory implementations.
-//!
-//! There is an in memory implementation, when the keys are stored in memory, in the same process,
-//! but it is isolated from the rest of the application, and they communicate through a channel with
-//! the defined API.
-use std::collections::HashMap;
-
-use bitcoin::bip32::DerivationPath;
-use cashu::mint::MintKeySetInfo;
-use cashu::{
-    BlindSignature, BlindedMessage, CurrencyUnit, Id, KeySet, KeysResponse, KeysetResponse, Proof,
-};
-
-use super::error::Error;
-
-/// Type alias to make the keyset info API more useful, queryable by unit and Id
-pub enum KeysetIdentifier {
-    /// Mint Keyset by unit
-    Unit(CurrencyUnit),
-    /// Mint Keyset by Id
-    Id(Id),
-}
-
-impl From<Id> for KeysetIdentifier {
-    fn from(id: Id) -> Self {
-        Self::Id(id)
-    }
-}
-
-impl From<CurrencyUnit> for KeysetIdentifier {
-    fn from(unit: CurrencyUnit) -> Self {
-        Self::Unit(unit)
-    }
-}
-
-#[async_trait::async_trait]
-/// Signatory trait
-pub trait Signatory {
-    /// Blind sign a message
-    async fn blind_sign(&self, blinded_message: BlindedMessage) -> Result<BlindSignature, Error>;
-
-    /// Verify [`Proof`] meets conditions and is signed
-    async fn verify_proof(&self, proof: Proof) -> Result<(), Error>;
-
-    /// Retrieve a keyset by id
-    async fn keyset(&self, keyset_id: Id) -> Result<Option<KeySet>, Error>;
-
-    /// Retrieve the public keys of a keyset
-    async fn keyset_pubkeys(&self, keyset_id: Id) -> Result<KeysResponse, Error>;
-
-    /// Retrieve the public keys of the active keyset for distribution to wallet
-    /// clients
-    async fn pubkeys(&self) -> Result<KeysResponse, Error>;
-
-    /// Return a list of all supported keysets
-    async fn keysets(&self) -> Result<KeysetResponse, Error>;
-
-    /// Add current keyset to inactive keysets
-    /// Generate new keyset
-    async fn rotate_keyset(
-        &self,
-        unit: CurrencyUnit,
-        derivation_path_index: u32,
-        max_order: u8,
-        input_fee_ppk: u64,
-        custom_paths: HashMap<CurrencyUnit, DerivationPath>,
-    ) -> Result<MintKeySetInfo, Error>;
-
-    /// Get Mint Keyset Info by Unit or Id
-    async fn get_keyset_info(&self, keyset_id: KeysetIdentifier) -> Result<MintKeySetInfo, Error>;
-}

+ 39 - 0
crates/cdk-common/src/state.rs

@@ -0,0 +1,39 @@
+//! State transition rules
+
+use cashu::State;
+
+/// State transition Error
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+    /// Pending Token
+    #[error("Token already pending for another update")]
+    Pending,
+    /// Already spent
+    #[error("Token already spent")]
+    AlreadySpent,
+    /// Invalid transition
+    #[error("Invalid transition: From {0} to {1}")]
+    InvalidTransition(State, State),
+}
+
+#[inline]
+/// Check if the state transition is allowed
+pub fn check_state_transition(current_state: State, new_state: State) -> Result<(), Error> {
+    let is_valid_transition = match current_state {
+        State::Unspent => matches!(new_state, State::Pending | State::Spent),
+        State::Pending => matches!(new_state, State::Unspent | State::Spent),
+        // Any other state shouldn't be updated by the mint, and the wallet does not use this
+        // function
+        _ => false,
+    };
+
+    if !is_valid_transition {
+        Err(match current_state {
+            State::Pending => Error::Pending,
+            State::Spent => Error::AlreadySpent,
+            _ => Error::InvalidTransition(current_state, new_state),
+        })
+    } else {
+        Ok(())
+    }
+}

+ 13 - 1
crates/cdk-common/src/subscription.rs

@@ -1,11 +1,18 @@
 //! Subscription types and traits
+#[cfg(feature = "mint")]
 use std::str::FromStr;
 
-use cashu::nut17::{self, Error, Kind, Notification};
+use cashu::nut17::{self};
+#[cfg(feature = "mint")]
+use cashu::nut17::{Error, Kind, Notification};
+#[cfg(feature = "mint")]
 use cashu::{NotificationPayload, PublicKey};
+#[cfg(feature = "mint")]
 use serde::{Deserialize, Serialize};
+#[cfg(feature = "mint")]
 use uuid::Uuid;
 
+#[cfg(feature = "mint")]
 use crate::pub_sub::index::{Index, Indexable, SubscriptionGlobalId};
 use crate::pub_sub::SubId;
 
@@ -15,15 +22,18 @@ use crate::pub_sub::SubId;
 pub type Params = nut17::Params<SubId>;
 
 /// Wrapper around `nut17::Params` to implement `Indexable` for `Notification`.
+#[cfg(feature = "mint")]
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct IndexableParams(Params);
 
+#[cfg(feature = "mint")]
 impl From<Params> for IndexableParams {
     fn from(params: Params) -> Self {
         Self(params)
     }
 }
 
+#[cfg(feature = "mint")]
 impl TryFrom<IndexableParams> for Vec<Index<Notification>> {
     type Error = Error;
     fn try_from(params: IndexableParams) -> Result<Self, Self::Error> {
@@ -49,12 +59,14 @@ impl TryFrom<IndexableParams> for Vec<Index<Notification>> {
     }
 }
 
+#[cfg(feature = "mint")]
 impl AsRef<SubId> for IndexableParams {
     fn as_ref(&self) -> &SubId {
         &self.0.id
     }
 }
 
+#[cfg(feature = "mint")]
 impl Indexable for NotificationPayload<Uuid> {
     type Type = Notification;
 

+ 294 - 0
crates/cdk-common/src/wallet.rs

@@ -0,0 +1,294 @@
+//! Wallet Types
+
+use std::collections::HashMap;
+use std::fmt;
+use std::str::FromStr;
+
+use bitcoin::hashes::{sha256, Hash, HashEngine};
+use cashu::util::hex;
+use cashu::{nut00, Proofs, PublicKey};
+use serde::{Deserialize, Serialize};
+
+use crate::mint_url::MintUrl;
+use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState, SecretKey};
+use crate::{Amount, Error};
+
+/// Wallet Key
+#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct WalletKey {
+    /// Mint Url
+    pub mint_url: MintUrl,
+    /// Currency Unit
+    pub unit: CurrencyUnit,
+}
+
+impl fmt::Display for WalletKey {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "mint_url: {}, unit: {}", self.mint_url, self.unit,)
+    }
+}
+
+impl WalletKey {
+    /// Create new [`WalletKey`]
+    pub fn new(mint_url: MintUrl, unit: CurrencyUnit) -> Self {
+        Self { mint_url, unit }
+    }
+}
+
+/// Mint Quote Info
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct MintQuote {
+    /// Quote id
+    pub id: String,
+    /// Mint Url
+    pub mint_url: MintUrl,
+    /// Amount of quote
+    pub amount: Amount,
+    /// Unit of quote
+    pub unit: CurrencyUnit,
+    /// Quote payment request e.g. bolt11
+    pub request: String,
+    /// Quote state
+    pub state: MintQuoteState,
+    /// Expiration time of quote
+    pub expiry: u64,
+    /// Secretkey for signing mint quotes [NUT-20]
+    pub secret_key: Option<SecretKey>,
+}
+
+/// Melt Quote Info
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct MeltQuote {
+    /// Quote id
+    pub id: String,
+    /// Quote unit
+    pub unit: CurrencyUnit,
+    /// Quote amount
+    pub amount: Amount,
+    /// Quote Payment request e.g. bolt11
+    pub request: String,
+    /// Quote fee reserve
+    pub fee_reserve: Amount,
+    /// Quote state
+    pub state: MeltQuoteState,
+    /// Expiration time of quote
+    pub expiry: u64,
+    /// Payment preimage
+    pub payment_preimage: Option<String>,
+}
+
+/// Send Kind
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
+pub enum SendKind {
+    #[default]
+    /// Allow online swap before send if wallet does not have exact amount
+    OnlineExact,
+    /// Prefer offline send if difference is less then tolerance
+    OnlineTolerance(Amount),
+    /// Wallet cannot do an online swap and selected proof must be exactly send amount
+    OfflineExact,
+    /// Wallet must remain offline but can over pay if below tolerance
+    OfflineTolerance(Amount),
+}
+
+impl SendKind {
+    /// Check if send kind is online
+    pub fn is_online(&self) -> bool {
+        matches!(self, Self::OnlineExact | Self::OnlineTolerance(_))
+    }
+
+    /// Check if send kind is offline
+    pub fn is_offline(&self) -> bool {
+        matches!(self, Self::OfflineExact | Self::OfflineTolerance(_))
+    }
+
+    /// Check if send kind is exact
+    pub fn is_exact(&self) -> bool {
+        matches!(self, Self::OnlineExact | Self::OfflineExact)
+    }
+
+    /// Check if send kind has tolerance
+    pub fn has_tolerance(&self) -> bool {
+        matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))
+    }
+}
+
+/// Wallet Transaction
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct Transaction {
+    /// Mint Url
+    pub mint_url: MintUrl,
+    /// Transaction direction
+    pub direction: TransactionDirection,
+    /// Amount
+    pub amount: Amount,
+    /// Fee
+    pub fee: Amount,
+    /// Currency Unit
+    pub unit: CurrencyUnit,
+    /// Proof Ys
+    pub ys: Vec<PublicKey>,
+    /// Unix timestamp
+    pub timestamp: u64,
+    /// Memo
+    pub memo: Option<String>,
+    /// User-defined metadata
+    pub metadata: HashMap<String, String>,
+}
+
+impl Transaction {
+    /// Transaction ID
+    pub fn id(&self) -> TransactionId {
+        TransactionId::new(self.ys.clone())
+    }
+
+    /// Check if transaction matches conditions
+    pub fn matches_conditions(
+        &self,
+        mint_url: &Option<MintUrl>,
+        direction: &Option<TransactionDirection>,
+        unit: &Option<CurrencyUnit>,
+    ) -> bool {
+        if let Some(mint_url) = mint_url {
+            if &self.mint_url != mint_url {
+                return false;
+            }
+        }
+        if let Some(direction) = direction {
+            if &self.direction != direction {
+                return false;
+            }
+        }
+        if let Some(unit) = unit {
+            if &self.unit != unit {
+                return false;
+            }
+        }
+        true
+    }
+}
+
+impl PartialOrd for Transaction {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for Transaction {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.timestamp.cmp(&other.timestamp).reverse()
+    }
+}
+
+/// Transaction Direction
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum TransactionDirection {
+    /// Incoming transaction (i.e., receive or mint)
+    Incoming,
+    /// Outgoing transaction (i.e., send or melt)
+    Outgoing,
+}
+
+impl std::fmt::Display for TransactionDirection {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            TransactionDirection::Incoming => write!(f, "Incoming"),
+            TransactionDirection::Outgoing => write!(f, "Outgoing"),
+        }
+    }
+}
+
+impl FromStr for TransactionDirection {
+    type Err = Error;
+
+    fn from_str(value: &str) -> Result<Self, Self::Err> {
+        match value {
+            "Incoming" => Ok(Self::Incoming),
+            "Outgoing" => Ok(Self::Outgoing),
+            _ => Err(Error::InvalidTransactionDirection),
+        }
+    }
+}
+
+/// Transaction ID
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
+#[serde(transparent)]
+pub struct TransactionId([u8; 32]);
+
+impl TransactionId {
+    /// Create new [`TransactionId`]
+    pub fn new(ys: Vec<PublicKey>) -> Self {
+        let mut ys = ys;
+        ys.sort();
+        let mut hasher = sha256::Hash::engine();
+        for y in ys {
+            hasher.input(&y.to_bytes());
+        }
+        let hash = sha256::Hash::from_engine(hasher);
+        Self(hash.to_byte_array())
+    }
+
+    /// From proofs
+    pub fn from_proofs(proofs: Proofs) -> Result<Self, nut00::Error> {
+        let ys = proofs
+            .iter()
+            .map(|proof| proof.y())
+            .collect::<Result<Vec<PublicKey>, nut00::Error>>()?;
+        Ok(Self::new(ys))
+    }
+
+    /// From bytes
+    pub fn from_bytes(bytes: [u8; 32]) -> Self {
+        Self(bytes)
+    }
+
+    /// From hex string
+    pub fn from_hex(value: &str) -> Result<Self, Error> {
+        let bytes = hex::decode(value)?;
+        let mut array = [0u8; 32];
+        array.copy_from_slice(&bytes);
+        Ok(Self(array))
+    }
+
+    /// From slice
+    pub fn from_slice(slice: &[u8]) -> Result<Self, Error> {
+        if slice.len() != 32 {
+            return Err(Error::InvalidTransactionId);
+        }
+        let mut array = [0u8; 32];
+        array.copy_from_slice(slice);
+        Ok(Self(array))
+    }
+
+    /// Get inner value
+    pub fn as_bytes(&self) -> &[u8; 32] {
+        &self.0
+    }
+
+    /// Get inner value as slice
+    pub fn as_slice(&self) -> &[u8] {
+        &self.0
+    }
+}
+
+impl std::fmt::Display for TransactionId {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", hex::encode(self.0))
+    }
+}
+
+impl FromStr for TransactionId {
+    type Err = Error;
+
+    fn from_str(value: &str) -> Result<Self, Self::Err> {
+        Self::from_hex(value)
+    }
+}
+
+impl TryFrom<Proofs> for TransactionId {
+    type Error = nut00::Error;
+
+    fn try_from(proofs: Proofs) -> Result<Self, Self::Error> {
+        Self::from_proofs(proofs)
+    }
+}

+ 23 - 0
crates/cdk-common/src/ws.rs

@@ -12,19 +12,41 @@ use uuid::Uuid;
 
 use crate::pub_sub::SubId;
 
+/// Request to unsubscribe from a websocket subscription
 pub type WsUnsubscribeRequest = nut17::ws::WsUnsubscribeRequest<SubId>;
+
+/// Notification message sent over websocket
 pub type WsNotification = nut17::ws::WsNotification<SubId>;
+
+/// Response to a subscription request
 pub type WsSubscribeResponse = nut17::ws::WsSubscribeResponse<SubId>;
+
+/// Result part of a websocket response
 pub type WsResponseResult = nut17::ws::WsResponseResult<SubId>;
+
+/// Response to an unsubscribe request
 pub type WsUnsubscribeResponse = nut17::ws::WsUnsubscribeResponse<SubId>;
+
+/// Generic websocket request
 pub type WsRequest = nut17::ws::WsRequest<SubId>;
+
+/// Generic websocket response
 pub type WsResponse = nut17::ws::WsResponse<SubId>;
+
+/// Method-specific websocket request
 pub type WsMethodRequest = nut17::ws::WsMethodRequest<SubId>;
+
+/// Error body for websocket responses
 pub type WsErrorBody = nut17::ws::WsErrorBody;
+
+/// Either a websocket message or a response
 pub type WsMessageOrResponse = nut17::ws::WsMessageOrResponse<SubId>;
+
+/// Inner content of a notification with generic payload type
 pub type NotificationInner<T> = nut17::ws::NotificationInner<T, SubId>;
 
 #[cfg(feature = "mint")]
+/// Converts a notification with UUID identifiers to a notification with string identifiers
 pub fn notification_uuid_to_notification_string(
     notification: NotificationInner<Uuid>,
 ) -> NotificationInner<String> {
@@ -43,6 +65,7 @@ pub fn notification_uuid_to_notification_string(
 }
 
 #[cfg(feature = "mint")]
+/// Converts a notification to a websocket message that can be sent to clients
 pub fn notification_to_ws_message(notification: NotificationInner<Uuid>) -> WsMessageOrResponse {
     nut17::ws::WsMessageOrResponse::Notification(nut17::ws::WsNotification {
         jsonrpc: JSON_RPC_VERSION.to_owned(),

+ 17 - 16
crates/cdk-fake-wallet/Cargo.toml

@@ -1,24 +1,25 @@
 [package]
 name = "cdk-fake-wallet"
-version = "0.7.1"
-edition = "2021"
+version.workspace = true
+edition.workspace = true
 authors = ["CDK Developers"]
-license = "MIT"
+license.workspace = true
 homepage = "https://github.com/cashubtc/cdk"
 repository = "https://github.com/cashubtc/cdk.git"
-rust-version = "1.63.0" # MSRV
+rust-version.workspace = true # MSRV
 description = "CDK fake ln backend"
+readme = "README.md"
 
 [dependencies]
-async-trait = "0.1.74"
-bitcoin = { version = "0.32.2", default-features = false }
-cdk = { path = "../cdk", version = "0.7.1", default-features = false, features = ["mint"] }
-futures = { version = "0.3.28", default-features = false }
-tokio = { version = "1", default-features = false }
-tokio-util = { version = "0.7.11", default-features = false }
-tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
-thiserror = "1"
-serde = "1"
-serde_json = "1"
-lightning-invoice = { version = "0.32.0", features = ["serde", "std"] }
-tokio-stream = "0.1.15"
+async-trait.workspace = true
+bitcoin.workspace = true
+cdk = { workspace = true, features = ["mint"] }
+futures.workspace = true
+tokio.workspace = true
+tokio-util.workspace = true
+tracing.workspace = true
+thiserror.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+lightning-invoice.workspace = true
+tokio-stream.workspace = true

+ 35 - 0
crates/cdk-fake-wallet/README.md

@@ -0,0 +1,35 @@
+# CDK Fake Wallet
+
+[![crates.io](https://img.shields.io/crates/v/cdk-fake-wallet.svg)](https://crates.io/crates/cdk-fake-wallet) [![Documentation](https://docs.rs/cdk-fake-wallet/badge.svg)](https://docs.rs/cdk-fake-wallet)
+
+The CDK Fake Wallet is a component of the [Cashu Development Kit](https://github.com/cashubtc/cdk) that provides a simulated Lightning Network backend for testing Cashu mints.
+
+## Overview
+
+This crate implements the `MintPayment` trait with a fake Lightning backend that automatically completes payments without requiring actual Lightning Network transactions. It's designed for development and testing purposes only.
+
+## Features
+
+- Simulated Lightning Network payments
+- Automatic completion of payment quotes
+- Support for testing mint functionality without real funds
+- Implementation of the standard `MintPayment` interface
+
+## Usage
+
+Add this to your `Cargo.toml`:
+
+```toml
+[dependencies]
+cdk-fake-wallet = "*"
+```
+
+## Warning
+
+**This is for testing purposes only!** 
+
+The fake wallet should never be used in production environments as it does not perform actual Lightning Network transactions. It simply simulates the payment flow by automatically marking invoices as paid.
+
+## License
+
+This project is licensed under the [MIT License](https://github.com/cashubtc/cdk/blob/main/LICENSE).

+ 1 - 1
crates/cdk-fake-wallet/src/error.rs

@@ -16,7 +16,7 @@ pub enum Error {
     NoReceiver,
 }
 
-impl From<Error> for cdk::cdk_lightning::Error {
+impl From<Error> for cdk::cdk_payment::Error {
     fn from(e: Error) -> Self {
         Self::Lightning(Box::new(e))
     }

+ 62 - 44
crates/cdk-fake-wallet/src/lib.rs

@@ -2,9 +2,11 @@
 //!
 //! Used for testing where quotes are auto filled
 
+#![doc = include_str!("../README.md")]
 #![warn(missing_docs)]
 #![warn(rustdoc::bare_urls)]
 
+use std::cmp::max;
 use std::collections::{HashMap, HashSet};
 use std::pin::Pin;
 use std::str::FromStr;
@@ -15,23 +17,25 @@ use async_trait::async_trait;
 use bitcoin::hashes::{sha256, Hash};
 use bitcoin::secp256k1::rand::{thread_rng, Rng};
 use bitcoin::secp256k1::{Secp256k1, SecretKey};
-use cdk::amount::{Amount, MSAT_IN_SAT};
-use cdk::cdk_lightning::{
-    self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings,
+use cdk::amount::{to_unit, Amount};
+use cdk::cdk_payment::{
+    self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
+    PaymentQuoteResponse,
 };
-use cdk::mint;
-use cdk::mint::FeeReserve;
-use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
-use cdk::util::unix_time;
+use cdk::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState};
+use cdk::types::FeeReserve;
+use cdk::{ensure_cdk, mint};
 use error::Error;
 use futures::stream::StreamExt;
 use futures::Stream;
 use lightning_invoice::{Bolt11Invoice, Currency, InvoiceBuilder, PaymentSecret};
 use serde::{Deserialize, Serialize};
+use serde_json::Value;
 use tokio::sync::Mutex;
 use tokio::time;
 use tokio_stream::wrappers::ReceiverStream;
 use tokio_util::sync::CancellationToken;
+use tracing::instrument;
 
 pub mod error;
 
@@ -49,7 +53,7 @@ pub struct FakeWallet {
 }
 
 impl FakeWallet {
-    /// Creat new [`FakeWallet`]
+    /// Create new [`FakeWallet`]
     pub fn new(
         fee_reserve: FeeReserve,
         payment_states: HashMap<String, MeltQuoteState>,
@@ -96,65 +100,80 @@ impl Default for FakeInvoiceDescription {
 }
 
 #[async_trait]
-impl MintLightning for FakeWallet {
-    type Err = cdk_lightning::Error;
+impl MintPayment for FakeWallet {
+    type Err = cdk_payment::Error;
 
-    fn get_settings(&self) -> Settings {
-        Settings {
+    #[instrument(skip_all)]
+    async fn get_settings(&self) -> Result<Value, Self::Err> {
+        Ok(serde_json::to_value(Bolt11Settings {
             mpp: true,
             unit: CurrencyUnit::Msat,
             invoice_description: true,
-        }
+            amountless: false,
+        })?)
     }
 
+    #[instrument(skip_all)]
     fn is_wait_invoice_active(&self) -> bool {
         self.wait_invoice_is_active.load(Ordering::SeqCst)
     }
 
+    #[instrument(skip_all)]
     fn cancel_wait_invoice(&self) {
         self.wait_invoice_cancel_token.cancel()
     }
 
-    async fn wait_any_invoice(
+    #[instrument(skip_all)]
+    async fn wait_any_incoming_payment(
         &self,
     ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
+        tracing::info!("Starting stream for fake invoices");
         let receiver = self.receiver.lock().await.take().ok_or(Error::NoReceiver)?;
         let receiver_stream = ReceiverStream::new(receiver);
         Ok(Box::pin(receiver_stream.map(|label| label)))
     }
 
+    #[instrument(skip_all)]
     async fn get_payment_quote(
         &self,
-        melt_quote_request: &MeltQuoteBolt11Request,
+        request: &str,
+        unit: &CurrencyUnit,
+        options: Option<MeltOptions>,
     ) -> Result<PaymentQuoteResponse, Self::Err> {
-        let amount = melt_quote_request.amount_msat()?;
+        let bolt11 = Bolt11Invoice::from_str(request)?;
+
+        let amount_msat = match options {
+            Some(amount) => amount.amount_msat(),
+            None => bolt11
+                .amount_milli_satoshis()
+                .ok_or(Error::UnknownInvoiceAmount)?
+                .into(),
+        };
 
-        let amount = amount / MSAT_IN_SAT.into();
+        let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
 
         let relative_fee_reserve =
             (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
 
         let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
 
-        let fee = match relative_fee_reserve > absolute_fee_reserve {
-            true => relative_fee_reserve,
-            false => absolute_fee_reserve,
-        };
+        let fee = max(relative_fee_reserve, absolute_fee_reserve);
 
         Ok(PaymentQuoteResponse {
-            request_lookup_id: melt_quote_request.request.payment_hash().to_string(),
+            request_lookup_id: bolt11.payment_hash().to_string(),
             amount,
             fee: fee.into(),
             state: MeltQuoteState::Unpaid,
         })
     }
 
-    async fn pay_invoice(
+    #[instrument(skip_all)]
+    async fn make_payment(
         &self,
         melt_quote: mint::MeltQuote,
         _partial_msats: Option<Amount>,
         _max_fee_msats: Option<Amount>,
-    ) -> Result<PayInvoiceResponse, Self::Err> {
+    ) -> Result<MakePaymentResponse, Self::Err> {
         let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?;
 
         let payment_hash = bolt11.payment_hash().to_string();
@@ -182,30 +201,26 @@ impl MintLightning for FakeWallet {
                 fail.insert(payment_hash.clone());
             }
 
-            if description.pay_err {
-                return Err(Error::UnknownInvoice.into());
-            }
+            ensure_cdk!(!description.pay_err, Error::UnknownInvoice.into());
         }
 
-        Ok(PayInvoiceResponse {
-            payment_preimage: Some("".to_string()),
+        Ok(MakePaymentResponse {
+            payment_proof: Some("".to_string()),
             payment_lookup_id: payment_hash,
             status: payment_status,
-            total_spent: melt_quote.amount,
+            total_spent: melt_quote.amount + 1.into(),
             unit: melt_quote.unit,
         })
     }
 
-    async fn create_invoice(
+    #[instrument(skip_all)]
+    async fn create_incoming_payment_request(
         &self,
         amount: Amount,
         _unit: &CurrencyUnit,
         description: String,
-        unix_expiry: u64,
-    ) -> Result<CreateInvoiceResponse, Self::Err> {
-        let time_now = unix_time();
-        assert!(unix_expiry > time_now);
-
+        _unix_expiry: Option<u64>,
+    ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
         // Since this is fake we just use the amount no matter the unit to create an invoice
         let amount_msat = amount;
 
@@ -231,24 +246,26 @@ impl MintLightning for FakeWallet {
 
         let expiry = invoice.expires_at().map(|t| t.as_secs());
 
-        Ok(CreateInvoiceResponse {
+        Ok(CreateIncomingPaymentResponse {
             request_lookup_id: payment_hash.to_string(),
-            request: invoice,
+            request: invoice.to_string(),
             expiry,
         })
     }
 
-    async fn check_incoming_invoice_status(
+    #[instrument(skip_all)]
+    async fn check_incoming_payment_status(
         &self,
         _request_lookup_id: &str,
     ) -> Result<MintQuoteState, Self::Err> {
         Ok(MintQuoteState::Paid)
     }
 
+    #[instrument(skip_all)]
     async fn check_outgoing_payment(
         &self,
         request_lookup_id: &str,
-    ) -> Result<PayInvoiceResponse, Self::Err> {
+    ) -> Result<MakePaymentResponse, Self::Err> {
         // For fake wallet if the state is not explicitly set default to paid
         let states = self.payment_states.lock().await;
         let status = states.get(request_lookup_id).cloned();
@@ -258,20 +275,21 @@ impl MintLightning for FakeWallet {
         let fail_payments = self.failed_payment_check.lock().await;
 
         if fail_payments.contains(request_lookup_id) {
-            return Err(cdk_lightning::Error::InvoicePaymentPending);
+            return Err(cdk_payment::Error::InvoicePaymentPending);
         }
 
-        Ok(PayInvoiceResponse {
-            payment_preimage: Some("".to_string()),
+        Ok(MakePaymentResponse {
+            payment_proof: Some("".to_string()),
             payment_lookup_id: request_lookup_id.to_string(),
             status,
             total_spent: Amount::ZERO,
-            unit: self.get_settings().unit,
+            unit: CurrencyUnit::Msat,
         })
     }
 }
 
 /// Create fake invoice
+#[instrument]
 pub fn create_fake_invoice(amount_msat: u64, description: String) -> Bolt11Invoice {
     let private_key = SecretKey::from_slice(
         &[

+ 39 - 45
crates/cdk-integration-tests/Cargo.toml

@@ -1,68 +1,62 @@
 [package]
 name = "cdk-integration-tests"
-version = "0.7.0"
-edition = "2021"
+version.workspace = true
+edition.workspace = true
 authors = ["CDK Developers"]
 description = "Core Cashu Development Kit library implementing the Cashu protocol"
-license = "MIT"
+license.workspace = true
 homepage = "https://github.com/cashubtc/cdk"
 repository = "https://github.com/cashubtc/cdk.git"
-rust-version = "1.63.0"                                                            # MSRV
+rust-version.workspace = true                                                            # MSRV
 
 
 [features]
 http_subscription = ["cdk/http_subscription"]
 
 [dependencies]
-async-trait = "0.1"
-axum = "0.6.20"
-rand = "0.8.5"
-bip39 = { version = "2.0", features = ["rand"] }
-anyhow = "1"
-cashu = { path = "../cashu", features = ["mint", "wallet"] }
-cdk = { path = "../cdk", features = ["mint", "wallet"] }
-cdk-cln = { path = "../cdk-cln" }
-cdk-lnd = { path = "../cdk-lnd" }
-cdk-axum = { path = "../cdk-axum" }
-cdk-sqlite = { path = "../cdk-sqlite" }
-cdk-redb = { path = "../cdk-redb" }
-cdk-fake-wallet = { path = "../cdk-fake-wallet" }
-tower-http = { version = "0.4.4", features = ["cors"] }
-futures = { version = "0.3.28", default-features = false, features = [
+async-trait.workspace = true
+axum.workspace = true
+rand.workspace = true
+bip39 = { workspace = true, features = ["rand"] }
+anyhow.workspace = true
+cashu = { workspace = true, features = ["mint", "wallet"] }
+cdk = { workspace = true, features = ["mint", "wallet", "auth"] }
+cdk-cln = { workspace = true }
+cdk-lnd = { workspace = true }
+cdk-axum = { workspace = true }
+cdk-sqlite = { workspace = true }
+cdk-redb = { workspace = true }
+cdk-fake-wallet = { workspace = true }
+futures = { workspace = true, default-features = false, features = [
     "executor",
 ] }
-once_cell = "1.19.0"
-uuid = { version = "=1.12.1", features = ["v4"] }
-serde = "1"
-serde_json = "1"
+once_cell.workspace = true
+uuid.workspace = true
+serde.workspace = true
+serde_json.workspace = true
 # ln-regtest-rs = { path = "../../../../ln-regtest-rs" }
-ln-regtest-rs = { git = "https://github.com/thesimplekid/ln-regtest-rs", rev = "bf09ad6" }
-lightning-invoice = { version = "0.32.0", features = ["serde", "std"] }
-tracing = { version = "0.1", default-features = false, features = [
-    "attributes",
-    "log",
-] }
-tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
+ln-regtest-rs = { git = "https://github.com/thesimplekid/ln-regtest-rs", rev = "ed24716" }
+lightning-invoice.workspace = true
+tracing.workspace = true
+tracing-subscriber.workspace = true
+tokio-tungstenite.workspace = true
+tower-http = { workspace = true, features = ["cors"] }
 tower-service = "0.3.3"
-tokio-tungstenite = "0.24.0"
+reqwest.workspace = true
+bitcoin = "0.32.0"
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-tokio = { version = "1", features = [
-    "rt-multi-thread",
-    "time",
-    "macros",
-    "sync",
-] }
+tokio.workspace = true
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
-tokio = { version = "1", features = ["rt", "macros", "sync", "time"] }
+tokio = { workspace = true, features = ["rt", "macros", "sync", "time"] }
 getrandom = { version = "0.2", features = ["js"] }
-instant = { version = "0.1", features = ["wasm-bindgen", "inaccurate"] }
+instant = { workspace = true, features = ["wasm-bindgen", "inaccurate"] }
 
 [dev-dependencies]
-bip39 = { version = "2.0", features = ["rand"] }
-anyhow = "1"
-cdk = { path = "../cdk", features = ["mint", "wallet"] }
-cdk-axum = { path = "../cdk-axum" }
-cdk-fake-wallet = { path = "../cdk-fake-wallet" }
-tower-http = { version = "0.4.4", features = ["cors"] }
+bip39 = { workspace = true, features = ["rand"] }
+anyhow.workspace = true
+cdk = { workspace = true, features = ["mint", "wallet"] }
+cdk-axum = { workspace = true }
+cdk-fake-wallet = { workspace = true }
+tower-http = { workspace = true, features = ["cors"] }

+ 0 - 33
crates/cdk-integration-tests/src/bin/fake_wallet.rs

@@ -1,33 +0,0 @@
-use std::env;
-
-use anyhow::Result;
-use cdk::cdk_database::mint_memory::MintMemoryDatabase;
-use cdk_integration_tests::init_fake_wallet::start_fake_mint;
-use cdk_integration_tests::init_regtest::get_temp_dir;
-use cdk_redb::MintRedbDatabase;
-use cdk_sqlite::MintSqliteDatabase;
-
-#[tokio::main]
-async fn main() -> Result<()> {
-    let addr = "127.0.0.1";
-    let port = 8086;
-
-    let mint_db_kind = env::var("MINT_DATABASE")?;
-
-    match mint_db_kind.as_str() {
-        "MEMORY" => {
-            start_fake_mint(addr, port, MintMemoryDatabase::default()).await?;
-        }
-        "SQLITE" => {
-            let sqlite_db = MintSqliteDatabase::new(&get_temp_dir().join("mint")).await?;
-            sqlite_db.migrate().await;
-            start_fake_mint(addr, port, sqlite_db).await?;
-        }
-        "REDB" => {
-            let redb_db = MintRedbDatabase::new(&get_temp_dir().join("mint")).unwrap();
-            start_fake_mint(addr, port, redb_db).await?;
-        }
-        _ => panic!("Unknown mint db type: {}", mint_db_kind),
-    };
-    Ok(())
-}

+ 0 - 209
crates/cdk-integration-tests/src/bin/regtest_mint.rs

@@ -1,209 +0,0 @@
-use std::env;
-
-use anyhow::Result;
-use cdk::cdk_database::mint_memory::MintMemoryDatabase;
-use cdk_integration_tests::init_regtest::{
-    create_cln_backend, create_lnd_backend, create_mint, fund_ln, generate_block, get_bitcoin_dir,
-    get_cln_dir, get_lnd_cert_file_path, get_lnd_dir, get_lnd_macaroon_path, get_temp_dir,
-    init_bitcoin_client, init_bitcoind, init_lnd, open_channel, BITCOIN_RPC_PASS, BITCOIN_RPC_USER,
-    LND_ADDR, LND_RPC_ADDR, LND_TWO_ADDR, LND_TWO_RPC_ADDR,
-};
-use cdk_redb::MintRedbDatabase;
-use cdk_sqlite::MintSqliteDatabase;
-use ln_regtest_rs::cln::Clnd;
-use ln_regtest_rs::ln_client::{ClnClient, LightningClient, LndClient};
-use tracing_subscriber::EnvFilter;
-
-const CLN_ADDR: &str = "127.0.0.1:19846";
-const CLN_TWO_ADDR: &str = "127.0.0.1:19847";
-
-#[tokio::main]
-async fn main() -> Result<()> {
-    let default_filter = "debug";
-
-    let sqlx_filter = "sqlx=warn";
-    let hyper_filter = "hyper=warn";
-    let h2_filter = "h2=warn";
-    let rustls_filter = "rustls=warn";
-
-    let env_filter = EnvFilter::new(format!(
-        "{},{},{},{},{}",
-        default_filter, sqlx_filter, hyper_filter, h2_filter, rustls_filter
-    ));
-
-    tracing_subscriber::fmt().with_env_filter(env_filter).init();
-
-    let mut bitcoind = init_bitcoind();
-    bitcoind.start_bitcoind()?;
-
-    let bitcoin_client = init_bitcoin_client()?;
-    bitcoin_client.create_wallet().ok();
-    bitcoin_client.load_wallet()?;
-
-    let new_add = bitcoin_client.get_new_address()?;
-    bitcoin_client.generate_blocks(&new_add, 200).unwrap();
-
-    let cln_one_dir = get_cln_dir("one");
-    let mut clnd = Clnd::new(
-        get_bitcoin_dir(),
-        cln_one_dir.clone(),
-        CLN_ADDR.into(),
-        BITCOIN_RPC_USER.to_string(),
-        BITCOIN_RPC_PASS.to_string(),
-    );
-    clnd.start_clnd()?;
-
-    let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?;
-
-    cln_client.wait_chain_sync().await.unwrap();
-
-    fund_ln(&bitcoin_client, &cln_client).await.unwrap();
-
-    // Create second cln
-    let cln_two_dir = get_cln_dir("two");
-    let mut clnd_two = Clnd::new(
-        get_bitcoin_dir(),
-        cln_two_dir.clone(),
-        CLN_TWO_ADDR.into(),
-        BITCOIN_RPC_USER.to_string(),
-        BITCOIN_RPC_PASS.to_string(),
-    );
-    clnd_two.start_clnd()?;
-
-    let cln_two_client = ClnClient::new(cln_two_dir.clone(), None).await?;
-
-    cln_two_client.wait_chain_sync().await.unwrap();
-
-    fund_ln(&bitcoin_client, &cln_two_client).await.unwrap();
-
-    let lnd_dir = get_lnd_dir("one");
-    println!("{}", lnd_dir.display());
-
-    let mut lnd = init_lnd(lnd_dir.clone(), LND_ADDR, LND_RPC_ADDR).await;
-    lnd.start_lnd().unwrap();
-    tracing::info!("Started lnd node");
-
-    let lnd_client = LndClient::new(
-        format!("https://{}", LND_RPC_ADDR),
-        get_lnd_cert_file_path(&lnd_dir),
-        get_lnd_macaroon_path(&lnd_dir),
-    )
-    .await?;
-
-    lnd_client.wait_chain_sync().await.unwrap();
-
-    fund_ln(&bitcoin_client, &lnd_client).await.unwrap();
-
-    // create second lnd node
-    let lnd_two_dir = get_lnd_dir("two");
-    let mut lnd_two = init_lnd(lnd_two_dir.clone(), LND_TWO_ADDR, LND_TWO_RPC_ADDR).await;
-    lnd_two.start_lnd().unwrap();
-    tracing::info!("Started second lnd node");
-
-    let lnd_two_client = LndClient::new(
-        format!("https://{}", LND_TWO_RPC_ADDR),
-        get_lnd_cert_file_path(&lnd_two_dir),
-        get_lnd_macaroon_path(&lnd_two_dir),
-    )
-    .await?;
-
-    lnd_two_client.wait_chain_sync().await.unwrap();
-
-    fund_ln(&bitcoin_client, &lnd_two_client).await.unwrap();
-
-    // Open channels concurrently
-    // Open channels
-    {
-        open_channel(&cln_client, &lnd_client).await.unwrap();
-        tracing::info!("Opened channel between cln and lnd one");
-        generate_block(&bitcoin_client)?;
-        // open_channel(&bitcoin_client, &cln_client, &cln_two_client)
-        //     .await
-        //     .unwrap();
-        // tracing::info!("Opened channel between cln and cln two");
-
-        open_channel(&lnd_client, &lnd_two_client).await.unwrap();
-        tracing::info!("Opened channel between lnd and lnd two");
-        generate_block(&bitcoin_client)?;
-
-        // open_channel(&cln_client, &lnd_two_client).await.unwrap();
-        // tracing::info!("Opened channel between cln and lnd two");
-        open_channel(&cln_two_client, &lnd_client).await.unwrap();
-        tracing::info!("Opened channel between cln two and lnd");
-        generate_block(&bitcoin_client)?;
-
-        open_channel(&cln_client, &lnd_two_client).await.unwrap();
-        tracing::info!("Opened channel between cln and lnd two");
-        generate_block(&bitcoin_client)?;
-
-        cln_client.wait_channels_active().await?;
-        cln_two_client.wait_channels_active().await?;
-        lnd_client.wait_channels_active().await?;
-        lnd_two_client.wait_channels_active().await?;
-    }
-
-    let mint_addr = "127.0.0.1";
-    let cln_mint_port = 8085;
-
-    let mint_db_kind = env::var("MINT_DATABASE")?;
-
-    let lnd_mint_db_path = get_temp_dir().join("lnd_mint");
-    let cln_mint_db_path = get_temp_dir().join("cln_mint");
-
-    let cln_backend = create_cln_backend(&cln_client).await?;
-    let lnd_mint_port = 8087;
-
-    let lnd_backend = create_lnd_backend(&lnd_two_client).await?;
-
-    match mint_db_kind.as_str() {
-        "MEMORY" => {
-            tokio::spawn(async move {
-                create_mint(
-                    mint_addr,
-                    cln_mint_port,
-                    MintMemoryDatabase::default(),
-                    cln_backend,
-                )
-                .await
-                .expect("Could not start cln mint");
-            });
-
-            create_mint(
-                mint_addr,
-                lnd_mint_port,
-                MintMemoryDatabase::default(),
-                lnd_backend,
-            )
-            .await?;
-        }
-        "SQLITE" => {
-            tokio::spawn(async move {
-                let cln_sqlite_db = MintSqliteDatabase::new(&cln_mint_db_path)
-                    .await
-                    .expect("Could not create CLN mint db");
-                cln_sqlite_db.migrate().await;
-                create_mint(mint_addr, cln_mint_port, cln_sqlite_db, cln_backend)
-                    .await
-                    .expect("Could not start cln mint");
-            });
-
-            let lnd_sqlite_db = MintSqliteDatabase::new(&lnd_mint_db_path).await?;
-            lnd_sqlite_db.migrate().await;
-            create_mint(mint_addr, lnd_mint_port, lnd_sqlite_db, lnd_backend).await?;
-        }
-        "REDB" => {
-            tokio::spawn(async move {
-                let cln_redb_db = MintRedbDatabase::new(&cln_mint_db_path).unwrap();
-                create_mint(mint_addr, cln_mint_port, cln_redb_db, cln_backend)
-                    .await
-                    .expect("Could not start cln mint");
-            });
-
-            let lnd_redb_db = MintRedbDatabase::new(&lnd_mint_db_path).unwrap();
-            create_mint(mint_addr, lnd_mint_port, lnd_redb_db, lnd_backend).await?;
-        }
-        _ => panic!("Unknown mint db type: {}", mint_db_kind),
-    };
-
-    Ok(())
-}

+ 63 - 0
crates/cdk-integration-tests/src/bin/start_regtest.rs

@@ -0,0 +1,63 @@
+use std::fs::OpenOptions;
+use std::io::Write;
+use std::sync::Arc;
+use std::time::Duration;
+
+use anyhow::{bail, Result};
+use cdk_integration_tests::init_regtest::{get_temp_dir, start_regtest_end};
+use tokio::signal;
+use tokio::sync::{oneshot, Notify};
+use tokio::time::timeout;
+use tracing_subscriber::EnvFilter;
+
+fn signal_progress() {
+    let temp_dir = get_temp_dir();
+    let mut pipe = OpenOptions::new()
+        .write(true)
+        .open(temp_dir.join("progress_pipe"))
+        .expect("Failed to open pipe");
+
+    pipe.write_all(b"checkpoint1\n")
+        .expect("Failed to write to pipe");
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    let default_filter = "debug";
+
+    let sqlx_filter = "sqlx=warn";
+    let hyper_filter = "hyper=warn";
+    let h2_filter = "h2=warn";
+    let rustls_filter = "rustls=warn";
+
+    let env_filter = EnvFilter::new(format!(
+        "{},{},{},{},{}",
+        default_filter, sqlx_filter, hyper_filter, h2_filter, rustls_filter
+    ));
+
+    tracing_subscriber::fmt().with_env_filter(env_filter).init();
+
+    let shutdown_regtest = Arc::new(Notify::new());
+    let shutdown_clone = shutdown_regtest.clone();
+    let (tx, rx) = oneshot::channel();
+    tokio::spawn(async move {
+        start_regtest_end(tx, shutdown_clone)
+            .await
+            .expect("Error starting regtest");
+    });
+
+    match timeout(Duration::from_secs(300), rx).await {
+        Ok(_) => {
+            tracing::info!("Regtest set up");
+            signal_progress();
+        }
+        Err(_) => {
+            tracing::error!("regtest setup timed out after 5 minutes");
+            bail!("Could not set up regtest");
+        }
+    }
+
+    signal::ctrl_c().await?;
+
+    Ok(())
+}

+ 106 - 0
crates/cdk-integration-tests/src/init_auth_mint.rs

@@ -0,0 +1,106 @@
+use std::collections::{HashMap, HashSet};
+use std::sync::Arc;
+
+use anyhow::Result;
+use bip39::Mnemonic;
+use cashu::{AuthRequired, Method, ProtectedEndpoint, RoutePath};
+use cdk::cdk_database::{self, MintAuthDatabase, MintDatabase, MintKeysDatabase};
+use cdk::mint::{MintBuilder, MintMeltLimits};
+use cdk::nuts::{CurrencyUnit, PaymentMethod};
+use cdk::types::FeeReserve;
+use cdk::wallet::AuthWallet;
+use cdk_fake_wallet::FakeWallet;
+
+pub async fn start_fake_mint_with_auth<D, A, K>(
+    _addr: &str,
+    _port: u16,
+    openid_discovery: String,
+    database: D,
+    auth_database: A,
+    key_store: K,
+) -> Result<()>
+where
+    D: MintDatabase<cdk_database::Error> + Send + Sync + 'static,
+    A: MintAuthDatabase<Err = cdk_database::Error> + Send + Sync + 'static,
+    K: MintKeysDatabase<Err = cdk_database::Error> + Send + Sync + 'static,
+{
+    let fee_reserve = FeeReserve {
+        min_fee_reserve: 1.into(),
+        percent_fee_reserve: 1.0,
+    };
+
+    let fake_wallet = FakeWallet::new(fee_reserve, HashMap::default(), HashSet::default(), 0);
+
+    let mut mint_builder = MintBuilder::new();
+
+    mint_builder = mint_builder
+        .with_localstore(Arc::new(database))
+        .with_keystore(Arc::new(key_store));
+
+    mint_builder = mint_builder
+        .add_ln_backend(
+            CurrencyUnit::Sat,
+            PaymentMethod::Bolt11,
+            MintMeltLimits::new(1, 300),
+            Arc::new(fake_wallet),
+        )
+        .await?;
+
+    mint_builder =
+        mint_builder.set_clear_auth_settings(openid_discovery, "cashu-client".to_string());
+
+    mint_builder = mint_builder.set_blind_auth_settings(50);
+
+    let blind_auth_endpoints = vec![
+        ProtectedEndpoint::new(Method::Post, RoutePath::MintQuoteBolt11),
+        ProtectedEndpoint::new(Method::Post, RoutePath::MintBolt11),
+        ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
+        ProtectedEndpoint::new(Method::Post, RoutePath::MeltQuoteBolt11),
+        ProtectedEndpoint::new(Method::Get, RoutePath::MeltQuoteBolt11),
+        ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt11),
+        ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
+        ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate),
+        ProtectedEndpoint::new(Method::Post, RoutePath::Restore),
+    ];
+
+    let blind_auth_endpoints =
+        blind_auth_endpoints
+            .into_iter()
+            .fold(HashMap::new(), |mut acc, e| {
+                acc.insert(e, AuthRequired::Blind);
+                acc
+            });
+
+    auth_database
+        .add_protected_endpoints(blind_auth_endpoints)
+        .await?;
+
+    let mut clear_auth_endpoint = HashMap::new();
+    clear_auth_endpoint.insert(
+        ProtectedEndpoint::new(Method::Post, RoutePath::MintBlindAuth),
+        AuthRequired::Clear,
+    );
+
+    auth_database
+        .add_protected_endpoints(clear_auth_endpoint)
+        .await?;
+
+    mint_builder = mint_builder.with_auth_localstore(Arc::new(auth_database));
+
+    let mnemonic = Mnemonic::generate(12)?;
+
+    mint_builder = mint_builder
+        .with_description("fake test mint".to_string())
+        .with_seed(mnemonic.to_seed_normalized("").to_vec());
+
+    let _mint = mint_builder.build().await?;
+
+    todo!("Need to start this a cdk mintd keeping as ref for now");
+}
+
+pub async fn top_up_blind_auth_proofs(auth_wallet: Arc<AuthWallet>, count: u64) {
+    let _proofs = auth_wallet
+        .mint_blind_auth(count.into())
+        .await
+        .expect("could not mint blind auth");
+}

+ 0 - 83
crates/cdk-integration-tests/src/init_fake_wallet.rs

@@ -1,83 +0,0 @@
-use std::collections::{HashMap, HashSet};
-use std::sync::Arc;
-
-use anyhow::Result;
-use bip39::Mnemonic;
-use cdk::cdk_database::{self, MintDatabase};
-use cdk::mint::{FeeReserve, MintBuilder, MintMeltLimits};
-use cdk::nuts::{CurrencyUnit, PaymentMethod};
-use cdk::types::QuoteTTL;
-use cdk_fake_wallet::FakeWallet;
-use tracing_subscriber::EnvFilter;
-
-use crate::init_mint::start_mint;
-
-pub async fn start_fake_mint<D>(addr: &str, port: u16, database: D) -> Result<()>
-where
-    D: MintDatabase<Err = cdk_database::Error> + Send + Sync + 'static,
-{
-    let default_filter = "debug";
-
-    let sqlx_filter = "sqlx=warn";
-    let hyper_filter = "hyper=warn";
-
-    let env_filter = EnvFilter::new(format!(
-        "{},{},{}",
-        default_filter, sqlx_filter, hyper_filter
-    ));
-
-    // Parse input
-    tracing_subscriber::fmt().with_env_filter(env_filter).init();
-
-    let fee_reserve = FeeReserve {
-        min_fee_reserve: 1.into(),
-        percent_fee_reserve: 0.0,
-    };
-
-    let fake_wallet = FakeWallet::new(fee_reserve, HashMap::default(), HashSet::default(), 0);
-
-    let mut mint_builder = MintBuilder::new();
-
-    let localstore = Arc::new(database);
-    mint_builder = mint_builder.with_localstore(localstore.clone());
-
-    mint_builder = mint_builder.add_ln_backend(
-        CurrencyUnit::Sat,
-        PaymentMethod::Bolt11,
-        MintMeltLimits::new(1, 5_000),
-        Arc::new(fake_wallet),
-    );
-
-    let fee_reserve = FeeReserve {
-        min_fee_reserve: 1.into(),
-        percent_fee_reserve: 0.0,
-    };
-
-    let fake_wallet = FakeWallet::new(fee_reserve, HashMap::default(), HashSet::default(), 0);
-
-    mint_builder = mint_builder.add_ln_backend(
-        CurrencyUnit::Usd,
-        PaymentMethod::Bolt11,
-        MintMeltLimits::new(1, 5_000),
-        Arc::new(fake_wallet),
-    );
-
-    let mnemonic = Mnemonic::generate(12)?;
-
-    mint_builder = mint_builder
-        .with_name("fake test mint".to_string())
-        .with_description("fake test mint".to_string())
-        .with_seed(mnemonic.to_seed_normalized("").to_vec());
-
-    localstore
-        .set_mint_info(mint_builder.mint_info.clone())
-        .await?;
-    let quote_ttl = QuoteTTL::new(10000, 10000);
-    localstore.set_quote_ttl(quote_ttl).await?;
-
-    let mint = mint_builder.build().await?;
-
-    start_mint(addr, port, mint).await?;
-
-    Ok(())
-}

+ 0 - 37
crates/cdk-integration-tests/src/init_mint.rs

@@ -1,37 +0,0 @@
-use std::sync::Arc;
-
-use anyhow::Result;
-use axum::Router;
-use cdk::mint::Mint;
-use tokio::sync::Notify;
-use tower_http::cors::CorsLayer;
-use tracing::instrument;
-
-#[instrument(skip_all)]
-pub async fn start_mint(addr: &str, port: u16, mint: Mint) -> Result<()> {
-    let mint_arc = Arc::new(mint);
-
-    let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc))
-        .await
-        .unwrap();
-
-    let mint_service = Router::new()
-        .merge(v1_service)
-        .layer(CorsLayer::permissive());
-
-    let mint = Arc::clone(&mint_arc);
-
-    let shutdown = Arc::new(Notify::new());
-
-    tokio::spawn({
-        let shutdown = Arc::clone(&shutdown);
-        async move { mint.wait_for_paid_invoices(shutdown).await }
-    });
-
-    tracing::info!("Staring Axum server");
-    axum::Server::bind(&format!("{}:{}", addr, port).as_str().parse().unwrap())
-        .serve(mint_service.into_make_service())
-        .await?;
-
-    Ok(())
-}

+ 155 - 41
crates/cdk-integration-tests/src/init_pure_tests.rs

@@ -1,14 +1,16 @@
 use std::collections::{HashMap, HashSet};
 use std::fmt::{Debug, Formatter};
+use std::path::PathBuf;
 use std::str::FromStr;
 use std::sync::Arc;
+use std::{env, fs};
 
+use anyhow::{anyhow, bail, Result};
 use async_trait::async_trait;
 use bip39::Mnemonic;
 use cdk::amount::SplitTarget;
-use cdk::cdk_database::mint_memory::MintMemoryDatabase;
-use cdk::cdk_database::{MintDatabase, WalletMemoryDatabase};
-use cdk::mint::{FeeReserve, MintBuilder, MintMeltLimits};
+use cdk::cdk_database::{self, WalletDatabase};
+use cdk::mint::{MintBuilder, MintMeltLimits};
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{
     CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysetResponse,
@@ -16,25 +18,28 @@ use cdk::nuts::{
     MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, PaymentMethod,
     RestoreRequest, RestoreResponse, SwapRequest, SwapResponse,
 };
-use cdk::types::QuoteTTL;
+use cdk::types::{FeeReserve, QuoteTTL};
 use cdk::util::unix_time;
-use cdk::wallet::client::MintConnector;
-use cdk::wallet::Wallet;
+use cdk::wallet::{AuthWallet, MintConnector, Wallet, WalletBuilder};
 use cdk::{Amount, Error, Mint};
 use cdk_fake_wallet::FakeWallet;
-use tokio::sync::Notify;
+use tokio::sync::{Notify, RwLock};
 use tracing_subscriber::EnvFilter;
 use uuid::Uuid;
 
 use crate::wait_for_mint_to_be_paid;
 
 pub struct DirectMintConnection {
-    pub mint: Arc<Mint>,
+    pub mint: Mint,
+    auth_wallet: Arc<RwLock<Option<AuthWallet>>>,
 }
 
 impl DirectMintConnection {
-    pub fn new(mint: Arc<Mint>) -> Self {
-        Self { mint }
+    pub fn new(mint: Mint) -> Self {
+        Self {
+            mint,
+            auth_wallet: Arc::new(RwLock::new(None)),
+        }
     }
 }
 
@@ -141,9 +146,21 @@ impl MintConnector for DirectMintConnection {
     async fn post_restore(&self, request: RestoreRequest) -> Result<RestoreResponse, Error> {
         self.mint.restore(request).await
     }
+
+    /// Get the auth wallet for the client
+    async fn get_auth_wallet(&self) -> Option<AuthWallet> {
+        self.auth_wallet.read().await.clone()
+    }
+
+    /// Set auth wallet on client
+    async fn set_auth_wallet(&self, wallet: Option<AuthWallet>) {
+        let mut auth_wallet = self.auth_wallet.write().await;
+
+        *auth_wallet = wallet;
+    }
 }
 
-pub async fn create_and_start_test_mint() -> anyhow::Result<Arc<Mint>> {
+pub fn setup_tracing() {
     let default_filter = "debug";
 
     let sqlx_filter = "sqlx=warn";
@@ -154,41 +171,86 @@ pub async fn create_and_start_test_mint() -> anyhow::Result<Arc<Mint>> {
         default_filter, sqlx_filter, hyper_filter
     ));
 
-    tracing_subscriber::fmt().with_env_filter(env_filter).init();
-
-    let mut mint_builder = MintBuilder::new();
-
-    let database = MintMemoryDatabase::default();
+    // Ok if successful, Err if already initialized
+    // Allows us to setup tracing at the start of several parallel tests
+    let _ = tracing_subscriber::fmt()
+        .with_env_filter(env_filter)
+        .try_init();
+}
 
-    let localstore = Arc::new(database);
-    mint_builder = mint_builder.with_localstore(localstore.clone());
+pub async fn create_and_start_test_mint() -> Result<Mint> {
+    // Read environment variable to determine database type
+    let db_type = env::var("CDK_TEST_DB_TYPE").expect("Database type set");
+
+    let mut mint_builder = match db_type.to_lowercase().as_str() {
+        "sqlite" => {
+            // Create a temporary directory for SQLite database
+            let temp_dir = create_temp_dir("cdk-test-sqlite-mint")?;
+            let path = temp_dir.join("mint.db").to_str().unwrap().to_string();
+            let database = Arc::new(
+                cdk_sqlite::MintSqliteDatabase::new(&path)
+                    .await
+                    .expect("Could not create sqlite db"),
+            );
+            MintBuilder::new()
+                .with_localstore(database.clone())
+                .with_keystore(database)
+        }
+        "redb" => {
+            // Create a temporary directory for ReDB database
+            let temp_dir = create_temp_dir("cdk-test-redb-mint")?;
+            let path = temp_dir.join("mint.redb");
+            let database = Arc::new(
+                cdk_redb::MintRedbDatabase::new(&path)
+                    .expect("Could not create redb mint database"),
+            );
+            MintBuilder::new()
+                .with_localstore(database.clone())
+                .with_keystore(database)
+        }
+        "memory" => MintBuilder::new()
+            .with_localstore(Arc::new(cdk_sqlite::mint::memory::empty().await?))
+            .with_keystore(Arc::new(cdk_sqlite::mint::memory::empty().await?)),
+        _ => {
+            bail!("Db type not set")
+        }
+    };
 
     let fee_reserve = FeeReserve {
         min_fee_reserve: 1.into(),
         percent_fee_reserve: 1.0,
     };
 
-    let ln_fake_backend = Arc::new(FakeWallet::new(
+    let ln_fake_backend = FakeWallet::new(
         fee_reserve.clone(),
         HashMap::default(),
         HashSet::default(),
         0,
-    ));
-
-    mint_builder = mint_builder.add_ln_backend(
-        CurrencyUnit::Sat,
-        PaymentMethod::Bolt11,
-        MintMeltLimits::new(1, 1_000),
-        ln_fake_backend,
     );
 
+    mint_builder = mint_builder
+        .add_ln_backend(
+            CurrencyUnit::Sat,
+            PaymentMethod::Bolt11,
+            MintMeltLimits::new(1, 10_000),
+            Arc::new(ln_fake_backend),
+        )
+        .await?;
+
     let mnemonic = Mnemonic::generate(12)?;
 
     mint_builder = mint_builder
         .with_name("pure test mint".to_string())
         .with_description("pure test mint".to_string())
+        .with_urls(vec!["https://aaa".to_string()])
         .with_seed(mnemonic.to_seed_normalized("").to_vec());
 
+    let localstore = mint_builder
+        .localstore
+        .as_ref()
+        .map(|x| x.clone())
+        .expect("localstore");
+
     localstore
         .set_mint_info(mint_builder.mint_info.clone())
         .await?;
@@ -197,42 +259,94 @@ pub async fn create_and_start_test_mint() -> anyhow::Result<Arc<Mint>> {
 
     let mint = mint_builder.build().await?;
 
-    let mint_arc = Arc::new(mint);
-
-    let mint_arc_clone = Arc::clone(&mint_arc);
+    let mint_clone = mint.clone();
     let shutdown = Arc::new(Notify::new());
     tokio::spawn({
         let shutdown = Arc::clone(&shutdown);
-        async move { mint_arc_clone.wait_for_paid_invoices(shutdown).await }
+        async move { mint_clone.wait_for_paid_invoices(shutdown).await }
     });
 
-    Ok(mint_arc)
+    Ok(mint)
 }
 
-pub fn create_test_wallet_for_mint(mint: Arc<Mint>) -> anyhow::Result<Arc<Wallet>> {
-    let connector = DirectMintConnection::new(mint);
+pub async fn create_test_wallet_for_mint(mint: Mint) -> Result<Wallet> {
+    let connector = DirectMintConnection::new(mint.clone());
+
+    let mint_info = mint.mint_info().await?;
+    let mint_url = mint_info
+        .urls
+        .as_ref()
+        .ok_or(anyhow!("Test mint URLs list is unset"))?
+        .first()
+        .ok_or(anyhow!("Test mint has empty URLs list"))?;
 
     let seed = Mnemonic::generate(12)?.to_seed_normalized("");
-    let mint_url = "http://aa".to_string();
     let unit = CurrencyUnit::Sat;
-    let localstore = WalletMemoryDatabase::default();
-    let mut wallet = Wallet::new(&mint_url, unit, Arc::new(localstore), &seed, None)?;
-
-    wallet.set_client(connector);
 
-    Ok(Arc::new(wallet))
+    // Read environment variable to determine database type
+    let db_type = env::var("CDK_TEST_DB_TYPE").expect("Database type set");
+
+    let localstore: Arc<dyn WalletDatabase<Err = cdk_database::Error> + Send + Sync> =
+        match db_type.to_lowercase().as_str() {
+            "sqlite" => {
+                // Create a temporary directory for SQLite database
+                let temp_dir = create_temp_dir("cdk-test-sqlite-wallet")?;
+                let path = temp_dir.join("wallet.db").to_str().unwrap().to_string();
+                let database = cdk_sqlite::WalletSqliteDatabase::new(&path)
+                    .await
+                    .expect("Could not create sqlite db");
+                Arc::new(database)
+            }
+            "redb" => {
+                // Create a temporary directory for ReDB database
+                let temp_dir = create_temp_dir("cdk-test-redb-wallet")?;
+                let path = temp_dir.join("wallet.redb");
+                let database = cdk_redb::WalletRedbDatabase::new(&path)
+                    .expect("Could not create redb mint database");
+                Arc::new(database)
+            }
+            "memory" => {
+                let database = cdk_sqlite::wallet::memory::empty().await?;
+                Arc::new(database)
+            }
+            _ => {
+                bail!("Db type not set")
+            }
+        };
+
+    let wallet = WalletBuilder::new()
+        .mint_url(mint_url.parse().unwrap())
+        .unit(unit)
+        .localstore(localstore)
+        .seed(&seed)
+        .client(connector)
+        .build()?;
+
+    Ok(wallet)
 }
 
 /// Creates a mint quote for the given amount and checks its state in a loop. Returns when
 /// amount is minted.
-pub async fn fund_wallet(wallet: Arc<Wallet>, amount: u64) -> anyhow::Result<Amount> {
+/// Creates a temporary directory with a unique name based on the prefix
+fn create_temp_dir(prefix: &str) -> Result<PathBuf> {
+    let temp_dir = env::temp_dir();
+    let unique_dir = temp_dir.join(format!("{}-{}", prefix, Uuid::new_v4()));
+    fs::create_dir_all(&unique_dir)?;
+    Ok(unique_dir)
+}
+
+pub async fn fund_wallet(
+    wallet: Wallet,
+    amount: u64,
+    split_target: Option<SplitTarget>,
+) -> Result<Amount> {
     let desired_amount = Amount::from(amount);
     let quote = wallet.mint_quote(desired_amount, None).await?;
 
     wait_for_mint_to_be_paid(&wallet, &quote.id, 60).await?;
 
     Ok(wallet
-        .mint(&quote.id, SplitTarget::default(), None)
+        .mint(&quote.id, split_target.unwrap_or_default(), None)
         .await?
         .total_amount()?)
 }

+ 130 - 62
crates/cdk-integration-tests/src/init_regtest.rs

@@ -1,24 +1,18 @@
-use std::collections::HashMap;
 use std::env;
 use std::path::{Path, PathBuf};
 use std::sync::Arc;
 
 use anyhow::Result;
-use bip39::Mnemonic;
-use cdk::cdk_database::{self, MintDatabase};
-use cdk::cdk_lightning::{self, MintLightning};
-use cdk::mint::{FeeReserve, MemorySignatory, MintBuilder, MintMeltLimits};
-use cdk::nuts::{CurrencyUnit, PaymentMethod};
-use cdk::types::QuoteTTL;
+use cdk::types::FeeReserve;
 use cdk_cln::Cln as CdkCln;
 use cdk_lnd::Lnd as CdkLnd;
 use ln_regtest_rs::bitcoin_client::BitcoinClient;
 use ln_regtest_rs::bitcoind::Bitcoind;
+use ln_regtest_rs::cln::Clnd;
 use ln_regtest_rs::ln_client::{ClnClient, LightningClient, LndClient};
 use ln_regtest_rs::lnd::Lnd;
-use tracing::instrument;
-
-use crate::init_mint::start_mint;
+use tokio::sync::oneshot::Sender;
+use tokio::sync::Notify;
 
 pub const BITCOIND_ADDR: &str = "127.0.0.1:18443";
 pub const ZMQ_RAW_BLOCK: &str = "tcp://127.0.0.1:28332";
@@ -34,12 +28,15 @@ pub const LND_RPC_ADDR: &str = "localhost:10009";
 pub const LND_TWO_ADDR: &str = "0.0.0.0:18410";
 pub const LND_TWO_RPC_ADDR: &str = "localhost:10010";
 
+pub const CLN_ADDR: &str = "127.0.0.1:19846";
+pub const CLN_TWO_ADDR: &str = "127.0.0.1:19847";
+
 pub fn get_mint_addr() -> String {
-    env::var("cdk_itests_mint_addr").expect("Temp dir set")
+    env::var("CDK_ITESTS_MINT_ADDR").expect("Mint address not set")
 }
 
 pub fn get_mint_port(which: &str) -> u16 {
-    let dir = env::var(format!("cdk_itests_mint_port_{}", which)).expect("Temp dir set");
+    let dir = env::var(format!("CDK_ITESTS_MINT_PORT_{}", which)).expect("Mint port not set");
     dir.parse().unwrap()
 }
 
@@ -52,7 +49,7 @@ pub fn get_mint_ws_url(which: &str) -> String {
 }
 
 pub fn get_temp_dir() -> PathBuf {
-    let dir = env::var("cdk_itests").expect("Temp dir set");
+    let dir = env::var("CDK_ITESTS_DIR").expect("Temp dir not set");
     std::fs::create_dir_all(&dir).unwrap();
     dir.parse().expect("Valid path buf")
 }
@@ -150,55 +147,6 @@ pub async fn create_lnd_backend(lnd_client: &LndClient) -> Result<CdkLnd> {
     .await?)
 }
 
-#[instrument(skip_all)]
-pub async fn create_mint<D, L>(addr: &str, port: u16, database: D, lighting: L) -> Result<()>
-where
-    D: MintDatabase<Err = cdk_database::Error> + Send + Sync + 'static,
-    L: MintLightning<Err = cdk_lightning::Error> + Send + Sync + 'static,
-{
-    let mut mint_builder = MintBuilder::new();
-
-    let localstore = Arc::new(database);
-
-    mint_builder = mint_builder.with_localstore(localstore.clone());
-
-    mint_builder = mint_builder.add_ln_backend(
-        CurrencyUnit::Sat,
-        PaymentMethod::Bolt11,
-        MintMeltLimits::new(1, 5_000),
-        Arc::new(lighting),
-    );
-
-    let mnemonic = Mnemonic::generate(12)?;
-
-    let signatory_manager = MemorySignatory::new(
-        localstore.clone(),
-        &mnemonic.to_seed_normalized(""),
-        mint_builder.supported_units.clone(),
-        HashMap::new(),
-    )
-    .await
-    .expect("valid signatory");
-
-    mint_builder = mint_builder
-        .with_name("regtest mint".to_string())
-        .with_signatory(Arc::new(signatory_manager))
-        .with_description("regtest mint".to_string())
-        .with_seed(mnemonic.to_seed_normalized("").to_vec());
-
-    let mint = mint_builder.build().await?;
-
-    localstore
-        .set_mint_info(mint_builder.mint_info.clone())
-        .await?;
-    let quote_ttl = QuoteTTL::new(10000, 10000);
-    localstore.set_quote_ttl(quote_ttl).await?;
-
-    start_mint(addr, port, mint).await?;
-
-    Ok(())
-}
-
 pub async fn fund_ln<C>(bitcoin_client: &BitcoinClient, ln_client: &C) -> Result<()>
 where
     C: LightningClient,
@@ -243,3 +191,123 @@ where
 
     Ok(())
 }
+
+pub async fn start_regtest_end(sender: Sender<()>, notify: Arc<Notify>) -> anyhow::Result<()> {
+    let mut bitcoind = init_bitcoind();
+    bitcoind.start_bitcoind()?;
+
+    let bitcoin_client = init_bitcoin_client()?;
+    bitcoin_client.create_wallet().ok();
+    bitcoin_client.load_wallet()?;
+
+    let new_add = bitcoin_client.get_new_address()?;
+    bitcoin_client.generate_blocks(&new_add, 200).unwrap();
+
+    let cln_one_dir = get_cln_dir("one");
+    let mut clnd = Clnd::new(
+        get_bitcoin_dir(),
+        cln_one_dir.clone(),
+        CLN_ADDR.into(),
+        BITCOIN_RPC_USER.to_string(),
+        BITCOIN_RPC_PASS.to_string(),
+    );
+    clnd.start_clnd()?;
+
+    let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?;
+
+    cln_client.wait_chain_sync().await.unwrap();
+
+    fund_ln(&bitcoin_client, &cln_client).await.unwrap();
+
+    // Create second cln
+    let cln_two_dir = get_cln_dir("two");
+    let mut clnd_two = Clnd::new(
+        get_bitcoin_dir(),
+        cln_two_dir.clone(),
+        CLN_TWO_ADDR.into(),
+        BITCOIN_RPC_USER.to_string(),
+        BITCOIN_RPC_PASS.to_string(),
+    );
+    clnd_two.start_clnd()?;
+
+    let cln_two_client = ClnClient::new(cln_two_dir.clone(), None).await?;
+
+    cln_two_client.wait_chain_sync().await.unwrap();
+
+    fund_ln(&bitcoin_client, &cln_two_client).await.unwrap();
+
+    let lnd_dir = get_lnd_dir("one");
+    println!("{}", lnd_dir.display());
+
+    let mut lnd = init_lnd(lnd_dir.clone(), LND_ADDR, LND_RPC_ADDR).await;
+    lnd.start_lnd().unwrap();
+    tracing::info!("Started lnd node");
+
+    let lnd_client = LndClient::new(
+        format!("https://{}", LND_RPC_ADDR),
+        get_lnd_cert_file_path(&lnd_dir),
+        get_lnd_macaroon_path(&lnd_dir),
+    )
+    .await?;
+
+    lnd_client.wait_chain_sync().await.unwrap();
+
+    fund_ln(&bitcoin_client, &lnd_client).await.unwrap();
+
+    // create second lnd node
+    let lnd_two_dir = get_lnd_dir("two");
+    let mut lnd_two = init_lnd(lnd_two_dir.clone(), LND_TWO_ADDR, LND_TWO_RPC_ADDR).await;
+    lnd_two.start_lnd().unwrap();
+    tracing::info!("Started second lnd node");
+
+    let lnd_two_client = LndClient::new(
+        format!("https://{}", LND_TWO_RPC_ADDR),
+        get_lnd_cert_file_path(&lnd_two_dir),
+        get_lnd_macaroon_path(&lnd_two_dir),
+    )
+    .await?;
+
+    lnd_two_client.wait_chain_sync().await.unwrap();
+
+    fund_ln(&bitcoin_client, &lnd_two_client).await.unwrap();
+
+    // Open channels concurrently
+    // Open channels
+    {
+        open_channel(&cln_client, &lnd_client).await.unwrap();
+        tracing::info!("Opened channel between cln and lnd one");
+        generate_block(&bitcoin_client)?;
+        // open_channel(&bitcoin_client, &cln_client, &cln_two_client)
+        //     .await
+        //     .unwrap();
+        // tracing::info!("Opened channel between cln and cln two");
+
+        open_channel(&lnd_client, &lnd_two_client).await.unwrap();
+        tracing::info!("Opened channel between lnd and lnd two");
+        generate_block(&bitcoin_client)?;
+
+        // open_channel(&cln_client, &lnd_two_client).await.unwrap();
+        // tracing::info!("Opened channel between cln and lnd two");
+        open_channel(&cln_two_client, &lnd_client).await.unwrap();
+        tracing::info!("Opened channel between cln two and lnd");
+        generate_block(&bitcoin_client)?;
+
+        open_channel(&cln_client, &lnd_two_client).await.unwrap();
+        tracing::info!("Opened channel between cln and lnd two");
+        generate_block(&bitcoin_client)?;
+
+        cln_client.wait_channels_active().await?;
+        cln_two_client.wait_channels_active().await?;
+        lnd_client.wait_channels_active().await?;
+        lnd_two_client.wait_channels_active().await?;
+    }
+
+    // Send notification that regtest set up is complete
+    sender.send(()).expect("Could not send oneshot");
+
+    // Wait until we are told to shutdown
+    // If we return the bitcoind, lnd, and cln will be dropped and shutdown
+    notify.notified().await;
+
+    Ok(())
+}

+ 145 - 112
crates/cdk-integration-tests/src/lib.rs

@@ -1,120 +1,35 @@
-use std::str::FromStr;
+use std::env;
 use std::sync::Arc;
 
-use anyhow::{bail, Result};
+use anyhow::{anyhow, bail, Result};
+use cashu::Bolt11Invoice;
 use cdk::amount::{Amount, SplitTarget};
-use cdk::dhke::construct_proofs;
-use cdk::mint_url::MintUrl;
-use cdk::nuts::nut00::ProofsMethods;
-use cdk::nuts::nut17::Params;
-use cdk::nuts::{
-    CurrencyUnit, Id, KeySet, MintBolt11Request, MintQuoteBolt11Request, MintQuoteState,
-    NotificationPayload, PreMintSecrets, Proofs, State,
-};
-use cdk::wallet::client::{HttpClient, MintConnector};
-use cdk::wallet::subscription::SubscriptionManager;
+use cdk::nuts::{MintQuoteState, NotificationPayload, State};
 use cdk::wallet::WalletSubscription;
 use cdk::Wallet;
-use tokio::time::{timeout, Duration};
+use cdk_fake_wallet::create_fake_invoice;
+use init_regtest::{get_lnd_dir, get_mint_url, LND_RPC_ADDR};
+use ln_regtest_rs::ln_client::{LightningClient, LndClient};
+use tokio::time::{sleep, timeout, Duration};
 
-pub mod init_fake_wallet;
-pub mod init_mint;
+pub mod init_auth_mint;
 pub mod init_pure_tests;
 pub mod init_regtest;
 
-pub async fn wallet_mint(
-    wallet: Arc<Wallet>,
-    amount: Amount,
-    split_target: SplitTarget,
-    description: Option<String>,
-) -> Result<()> {
-    let quote = wallet.mint_quote(amount, description).await?;
-
-    let mut subscription = wallet
-        .subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote
-            .id
-            .clone()]))
-        .await;
-
-    while let Some(msg) = subscription.recv().await {
-        if let NotificationPayload::MintQuoteBolt11Response(response) = msg {
-            if response.state == MintQuoteState::Paid {
-                break;
-            }
-        }
-    }
-
-    let proofs = wallet.mint(&quote.id, split_target, None).await?;
-
-    let receive_amount = proofs.total_amount()?;
-
-    println!("Minted: {}", receive_amount);
-
-    Ok(())
-}
-
-pub async fn mint_proofs(
-    mint_url: &str,
-    amount: Amount,
-    keyset_id: Id,
-    mint_keys: &KeySet,
-    description: Option<String>,
-) -> anyhow::Result<Proofs> {
-    println!("Minting for ecash");
-    println!();
-
-    let wallet_client = HttpClient::new(MintUrl::from_str(mint_url)?);
-
-    let request = MintQuoteBolt11Request {
-        amount,
-        unit: CurrencyUnit::Sat,
-        description,
-        pubkey: None,
-    };
+pub async fn fund_wallet(wallet: Arc<Wallet>, amount: Amount) {
+    let quote = wallet
+        .mint_quote(amount, None)
+        .await
+        .expect("Could not get mint quote");
 
-    let mint_quote = wallet_client.post_mint_quote(request).await?;
+    wait_for_mint_to_be_paid(&wallet, &quote.id, 60)
+        .await
+        .expect("Waiting for mint failed");
 
-    println!("Please pay: {}", mint_quote.request);
-
-    let subscription_client = SubscriptionManager::new(Arc::new(wallet_client.clone()));
-
-    let mut subscription = subscription_client
-        .subscribe(
-            mint_url.parse()?,
-            Params {
-                filters: vec![mint_quote.quote.clone()],
-                kind: cdk::nuts::nut17::Kind::Bolt11MintQuote,
-                id: "sub".into(),
-            },
-        )
-        .await;
-
-    while let Some(msg) = subscription.recv().await {
-        if let NotificationPayload::MintQuoteBolt11Response(response) = msg {
-            if response.state == MintQuoteState::Paid {
-                break;
-            }
-        }
-    }
-
-    let premint_secrets = PreMintSecrets::random(keyset_id, amount, &SplitTarget::default())?;
-
-    let request = MintBolt11Request {
-        quote: mint_quote.quote,
-        outputs: premint_secrets.blinded_messages(),
-        signature: None,
-    };
-
-    let mint_response = wallet_client.post_mint(request).await?;
-
-    let pre_swap_proofs = construct_proofs(
-        mint_response.signatures,
-        premint_secrets.rs(),
-        premint_secrets.secrets(),
-        &mint_keys.clone().keys,
-    )?;
-
-    Ok(pre_swap_proofs)
+    let _proofs = wallet
+        .mint(&quote.id, SplitTarget::default(), None)
+        .await
+        .expect("Could not mint");
 }
 
 // Get all pending from wallet and attempt to swap
@@ -164,7 +79,6 @@ pub async fn wait_for_mint_to_be_paid(
             mint_quote_id.to_owned(),
         ]))
         .await;
-
     // Create the timeout future
     let wait_future = async {
         while let Some(msg) = subscription.recv().await {
@@ -174,12 +88,131 @@ pub async fn wait_for_mint_to_be_paid(
                 }
             }
         }
-        Ok(())
+        Err(anyhow!("Subscription ended without quote being paid"))
     };
 
-    // Wait for either the payment to complete or timeout
-    match timeout(Duration::from_secs(timeout_secs), wait_future).await {
-        Ok(result) => result,
-        Err(_) => Err(anyhow::anyhow!("Timeout waiting for mint quote to be paid")),
+    let timeout_future = timeout(Duration::from_secs(timeout_secs), wait_future);
+
+    let check_interval = Duration::from_secs(5);
+
+    let periodic_task = async {
+        loop {
+            match wallet.mint_quote_state(mint_quote_id).await {
+                Ok(result) => {
+                    if result.state == MintQuoteState::Paid {
+                        tracing::info!("mint quote paid via poll");
+                        return Ok(());
+                    }
+                }
+                Err(e) => {
+                    tracing::error!("Could not check mint quote status: {:?}", e);
+                }
+            }
+            sleep(check_interval).await;
+        }
+    };
+
+    tokio::select! {
+        result = timeout_future => {
+            match result {
+                Ok(payment_result) => payment_result,
+                Err(_) => Err(anyhow!("Timeout waiting for mint quote to be paid")),
+            }
+        }
+        result = periodic_task => {
+            result // Now propagates the result from periodic checks
+        }
+    }
+}
+
+/// Gets the mint URL from environment variable or falls back to default
+///
+/// Checks the CDK_TEST_MINT_URL environment variable:
+/// - If set, returns that URL
+/// - Otherwise falls back to the default URL from get_mint_url("0")
+pub fn get_mint_url_from_env() -> String {
+    match env::var("CDK_TEST_MINT_URL") {
+        Ok(url) => url,
+        Err(_) => get_mint_url("0"),
+    }
+}
+
+/// Gets the second mint URL from environment variable or falls back to default
+///
+/// Checks the CDK_TEST_MINT_URL_2 environment variable:
+/// - If set, returns that URL
+/// - Otherwise falls back to the default URL from get_mint_url("1")
+pub fn get_second_mint_url_from_env() -> String {
+    match env::var("CDK_TEST_MINT_URL_2") {
+        Ok(url) => url,
+        Err(_) => get_mint_url("1"),
+    }
+}
+
+// This is the ln wallet we use to send/receive ln payements as the wallet
+pub async fn init_lnd_client() -> LndClient {
+    let lnd_dir = get_lnd_dir("one");
+    let cert_file = lnd_dir.join("tls.cert");
+    let macaroon_file = lnd_dir.join("data/chain/bitcoin/regtest/admin.macaroon");
+    LndClient::new(
+        format!("https://{}", LND_RPC_ADDR),
+        cert_file,
+        macaroon_file,
+    )
+    .await
+    .unwrap()
+}
+
+/// Pays a Bolt11Invoice if it's on the regtest network, otherwise returns Ok
+///
+/// This is useful for tests that need to pay invoices in regtest mode but
+/// should be skipped in other environments.
+pub async fn pay_if_regtest(invoice: &Bolt11Invoice) -> Result<()> {
+    // Check if the invoice is for the regtest network
+    if invoice.network() == bitcoin::Network::Regtest {
+        println!("Regtest invoice");
+        let lnd_client = init_lnd_client().await;
+        lnd_client.pay_invoice(invoice.to_string()).await?;
+        Ok(())
+    } else {
+        // Not a regtest invoice, just return Ok
+        Ok(())
+    }
+}
+
+/// Determines if we're running in regtest mode based on environment variable
+///
+/// Checks the CDK_TEST_REGTEST environment variable:
+/// - If set to "1", "true", or "yes" (case insensitive), returns true
+/// - Otherwise returns false
+pub fn is_regtest_env() -> bool {
+    match env::var("CDK_TEST_REGTEST") {
+        Ok(val) => {
+            let val = val.to_lowercase();
+            val == "1" || val == "true" || val == "yes"
+        }
+        Err(_) => false,
+    }
+}
+
+/// Creates a real invoice if in regtest mode, otherwise returns a fake invoice
+///
+/// Uses the is_regtest_env() function to determine whether to
+/// create a real regtest invoice or a fake one for testing.
+pub async fn create_invoice_for_env(amount_sat: Option<u64>) -> Result<String> {
+    if is_regtest_env() {
+        // In regtest mode, create a real invoice
+        let lnd_client = init_lnd_client().await;
+        lnd_client
+            .create_invoice(amount_sat)
+            .await
+            .map_err(|e| anyhow!("Failed to create regtest invoice: {}", e))
+    } else {
+        // Not in regtest mode, create a fake invoice
+        let fake_invoice = create_fake_invoice(
+            amount_sat.expect("Amount must be defined") * 1_000,
+            "".to_string(),
+        );
+        Ok(fake_invoice.to_string())
     }
 }

+ 863 - 0
crates/cdk-integration-tests/tests/fake_auth.rs

@@ -0,0 +1,863 @@
+use std::env;
+use std::str::FromStr;
+use std::sync::Arc;
+
+use bip39::Mnemonic;
+use cashu::{MintAuthRequest, MintInfo};
+use cdk::amount::{Amount, SplitTarget};
+use cdk::mint_url::MintUrl;
+use cdk::nuts::nut00::ProofsMethods;
+use cdk::nuts::{
+    AuthProof, AuthToken, BlindAuthToken, CheckStateRequest, CurrencyUnit, MeltBolt11Request,
+    MeltQuoteBolt11Request, MeltQuoteState, MintBolt11Request, MintQuoteBolt11Request,
+    RestoreRequest, State, SwapRequest,
+};
+use cdk::wallet::{AuthHttpClient, AuthMintConnector, HttpClient, MintConnector, WalletBuilder};
+use cdk::{Error, OidcClient};
+use cdk_fake_wallet::create_fake_invoice;
+use cdk_integration_tests::{fund_wallet, wait_for_mint_to_be_paid};
+use cdk_sqlite::wallet::memory;
+
+const MINT_URL: &str = "http://127.0.0.1:8087";
+const ENV_OIDC_USER: &str = "CDK_TEST_OIDC_USER";
+const ENV_OIDC_PASSWORD: &str = "CDK_TEST_OIDC_PASSWORD";
+
+fn get_oidc_credentials() -> (String, String) {
+    let user = env::var(ENV_OIDC_USER).unwrap_or_else(|_| "test".to_string());
+    let password = env::var(ENV_OIDC_PASSWORD).unwrap_or_else(|_| "test".to_string());
+    (user, password)
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_invalid_credentials() {
+    let db = Arc::new(memory::empty().await.unwrap());
+
+    let wallet = WalletBuilder::new()
+        .mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
+        .unit(CurrencyUnit::Sat)
+        .localstore(db.clone())
+        .seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
+        .build()
+        .expect("Wallet");
+
+    let mint_info = wallet
+        .get_mint_info()
+        .await
+        .expect("mint info")
+        .expect("could not get mint info");
+
+    // Try to get a token with invalid credentials
+    let token_result =
+        get_custom_access_token(&mint_info, "invalid_user", "invalid_password").await;
+
+    // Should fail with an error
+    assert!(
+        token_result.is_err(),
+        "Expected authentication to fail with invalid credentials"
+    );
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_quote_status_without_auth() {
+    let client = HttpClient::new(MintUrl::from_str(MINT_URL).expect("Valid mint url"), None);
+
+    // Test mint quote status
+    {
+        let quote_res = client
+            .get_mint_quote_status("123e4567-e89b-12d3-a456-426614174000")
+            .await;
+
+        assert!(
+            matches!(quote_res, Err(Error::BlindAuthRequired)),
+            "Expected AuthRequired error, got {:?}",
+            quote_res
+        );
+    }
+
+    // Test melt quote status
+    {
+        let quote_res = client
+            .get_melt_quote_status("123e4567-e89b-12d3-a456-426614174000")
+            .await;
+
+        assert!(
+            matches!(quote_res, Err(Error::BlindAuthRequired)),
+            "Expected AuthRequired error, got {:?}",
+            quote_res
+        );
+    }
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_mint_without_auth() {
+    let client = HttpClient::new(MintUrl::from_str(MINT_URL).expect("Valid mint url"), None);
+    {
+        let request = MintQuoteBolt11Request {
+            unit: CurrencyUnit::Sat,
+            amount: 10.into(),
+            description: None,
+            pubkey: None,
+        };
+
+        let quote_res = client.post_mint_quote(request).await;
+
+        assert!(
+            matches!(quote_res, Err(Error::BlindAuthRequired)),
+            "Expected AuthRequired error, got {:?}",
+            quote_res
+        );
+    }
+
+    {
+        let request = MintBolt11Request {
+            quote: "123e4567-e89b-12d3-a456-426614174000".to_string(),
+            outputs: vec![],
+            signature: None,
+        };
+
+        let mint_res = client.post_mint(request).await;
+
+        assert!(
+            matches!(mint_res, Err(Error::BlindAuthRequired)),
+            "Expected AuthRequired error, got {:?}",
+            mint_res
+        );
+    }
+
+    {
+        let mint_res = client
+            .get_mint_quote_status("123e4567-e89b-12d3-a456-426614174000")
+            .await;
+
+        assert!(
+            matches!(mint_res, Err(Error::BlindAuthRequired)),
+            "Expected AuthRequired error, got {:?}",
+            mint_res
+        );
+    }
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_mint_bat_without_cat() {
+    let client = AuthHttpClient::new(MintUrl::from_str(MINT_URL).expect("valid mint url"), None);
+
+    let res = client
+        .post_mint_blind_auth(MintAuthRequest { outputs: vec![] })
+        .await;
+
+    assert!(
+        matches!(res, Err(Error::ClearAuthRequired)),
+        "Expected AuthRequired error, got {:?}",
+        res
+    );
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_swap_without_auth() {
+    let client = HttpClient::new(MintUrl::from_str(MINT_URL).expect("Valid mint url"), None);
+
+    let request = SwapRequest::new(vec![], vec![]);
+
+    let quote_res = client.post_swap(request).await;
+
+    assert!(
+        matches!(quote_res, Err(Error::BlindAuthRequired)),
+        "Expected AuthRequired error, got {:?}",
+        quote_res
+    );
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_melt_without_auth() {
+    let client = HttpClient::new(MintUrl::from_str(MINT_URL).expect("Valid mint url"), None);
+
+    // Test melt quote request
+    {
+        let request = MeltQuoteBolt11Request {
+            request: create_fake_invoice(100, "".to_string()),
+            unit: CurrencyUnit::Sat,
+            options: None,
+        };
+
+        let quote_res = client.post_melt_quote(request).await;
+
+        assert!(
+            matches!(quote_res, Err(Error::BlindAuthRequired)),
+            "Expected AuthRequired error, got {:?}",
+            quote_res
+        );
+    }
+
+    // Test melt quote
+    {
+        let request = MeltQuoteBolt11Request {
+            request: create_fake_invoice(100, "".to_string()),
+            unit: CurrencyUnit::Sat,
+            options: None,
+        };
+
+        let quote_res = client.post_melt_quote(request).await;
+
+        assert!(
+            matches!(quote_res, Err(Error::BlindAuthRequired)),
+            "Expected AuthRequired error, got {:?}",
+            quote_res
+        );
+    }
+
+    // Test melt
+    {
+        let request = MeltBolt11Request::new(
+            "123e4567-e89b-12d3-a456-426614174000".to_string(),
+            vec![],
+            None,
+        );
+
+        let melt_res = client.post_melt(request).await;
+
+        assert!(
+            matches!(melt_res, Err(Error::BlindAuthRequired)),
+            "Expected AuthRequired error, got {:?}",
+            melt_res
+        );
+    }
+
+    // Check melt quote state
+    {
+        let melt_res = client
+            .get_melt_quote_status("123e4567-e89b-12d3-a456-426614174000")
+            .await;
+
+        assert!(
+            matches!(melt_res, Err(Error::BlindAuthRequired)),
+            "Expected AuthRequired error, got {:?}",
+            melt_res
+        );
+    }
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_check_without_auth() {
+    let client = HttpClient::new(MintUrl::from_str(MINT_URL).expect("Valid mint url"), None);
+
+    let request = CheckStateRequest { ys: vec![] };
+
+    let quote_res = client.post_check_state(request).await;
+
+    assert!(
+        matches!(quote_res, Err(Error::BlindAuthRequired)),
+        "Expected AuthRequired error, got {:?}",
+        quote_res
+    );
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_restore_without_auth() {
+    let client = HttpClient::new(MintUrl::from_str(MINT_URL).expect("Valid mint url"), None);
+
+    let request = RestoreRequest { outputs: vec![] };
+
+    let restore_res = client.post_restore(request).await;
+
+    assert!(
+        matches!(restore_res, Err(Error::BlindAuthRequired)),
+        "Expected AuthRequired error, got {:?}",
+        restore_res
+    );
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_mint_blind_auth() {
+    let db = Arc::new(memory::empty().await.unwrap());
+
+    let wallet = WalletBuilder::new()
+        .mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
+        .unit(CurrencyUnit::Sat)
+        .localstore(db.clone())
+        .seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
+        .build()
+        .expect("Wallet");
+    let mint_info = wallet.get_mint_info().await.unwrap().unwrap();
+
+    let (access_token, _) = get_access_token(&mint_info).await;
+
+    wallet.set_cat(access_token).await.unwrap();
+
+    wallet
+        .mint_blind_auth(10.into())
+        .await
+        .expect("Could not mint blind auth");
+
+    let proofs = wallet
+        .get_unspent_auth_proofs()
+        .await
+        .expect("Could not get auth proofs");
+
+    assert!(proofs.len() == 10)
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_mint_with_auth() {
+    let db = Arc::new(memory::empty().await.unwrap());
+
+    let wallet = WalletBuilder::new()
+        .mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
+        .unit(CurrencyUnit::Sat)
+        .localstore(db.clone())
+        .seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
+        .build()
+        .expect("Wallet");
+
+    let mint_info = wallet
+        .get_mint_info()
+        .await
+        .expect("mint info")
+        .expect("could not get mint info");
+
+    let (access_token, _) = get_access_token(&mint_info).await;
+
+    println!("st{}", access_token);
+
+    wallet.set_cat(access_token).await.unwrap();
+
+    wallet
+        .mint_blind_auth(10.into())
+        .await
+        .expect("Could not mint blind auth");
+
+    let wallet = Arc::new(wallet);
+
+    let mint_amount: Amount = 100.into();
+
+    let mint_quote = wallet
+        .mint_quote(mint_amount, None)
+        .await
+        .expect("failed to get mint quote");
+
+    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
+        .await
+        .expect("failed to wait for payment");
+
+    let proofs = wallet
+        .mint(&mint_quote.id, SplitTarget::default(), None)
+        .await
+        .expect("could not mint");
+
+    assert!(proofs.total_amount().expect("Could not get proofs amount") == mint_amount);
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_swap_with_auth() {
+    let db = Arc::new(memory::empty().await.unwrap());
+
+    let wallet = WalletBuilder::new()
+        .mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
+        .unit(CurrencyUnit::Sat)
+        .localstore(db.clone())
+        .seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
+        .build()
+        .expect("Wallet");
+    let mint_info = wallet.get_mint_info().await.unwrap().unwrap();
+    let (access_token, _) = get_access_token(&mint_info).await;
+
+    wallet.set_cat(access_token).await.unwrap();
+
+    let wallet = Arc::new(wallet);
+
+    wallet.mint_blind_auth(10.into()).await.unwrap();
+
+    fund_wallet(wallet.clone(), 100.into()).await;
+
+    let proofs = wallet
+        .get_unspent_proofs()
+        .await
+        .expect("Could not get proofs");
+
+    let swapped_proofs = wallet
+        .swap(
+            Some(proofs.total_amount().unwrap()),
+            SplitTarget::default(),
+            proofs.clone(),
+            None,
+            false,
+        )
+        .await
+        .expect("Could not swap")
+        .expect("Could not swap");
+
+    let check_spent = wallet
+        .check_proofs_spent(proofs.clone())
+        .await
+        .expect("Could not check proofs");
+
+    for state in check_spent {
+        if state.state != State::Spent {
+            panic!("Input proofs should be spent");
+        }
+    }
+
+    assert!(swapped_proofs.total_amount().unwrap() == proofs.total_amount().unwrap())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_melt_with_auth() {
+    let db = Arc::new(memory::empty().await.unwrap());
+
+    let wallet = WalletBuilder::new()
+        .mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
+        .unit(CurrencyUnit::Sat)
+        .localstore(db.clone())
+        .seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
+        .build()
+        .expect("Wallet");
+
+    let mint_info = wallet
+        .get_mint_info()
+        .await
+        .expect("Mint info not found")
+        .expect("Mint info not found");
+
+    let (access_token, _) = get_access_token(&mint_info).await;
+
+    wallet.set_cat(access_token).await.unwrap();
+
+    let wallet = Arc::new(wallet);
+
+    wallet.mint_blind_auth(10.into()).await.unwrap();
+
+    fund_wallet(wallet.clone(), 100.into()).await;
+
+    let bolt11 = create_fake_invoice(2_000, "".to_string());
+
+    let melt_quote = wallet
+        .melt_quote(bolt11.to_string(), None)
+        .await
+        .expect("Could not get melt quote");
+
+    let after_melt = wallet.melt(&melt_quote.id).await.expect("Could not melt");
+
+    assert!(after_melt.state == MeltQuoteState::Paid);
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_mint_auth_over_max() {
+    let db = Arc::new(memory::empty().await.unwrap());
+
+    let wallet = WalletBuilder::new()
+        .mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
+        .unit(CurrencyUnit::Sat)
+        .localstore(db.clone())
+        .seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
+        .build()
+        .expect("Wallet");
+
+    let wallet = Arc::new(wallet);
+
+    let mint_info = wallet
+        .get_mint_info()
+        .await
+        .expect("Mint info not found")
+        .expect("Mint info not found");
+
+    let (access_token, _) = get_access_token(&mint_info).await;
+
+    wallet.set_cat(access_token).await.unwrap();
+
+    let auth_proofs = wallet
+        .mint_blind_auth((mint_info.nuts.nut22.expect("Auth enabled").bat_max_mint + 1).into())
+        .await;
+
+    assert!(
+        matches!(
+            auth_proofs,
+            Err(Error::AmountOutofLimitRange(
+                Amount::ZERO,
+                Amount::ZERO,
+                Amount::ZERO,
+            ))
+        ),
+        "Expected amount out of limit error, got {:?}",
+        auth_proofs
+    );
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_reuse_auth_proof() {
+    let db = Arc::new(memory::empty().await.unwrap());
+
+    let wallet = WalletBuilder::new()
+        .mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
+        .unit(CurrencyUnit::Sat)
+        .localstore(db.clone())
+        .seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
+        .build()
+        .expect("Wallet");
+    let mint_info = wallet.get_mint_info().await.unwrap().unwrap();
+
+    let (access_token, _) = get_access_token(&mint_info).await;
+
+    wallet.set_cat(access_token).await.unwrap();
+
+    wallet.mint_blind_auth(1.into()).await.unwrap();
+
+    let proofs = wallet
+        .localstore
+        .get_proofs(None, Some(CurrencyUnit::Auth), None, None)
+        .await
+        .unwrap();
+
+    assert!(proofs.len() == 1);
+
+    {
+        let quote = wallet
+            .mint_quote(10.into(), None)
+            .await
+            .expect("Quote should be allowed");
+
+        assert!(quote.amount == 10.into());
+    }
+
+    wallet
+        .localstore
+        .update_proofs(proofs, vec![])
+        .await
+        .unwrap();
+
+    {
+        let quote_res = wallet.mint_quote(10.into(), None).await;
+        assert!(
+            matches!(quote_res, Err(Error::TokenAlreadySpent)),
+            "Expected AuthRequired error, got {:?}",
+            quote_res
+        );
+    }
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_melt_with_invalid_auth() {
+    let db = Arc::new(memory::empty().await.unwrap());
+
+    let wallet = WalletBuilder::new()
+        .mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
+        .unit(CurrencyUnit::Sat)
+        .localstore(db.clone())
+        .seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
+        .build()
+        .expect("Wallet");
+    let mint_info = wallet.get_mint_info().await.unwrap().unwrap();
+
+    let (access_token, _) = get_access_token(&mint_info).await;
+
+    wallet.set_cat(access_token).await.unwrap();
+
+    wallet.mint_blind_auth(10.into()).await.unwrap();
+
+    fund_wallet(Arc::new(wallet.clone()), 1.into()).await;
+
+    let proofs = wallet
+        .get_unspent_proofs()
+        .await
+        .expect("wallet has proofs");
+
+    println!("{:#?}", proofs);
+    let proof = proofs.first().expect("wallet has one proof");
+
+    let client = HttpClient::new(MintUrl::from_str(MINT_URL).expect("Valid mint url"), None);
+    {
+        let invalid_auth_proof = AuthProof {
+            keyset_id: proof.keyset_id,
+            secret: proof.secret.clone(),
+            c: proof.c,
+            dleq: proof.dleq.clone(),
+        };
+
+        let _auth_token = AuthToken::BlindAuth(BlindAuthToken::new(invalid_auth_proof));
+
+        let request = MintQuoteBolt11Request {
+            unit: CurrencyUnit::Sat,
+            amount: 10.into(),
+            description: None,
+            pubkey: None,
+        };
+
+        let quote_res = client.post_mint_quote(request).await;
+
+        assert!(
+            matches!(quote_res, Err(Error::BlindAuthRequired)),
+            "Expected AuthRequired error, got {:?}",
+            quote_res
+        );
+    }
+
+    {
+        let (access_token, _) = get_access_token(&mint_info).await;
+
+        wallet.set_cat(access_token).await.unwrap();
+    }
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_refresh_access_token() {
+    let db = Arc::new(memory::empty().await.unwrap());
+
+    let wallet = WalletBuilder::new()
+        .mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
+        .unit(CurrencyUnit::Sat)
+        .localstore(db.clone())
+        .seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
+        .build()
+        .expect("Wallet");
+
+    let mint_info = wallet
+        .get_mint_info()
+        .await
+        .expect("mint info")
+        .expect("could not get mint info");
+
+    let (access_token, refresh_token) = get_access_token(&mint_info).await;
+
+    // Set the initial access token and refresh token
+    wallet.set_cat(access_token.clone()).await.unwrap();
+    wallet
+        .set_refresh_token(refresh_token.clone())
+        .await
+        .unwrap();
+
+    // Mint some blind auth tokens with the initial access token
+    wallet.mint_blind_auth(5.into()).await.unwrap();
+
+    // Refresh the access token
+    wallet.refresh_access_token().await.unwrap();
+
+    // Verify we can still perform operations with the refreshed token
+    let mint_amount: Amount = 10.into();
+
+    // Try to mint more blind auth tokens with the refreshed token
+    let auth_proofs = wallet.mint_blind_auth(5.into()).await.unwrap();
+    assert_eq!(auth_proofs.len(), 5);
+
+    let total_auth_proofs = wallet.get_unspent_auth_proofs().await.unwrap();
+    assert_eq!(total_auth_proofs.len(), 10); // 5 from before refresh + 5 after refresh
+
+    // Try to get a mint quote with the refreshed token
+    let mint_quote = wallet
+        .mint_quote(mint_amount, None)
+        .await
+        .expect("failed to get mint quote with refreshed token");
+
+    assert_eq!(mint_quote.amount, mint_amount);
+
+    // Verify the total number of auth tokens
+    let total_auth_proofs = wallet.get_unspent_auth_proofs().await.unwrap();
+    assert_eq!(total_auth_proofs.len(), 9); // 5 from before refresh + 5 after refresh - 1 for the quote
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_invalid_refresh_token() {
+    let db = Arc::new(memory::empty().await.unwrap());
+
+    let wallet = WalletBuilder::new()
+        .mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
+        .unit(CurrencyUnit::Sat)
+        .localstore(db.clone())
+        .seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
+        .build()
+        .expect("Wallet");
+
+    let mint_info = wallet
+        .get_mint_info()
+        .await
+        .expect("mint info")
+        .expect("could not get mint info");
+
+    let (access_token, _) = get_access_token(&mint_info).await;
+
+    // Set the initial access token
+    wallet.set_cat(access_token.clone()).await.unwrap();
+
+    // Set an invalid refresh token
+    wallet
+        .set_refresh_token("invalid_refresh_token".to_string())
+        .await
+        .unwrap();
+
+    // Attempt to refresh the access token with an invalid refresh token
+    let refresh_result = wallet.refresh_access_token().await;
+
+    // Should fail with an error
+    assert!(refresh_result.is_err(), "Expected refresh token error");
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_auth_token_spending_order() {
+    let db = Arc::new(memory::empty().await.unwrap());
+
+    let wallet = WalletBuilder::new()
+        .mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
+        .unit(CurrencyUnit::Sat)
+        .localstore(db.clone())
+        .seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
+        .build()
+        .expect("Wallet");
+
+    let mint_info = wallet
+        .get_mint_info()
+        .await
+        .expect("mint info")
+        .expect("could not get mint info");
+
+    let (access_token, _) = get_access_token(&mint_info).await;
+
+    wallet.set_cat(access_token).await.unwrap();
+
+    // Mint auth tokens in two batches to test ordering
+    wallet.mint_blind_auth(2.into()).await.unwrap();
+
+    // Get the first batch of auth proofs
+    let first_batch = wallet.get_unspent_auth_proofs().await.unwrap();
+    assert_eq!(first_batch.len(), 2);
+
+    // Mint a second batch
+    wallet.mint_blind_auth(3.into()).await.unwrap();
+
+    // Get all auth proofs
+    let all_proofs = wallet.get_unspent_auth_proofs().await.unwrap();
+    assert_eq!(all_proofs.len(), 5);
+
+    // Use tokens and verify they're used in the expected order (FIFO)
+    for i in 0..3 {
+        let mint_quote = wallet
+            .mint_quote(10.into(), None)
+            .await
+            .expect("failed to get mint quote");
+
+        assert_eq!(mint_quote.amount, 10.into());
+
+        // Check remaining tokens after each operation
+        let remaining = wallet.get_unspent_auth_proofs().await.unwrap();
+        assert_eq!(
+            remaining.len(),
+            5 - (i + 1),
+            "Expected {} remaining auth tokens after {} operations",
+            5 - (i + 1),
+            i + 1
+        );
+    }
+}
+
+async fn get_access_token(mint_info: &MintInfo) -> (String, String) {
+    let openid_discovery = mint_info
+        .nuts
+        .nut21
+        .clone()
+        .expect("Nutxx defined")
+        .openid_discovery;
+
+    let oidc_client = OidcClient::new(openid_discovery);
+
+    // Get the token endpoint from the OIDC configuration
+    let token_url = oidc_client
+        .get_oidc_config()
+        .await
+        .expect("Failed to get OIDC config")
+        .token_endpoint;
+
+    // Create the request parameters
+    let (user, password) = get_oidc_credentials();
+    let params = [
+        ("grant_type", "password"),
+        ("client_id", "cashu-client"),
+        ("username", &user),
+        ("password", &password),
+    ];
+
+    // Make the token request directly
+    let client = reqwest::Client::new();
+    let response = client
+        .post(token_url)
+        .form(&params)
+        .send()
+        .await
+        .expect("Failed to send token request");
+
+    let token_response: serde_json::Value = response
+        .json()
+        .await
+        .expect("Failed to parse token response");
+
+    let access_token = token_response["access_token"]
+        .as_str()
+        .expect("No access token in response")
+        .to_string();
+
+    let refresh_token = token_response["refresh_token"]
+        .as_str()
+        .expect("No access token in response")
+        .to_string();
+
+    (access_token, refresh_token)
+}
+
+/// Get a new access token with custom credentials
+async fn get_custom_access_token(
+    mint_info: &MintInfo,
+    username: &str,
+    password: &str,
+) -> Result<(String, String), Error> {
+    let openid_discovery = mint_info
+        .nuts
+        .nut21
+        .clone()
+        .expect("Nutxx defined")
+        .openid_discovery;
+
+    let oidc_client = OidcClient::new(openid_discovery);
+
+    // Get the token endpoint from the OIDC configuration
+    let token_url = oidc_client
+        .get_oidc_config()
+        .await
+        .map_err(|_| Error::Custom("Failed to get OIDC config".to_string()))?
+        .token_endpoint;
+
+    // Create the request parameters
+    let params = [
+        ("grant_type", "password"),
+        ("client_id", "cashu-client"),
+        ("username", username),
+        ("password", password),
+    ];
+
+    // Make the token request directly
+    let client = reqwest::Client::new();
+    let response = client
+        .post(token_url)
+        .form(&params)
+        .send()
+        .await
+        .map_err(|_| Error::Custom("Failed to send token request".to_string()))?;
+
+    if !response.status().is_success() {
+        return Err(Error::Custom(format!(
+            "Token request failed with status: {}",
+            response.status()
+        )));
+    }
+
+    let token_response: serde_json::Value = response
+        .json()
+        .await
+        .map_err(|_| Error::Custom("Failed to parse token response".to_string()))?;
+
+    let access_token = token_response["access_token"]
+        .as_str()
+        .ok_or_else(|| Error::Custom("No access token in response".to_string()))?
+        .to_string();
+
+    let refresh_token = token_response["refresh_token"]
+        .as_str()
+        .ok_or_else(|| Error::Custom("No refresh token in response".to_string()))?
+        .to_string();
+
+    Ok((access_token, refresh_token))
+}

+ 259 - 100
crates/cdk-integration-tests/tests/fake_wallet.rs

@@ -2,27 +2,28 @@ use std::sync::Arc;
 
 use anyhow::{bail, Result};
 use bip39::Mnemonic;
+use cashu::Amount;
 use cdk::amount::SplitTarget;
-use cdk::cdk_database::WalletMemoryDatabase;
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{
     CurrencyUnit, MeltBolt11Request, MeltQuoteState, MintBolt11Request, PreMintSecrets, Proofs,
     SecretKey, State, SwapRequest,
 };
-use cdk::wallet::client::{HttpClient, MintConnector};
-use cdk::wallet::Wallet;
+use cdk::wallet::types::TransactionDirection;
+use cdk::wallet::{HttpClient, MintConnector, Wallet};
 use cdk_fake_wallet::{create_fake_invoice, FakeInvoiceDescription};
 use cdk_integration_tests::{attempt_to_swap_pending, wait_for_mint_to_be_paid};
+use cdk_sqlite::wallet::memory;
 
 const MINT_URL: &str = "http://127.0.0.1:8086";
 
-// If both pay and check return pending input proofs should remain pending
+/// Tests that when both pay and check return pending status, input proofs should remain pending
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_fake_tokens_pending() -> Result<()> {
     let wallet = Wallet::new(
         MINT_URL,
         CurrencyUnit::Sat,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -55,14 +56,14 @@ async fn test_fake_tokens_pending() -> Result<()> {
     Ok(())
 }
 
-// If the pay error fails and the check returns unknown or failed
-// The inputs proofs should be unset as spending
+/// Tests that if the pay error fails and the check returns unknown or failed,
+/// the input proofs should be unset as spending (returned to unspent state)
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_fake_melt_payment_fail() -> Result<()> {
     let wallet = Wallet::new(
         MINT_URL,
         CurrencyUnit::Sat,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -113,19 +114,19 @@ async fn test_fake_melt_payment_fail() -> Result<()> {
     }
 
     let wallet_bal = wallet.total_balance().await?;
-    assert!(wallet_bal == 100.into());
+    assert_eq!(wallet_bal, 100.into());
 
     Ok(())
 }
 
-// When both the pay_invoice and check_invoice both fail
-// the proofs should remain as pending
+/// Tests that when both the pay_invoice and check_invoice both fail,
+/// the proofs should remain in pending state
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_fake_melt_payment_fail_and_check() -> Result<()> {
     let wallet = Wallet::new(
         MINT_URL,
         CurrencyUnit::Sat,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -163,14 +164,14 @@ async fn test_fake_melt_payment_fail_and_check() -> Result<()> {
     Ok(())
 }
 
-// In the case that the ln backend returns a failed status but does not error
-// The mint should do a second check, then remove proofs from pending
+/// Tests that when the ln backend returns a failed status but does not error,
+/// the mint should do a second check, then remove proofs from pending state
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_fake_melt_payment_return_fail_status() -> Result<()> {
     let wallet = Wallet::new(
         MINT_URL,
         CurrencyUnit::Sat,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -223,14 +224,14 @@ async fn test_fake_melt_payment_return_fail_status() -> Result<()> {
     Ok(())
 }
 
-// In the case that the ln backend returns a failed status but does not error
-// The mint should do a second check, then remove proofs from pending
+/// Tests that when the ln backend returns an error with unknown status,
+/// the mint should do a second check, then remove proofs from pending state
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_fake_melt_payment_error_unknown() -> Result<()> {
     let wallet = Wallet::new(
         MINT_URL,
         CurrencyUnit::Sat,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -256,7 +257,7 @@ async fn test_fake_melt_payment_error_unknown() -> Result<()> {
 
     // The melt should error at the payment invoice command
     let melt = wallet.melt(&melt_quote.id).await;
-    assert!(melt.is_err());
+    assert_eq!(melt.unwrap_err().to_string(), "Payment failed");
 
     let fake_description = FakeInvoiceDescription {
         pay_invoice_state: MeltQuoteState::Unknown,
@@ -271,7 +272,7 @@ async fn test_fake_melt_payment_error_unknown() -> Result<()> {
 
     // The melt should error at the payment invoice command
     let melt = wallet.melt(&melt_quote.id).await;
-    assert!(melt.is_err());
+    assert_eq!(melt.unwrap_err().to_string(), "Payment failed");
 
     let pending = wallet
         .localstore
@@ -283,15 +284,14 @@ async fn test_fake_melt_payment_error_unknown() -> Result<()> {
     Ok(())
 }
 
-// In the case that the ln backend returns an err
-// The mint should do a second check, that returns paid
-// Proofs should remain pending
+/// Tests that when the ln backend returns an error but the second check returns paid,
+/// proofs should remain in pending state
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_fake_melt_payment_err_paid() -> Result<()> {
     let wallet = Wallet::new(
         MINT_URL,
         CurrencyUnit::Sat,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -324,12 +324,13 @@ async fn test_fake_melt_payment_err_paid() -> Result<()> {
     Ok(())
 }
 
+/// Tests that change outputs in a melt quote are correctly handled
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_fake_melt_change_in_quote() -> Result<()> {
     let wallet = Wallet::new(
         MINT_URL,
         CurrencyUnit::Sat,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -342,6 +343,17 @@ async fn test_fake_melt_change_in_quote() -> Result<()> {
         .mint(&mint_quote.id, SplitTarget::default(), None)
         .await?;
 
+    let transaction = wallet
+        .list_transactions(Some(TransactionDirection::Incoming))
+        .await?
+        .pop()
+        .expect("No transaction found");
+    assert_eq!(wallet.mint_url, transaction.mint_url);
+    assert_eq!(TransactionDirection::Incoming, transaction.direction);
+    assert_eq!(Amount::from(100), transaction.amount);
+    assert_eq!(Amount::from(0), transaction.fee);
+    assert_eq!(CurrencyUnit::Sat, transaction.unit);
+
     let fake_description = FakeInvoiceDescription::default();
 
     let invoice = create_fake_invoice(9000, serde_json::to_string(&fake_description).unwrap());
@@ -354,13 +366,13 @@ async fn test_fake_melt_change_in_quote() -> Result<()> {
 
     let premint_secrets = PreMintSecrets::random(keyset.id, 100.into(), &SplitTarget::default())?;
 
-    let client = HttpClient::new(MINT_URL.parse()?);
+    let client = HttpClient::new(MINT_URL.parse()?, None);
 
-    let melt_request = MeltBolt11Request {
-        quote: melt_quote.id.clone(),
-        inputs: proofs.clone(),
-        outputs: Some(premint_secrets.blinded_messages()),
-    };
+    let melt_request = MeltBolt11Request::new(
+        melt_quote.id.clone(),
+        proofs.clone(),
+        Some(premint_secrets.blinded_messages()),
+    );
 
     let melt_response = client.post_melt(melt_request).await?;
 
@@ -374,15 +386,53 @@ async fn test_fake_melt_change_in_quote() -> Result<()> {
     check.sort_by(|a, b| a.amount.cmp(&b.amount));
 
     assert_eq!(melt_change, check);
+
+    Ok(())
+}
+
+/// Tests that the correct database type is used based on environment variables
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_database_type() -> Result<()> {
+    // Get the database type and work dir from environment
+    let db_type = std::env::var("CDK_MINTD_DATABASE").expect("MINT_DATABASE env var should be set");
+    let work_dir =
+        std::env::var("CDK_MINTD_WORK_DIR").expect("CDK_MINTD_WORK_DIR env var should be set");
+
+    // Check that the correct database file exists
+    match db_type.as_str() {
+        "REDB" => {
+            let db_path = std::path::Path::new(&work_dir).join("cdk-mintd.redb");
+            assert!(
+                db_path.exists(),
+                "Expected redb database file to exist at {:?}",
+                db_path
+            );
+        }
+        "SQLITE" => {
+            let db_path = std::path::Path::new(&work_dir).join("cdk-mintd.sqlite");
+            assert!(
+                db_path.exists(),
+                "Expected sqlite database file to exist at {:?}",
+                db_path
+            );
+        }
+        "MEMORY" => {
+            // Memory database has no file to check
+            println!("Memory database in use - no file to check");
+        }
+        _ => bail!("Unknown database type: {}", db_type),
+    }
+
     Ok(())
 }
 
+/// Tests minting tokens with a valid witness signature
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_fake_mint_with_witness() -> Result<()> {
     let wallet = Wallet::new(
         MINT_URL,
         CurrencyUnit::Sat,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -401,12 +451,13 @@ async fn test_fake_mint_with_witness() -> Result<()> {
     Ok(())
 }
 
+/// Tests that minting without a witness signature fails with the correct error
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_fake_mint_without_witness() -> Result<()> {
     let wallet = Wallet::new(
         MINT_URL,
         CurrencyUnit::Sat,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -415,7 +466,7 @@ async fn test_fake_mint_without_witness() -> Result<()> {
 
     wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
 
-    let http_client = HttpClient::new(MINT_URL.parse()?);
+    let http_client = HttpClient::new(MINT_URL.parse()?, None);
 
     let active_keyset_id = wallet.get_active_mint_keyset().await?.id;
 
@@ -437,12 +488,13 @@ async fn test_fake_mint_without_witness() -> Result<()> {
     }
 }
 
+/// Tests that minting with an incorrect witness signature fails with the correct error
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_fake_mint_with_wrong_witness() -> Result<()> {
     let wallet = Wallet::new(
         MINT_URL,
         CurrencyUnit::Sat,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -451,7 +503,7 @@ async fn test_fake_mint_with_wrong_witness() -> Result<()> {
 
     wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
 
-    let http_client = HttpClient::new(MINT_URL.parse()?);
+    let http_client = HttpClient::new(MINT_URL.parse()?, None);
 
     let active_keyset_id = wallet.get_active_mint_keyset().await?.id;
 
@@ -477,12 +529,13 @@ async fn test_fake_mint_with_wrong_witness() -> Result<()> {
     }
 }
 
+/// Tests that attempting to mint more tokens than allowed by the quote fails
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_fake_mint_inflated() -> Result<()> {
     let wallet = Wallet::new(
         MINT_URL,
         CurrencyUnit::Sat,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -510,7 +563,7 @@ async fn test_fake_mint_inflated() -> Result<()> {
     if let Some(secret_key) = quote_info.secret_key {
         mint_request.sign(secret_key)?;
     }
-    let http_client = HttpClient::new(MINT_URL.parse()?);
+    let http_client = HttpClient::new(MINT_URL.parse()?, None);
 
     let response = http_client.post_mint(mint_request.clone()).await;
 
@@ -529,12 +582,13 @@ async fn test_fake_mint_inflated() -> Result<()> {
     Ok(())
 }
 
+/// Tests that attempting to mint with multiple currency units in the same request fails
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_fake_mint_multiple_units() -> Result<()> {
     let wallet = Wallet::new(
         MINT_URL,
         CurrencyUnit::Sat,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -550,7 +604,7 @@ async fn test_fake_mint_multiple_units() -> Result<()> {
     let wallet_usd = Wallet::new(
         MINT_URL,
         CurrencyUnit::Usd,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -580,7 +634,7 @@ async fn test_fake_mint_multiple_units() -> Result<()> {
     if let Some(secret_key) = quote_info.secret_key {
         mint_request.sign(secret_key)?;
     }
-    let http_client = HttpClient::new(MINT_URL.parse()?);
+    let http_client = HttpClient::new(MINT_URL.parse()?, None);
 
     let response = http_client.post_mint(mint_request.clone()).await;
 
@@ -599,12 +653,13 @@ async fn test_fake_mint_multiple_units() -> Result<()> {
     Ok(())
 }
 
+/// Tests that attempting to swap tokens with multiple currency units fails
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_fake_mint_multiple_unit_swap() -> Result<()> {
     let wallet = Wallet::new(
         MINT_URL,
         CurrencyUnit::Sat,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -618,7 +673,7 @@ async fn test_fake_mint_multiple_unit_swap() -> Result<()> {
     let wallet_usd = Wallet::new(
         MINT_URL,
         CurrencyUnit::Usd,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -642,12 +697,9 @@ async fn test_fake_mint_multiple_unit_swap() -> Result<()> {
         let pre_mint =
             PreMintSecrets::random(active_keyset_id, inputs.total_amount()?, &SplitTarget::None)?;
 
-        let swap_request = SwapRequest {
-            inputs,
-            outputs: pre_mint.blinded_messages(),
-        };
+        let swap_request = SwapRequest::new(inputs, pre_mint.blinded_messages());
 
-        let http_client = HttpClient::new(MINT_URL.parse()?);
+        let http_client = HttpClient::new(MINT_URL.parse()?, None);
         let response = http_client.post_swap(swap_request.clone()).await;
 
         match response {
@@ -679,12 +731,9 @@ async fn test_fake_mint_multiple_unit_swap() -> Result<()> {
 
         usd_outputs.append(&mut sat_outputs);
 
-        let swap_request = SwapRequest {
-            inputs,
-            outputs: usd_outputs,
-        };
+        let swap_request = SwapRequest::new(inputs, usd_outputs);
 
-        let http_client = HttpClient::new(MINT_URL.parse()?);
+        let http_client = HttpClient::new(MINT_URL.parse()?, None);
         let response = http_client.post_swap(swap_request.clone()).await;
 
         match response {
@@ -703,12 +752,13 @@ async fn test_fake_mint_multiple_unit_swap() -> Result<()> {
     Ok(())
 }
 
+/// Tests that attempting to melt tokens with multiple currency units fails
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_fake_mint_multiple_unit_melt() -> Result<()> {
     let wallet = Wallet::new(
         MINT_URL,
         CurrencyUnit::Sat,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -727,7 +777,7 @@ async fn test_fake_mint_multiple_unit_melt() -> Result<()> {
     let wallet_usd = Wallet::new(
         MINT_URL,
         CurrencyUnit::Usd,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -752,13 +802,9 @@ async fn test_fake_mint_multiple_unit_melt() -> Result<()> {
         let invoice = create_fake_invoice((input_amount - 1) * 1000, "".to_string());
         let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
 
-        let melt_request = MeltBolt11Request {
-            quote: melt_quote.id,
-            inputs,
-            outputs: None,
-        };
+        let melt_request = MeltBolt11Request::new(melt_quote.id, inputs, None);
 
-        let http_client = HttpClient::new(MINT_URL.parse()?);
+        let http_client = HttpClient::new(MINT_URL.parse()?, None);
         let response = http_client.post_melt(melt_request.clone()).await;
 
         match response {
@@ -796,13 +842,10 @@ async fn test_fake_mint_multiple_unit_melt() -> Result<()> {
         usd_outputs.append(&mut sat_outputs);
         let quote = wallet.melt_quote(invoice.to_string(), None).await?;
 
-        let melt_request = MeltBolt11Request {
-            quote: quote.id,
-            inputs,
-            outputs: Some(usd_outputs),
-        };
+        let melt_request = MeltBolt11Request::new(quote.id, inputs, Some(usd_outputs));
+
+        let http_client = HttpClient::new(MINT_URL.parse()?, None);
 
-        let http_client = HttpClient::new(MINT_URL.parse()?);
         let response = http_client.post_melt(melt_request.clone()).await;
 
         match response {
@@ -821,13 +864,13 @@ async fn test_fake_mint_multiple_unit_melt() -> Result<()> {
     Ok(())
 }
 
-/// Test swap where input unit != output unit
+/// Tests that swapping tokens where input unit doesn't match output unit fails
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_fake_mint_input_output_mismatch() -> Result<()> {
     let wallet = Wallet::new(
         MINT_URL,
         CurrencyUnit::Sat,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -841,11 +884,10 @@ async fn test_fake_mint_input_output_mismatch() -> Result<()> {
     let wallet_usd = Wallet::new(
         MINT_URL,
         CurrencyUnit::Usd,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
-
     let usd_active_keyset_id = wallet_usd.get_active_mint_keyset().await?.id;
 
     let inputs = proofs;
@@ -856,12 +898,9 @@ async fn test_fake_mint_input_output_mismatch() -> Result<()> {
         &SplitTarget::None,
     )?;
 
-    let swap_request = SwapRequest {
-        inputs,
-        outputs: pre_mint.blinded_messages(),
-    };
+    let swap_request = SwapRequest::new(inputs, pre_mint.blinded_messages());
 
-    let http_client = HttpClient::new(MINT_URL.parse()?);
+    let http_client = HttpClient::new(MINT_URL.parse()?, None);
     let response = http_client.post_swap(swap_request.clone()).await;
 
     match response {
@@ -877,13 +916,13 @@ async fn test_fake_mint_input_output_mismatch() -> Result<()> {
     Ok(())
 }
 
-/// Test swap where input is less the output
+/// Tests that swapping tokens where output amount is greater than input amount fails
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_fake_mint_swap_inflated() -> Result<()> {
     let wallet = Wallet::new(
         MINT_URL,
         CurrencyUnit::Sat,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -896,17 +935,78 @@ async fn test_fake_mint_swap_inflated() -> Result<()> {
     let active_keyset_id = wallet.get_active_mint_keyset().await?.id;
     let pre_mint = PreMintSecrets::random(active_keyset_id, 101.into(), &SplitTarget::None)?;
 
-    let swap_request = SwapRequest {
-        inputs: proofs,
-        outputs: pre_mint.blinded_messages(),
-    };
+    let swap_request = SwapRequest::new(proofs, pre_mint.blinded_messages());
+
+    let http_client = HttpClient::new(MINT_URL.parse()?, None);
+    let response = http_client.post_swap(swap_request.clone()).await;
+
+    match response {
+        Err(err) => match err {
+            cdk::Error::TransactionUnbalanced(_, _, _) => (),
+            err => {
+                bail!("Wrong mint error returned: {}", err.to_string());
+            }
+        },
+        Ok(_) => {
+            bail!("Should not have allowed to mint with multiple units");
+        }
+    }
+
+    Ok(())
+}
+
+/// Tests that tokens cannot be spent again after a failed swap attempt
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_fake_mint_swap_spend_after_fail() -> Result<()> {
+    let wallet = Wallet::new(
+        MINT_URL,
+        CurrencyUnit::Sat,
+        Arc::new(memory::empty().await?),
+        &Mnemonic::generate(12)?.to_seed_normalized(""),
+        None,
+    )?;
+
+    let mint_quote = wallet.mint_quote(100.into(), None).await?;
+
+    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
+
+    let proofs = wallet.mint(&mint_quote.id, SplitTarget::None, None).await?;
+    let active_keyset_id = wallet.get_active_mint_keyset().await?.id;
+
+    let pre_mint = PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::None)?;
+
+    let swap_request = SwapRequest::new(proofs.clone(), pre_mint.blinded_messages());
+
+    let http_client = HttpClient::new(MINT_URL.parse()?, None);
+    let response = http_client.post_swap(swap_request.clone()).await;
+
+    assert!(response.is_ok());
+
+    let pre_mint = PreMintSecrets::random(active_keyset_id, 101.into(), &SplitTarget::None)?;
+
+    let swap_request = SwapRequest::new(proofs.clone(), pre_mint.blinded_messages());
 
-    let http_client = HttpClient::new(MINT_URL.parse()?);
+    let http_client = HttpClient::new(MINT_URL.parse()?, None);
     let response = http_client.post_swap(swap_request.clone()).await;
 
     match response {
         Err(err) => match err {
             cdk::Error::TransactionUnbalanced(_, _, _) => (),
+            err => bail!("Wrong mint error returned expected TransactionUnbalanced, got: {err}"),
+        },
+        Ok(_) => bail!("Should not have allowed swap with unbalanced"),
+    }
+
+    let pre_mint = PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::None)?;
+
+    let swap_request = SwapRequest::new(proofs, pre_mint.blinded_messages());
+
+    let http_client = HttpClient::new(MINT_URL.parse()?, None);
+    let response = http_client.post_swap(swap_request.clone()).await;
+
+    match response {
+        Err(err) => match err {
+            cdk::Error::TokenAlreadySpent => (),
             err => {
                 bail!("Wrong mint error returned: {}", err.to_string());
             }
@@ -919,13 +1019,79 @@ async fn test_fake_mint_swap_inflated() -> Result<()> {
     Ok(())
 }
 
-/// Test swap where input unit != output unit
+/// Tests that tokens cannot be melted after a failed swap attempt
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_fake_mint_melt_spend_after_fail() -> Result<()> {
+    let wallet = Wallet::new(
+        MINT_URL,
+        CurrencyUnit::Sat,
+        Arc::new(memory::empty().await?),
+        &Mnemonic::generate(12)?.to_seed_normalized(""),
+        None,
+    )?;
+
+    let mint_quote = wallet.mint_quote(100.into(), None).await?;
+
+    wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
+
+    let proofs = wallet.mint(&mint_quote.id, SplitTarget::None, None).await?;
+    let active_keyset_id = wallet.get_active_mint_keyset().await?.id;
+
+    let pre_mint = PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::None)?;
+
+    let swap_request = SwapRequest::new(proofs.clone(), pre_mint.blinded_messages());
+
+    let http_client = HttpClient::new(MINT_URL.parse()?, None);
+    let response = http_client.post_swap(swap_request.clone()).await;
+
+    assert!(response.is_ok());
+
+    let pre_mint = PreMintSecrets::random(active_keyset_id, 101.into(), &SplitTarget::None)?;
+
+    let swap_request = SwapRequest::new(proofs.clone(), pre_mint.blinded_messages());
+
+    let http_client = HttpClient::new(MINT_URL.parse()?, None);
+    let response = http_client.post_swap(swap_request.clone()).await;
+
+    match response {
+        Err(err) => match err {
+            cdk::Error::TransactionUnbalanced(_, _, _) => (),
+            err => bail!("Wrong mint error returned expected TransactionUnbalanced, got: {err}"),
+        },
+        Ok(_) => bail!("Should not have allowed swap with unbalanced"),
+    }
+
+    let input_amount: u64 = proofs.total_amount()?.into();
+    let invoice = create_fake_invoice((input_amount - 1) * 1000, "".to_string());
+    let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
+
+    let melt_request = MeltBolt11Request::new(melt_quote.id, proofs, None);
+
+    let http_client = HttpClient::new(MINT_URL.parse()?, None);
+    let response = http_client.post_melt(melt_request.clone()).await;
+
+    match response {
+        Err(err) => match err {
+            cdk::Error::TokenAlreadySpent => (),
+            err => {
+                bail!("Wrong mint error returned: {}", err.to_string());
+            }
+        },
+        Ok(_) => {
+            bail!("Should not have allowed to melt with multiple units");
+        }
+    }
+
+    Ok(())
+}
+
+/// Tests that attempting to swap with duplicate proofs fails
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_fake_mint_duplicate_proofs_swap() -> Result<()> {
     let wallet = Wallet::new(
         MINT_URL,
         CurrencyUnit::Sat,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -943,12 +1109,9 @@ async fn test_fake_mint_duplicate_proofs_swap() -> Result<()> {
     let pre_mint =
         PreMintSecrets::random(active_keyset_id, inputs.total_amount()?, &SplitTarget::None)?;
 
-    let swap_request = SwapRequest {
-        inputs: inputs.clone(),
-        outputs: pre_mint.blinded_messages(),
-    };
+    let swap_request = SwapRequest::new(inputs.clone(), pre_mint.blinded_messages());
 
-    let http_client = HttpClient::new(MINT_URL.parse()?);
+    let http_client = HttpClient::new(MINT_URL.parse()?, None);
     let response = http_client.post_swap(swap_request.clone()).await;
 
     match response {
@@ -970,9 +1133,9 @@ async fn test_fake_mint_duplicate_proofs_swap() -> Result<()> {
 
     let outputs = vec![blinded_message[0].clone(), blinded_message[0].clone()];
 
-    let swap_request = SwapRequest { inputs, outputs };
+    let swap_request = SwapRequest::new(inputs, outputs);
 
-    let http_client = HttpClient::new(MINT_URL.parse()?);
+    let http_client = HttpClient::new(MINT_URL.parse()?, None);
     let response = http_client.post_swap(swap_request.clone()).await;
 
     match response {
@@ -993,13 +1156,13 @@ async fn test_fake_mint_duplicate_proofs_swap() -> Result<()> {
     Ok(())
 }
 
-/// Test duplicate proofs in melt
+/// Tests that attempting to melt with duplicate proofs fails
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_fake_mint_duplicate_proofs_melt() -> Result<()> {
     let wallet = Wallet::new(
         MINT_URL,
         CurrencyUnit::Sat,
-        Arc::new(WalletMemoryDatabase::default()),
+        Arc::new(memory::empty().await?),
         &Mnemonic::generate(12)?.to_seed_normalized(""),
         None,
     )?;
@@ -1016,13 +1179,9 @@ async fn test_fake_mint_duplicate_proofs_melt() -> Result<()> {
 
     let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
 
-    let melt_request = MeltBolt11Request {
-        quote: melt_quote.id,
-        inputs,
-        outputs: None,
-    };
+    let melt_request = MeltBolt11Request::new(melt_quote.id, inputs, None);
 
-    let http_client = HttpClient::new(MINT_URL.parse()?);
+    let http_client = HttpClient::new(MINT_URL.parse()?, None);
     let response = http_client.post_melt(melt_request.clone()).await;
 
     match response {

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно