Explorar el Código

Abstract HTTP client with cdk-http crate (#1573)

* Add cdk-http-client crate

Extract HTTP client functionality from cdk-common into a dedicated crate to
improve modularity and separation of concerns.

Changes:
- Create new cdk-http-client crate with HttpClient, HttpClientBuilder,
  RequestBuilder, RawResponse, and HttpError types
- Move HTTP client code from cdk-common/src/http/ to the new crate
- Add comprehensive test suite:
  - 20 unit tests for error handling, client/builder construction
  - 28 integration tests using mockito for HTTP mocking
- Add proxy support with optional regex-based host matching (non-WASM)
- Add TLS certificate validation bypass option (non-WASM)

The new crate provides a clean abstraction over reqwest with:
- Simple convenience methods (fetch, post_json, post_form, patch_json)
- Raw response access for streaming/custom handling
- Fluent RequestBuilder API for complex requests
- Cross-platform support (native and WASM)
C hace 19 horas
padre
commit
a8b83659d0
Se han modificado 36 ficheros con 1773 adiciones y 236 borrados
  1. 67 5
      Cargo.lock
  2. 109 45
      Cargo.lock.msrv
  3. 1 0
      Cargo.toml
  4. 1 2
      crates/cdk-cli/Cargo.toml
  5. 10 14
      crates/cdk-cli/src/sub_commands/cat_device_login.rs
  6. 3 10
      crates/cdk-cli/src/sub_commands/cat_login.rs
  7. 3 3
      crates/cdk-cli/src/sub_commands/mint_blind_auth.rs
  8. 2 1
      crates/cdk-common/Cargo.toml
  9. 5 0
      crates/cdk-common/src/lib.rs
  10. 1 2
      crates/cdk-fake-wallet/Cargo.toml
  11. 1 4
      crates/cdk-fake-wallet/src/lib.rs
  12. 32 0
      crates/cdk-http-client/Cargo.toml
  13. 36 0
      crates/cdk-http-client/README.md
  14. 329 0
      crates/cdk-http-client/src/client.rs
  15. 130 0
      crates/cdk-http-client/src/error.rs
  16. 31 0
      crates/cdk-http-client/src/lib.rs
  17. 63 0
      crates/cdk-http-client/src/request.rs
  18. 85 0
      crates/cdk-http-client/src/response.rs
  19. 760 0
      crates/cdk-http-client/tests/integration.rs
  20. 2 2
      crates/cdk-integration-tests/Cargo.toml
  21. 3 2
      crates/cdk-integration-tests/src/shared.rs
  22. 8 9
      crates/cdk-integration-tests/tests/fake_auth.rs
  23. 6 5
      crates/cdk-integration-tests/tests/ldk_node.rs
  24. 12 18
      crates/cdk-integration-tests/tests/nutshell_wallet.rs
  25. 2 2
      crates/cdk-npubcash/Cargo.toml
  26. 5 5
      crates/cdk-npubcash/src/auth.rs
  27. 6 6
      crates/cdk-npubcash/src/client.rs
  28. 1 1
      crates/cdk-npubcash/src/error.rs
  29. 2 4
      crates/cdk/Cargo.toml
  30. 3 10
      crates/cdk/examples/auth_wallet.rs
  31. 3 5
      crates/cdk/examples/multimint-npubcash.rs
  32. 3 5
      crates/cdk/examples/npubcash.rs
  33. 9 0
      crates/cdk/src/lib.rs
  34. 9 28
      crates/cdk/src/oidc_client.rs
  35. 25 42
      crates/cdk/src/wallet/mint_connector/transport.rs
  36. 5 6
      crates/cdk/src/wallet/payment_request.rs

+ 67 - 5
Cargo.lock

@@ -348,6 +348,16 @@ dependencies = [
 ]
 
 [[package]]
+name = "assert-json-diff"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
 name = "async-compat"
 version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1202,7 +1212,6 @@ dependencies = [
  "nostr-sdk",
  "rand 0.9.2",
  "regex",
- "reqwest",
  "ring 0.17.14",
  "rustls 0.23.36",
  "serde",
@@ -1262,7 +1271,6 @@ dependencies = [
  "home",
  "lightning 0.2.0",
  "nostr-sdk",
- "reqwest",
  "serde",
  "serde_json",
  "serde_with",
@@ -1299,6 +1307,7 @@ dependencies = [
  "bitcoin 0.32.8",
  "cashu",
  "cbor-diag",
+ "cdk-http-client",
  "cdk-prometheus",
  "ciborium",
  "criterion",
@@ -1334,7 +1343,6 @@ dependencies = [
  "futures",
  "lightning 0.2.0",
  "lightning-invoice 0.34.0",
- "reqwest",
  "serde",
  "serde_json",
  "thiserror 2.0.18",
@@ -1375,6 +1383,20 @@ dependencies = [
 ]
 
 [[package]]
+name = "cdk-http-client"
+version = "0.14.0"
+dependencies = [
+ "mockito",
+ "regex",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+ "tokio",
+ "url",
+]
+
+[[package]]
 name = "cdk-integration-tests"
 version = "0.14.0"
 dependencies = [
@@ -1390,6 +1412,7 @@ dependencies = [
  "cdk-common",
  "cdk-fake-wallet",
  "cdk-ffi",
+ "cdk-http-client",
  "cdk-ldk-node",
  "cdk-lnd",
  "cdk-mintd",
@@ -1403,7 +1426,6 @@ dependencies = [
  "ln-regtest-rs",
  "once_cell",
  "rand 0.9.2",
- "reqwest",
  "serde",
  "serde_json",
  "tokio",
@@ -1554,10 +1576,10 @@ dependencies = [
  "cashu",
  "cdk",
  "cdk-common",
+ "cdk-http-client",
  "cdk-sqlite",
  "chrono",
  "nostr-sdk",
- "reqwest",
  "rustls 0.23.36",
  "serde",
  "serde_json",
@@ -1912,6 +1934,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
 
 [[package]]
+name = "colored"
+version = "3.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
 name = "combine"
 version = "4.6.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4493,6 +4524,31 @@ dependencies = [
 ]
 
 [[package]]
+name = "mockito"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e0603425789b4a70fcc4ac4f5a46a566c116ee3e2a6b768dc623f7719c611de"
+dependencies = [
+ "assert-json-diff",
+ "bytes",
+ "colored",
+ "futures-core",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "http-body-util",
+ "hyper 1.8.1",
+ "hyper-util",
+ "log",
+ "pin-project-lite",
+ "rand 0.9.2",
+ "regex",
+ "serde_json",
+ "serde_urlencoded",
+ "similar",
+ "tokio",
+]
+
+[[package]]
 name = "moka"
 version = "0.12.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6703,6 +6759,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
 
 [[package]]
+name = "similar"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
+
+[[package]]
 name = "simple_asn1"
 version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"

+ 109 - 45
Cargo.lock.msrv

@@ -348,6 +348,16 @@ dependencies = [
 ]
 
 [[package]]
+name = "assert-json-diff"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
 name = "async-compat"
 version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -534,9 +544,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
 
 [[package]]
 name = "aws-lc-rs"
-version = "1.15.3"
+version = "1.15.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e84ce723ab67259cfeb9877c6a639ee9eb7a27b28123abd71db7f0d5d0cc9d86"
+checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256"
 dependencies = [
  "aws-lc-sys",
  "zeroize",
@@ -544,9 +554,9 @@ dependencies = [
 
 [[package]]
 name = "aws-lc-sys"
-version = "0.36.0"
+version = "0.37.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43a442ece363113bd4bd4c8b18977a7798dd4d3c3383f34fb61936960e8f4ad8"
+checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a"
 dependencies = [
  "cc",
  "cmake",
@@ -1160,9 +1170,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.2.53"
+version = "1.2.54"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932"
+checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
 dependencies = [
  "find-msvc-tools",
  "jobserver",
@@ -1202,7 +1212,6 @@ dependencies = [
  "nostr-sdk",
  "rand 0.9.2",
  "regex",
- "reqwest",
  "ring 0.17.14",
  "rustls 0.23.36",
  "serde",
@@ -1262,7 +1271,6 @@ dependencies = [
  "home",
  "lightning 0.2.0",
  "nostr-sdk",
- "reqwest",
  "serde",
  "serde_json",
  "serde_with",
@@ -1299,6 +1307,7 @@ dependencies = [
  "bitcoin 0.32.8",
  "cashu",
  "cbor-diag",
+ "cdk-http-client",
  "cdk-prometheus",
  "ciborium",
  "criterion",
@@ -1334,7 +1343,6 @@ dependencies = [
  "futures",
  "lightning 0.2.0",
  "lightning-invoice 0.34.0",
- "reqwest",
  "serde",
  "serde_json",
  "thiserror 2.0.18",
@@ -1354,11 +1362,13 @@ dependencies = [
  "bip39",
  "cdk",
  "cdk-common",
+ "cdk-npubcash",
  "cdk-postgres",
  "cdk-sql-common",
  "cdk-sqlite",
  "futures",
  "log",
+ "nostr-sdk",
  "once_cell",
  "rand 0.9.2",
  "serde",
@@ -1373,6 +1383,20 @@ dependencies = [
 ]
 
 [[package]]
+name = "cdk-http-client"
+version = "0.14.0"
+dependencies = [
+ "mockito",
+ "regex",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+ "tokio",
+ "url",
+]
+
+[[package]]
 name = "cdk-integration-tests"
 version = "0.14.0"
 dependencies = [
@@ -1388,6 +1412,7 @@ dependencies = [
  "cdk-common",
  "cdk-fake-wallet",
  "cdk-ffi",
+ "cdk-http-client",
  "cdk-ldk-node",
  "cdk-lnd",
  "cdk-mintd",
@@ -1401,7 +1426,6 @@ dependencies = [
  "ln-regtest-rs",
  "once_cell",
  "rand 0.9.2",
- "reqwest",
  "serde",
  "serde_json",
  "tokio",
@@ -1552,10 +1576,10 @@ dependencies = [
  "cashu",
  "cdk",
  "cdk-common",
+ "cdk-http-client",
  "cdk-sqlite",
  "chrono",
  "nostr-sdk",
- "reqwest",
  "rustls 0.23.36",
  "serde",
  "serde_json",
@@ -1910,6 +1934,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
 
 [[package]]
+name = "colored"
+version = "3.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
+dependencies = [
+ "windows-sys 0.48.0",
+]
+
+[[package]]
 name = "combine"
 version = "4.6.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2323,12 +2356,12 @@ dependencies = [
 
 [[package]]
 name = "deranged"
-version = "0.5.5"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
+checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
 dependencies = [
  "powerfmt",
- "serde_core",
+ "serde",
 ]
 
 [[package]]
@@ -3618,7 +3651,7 @@ dependencies = [
  "libc",
  "percent-encoding",
  "pin-project-lite",
- "socket2 0.6.1",
+ "socket2 0.6.2",
  "tokio",
  "tower-service",
  "tracing",
@@ -4041,9 +4074,9 @@ dependencies = [
 
 [[package]]
 name = "libm"
-version = "0.2.15"
+version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
 
 [[package]]
 name = "libredox"
@@ -4491,10 +4524,35 @@ dependencies = [
 ]
 
 [[package]]
+name = "mockito"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e0603425789b4a70fcc4ac4f5a46a566c116ee3e2a6b768dc623f7719c611de"
+dependencies = [
+ "assert-json-diff",
+ "bytes",
+ "colored",
+ "futures-core",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "http-body-util",
+ "hyper 1.8.1",
+ "hyper-util",
+ "log",
+ "pin-project-lite",
+ "rand 0.9.2",
+ "regex",
+ "serde_json",
+ "serde_urlencoded",
+ "similar",
+ "tokio",
+]
+
+[[package]]
 name = "moka"
-version = "0.12.12"
+version = "0.12.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3dec6bd31b08944e08b58fd99373893a6c17054d6f3ea5006cc894f4f4eee2a"
+checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e"
 dependencies = [
  "async-lock",
  "crossbeam-channel",
@@ -5636,7 +5694,7 @@ dependencies = [
  "quinn-udp",
  "rustc-hash",
  "rustls 0.23.36",
- "socket2 0.6.1",
+ "socket2 0.6.2",
  "thiserror 2.0.18",
  "tokio",
  "tracing",
@@ -5673,16 +5731,16 @@ dependencies = [
  "cfg_aliases",
  "libc",
  "once_cell",
- "socket2 0.6.1",
+ "socket2 0.6.2",
  "tracing",
  "windows-sys 0.60.2",
 ]
 
 [[package]]
 name = "quote"
-version = "1.0.43"
+version = "1.0.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
+checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
 dependencies = [
  "proc-macro2",
 ]
@@ -6701,6 +6759,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
 
 [[package]]
+name = "similar"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
+
+[[package]]
 name = "simple_asn1"
 version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6763,9 +6827,9 @@ dependencies = [
 
 [[package]]
 name = "socket2"
-version = "0.6.1"
+version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
+checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
 dependencies = [
  "libc",
  "windows-sys 0.60.2",
@@ -7104,30 +7168,30 @@ dependencies = [
 
 [[package]]
 name = "time"
-version = "0.3.45"
+version = "0.3.41"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd"
+checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
 dependencies = [
  "deranged",
  "itoa",
  "num-conv",
  "powerfmt",
- "serde_core",
+ "serde",
  "time-core",
  "time-macros",
 ]
 
 [[package]]
 name = "time-core"
-version = "0.1.7"
+version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca"
+checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
 
 [[package]]
 name = "time-macros"
-version = "0.2.25"
+version = "0.2.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd"
+checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
 dependencies = [
  "num-conv",
  "time-core",
@@ -7245,7 +7309,7 @@ dependencies = [
  "parking_lot",
  "pin-project-lite",
  "signal-hook-registry",
- "socket2 0.6.1",
+ "socket2 0.6.2",
  "tokio-macros",
  "windows-sys 0.61.2",
 ]
@@ -7301,7 +7365,7 @@ dependencies = [
  "postgres-protocol",
  "postgres-types",
  "rand 0.9.2",
- "socket2 0.6.1",
+ "socket2 0.6.2",
  "tokio",
  "tokio-util",
  "whoami",
@@ -7547,7 +7611,7 @@ dependencies = [
  "hyper-util",
  "percent-encoding",
  "pin-project",
- "socket2 0.6.1",
+ "socket2 0.6.2",
  "sync_wrapper 1.0.2",
  "tokio",
  "tokio-rustls 0.26.4",
@@ -8542,9 +8606,9 @@ dependencies = [
 
 [[package]]
 name = "typed-index-collections"
-version = "3.3.0"
+version = "3.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fd393dbd1e7b23e0cab7396570309b4068aa504e9dac2cd41d827583b4e9ab7"
+checksum = "898160f1dfd383b4e92e17f0512a7d62f3c51c44937b23b6ffc3a1614a8eaccd"
 dependencies = [
  "bincode",
  "serde",
@@ -8884,9 +8948,9 @@ dependencies = [
 
 [[package]]
 name = "uuid"
-version = "1.19.0"
+version = "1.20.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
+checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
 dependencies = [
  "getrandom 0.3.4",
  "js-sys",
@@ -9678,18 +9742,18 @@ dependencies = [
 
 [[package]]
 name = "zerocopy"
-version = "0.8.33"
+version = "0.8.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
+checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d"
 dependencies = [
  "zerocopy-derive",
 ]
 
 [[package]]
 name = "zerocopy-derive"
-version = "0.8.33"
+version = "0.8.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
+checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -9792,9 +9856,9 @@ checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3"
 
 [[package]]
 name = "zmij"
-version = "1.0.16"
+version = "1.0.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65"
+checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
 
 [[package]]
 name = "zopfli"

+ 1 - 0
Cargo.toml

@@ -66,6 +66,7 @@ cdk-signatory = { path = "./crates/cdk-signatory", version = "=0.14.0", default-
 cdk-mintd = { path = "./crates/cdk-mintd", version = "=0.14.0", default-features = false }
 cdk-prometheus = { path = "./crates/cdk-prometheus", version = "=0.14.0", default-features = false }
 cdk-npubcash = { path = "./crates/cdk-npubcash", version = "=0.14.0" }
+cdk-http-client = { path = "./crates/cdk-http-client", version = "=0.14.0" }
 clap = { version = "4.5.31", features = ["derive"] }
 ciborium = { version = "0.2.2", default-features = false, features = ["std"] }
 cbor-diag = "0.1.12"

+ 1 - 2
crates/cdk-cli/Cargo.toml

@@ -25,7 +25,7 @@ bitcoin.workspace = true
 cdk = { workspace = true, default-features = false, features = ["wallet", "nostr", "bip353"]}
 cdk-redb = { workspace = true, features = ["wallet"], optional = true }
 cdk-sqlite = { workspace = true, features = ["wallet"] }
-cdk-common = { workspace = true, features = ["wallet"] }
+cdk-common = { workspace = true, features = ["wallet", "http"] }
 clap.workspace = true
 serde.workspace = true
 serde_json.workspace = true
@@ -34,7 +34,6 @@ tracing.workspace = true
 tracing-subscriber.workspace = true
 home.workspace = true
 nostr-sdk = { workspace = true }
-reqwest.workspace = true
 url.workspace = true
 serde_with.workspace = true
 lightning.workspace = true

+ 10 - 14
crates/cdk-cli/src/sub_commands/cat_device_login.rs

@@ -82,22 +82,18 @@ async fn get_device_code_token(mint_info: &MintInfo) -> (String, String) {
     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.clone().as_str()),
-            ("scope", "openid offline_access"),
-        ])
-        .send()
+    let client = cdk_common::HttpClient::new();
+    let device_code_data: serde_json::Value = client
+        .post_form(
+            &device_auth_url,
+            &[
+                ("client_id", client_id.clone().as_str()),
+                ("scope", "openid offline_access"),
+            ],
+        )
         .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");
@@ -140,7 +136,7 @@ async fn get_device_code_token(mint_info: &MintInfo) -> (String, String) {
             .await
             .expect("Failed to send token request");
 
-        if token_response.status().is_success() {
+        if token_response.is_success() {
             let token_data: serde_json::Value = token_response
                 .json()
                 .await

+ 3 - 10
crates/cdk-cli/src/sub_commands/cat_login.rs

@@ -95,19 +95,12 @@ async fn get_access_token(mint_info: &MintInfo, user: &str, password: &str) -> (
     ];
 
     // Make the token request directly
-    let client = reqwest::Client::new();
-    let response = client
-        .post(token_url)
-        .form(&params)
-        .send()
+    let client = cdk_common::HttpClient::new();
+    let token_response: serde_json::Value = client
+        .post_form(&token_url, &params)
         .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")

+ 3 - 3
crates/cdk-cli/src/sub_commands/mint_blind_auth.rs

@@ -171,10 +171,10 @@ async fn refresh_access_token(
     ];
 
     // Make the token refresh request
-    let client = reqwest::Client::new();
-    let response = client.post(token_url).form(&params).send().await?;
+    let client = cdk_common::HttpClient::new();
+    let response = client.post(&token_url).form(&params).send().await?;
 
-    if !response.status().is_success() {
+    if !response.is_success() {
         return Err(anyhow::anyhow!(
             "Token refresh failed with status: {}",
             response.status()

+ 2 - 1
crates/cdk-common/Cargo.toml

@@ -19,8 +19,10 @@ wallet = ["cashu/wallet", "dep:uuid"]
 mint = ["cashu/mint", "dep:uuid"]
 nostr = ["wallet", "cashu/nostr"]
 prometheus = ["cdk-prometheus/default"]
+http = ["dep:cdk-http-client"]
 
 [dependencies]
+cdk-http-client = { workspace = true, optional = true }
 async-trait.workspace = true
 bitcoin.workspace = true
 cashu.workspace = true
@@ -43,7 +45,6 @@ web-time.workspace = true
 parking_lot = "0.12.5"
 paste = "1.0.15"
 
-
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 tokio = { version = "1", default-features = false, features = ["rt", "rt-multi-thread", "macros", "test-util", "sync"] }
 

+ 5 - 0
crates/cdk-common/src/lib.rs

@@ -33,6 +33,11 @@ pub use cashu::nuts::{self, *};
 #[cfg(feature = "mint")]
 pub use cashu::quote_id::{self, *};
 pub use cashu::{dhke, ensure_cdk, mint_url, secret, util, SECP256K1};
+/// Re-export cdk-http-client types
+#[cfg(feature = "http")]
+pub use cdk_http_client::{
+    fetch, HttpClient, HttpClientBuilder, HttpError, RawResponse, RequestBuilder, Response,
+};
 // Re-export common types
 pub use common::FinalizedMelt;
 pub use error::Error;

+ 1 - 2
crates/cdk-fake-wallet/Cargo.toml

@@ -13,7 +13,7 @@ readme = "README.md"
 [dependencies]
 async-trait.workspace = true
 bitcoin.workspace = true
-cdk-common = { workspace = true, features = ["mint"] }
+cdk-common = { workspace = true, features = ["mint", "http"] }
 futures.workspace = true
 tokio.workspace = true
 tokio-util.workspace = true
@@ -24,7 +24,6 @@ serde_json.workspace = true
 lightning-invoice.workspace = true
 lightning.workspace = true
 tokio-stream.workspace = true
-reqwest.workspace = true
 uuid.workspace = true
 
 [lints]

+ 1 - 4
crates/cdk-fake-wallet/src/lib.rs

@@ -105,10 +105,7 @@ impl ExchangeRateCache {
     /// Fetch fresh rate and update cache
     async fn fetch_fresh_rate(&self, currency: &CurrencyUnit) -> Result<f64, Error> {
         let url = "https://mempool.space/api/v1/prices";
-        let response = reqwest::get(url)
-            .await
-            .map_err(|_| Error::UnknownInvoiceAmount)?
-            .json::<MempoolPricesResponse>()
+        let response: MempoolPricesResponse = cdk_common::fetch(url)
             .await
             .map_err(|_| Error::UnknownInvoiceAmount)?;
 

+ 32 - 0
crates/cdk-http-client/Cargo.toml

@@ -0,0 +1,32 @@
+[package]
+name = "cdk-http-client"
+version.workspace = true
+authors = ["CDK Developers"]
+description = "HTTP client abstraction for CDK"
+homepage = "https://github.com/cashubtc/cdk"
+repository = "https://github.com/cashubtc/cdk.git"
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+readme = "README.md"
+
+[dependencies]
+serde.workspace = true
+serde_json.workspace = true
+thiserror.workspace = true
+url.workspace = true
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+reqwest = { workspace = true }
+regex = { workspace = true }
+
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+reqwest = { version = "0.12", default-features = false, features = ["json"] }
+
+[dev-dependencies]
+tokio = { workspace = true, features = ["rt", "macros"] }
+mockito = "1"
+serde = { workspace = true }
+
+[lints]
+workspace = true

+ 36 - 0
crates/cdk-http-client/README.md

@@ -0,0 +1,36 @@
+# cdk-http-client
+
+HTTP client abstraction for the Cashu Development Kit (CDK).
+
+This crate provides an HTTP client wrapper that abstracts the underlying HTTP library (reqwest),
+allowing other CDK crates to avoid direct dependencies on reqwest.
+
+## Usage
+
+```rust
+use cdk_http_client::{HttpClient, Response};
+use serde::Deserialize;
+
+#[derive(Deserialize)]
+struct ApiResponse {
+    message: String,
+}
+
+async fn example() -> Response<ApiResponse> {
+    let client = HttpClient::new();
+    client.fetch("https://api.example.com/data").await
+}
+```
+
+## API
+
+### Builder methods (return `RequestBuilder`):
+- `get(url)` - GET request builder
+- `post(url)` - POST request builder
+- `patch(url)` - PATCH request builder
+
+### Convenience methods (return deserialized JSON):
+- `fetch<R>(url)` - simple GET returning JSON
+- `post_json<B, R>(url, body)` - POST with JSON body
+- `post_form<F, R>(url, form)` - POST with form data
+- `patch_json<B, R>(url, body)` - PATCH with JSON body

+ 329 - 0
crates/cdk-http-client/src/client.rs

@@ -0,0 +1,329 @@
+//! HTTP client wrapper
+
+use serde::de::DeserializeOwned;
+use serde::Serialize;
+
+use crate::error::HttpError;
+use crate::request::RequestBuilder;
+use crate::response::{RawResponse, Response};
+
+/// HTTP client wrapper
+#[derive(Debug, Clone)]
+pub struct HttpClient {
+    inner: reqwest::Client,
+}
+
+impl Default for HttpClient {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl HttpClient {
+    /// Create a new HTTP client with default settings
+    pub fn new() -> Self {
+        Self {
+            inner: reqwest::Client::new(),
+        }
+    }
+
+    /// Create a new HTTP client builder
+    pub fn builder() -> HttpClientBuilder {
+        HttpClientBuilder::default()
+    }
+
+    /// Create an HttpClient from a reqwest::Client
+    pub fn from_reqwest(client: reqwest::Client) -> Self {
+        Self { inner: client }
+    }
+
+    // === Simple convenience methods ===
+
+    /// GET request, returns JSON deserialized to R
+    pub async fn fetch<R>(&self, url: &str) -> Response<R>
+    where
+        R: DeserializeOwned,
+    {
+        let response = self.inner.get(url).send().await?;
+        let status = response.status();
+
+        if !status.is_success() {
+            let message = response.text().await.unwrap_or_default();
+            return Err(HttpError::Status {
+                status: status.as_u16(),
+                message,
+            });
+        }
+
+        response.json().await.map_err(HttpError::from)
+    }
+
+    /// POST with JSON body, returns JSON deserialized to R
+    pub async fn post_json<B, R>(&self, url: &str, body: &B) -> Response<R>
+    where
+        B: Serialize + ?Sized,
+        R: DeserializeOwned,
+    {
+        let response = self.inner.post(url).json(body).send().await?;
+        let status = response.status();
+
+        if !status.is_success() {
+            let message = response.text().await.unwrap_or_default();
+            return Err(HttpError::Status {
+                status: status.as_u16(),
+                message,
+            });
+        }
+
+        response.json().await.map_err(HttpError::from)
+    }
+
+    /// POST with form data, returns JSON deserialized to R
+    pub async fn post_form<F, R>(&self, url: &str, form: &F) -> Response<R>
+    where
+        F: Serialize + ?Sized,
+        R: DeserializeOwned,
+    {
+        let response = self.inner.post(url).form(form).send().await?;
+        let status = response.status();
+
+        if !status.is_success() {
+            let message = response.text().await.unwrap_or_default();
+            return Err(HttpError::Status {
+                status: status.as_u16(),
+                message,
+            });
+        }
+
+        response.json().await.map_err(HttpError::from)
+    }
+
+    /// PATCH with JSON body, returns JSON deserialized to R
+    pub async fn patch_json<B, R>(&self, url: &str, body: &B) -> Response<R>
+    where
+        B: Serialize + ?Sized,
+        R: DeserializeOwned,
+    {
+        let response = self.inner.patch(url).json(body).send().await?;
+        let status = response.status();
+
+        if !status.is_success() {
+            let message = response.text().await.unwrap_or_default();
+            return Err(HttpError::Status {
+                status: status.as_u16(),
+                message,
+            });
+        }
+
+        response.json().await.map_err(HttpError::from)
+    }
+
+    // === Raw request methods ===
+
+    /// GET request returning raw response body
+    pub async fn get_raw(&self, url: &str) -> Response<RawResponse> {
+        let response = self.inner.get(url).send().await?;
+        Ok(RawResponse::new(response))
+    }
+
+    // === Request builder methods ===
+
+    /// POST request builder for complex cases (custom headers, form data, etc.)
+    pub fn post(&self, url: &str) -> RequestBuilder {
+        RequestBuilder::new(self.inner.post(url))
+    }
+
+    /// GET request builder for complex cases (custom headers, etc.)
+    pub fn get(&self, url: &str) -> RequestBuilder {
+        RequestBuilder::new(self.inner.get(url))
+    }
+
+    /// PATCH request builder for complex cases (custom headers, JSON body, etc.)
+    pub fn patch(&self, url: &str) -> RequestBuilder {
+        RequestBuilder::new(self.inner.patch(url))
+    }
+}
+
+/// HTTP client builder for configuring proxy and TLS settings
+#[derive(Debug, Default)]
+pub struct HttpClientBuilder {
+    #[cfg(not(target_arch = "wasm32"))]
+    accept_invalid_certs: bool,
+    #[cfg(not(target_arch = "wasm32"))]
+    proxy: Option<ProxyConfig>,
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+#[derive(Debug)]
+struct ProxyConfig {
+    url: url::Url,
+    matcher: Option<regex::Regex>,
+}
+
+impl HttpClientBuilder {
+    /// Accept invalid TLS certificates (non-WASM only)
+    #[cfg(not(target_arch = "wasm32"))]
+    pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
+        self.accept_invalid_certs = accept;
+        self
+    }
+
+    /// Set a proxy URL (non-WASM only)
+    #[cfg(not(target_arch = "wasm32"))]
+    pub fn proxy(mut self, url: url::Url) -> Self {
+        self.proxy = Some(ProxyConfig { url, matcher: None });
+        self
+    }
+
+    /// Set a proxy URL with a host pattern matcher (non-WASM only)
+    #[cfg(not(target_arch = "wasm32"))]
+    pub fn proxy_with_matcher(mut self, url: url::Url, pattern: &str) -> Response<Self> {
+        let matcher = regex::Regex::new(pattern)
+            .map_err(|e| HttpError::Proxy(format!("Invalid proxy pattern: {}", e)))?;
+        self.proxy = Some(ProxyConfig {
+            url,
+            matcher: Some(matcher),
+        });
+        Ok(self)
+    }
+
+    /// Build the HTTP client
+    pub fn build(self) -> Response<HttpClient> {
+        #[cfg(not(target_arch = "wasm32"))]
+        {
+            let mut builder =
+                reqwest::Client::builder().danger_accept_invalid_certs(self.accept_invalid_certs);
+
+            if let Some(proxy_config) = self.proxy {
+                let proxy_url = proxy_config.url.to_string();
+                let proxy = if let Some(matcher) = proxy_config.matcher {
+                    reqwest::Proxy::custom(move |url| {
+                        if matcher.is_match(url.host_str().unwrap_or("")) {
+                            Some(proxy_url.clone())
+                        } else {
+                            None
+                        }
+                    })
+                } else {
+                    reqwest::Proxy::all(&proxy_url).map_err(|e| HttpError::Proxy(e.to_string()))?
+                };
+                builder = builder.proxy(proxy);
+            }
+
+            let client = builder.build().map_err(HttpError::from)?;
+            Ok(HttpClient { inner: client })
+        }
+
+        #[cfg(target_arch = "wasm32")]
+        {
+            Ok(HttpClient::new())
+        }
+    }
+}
+
+/// Convenience function for simple GET requests (replaces reqwest::get)
+pub async fn fetch<R: DeserializeOwned>(url: &str) -> Response<R> {
+    HttpClient::new().fetch(url).await
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_client_new() {
+        let client = HttpClient::new();
+        // Client should be constructable without panicking
+        let _ = format!("{:?}", client);
+    }
+
+    #[test]
+    fn test_client_default() {
+        let client = HttpClient::default();
+        // Default should produce a valid client
+        let _ = format!("{:?}", client);
+    }
+
+    #[test]
+    fn test_builder_returns_builder() {
+        let builder = HttpClient::builder();
+        let _ = format!("{:?}", builder);
+    }
+
+    #[test]
+    fn test_builder_build() {
+        let result = HttpClientBuilder::default().build();
+        assert!(result.is_ok());
+    }
+
+    #[test]
+    fn test_from_reqwest() {
+        let reqwest_client = reqwest::Client::new();
+        let client = HttpClient::from_reqwest(reqwest_client);
+        let _ = format!("{:?}", client);
+    }
+
+    #[cfg(not(target_arch = "wasm32"))]
+    mod non_wasm {
+        use super::*;
+
+        #[test]
+        fn test_builder_accept_invalid_certs() {
+            let result = HttpClientBuilder::default()
+                .danger_accept_invalid_certs(true)
+                .build();
+            assert!(result.is_ok());
+        }
+
+        #[test]
+        fn test_builder_accept_invalid_certs_false() {
+            let result = HttpClientBuilder::default()
+                .danger_accept_invalid_certs(false)
+                .build();
+            assert!(result.is_ok());
+        }
+
+        #[test]
+        fn test_builder_proxy() {
+            let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
+            let result = HttpClientBuilder::default().proxy(proxy_url).build();
+            assert!(result.is_ok());
+        }
+
+        #[test]
+        fn test_builder_proxy_with_valid_matcher() {
+            let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
+            let result =
+                HttpClientBuilder::default().proxy_with_matcher(proxy_url, r".*\.example\.com$");
+            assert!(result.is_ok());
+
+            let builder = result.expect("Valid matcher should succeed");
+            let client_result = builder.build();
+            assert!(client_result.is_ok());
+        }
+
+        #[test]
+        fn test_builder_proxy_with_invalid_matcher() {
+            let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
+            // Invalid regex pattern (unclosed bracket)
+            let result = HttpClientBuilder::default().proxy_with_matcher(proxy_url, r"[invalid");
+            assert!(result.is_err());
+
+            if let Err(HttpError::Proxy(msg)) = result {
+                assert!(msg.contains("Invalid proxy pattern"));
+            } else {
+                panic!("Expected HttpError::Proxy");
+            }
+        }
+
+        #[test]
+        fn test_builder_chained_config() {
+            let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
+            let result = HttpClientBuilder::default()
+                .danger_accept_invalid_certs(true)
+                .proxy(proxy_url)
+                .build();
+            assert!(result.is_ok());
+        }
+    }
+}

+ 130 - 0
crates/cdk-http-client/src/error.rs

@@ -0,0 +1,130 @@
+//! HTTP error types
+
+use thiserror::Error;
+
+/// HTTP errors that can occur during requests
+#[derive(Debug, Error)]
+pub enum HttpError {
+    /// HTTP error with status code
+    #[error("HTTP error ({status}): {message}")]
+    Status {
+        /// HTTP status code
+        status: u16,
+        /// Error message
+        message: String,
+    },
+    /// Connection error
+    #[error("Connection error: {0}")]
+    Connection(String),
+    /// Request timeout
+    #[error("Request timeout")]
+    Timeout,
+    /// Serialization error
+    #[error("Serialization error: {0}")]
+    Serialization(String),
+    /// Proxy error
+    #[error("Proxy error: {0}")]
+    Proxy(String),
+    /// Client build error
+    #[error("Client build error: {0}")]
+    Build(String),
+    /// Other error
+    #[error("{0}")]
+    Other(String),
+}
+
+impl From<reqwest::Error> for HttpError {
+    fn from(err: reqwest::Error) -> Self {
+        if err.is_timeout() {
+            HttpError::Timeout
+        } else if err.is_builder() {
+            HttpError::Build(err.to_string())
+        } else if let Some(status) = err.status() {
+            HttpError::Status {
+                status: status.as_u16(),
+                message: err.to_string(),
+            }
+        } else {
+            // is_connect() is not available on wasm32
+            #[cfg(not(target_arch = "wasm32"))]
+            if err.is_connect() {
+                return HttpError::Connection(err.to_string());
+            }
+            HttpError::Other(err.to_string())
+        }
+    }
+}
+
+impl From<serde_json::Error> for HttpError {
+    fn from(err: serde_json::Error) -> Self {
+        HttpError::Serialization(err.to_string())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_http_error_status_display() {
+        let error = HttpError::Status {
+            status: 404,
+            message: "Not Found".to_string(),
+        };
+        assert_eq!(format!("{}", error), "HTTP error (404): Not Found");
+    }
+
+    #[test]
+    fn test_http_error_connection_display() {
+        let error = HttpError::Connection("connection refused".to_string());
+        assert_eq!(format!("{}", error), "Connection error: connection refused");
+    }
+
+    #[test]
+    fn test_http_error_timeout_display() {
+        let error = HttpError::Timeout;
+        assert_eq!(format!("{}", error), "Request timeout");
+    }
+
+    #[test]
+    fn test_http_error_serialization_display() {
+        let error = HttpError::Serialization("invalid JSON".to_string());
+        assert_eq!(format!("{}", error), "Serialization error: invalid JSON");
+    }
+
+    #[test]
+    fn test_http_error_proxy_display() {
+        let error = HttpError::Proxy("proxy unreachable".to_string());
+        assert_eq!(format!("{}", error), "Proxy error: proxy unreachable");
+    }
+
+    #[test]
+    fn test_http_error_build_display() {
+        let error = HttpError::Build("invalid config".to_string());
+        assert_eq!(format!("{}", error), "Client build error: invalid config");
+    }
+
+    #[test]
+    fn test_http_error_other_display() {
+        let error = HttpError::Other("unknown error".to_string());
+        assert_eq!(format!("{}", error), "unknown error");
+    }
+
+    #[test]
+    fn test_from_serde_json_error() {
+        // Create an invalid JSON parse to get a serde_json::Error
+        let result: Result<String, _> = serde_json::from_str("not valid json");
+        let json_error = result.expect_err("Invalid JSON should produce an error");
+        let http_error: HttpError = json_error.into();
+
+        match http_error {
+            HttpError::Serialization(msg) => {
+                assert!(
+                    msg.contains("expected"),
+                    "Error message should describe JSON error"
+                );
+            }
+            _ => panic!("Expected HttpError::Serialization"),
+        }
+    }
+}

+ 31 - 0
crates/cdk-http-client/src/lib.rs

@@ -0,0 +1,31 @@
+//! HTTP client abstraction for CDK
+//!
+//! This crate provides an HTTP client wrapper that abstracts the underlying HTTP library (reqwest).
+//! Using this crate allows other CDK crates to avoid direct dependencies on reqwest.
+//!
+//! # Example
+//!
+//! ```no_run
+//! use cdk_http_client::{HttpClient, Response};
+//! use serde::Deserialize;
+//!
+//! #[derive(Deserialize)]
+//! struct ApiResponse {
+//!     message: String,
+//! }
+//!
+//! async fn example() -> Response<ApiResponse> {
+//!     let client = HttpClient::new();
+//!     client.fetch("https://api.example.com/data").await
+//! }
+//! ```
+
+mod client;
+mod error;
+mod request;
+mod response;
+
+pub use client::{fetch, HttpClient, HttpClientBuilder};
+pub use error::HttpError;
+pub use request::RequestBuilder;
+pub use response::{RawResponse, Response};

+ 63 - 0
crates/cdk-http-client/src/request.rs

@@ -0,0 +1,63 @@
+//! HTTP request builder
+
+use serde::de::DeserializeOwned;
+use serde::Serialize;
+
+use crate::error::HttpError;
+use crate::response::{RawResponse, Response};
+
+/// HTTP request builder for complex requests
+#[derive(Debug)]
+pub struct RequestBuilder {
+    inner: reqwest::RequestBuilder,
+}
+
+impl RequestBuilder {
+    /// Create a new RequestBuilder from a reqwest::RequestBuilder
+    pub(crate) fn new(inner: reqwest::RequestBuilder) -> Self {
+        Self { inner }
+    }
+
+    /// Add a header to the request
+    pub fn header(self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
+        Self {
+            inner: self.inner.header(key.as_ref(), value.as_ref()),
+        }
+    }
+
+    /// Set the request body as JSON
+    pub fn json<T: Serialize + ?Sized>(self, body: &T) -> Self {
+        Self {
+            inner: self.inner.json(body),
+        }
+    }
+
+    /// Set the request body as form data
+    pub fn form<T: Serialize + ?Sized>(self, body: &T) -> Self {
+        Self {
+            inner: self.inner.form(body),
+        }
+    }
+
+    /// Send the request and return a raw response
+    pub async fn send(self) -> Response<RawResponse> {
+        let response = self.inner.send().await?;
+        Ok(RawResponse::new(response))
+    }
+
+    /// Send the request and deserialize the response as JSON
+    pub async fn send_json<R: DeserializeOwned>(self) -> Response<R> {
+        let response = self.inner.send().await?;
+        let status = response.status();
+
+        if !status.is_success() {
+            let message = response.text().await.unwrap_or_default();
+            return Err(HttpError::Status {
+                status: status.as_u16(),
+                message,
+            });
+        }
+
+        response.json().await.map_err(HttpError::from)
+    }
+}

+ 85 - 0
crates/cdk-http-client/src/response.rs

@@ -0,0 +1,85 @@
+//! HTTP response types
+
+use serde::de::DeserializeOwned;
+
+use crate::error::HttpError;
+
+/// HTTP Response type - generic over the body type R and error type E
+/// This is the primary return type for all HTTP operations
+pub type Response<R, E = HttpError> = Result<R, E>;
+
+/// Raw HTTP response with status code and body access
+#[derive(Debug)]
+pub struct RawResponse {
+    status: u16,
+    inner: reqwest::Response,
+}
+
+impl RawResponse {
+    /// Create a new RawResponse from a reqwest::Response
+    pub(crate) fn new(response: reqwest::Response) -> Self {
+        Self {
+            status: response.status().as_u16(),
+            inner: response,
+        }
+    }
+
+    /// Get the HTTP status code
+    pub fn status(&self) -> u16 {
+        self.status
+    }
+
+    /// Check if the response status is a success (2xx)
+    pub fn is_success(&self) -> bool {
+        (200..300).contains(&self.status)
+    }
+
+    /// Check if the response status is a client error (4xx)
+    pub fn is_client_error(&self) -> bool {
+        (400..500).contains(&self.status)
+    }
+
+    /// Check if the response status is a server error (5xx)
+    pub fn is_server_error(&self) -> bool {
+        (500..600).contains(&self.status)
+    }
+
+    /// Get the response body as text
+    pub async fn text(self) -> Response<String> {
+        self.inner.text().await.map_err(HttpError::from)
+    }
+
+    /// Get the response body as JSON
+    pub async fn json<T: DeserializeOwned>(self) -> Response<T> {
+        self.inner.json().await.map_err(HttpError::from)
+    }
+
+    /// Get the response body as bytes
+    pub async fn bytes(self) -> Response<Vec<u8>> {
+        self.inner
+            .bytes()
+            .await
+            .map(|b| b.to_vec())
+            .map_err(HttpError::from)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    // Note: RawResponse tests require a real reqwest::Response,
+    // so they are in tests/integration.rs using mockito.
+
+    #[test]
+    fn test_response_type_is_result() {
+        // Response<R, E> is just a type alias for Result<R, E>
+        let success: Response<i32> = Ok(42);
+        assert!(success.is_ok());
+        assert!(matches!(success, Ok(42)));
+
+        let error: Response<i32> = Err(HttpError::Timeout);
+        assert!(error.is_err());
+        assert!(matches!(error, Err(HttpError::Timeout)));
+    }
+}

+ 760 - 0
crates/cdk-http-client/tests/integration.rs

@@ -0,0 +1,760 @@
+//! Integration tests for cdk-http-client using mockito
+
+use cdk_http_client::{HttpClient, HttpError};
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct TestPayload {
+    name: String,
+    value: i32,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct TestResponse {
+    success: bool,
+    data: String,
+}
+
+// === HttpClient::fetch tests ===
+
+#[tokio::test]
+async fn test_fetch_success() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/api/data")
+        .with_status(200)
+        .with_header("content-type", "application/json")
+        .with_body(r#"{"success": true, "data": "hello"}"#)
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let url = format!("{}/api/data", server.url());
+    let result: Result<TestResponse, _> = client.fetch(&url).await;
+
+    assert!(result.is_ok());
+    let response = result.expect("Fetch should succeed");
+    assert!(response.success);
+    assert_eq!(response.data, "hello");
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_fetch_error_status() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/api/error")
+        .with_status(404)
+        .with_body("Not Found")
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let url = format!("{}/api/error", server.url());
+    let result: Result<TestResponse, _> = client.fetch(&url).await;
+
+    assert!(result.is_err());
+    if let Err(HttpError::Status { status, message }) = result {
+        assert_eq!(status, 404);
+        assert_eq!(message, "Not Found");
+    } else {
+        panic!("Expected HttpError::Status");
+    }
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_fetch_server_error() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/api/server-error")
+        .with_status(500)
+        .with_body("Internal Server Error")
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let url = format!("{}/api/server-error", server.url());
+    let result: Result<TestResponse, _> = client.fetch(&url).await;
+
+    assert!(result.is_err());
+    if let Err(HttpError::Status { status, .. }) = result {
+        assert_eq!(status, 500);
+    } else {
+        panic!("Expected HttpError::Status");
+    }
+
+    mock.assert_async().await;
+}
+
+// === HttpClient::post_json tests ===
+
+#[tokio::test]
+async fn test_post_json_success() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("POST", "/api/submit")
+        .match_header("content-type", "application/json")
+        .match_body(mockito::Matcher::Json(serde_json::json!({
+            "name": "test",
+            "value": 42
+        })))
+        .with_status(200)
+        .with_header("content-type", "application/json")
+        .with_body(r#"{"success": true, "data": "received"}"#)
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let url = format!("{}/api/submit", server.url());
+    let payload = TestPayload {
+        name: "test".to_string(),
+        value: 42,
+    };
+    let result: Result<TestResponse, _> = client.post_json(&url, &payload).await;
+
+    assert!(result.is_ok());
+    let response = result.expect("POST JSON should succeed");
+    assert!(response.success);
+    assert_eq!(response.data, "received");
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_post_json_error_status() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("POST", "/api/submit")
+        .with_status(400)
+        .with_body("Bad Request")
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let url = format!("{}/api/submit", server.url());
+    let payload = TestPayload {
+        name: "test".to_string(),
+        value: 42,
+    };
+    let result: Result<TestResponse, _> = client.post_json(&url, &payload).await;
+
+    assert!(result.is_err());
+    if let Err(HttpError::Status { status, message }) = result {
+        assert_eq!(status, 400);
+        assert_eq!(message, "Bad Request");
+    } else {
+        panic!("Expected HttpError::Status");
+    }
+
+    mock.assert_async().await;
+}
+
+// === HttpClient::get_raw tests ===
+
+#[tokio::test]
+async fn test_get_raw_success() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/api/raw")
+        .with_status(200)
+        .with_body("raw content")
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let url = format!("{}/api/raw", server.url());
+    let result = client.get_raw(&url).await;
+
+    assert!(result.is_ok());
+    let response = result.expect("GET raw should succeed");
+    assert_eq!(response.status(), 200);
+    assert!(response.is_success());
+
+    mock.assert_async().await;
+}
+
+// === RawResponse tests ===
+
+#[tokio::test]
+async fn test_raw_response_is_success_with_200() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/")
+        .with_status(200)
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let response = client
+        .get_raw(&server.url())
+        .await
+        .expect("Request should succeed");
+
+    assert!(response.is_success());
+    assert!(!response.is_client_error());
+    assert!(!response.is_server_error());
+    assert_eq!(response.status(), 200);
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_raw_response_is_success_with_201() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/")
+        .with_status(201)
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let response = client
+        .get_raw(&server.url())
+        .await
+        .expect("Request should succeed");
+
+    assert!(response.is_success());
+    assert_eq!(response.status(), 201);
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_raw_response_is_success_with_299() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/")
+        .with_status(299)
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let response = client
+        .get_raw(&server.url())
+        .await
+        .expect("Request should succeed");
+
+    assert!(response.is_success());
+    assert_eq!(response.status(), 299);
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_raw_response_is_not_success_with_100() {
+    // Note: HTTP 1xx informational responses are special and may not be
+    // fully supported by all HTTP libraries. We test with 100 Continue.
+    // mockito may convert some 1xx codes to 500, so we just verify the
+    // logic works with the boundary check (200..300).
+    let mut server = mockito::Server::new_async().await;
+
+    // Use 301 redirect as a more reliable "not success" boundary test
+    let mock = server
+        .mock("GET", "/")
+        .with_status(301)
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let response = client
+        .get_raw(&server.url())
+        .await
+        .expect("Request should succeed");
+
+    // 301 is a redirect, not success
+    assert!(!response.is_success());
+    assert_eq!(response.status(), 301);
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_raw_response_is_not_success_with_300() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/")
+        .with_status(300)
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let response = client
+        .get_raw(&server.url())
+        .await
+        .expect("Request should succeed");
+
+    assert!(!response.is_success());
+    assert_eq!(response.status(), 300);
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_raw_response_is_client_error_with_400() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/")
+        .with_status(400)
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let response = client
+        .get_raw(&server.url())
+        .await
+        .expect("Request should succeed");
+
+    assert!(response.is_client_error());
+    assert!(!response.is_success());
+    assert!(!response.is_server_error());
+    assert_eq!(response.status(), 400);
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_raw_response_is_client_error_with_499() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/")
+        .with_status(499)
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let response = client
+        .get_raw(&server.url())
+        .await
+        .expect("Request should succeed");
+
+    assert!(response.is_client_error());
+    assert_eq!(response.status(), 499);
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_raw_response_is_not_client_error_with_399() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/")
+        .with_status(399)
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let response = client
+        .get_raw(&server.url())
+        .await
+        .expect("Request should succeed");
+
+    assert!(!response.is_client_error());
+    assert_eq!(response.status(), 399);
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_raw_response_is_server_error_with_500() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/")
+        .with_status(500)
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let response = client
+        .get_raw(&server.url())
+        .await
+        .expect("Request should succeed");
+
+    assert!(response.is_server_error());
+    assert!(!response.is_success());
+    assert!(!response.is_client_error());
+    assert_eq!(response.status(), 500);
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_raw_response_is_server_error_with_599() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/")
+        .with_status(599)
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let response = client
+        .get_raw(&server.url())
+        .await
+        .expect("Request should succeed");
+
+    assert!(response.is_server_error());
+    assert_eq!(response.status(), 599);
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_raw_response_is_not_server_error_with_499() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/")
+        .with_status(499)
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let response = client
+        .get_raw(&server.url())
+        .await
+        .expect("Request should succeed");
+
+    assert!(!response.is_server_error());
+    assert_eq!(response.status(), 499);
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_raw_response_text() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/")
+        .with_status(200)
+        .with_body("Hello, World!")
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let response = client
+        .get_raw(&server.url())
+        .await
+        .expect("Request should succeed");
+    let text = response
+        .text()
+        .await
+        .expect("Text extraction should succeed");
+
+    assert_eq!(text, "Hello, World!");
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_raw_response_json() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/")
+        .with_status(200)
+        .with_header("content-type", "application/json")
+        .with_body(r#"{"success": true, "data": "json_test"}"#)
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let response = client
+        .get_raw(&server.url())
+        .await
+        .expect("Request should succeed");
+    let json: TestResponse = response.json().await.expect("JSON parsing should succeed");
+
+    assert!(json.success);
+    assert_eq!(json.data, "json_test");
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_raw_response_bytes() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/")
+        .with_status(200)
+        .with_body(vec![0x01, 0x02, 0x03, 0x04])
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let response = client
+        .get_raw(&server.url())
+        .await
+        .expect("Request should succeed");
+    let bytes = response
+        .bytes()
+        .await
+        .expect("Bytes extraction should succeed");
+
+    assert_eq!(bytes, vec![0x01, 0x02, 0x03, 0x04]);
+
+    mock.assert_async().await;
+}
+
+// === RequestBuilder tests ===
+
+#[tokio::test]
+async fn test_request_builder_send() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/api/builder")
+        .with_status(200)
+        .with_body("builder response")
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let url = format!("{}/api/builder", server.url());
+    let response = client
+        .get(&url)
+        .send()
+        .await
+        .expect("Request should succeed");
+
+    assert_eq!(response.status(), 200);
+    assert_eq!(
+        response
+            .text()
+            .await
+            .expect("Text extraction should succeed"),
+        "builder response"
+    );
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_request_builder_send_json() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("POST", "/api/json")
+        .with_status(200)
+        .with_header("content-type", "application/json")
+        .with_body(r#"{"success": true, "data": "builder_json"}"#)
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let url = format!("{}/api/json", server.url());
+    let payload = TestPayload {
+        name: "builder".to_string(),
+        value: 100,
+    };
+
+    let result: TestResponse = client
+        .post(&url)
+        .json(&payload)
+        .send_json()
+        .await
+        .expect("Request should succeed");
+
+    assert!(result.success);
+    assert_eq!(result.data, "builder_json");
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_request_builder_with_headers() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/api/headers")
+        .match_header("X-Custom-Header", "custom-value")
+        .match_header("Authorization", "Bearer token123")
+        .with_status(200)
+        .with_body("headers received")
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let url = format!("{}/api/headers", server.url());
+    let response = client
+        .get(&url)
+        .header("X-Custom-Header", "custom-value")
+        .header("Authorization", "Bearer token123")
+        .send()
+        .await
+        .expect("Request should succeed");
+
+    assert_eq!(response.status(), 200);
+    assert_eq!(
+        response
+            .text()
+            .await
+            .expect("Text extraction should succeed"),
+        "headers received"
+    );
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_request_builder_post_with_form() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("POST", "/api/form")
+        .match_header(
+            "content-type",
+            mockito::Matcher::Regex("application/x-www-form-urlencoded.*".to_string()),
+        )
+        .with_status(200)
+        .with_header("content-type", "application/json")
+        .with_body(r#"{"success": true, "data": "form_received"}"#)
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let url = format!("{}/api/form", server.url());
+    let form_data = [("field1", "value1"), ("field2", "value2")];
+
+    let response: TestResponse = client
+        .post(&url)
+        .form(&form_data)
+        .send_json()
+        .await
+        .expect("Request should succeed");
+
+    assert!(response.success);
+    assert_eq!(response.data, "form_received");
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_request_builder_patch() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("PATCH", "/api/resource")
+        .with_status(200)
+        .with_header("content-type", "application/json")
+        .with_body(r#"{"success": true, "data": "patched"}"#)
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let url = format!("{}/api/resource", server.url());
+    let payload = TestPayload {
+        name: "update".to_string(),
+        value: 99,
+    };
+
+    let result: TestResponse = client
+        .patch(&url)
+        .json(&payload)
+        .send_json()
+        .await
+        .expect("Request should succeed");
+
+    assert!(result.success);
+    assert_eq!(result.data, "patched");
+
+    mock.assert_async().await;
+}
+
+// === Convenience function test ===
+
+#[tokio::test]
+async fn test_fetch_convenience_function() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/api/convenience")
+        .with_status(200)
+        .with_header("content-type", "application/json")
+        .with_body(r#"{"success": true, "data": "convenience"}"#)
+        .create_async()
+        .await;
+
+    let url = format!("{}/api/convenience", server.url());
+    let result: Result<TestResponse, _> = cdk_http_client::fetch(&url).await;
+
+    assert!(result.is_ok());
+    let response = result.expect("Fetch should succeed");
+    assert!(response.success);
+    assert_eq!(response.data, "convenience");
+
+    mock.assert_async().await;
+}
+
+// === Error handling tests ===
+
+#[tokio::test]
+async fn test_json_deserialization_error() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/api/invalid-json")
+        .with_status(200)
+        .with_header("content-type", "application/json")
+        .with_body("not valid json")
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let url = format!("{}/api/invalid-json", server.url());
+    let result: Result<TestResponse, _> = client.fetch(&url).await;
+
+    assert!(result.is_err());
+    // The error should be about JSON parsing, which becomes HttpError::Other from reqwest
+    let err = result.expect_err("Should be a deserialization error");
+    let err_str = format!("{}", err);
+    assert!(
+        err_str.contains("expected") || err_str.contains("JSON") || err_str.contains("error"),
+        "Error should mention parsing issue: {}",
+        err_str
+    );
+
+    mock.assert_async().await;
+}
+
+#[tokio::test]
+async fn test_raw_response_json_deserialization_error() {
+    let mut server = mockito::Server::new_async().await;
+
+    let mock = server
+        .mock("GET", "/")
+        .with_status(200)
+        .with_body("invalid json")
+        .create_async()
+        .await;
+
+    let client = HttpClient::new();
+    let response = client
+        .get_raw(&server.url())
+        .await
+        .expect("Request should succeed");
+    let result: Result<TestResponse, _> = response.json().await;
+
+    assert!(result.is_err());
+
+    mock.assert_async().await;
+}

+ 2 - 2
crates/cdk-integration-tests/Cargo.toml

@@ -28,7 +28,8 @@ cdk-axum = { workspace = true }
 cdk-sqlite = { workspace = true }
 cdk-redb = { workspace = true }
 cdk-fake-wallet = { workspace = true }
-cdk-common = { workspace = true, features = ["mint", "wallet"] }
+cdk-common = { workspace = true, features = ["mint", "wallet", "http"] }
+cdk-http-client = { workspace = true }
 cdk-mintd = { workspace = true, features = ["cln", "lnd", "fakewallet", "grpc-processor", "lnbits", "management-rpc", "sqlite", "postgres", "ldk-node", "prometheus"] }
 futures = { workspace = true, default-features = false, features = [
     "executor",
@@ -47,7 +48,6 @@ tokio-tungstenite.workspace = true
 tower-http = { workspace = true, features = ["cors"] }
 tower-service = "0.3.3"
 tokio-util.workspace = true
-reqwest.workspace = true
 bitcoin = "0.32.0"
 clap = { workspace = true, features = ["derive"] }
 web-time.workspace = true

+ 3 - 2
crates/cdk-integration-tests/src/shared.rs

@@ -35,6 +35,7 @@ pub async fn wait_for_mint_ready_with_shutdown(
 ) -> Result<()> {
     let url = format!("http://127.0.0.1:{port}/v1/info");
     let start_time = std::time::Instant::now();
+    let http_client = cdk_common::HttpClient::new();
 
     println!("Waiting for mint on port {port} to be ready...");
 
@@ -50,10 +51,10 @@ pub async fn wait_for_mint_ready_with_shutdown(
 
         tokio::select! {
             // Try to make a request to the mint info endpoint
-            result = reqwest::get(&url) => {
+            result = http_client.get_raw(&url) => {
                 match result {
                     Ok(response) => {
-                        if response.status().is_success() {
+                        if response.is_success() {
                             println!("Mint on port {port} is ready");
                             return Ok(());
                         } else {

+ 8 - 9
crates/cdk-integration-tests/tests/fake_auth.rs

@@ -15,6 +15,7 @@ use cdk::nuts::{
 use cdk::wallet::{AuthHttpClient, AuthMintConnector, HttpClient, MintConnector, WalletBuilder};
 use cdk::{Error, OidcClient};
 use cdk_fake_wallet::create_fake_invoice;
+use cdk_http_client::HttpClient as CommonHttpClient;
 use cdk_integration_tests::fund_wallet;
 use cdk_sqlite::wallet::memory;
 
@@ -783,15 +784,13 @@ async fn get_access_token(mint_info: &MintInfo) -> (String, String) {
     ];
 
     // Make the token request directly
-    let client = reqwest::Client::new();
-    let response = client
-        .post(token_url)
+    let client = CommonHttpClient::new();
+    let token_response: serde_json::Value = client
+        .post(&token_url)
         .form(&params)
         .send()
         .await
-        .expect("Failed to send token request");
-
-    let token_response: serde_json::Value = response
+        .expect("Failed to send token request")
         .json()
         .await
         .expect("Failed to parse token response");
@@ -840,15 +839,15 @@ async fn get_custom_access_token(
     ];
 
     // Make the token request directly
-    let client = reqwest::Client::new();
+    let client = CommonHttpClient::new();
     let response = client
-        .post(token_url)
+        .post(&token_url)
         .form(&params)
         .send()
         .await
         .map_err(|_| Error::Custom("Failed to send token request".to_string()))?;
 
-    if !response.status().is_success() {
+    if !response.is_success() {
         return Err(Error::Custom(format!(
             "Token request failed with status: {}",
             response.status()

+ 6 - 5
crates/cdk-integration-tests/tests/ldk_node.rs

@@ -1,4 +1,5 @@
 use anyhow::Result;
+use cdk_http_client::HttpClient;
 use cdk_integration_tests::get_mint_url_from_env;
 
 #[tokio::test]
@@ -7,10 +8,10 @@ async fn test_ldk_node_mint_info() -> Result<()> {
     let mint_url = get_mint_url_from_env();
 
     // Create an HTTP client
-    let client = reqwest::Client::new();
+    let client = HttpClient::new();
 
     // Make a request to the info endpoint
-    let response = client.get(format!("{}/v1/info", mint_url)).send().await?;
+    let response = client.get(&format!("{}/v1/info", mint_url)).send().await?;
 
     // Check that we got a successful response
     assert_eq!(response.status(), 200);
@@ -34,7 +35,7 @@ async fn test_ldk_node_mint_quote() -> Result<()> {
     let mint_url = get_mint_url_from_env();
 
     // Create an HTTP client
-    let client = reqwest::Client::new();
+    let client = HttpClient::new();
 
     // Create a mint quote request
     let quote_request = serde_json::json!({
@@ -44,7 +45,7 @@ async fn test_ldk_node_mint_quote() -> Result<()> {
 
     // Make a request to create a mint quote
     let response = client
-        .post(format!("{}/v1/mint/quote/bolt11", mint_url))
+        .post(&format!("{}/v1/mint/quote/bolt11", mint_url))
         .json(&quote_request)
         .send()
         .await?;
@@ -57,7 +58,7 @@ async fn test_ldk_node_mint_quote() -> Result<()> {
 
     // For now, we'll just check that we get a response (even if it's an error)
     // In a real test, we'd want to verify the quote was created correctly
-    assert!(status.is_success() || status.as_u16() < 500);
+    assert!(status < 300 || status < 500);
 
     Ok(())
 }

+ 12 - 18
crates/cdk-integration-tests/tests/nutshell_wallet.rs

@@ -1,7 +1,7 @@
 use std::time::Duration;
 
 use cdk_fake_wallet::create_fake_invoice;
-use reqwest::Client;
+use cdk_http_client::HttpClient;
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
 use tokio::time::sleep;
@@ -22,17 +22,17 @@ const DEFAULT_TEST_AMOUNT: u64 = 10000;
 
 /// Helper function to mint tokens via Lightning invoice
 async fn mint_tokens(base_url: &str, amount: u64) -> String {
-    let client = Client::new();
+    let client = HttpClient::new();
 
     // Create an invoice for the specified amount
     let invoice_url = format!("{}/lightning/create_invoice?amount={}", base_url, amount);
 
-    let invoice_response = client
+    let invoice_response: InvoiceResponse = client
         .post(&invoice_url)
         .send()
         .await
         .expect("Failed to send invoice creation request")
-        .json::<InvoiceResponse>()
+        .json()
         .await
         .expect("Failed to parse invoice response");
 
@@ -43,7 +43,7 @@ async fn mint_tokens(base_url: &str, amount: u64) -> String {
 
 /// Helper function to wait for payment confirmation
 async fn wait_for_payment_confirmation(base_url: &str, payment_request: &str) {
-    let client = Client::new();
+    let client = HttpClient::new();
     let check_url = format!(
         "{}/lightning/invoice_state?payment_request={}",
         base_url, payment_request
@@ -63,7 +63,7 @@ async fn wait_for_payment_confirmation(base_url: &str, payment_request: &str) {
             .await
             .expect("Failed to send payment check request");
 
-        if response.status().is_success() {
+        if response.is_success() {
             let state: Value = response
                 .json()
                 .await
@@ -90,19 +90,13 @@ async fn wait_for_payment_confirmation(base_url: &str, payment_request: &str) {
 
 /// Helper function to get the current wallet balance
 async fn get_wallet_balance(base_url: &str) -> u64 {
-    let client = Client::new();
+    let client = HttpClient::new();
     let balance_url = format!("{}/balance", base_url);
 
-    let balance_response = client
-        .get(&balance_url)
-        .send()
-        .await
-        .expect("Failed to send balance request");
-
-    let balance: Value = balance_response
-        .json()
+    let balance: Value = client
+        .fetch(&balance_url)
         .await
-        .expect("Failed to parse balance response");
+        .expect("Failed to fetch balance");
 
     balance["balance"]
         .as_u64()
@@ -149,7 +143,7 @@ async fn test_nutshell_wallet_swap() {
 
     let send_amount = 100;
     let send_url = format!("{}/send?amount={}", base_url, send_amount);
-    let client = Client::new();
+    let client = HttpClient::new();
 
     let response: Value = client
         .post(&send_url)
@@ -217,7 +211,7 @@ async fn test_nutshell_wallet_melt() {
     let payment_amount = 1000; // 1000 sats
     let fake_invoice = create_fake_invoice(payment_amount, "Test payment".to_string());
     let pay_url = format!("{}/lightning/pay_invoice?bolt11={}", base_url, fake_invoice);
-    let client = Client::new();
+    let client = HttpClient::new();
 
     // Step 4: Pay the invoice
     let _response: Value = client

+ 2 - 2
crates/cdk-npubcash/Cargo.toml

@@ -11,9 +11,9 @@ repository.workspace = true
 # Use workspace dependencies
 async-trait = { workspace = true }
 cashu = { workspace = true }
-cdk-common = { workspace = true, features = ["wallet"] }
+cdk-common = { workspace = true, features = ["wallet", "http"] }
+cdk-http-client = { workspace = true }
 nostr-sdk = { workspace = true }
-reqwest = { workspace = true }
 serde = { workspace = true }
 serde_json = { workspace = true }
 thiserror = { workspace = true }

+ 5 - 5
crates/cdk-npubcash/src/auth.rs

@@ -23,7 +23,7 @@ struct CachedToken {
 pub struct JwtAuthProvider {
     base_url: String,
     keys: Keys,
-    http_client: reqwest::Client,
+    http_client: cdk_common::HttpClient,
     cached_token: Arc<RwLock<Option<CachedToken>>>,
 }
 
@@ -38,7 +38,7 @@ impl JwtAuthProvider {
         Self {
             base_url,
             keys,
-            http_client: reqwest::Client::new(),
+            http_client: cdk_common::HttpClient::new(),
             cached_token: Arc::new(RwLock::new(None)),
         }
     }
@@ -108,7 +108,7 @@ impl JwtAuthProvider {
         &self,
         auth_url: &str,
         nostr_token: &str,
-    ) -> Result<reqwest::Response> {
+    ) -> Result<cdk_common::RawResponse> {
         tracing::debug!("Sending request to: {}", auth_url);
         tracing::debug!(
             "Authorization header: Nostr {}",
@@ -130,10 +130,10 @@ impl JwtAuthProvider {
     }
 
     /// Parse the JWT response from the API
-    async fn parse_jwt_response(&self, response: reqwest::Response) -> Result<String> {
+    async fn parse_jwt_response(&self, response: cdk_common::RawResponse) -> Result<String> {
         let status = response.status();
 
-        if !status.is_success() {
+        if !response.is_success() {
             let error_text = response.text().await.unwrap_or_default();
             tracing::error!("Auth failed - Status: {}, Body: {}", status, error_text);
             return Err(Error::Auth(format!(

+ 6 - 6
crates/cdk-npubcash/src/client.rs

@@ -2,7 +2,7 @@
 
 use std::sync::Arc;
 
-use reqwest::Client as HttpClient;
+use cdk_http_client::{HttpClient, RawResponse};
 use tracing::instrument;
 
 use crate::auth::JwtAuthProvider;
@@ -200,11 +200,11 @@ impl NpubCashClient {
         let status = response.status();
 
         // Handle error responses
-        if !status.is_success() {
+        if !response.is_success() {
             let error_text = response.text().await.unwrap_or_default();
             return Err(Error::Api {
                 message: error_text,
-                status: status.as_u16(),
+                status,
             });
         }
 
@@ -274,7 +274,7 @@ impl NpubCashClient {
     }
 
     /// Parse the HTTP response and deserialize the JSON body
-    async fn parse_response<T>(&self, response: reqwest::Response) -> Result<T>
+    async fn parse_response<T>(&self, response: RawResponse) -> Result<T>
     where
         T: serde::de::DeserializeOwned,
     {
@@ -284,11 +284,11 @@ impl NpubCashClient {
         let response_text = response.text().await?;
 
         // Handle error status codes
-        if !status.is_success() {
+        if !(200..300).contains(&status) {
             tracing::debug!("Error response ({}): {}", status, response_text);
             return Err(Error::Api {
                 message: response_text,
-                status: status.as_u16(),
+                status,
             });
         }
 

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

@@ -23,7 +23,7 @@ pub enum Error {
 
     /// HTTP request failed
     #[error("HTTP request failed: {0}")]
-    Http(#[from] reqwest::Error),
+    Http(#[from] cdk_common::HttpError),
 
     /// JSON serialization/deserialization error
     #[error("JSON serialization error: {0}")]

+ 2 - 4
crates/cdk/Cargo.toml

@@ -12,10 +12,10 @@ license.workspace = true
 
 [features]
 default = ["mint", "wallet", "nostr", "bip353"]
-wallet = ["dep:futures", "dep:reqwest", "cdk-common/wallet", "dep:rustls"]
+wallet = ["dep:futures", "cdk-common/wallet", "cdk-common/http", "dep:rustls"]
 nostr = ["wallet", "dep:nostr-sdk", "cdk-common/nostr"]
 npubcash = ["wallet", "nostr", "dep:cdk-npubcash"]
-mint = ["dep:futures", "dep:reqwest", "cdk-common/mint", "cdk-signatory"]
+mint = ["dep:futures", "cdk-common/mint", "cdk-common/http", "cdk-signatory"]
 bip353 = ["dep:hickory-resolver"]
 # We do not commit to a MSRV with swagger enabled
 swagger = ["mint", "dep:utoipa", "cdk-common/swagger"]
@@ -45,7 +45,6 @@ ciborium.workspace = true
 lightning.workspace = true
 lightning-invoice.workspace = true
 regex.workspace = true
-reqwest = { workspace = true, optional = true }
 serde.workspace = true
 serde_json.workspace = true
 serde_with.workspace = true
@@ -190,7 +189,6 @@ cdk-fake-wallet.workspace = true
 bip39.workspace = true
 tracing-subscriber.workspace = true
 criterion.workspace = true
-reqwest = { workspace = true }
 anyhow.workspace = true
 ureq = { version = "3.1.0", features = ["json"] }
 tokio = { workspace = true, features = ["full"] }

+ 3 - 10
crates/cdk/examples/auth_wallet.rs

@@ -127,19 +127,12 @@ async fn get_access_token(mint_info: &MintInfo) -> String {
     ];
 
     // Make the token request directly
-    let client = reqwest::Client::new();
-    let response = client
-        .post(token_url)
-        .form(&params)
-        .send()
+    let client = cdk_common::HttpClient::new();
+    let token_response: serde_json::Value = client
+        .post_form(&token_url, &params)
         .await
         .expect("Failed to send token request");
 
-    let token_response: serde_json::Value = response
-        .json()
-        .await
-        .expect("Failed to parse token response");
-
     token_response["access_token"]
         .as_str()
         .expect("No access token in response")

+ 3 - 5
crates/cdk/examples/multimint-npubcash.rs

@@ -143,19 +143,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
 
 /// Request an invoice via LNURL-pay
 async fn request_invoice(npub: &str, amount_msats: u64) -> Result<(), Box<dyn std::error::Error>> {
-    let http_client = reqwest::Client::new();
+    let http_client = cdk_common::HttpClient::new();
 
     let lnurlp_url = format!("{}/.well-known/lnurlp/{}", NPUBCASH_URL, npub);
-    let lnurlp_response: serde_json::Value =
-        http_client.get(&lnurlp_url).send().await?.json().await?;
+    let lnurlp_response: serde_json::Value = http_client.fetch(&lnurlp_url).await?;
 
     let callback = lnurlp_response["callback"]
         .as_str()
         .ok_or("No callback URL")?;
 
     let invoice_url = format!("{}?amount={}", callback, amount_msats);
-    let invoice_response: serde_json::Value =
-        http_client.get(&invoice_url).send().await?.json().await?;
+    let invoice_response: serde_json::Value = http_client.fetch(&invoice_url).await?;
 
     let pr = invoice_response["pr"]
         .as_str()

+ 3 - 5
crates/cdk/examples/npubcash.rs

@@ -136,19 +136,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
 
 /// Request an invoice via LNURL-pay
 async fn request_invoice(npub: &str, amount_msats: u64) -> Result<(), Box<dyn std::error::Error>> {
-    let http_client = reqwest::Client::new();
+    let http_client = cdk_common::HttpClient::new();
 
     let lnurlp_url = format!("{}/.well-known/lnurlp/{}", NPUBCASH_URL, npub);
-    let lnurlp_response: serde_json::Value =
-        http_client.get(&lnurlp_url).send().await?.json().await?;
+    let lnurlp_response: serde_json::Value = http_client.fetch(&lnurlp_url).await?;
 
     let callback = lnurlp_response["callback"]
         .as_str()
         .ok_or("No callback URL")?;
 
     let invoice_url = format!("{}?amount={}", callback, amount_msats);
-    let invoice_response: serde_json::Value =
-        http_client.get(&invoice_url).send().await?.json().await?;
+    let invoice_response: serde_json::Value = http_client.fetch(&invoice_url).await?;
 
     let pr = invoice_response["pr"]
         .as_str()

+ 9 - 0
crates/cdk/src/lib.rs

@@ -75,6 +75,15 @@ pub type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
 
 /// Re-export subscription
 pub use cdk_common::subscription;
+#[cfg(any(feature = "wallet", feature = "mint"))]
+pub mod http_client {
+    //! Re-export HTTP client types from cdk-http-client (via cdk-common)
+    //!
+    //! HTTP client abstraction for making HTTP requests.
+    pub use cdk_common::{
+        fetch, HttpClient, HttpClientBuilder, HttpError, RawResponse, RequestBuilder, Response,
+    };
+}
 /// Re-export futures::Stream
 #[cfg(any(feature = "wallet", feature = "mint"))]
 pub use futures::{Stream, StreamExt};

+ 9 - 28
crates/cdk/src/oidc_client.rs

@@ -4,9 +4,9 @@ use std::collections::HashMap;
 use std::ops::Deref;
 use std::sync::Arc;
 
+use cdk_common::HttpClient;
 use jsonwebtoken::jwk::{AlgorithmParameters, JwkSet};
 use jsonwebtoken::{decode, decode_header, DecodingKey, Validation};
-use reqwest::Client;
 use serde::Deserialize;
 #[cfg(feature = "wallet")]
 use serde::Serialize;
@@ -17,10 +17,10 @@ use tracing::instrument;
 /// OIDC Error
 #[derive(Debug, Error)]
 pub enum Error {
-    /// From Reqwest error
+    /// From HTTP error
     #[error(transparent)]
-    Reqwest(#[from] reqwest::Error),
-    /// From Reqwest error
+    Http(#[from] cdk_common::HttpError),
+    /// From JWT error
     #[error(transparent)]
     Jwt(#[from] jsonwebtoken::errors::Error),
     /// Missing kid header
@@ -56,7 +56,7 @@ pub struct OidcConfig {
 /// Http Client
 #[derive(Debug, Clone)]
 pub struct OidcClient {
-    client: Client,
+    client: HttpClient,
     openid_discovery: String,
     client_id: Option<String>,
     oidc_config: Arc<RwLock<Option<OidcConfig>>>,
@@ -91,7 +91,7 @@ impl OidcClient {
     /// Create new [`OidcClient`]
     pub fn new(openid_discovery: String, client_id: Option<String>) -> Self {
         Self {
-            client: Client::new(),
+            client: HttpClient::new(),
             openid_discovery,
             client_id,
             oidc_config: Arc::new(RwLock::new(None)),
@@ -103,13 +103,7 @@ impl OidcClient {
     #[instrument(skip(self))]
     pub async fn get_oidc_config(&self) -> Result<OidcConfig, Error> {
         tracing::debug!("Getting oidc config");
-        let oidc_config = self
-            .client
-            .get(&self.openid_discovery)
-            .send()
-            .await?
-            .json::<OidcConfig>()
-            .await?;
+        let oidc_config: OidcConfig = self.client.fetch(&self.openid_discovery).await?;
 
         let mut current_config = self.oidc_config.write().await;
 
@@ -122,13 +116,7 @@ impl OidcClient {
     #[instrument(skip(self))]
     pub async fn get_jwkset(&self, jwks_uri: &str) -> Result<JwkSet, Error> {
         tracing::debug!("Getting jwks set");
-        let jwks_set = self
-            .client
-            .get(jwks_uri)
-            .send()
-            .await?
-            .json::<JwkSet>()
-            .await?;
+        let jwks_set: JwkSet = self.client.fetch(jwks_uri).await?;
 
         let mut current_set = self.jwks_set.write().await;
 
@@ -248,14 +236,7 @@ impl OidcClient {
             refresh_token,
         };
 
-        let response = self
-            .client
-            .post(token_url)
-            .form(&request)
-            .send()
-            .await?
-            .json::<TokenResponse>()
-            .await?;
+        let response: TokenResponse = self.client.post_form(&token_url, &request).await?;
 
         Ok(response)
     }

+ 25 - 42
crates/cdk/src/wallet/mint_connector/transport.rs

@@ -1,14 +1,13 @@
 //! HTTP Transport trait with a default implementation
 use std::fmt::Debug;
 
-use cdk_common::AuthToken;
+use cdk_common::{AuthToken, HttpClient, HttpClientBuilder};
 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
 use hickory_resolver::config::ResolverConfig;
 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
 use hickory_resolver::name_server::TokioConnectionProvider;
 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
 use hickory_resolver::Resolver;
-use reqwest::Client;
 use serde::de::DeserializeOwned;
 use serde::Serialize;
 use url::Url;
@@ -56,7 +55,7 @@ pub trait Transport: Default + Send + Sync + Debug + Clone {
 /// Async transport for Http
 #[derive(Debug, Clone)]
 pub struct Async {
-    inner: Client,
+    inner: HttpClient,
 }
 
 impl Default for Async {
@@ -67,7 +66,7 @@ impl Default for Async {
         }
 
         Self {
-            inner: Client::new(),
+            inner: HttpClient::new(),
         }
     }
 }
@@ -92,27 +91,23 @@ impl Transport for Async {
         host_matcher: Option<&str>,
         accept_invalid_certs: bool,
     ) -> Result<(), Error> {
-        let builder = reqwest::Client::builder().danger_accept_invalid_certs(accept_invalid_certs);
+        let builder =
+            HttpClientBuilder::default().danger_accept_invalid_certs(accept_invalid_certs);
 
         let builder = match host_matcher {
             Some(pattern) => {
                 // When a matcher is provided, only apply the proxy to matched hosts
-                let regex = regex::Regex::new(pattern).map_err(|e| Error::Custom(e.to_string()))?;
-                builder.proxy(reqwest::Proxy::custom(move |url| {
-                    url.host_str()
-                        .filter(|host| regex.is_match(host))
-                        .map(|_| proxy.clone())
-                }))
+                builder
+                    .proxy_with_matcher(proxy, pattern)
+                    .map_err(|e| Error::Custom(e.to_string()))?
             }
             // Apply proxy to all requests when no matcher is provided
-            None => {
-                builder.proxy(reqwest::Proxy::all(proxy).map_err(|e| Error::Custom(e.to_string()))?)
-            }
+            None => builder.proxy(proxy),
         };
 
         self.inner = builder
             .build()
-            .map_err(|e| Error::HttpError(e.status().map(|s| s.as_u16()), e.to_string()))?;
+            .map_err(|e| Error::HttpError(None, e.to_string()))?;
         Ok(())
     }
 
@@ -144,7 +139,8 @@ impl Transport for Async {
     where
         R: DeserializeOwned,
     {
-        let mut request = self.inner.get(url);
+        let url_str = url.to_string();
+        let mut request = self.inner.get(&url_str);
 
         if let Some(auth) = auth {
             request = request.header(auth.header_key(), auth.to_string());
@@ -153,20 +149,10 @@ impl Transport for Async {
         let response = request
             .send()
             .await
-            .map_err(|e| {
-                Error::HttpError(
-                    e.status().map(|status_code| status_code.as_u16()),
-                    e.to_string(),
-                )
-            })?
+            .map_err(|e| Error::HttpError(None, e.to_string()))?
             .text()
             .await
-            .map_err(|e| {
-                Error::HttpError(
-                    e.status().map(|status_code| status_code.as_u16()),
-                    e.to_string(),
-                )
-            })?;
+            .map_err(|e| Error::HttpError(None, e.to_string()))?;
 
         serde_json::from_str::<R>(&response).map_err(|err| {
             tracing::warn!("Http Response error: {}", err);
@@ -187,25 +173,22 @@ impl Transport for Async {
         P: Serialize + ?Sized + Send + Sync,
         R: DeserializeOwned,
     {
-        let mut request = self.inner.post(url).json(&payload);
+        let url_str = url.to_string();
+        let mut request = self.inner.post(&url_str).json(&payload);
 
         if let Some(auth) = auth_token {
             request = request.header(auth.header_key(), auth.to_string());
         }
 
-        let response = request.send().await.map_err(|e| {
-            Error::HttpError(
-                e.status().map(|status_code| status_code.as_u16()),
-                e.to_string(),
-            )
-        })?;
-
-        let response = response.text().await.map_err(|e| {
-            Error::HttpError(
-                e.status().map(|status_code| status_code.as_u16()),
-                e.to_string(),
-            )
-        })?;
+        let response = request
+            .send()
+            .await
+            .map_err(|e| Error::HttpError(None, e.to_string()))?;
+
+        let response = response
+            .text()
+            .await
+            .map_err(|e| Error::HttpError(None, e.to_string()))?;
 
         serde_json::from_str::<R>(&response).map_err(|err| {
             tracing::warn!("Http Response error: {}", err);

+ 5 - 6
crates/cdk/src/wallet/payment_request.rs

@@ -9,14 +9,13 @@ use std::sync::Arc;
 
 use anyhow::Result;
 use bitcoin::hashes::sha256::Hash as Sha256Hash;
-use cdk_common::{Amount, PaymentRequest, PaymentRequestPayload, TransportType};
+use cdk_common::{Amount, HttpClient, PaymentRequest, PaymentRequestPayload, TransportType};
 #[cfg(feature = "nostr")]
 use nostr_sdk::nips::nip19::Nip19Profile;
 #[cfg(feature = "nostr")]
 use nostr_sdk::prelude::*;
 #[cfg(feature = "nostr")]
 use nostr_sdk::{Client as NostrClient, EventBuilder, FromBech32, Keys, ToBech32};
-use reqwest::Client;
 
 use crate::error::Error;
 use crate::mint_url::MintUrl;
@@ -165,22 +164,22 @@ impl Wallet {
                 }
 
                 TransportType::HttpPost => {
-                    let client = Client::new();
+                    let client = HttpClient::new();
 
                     let res = client
-                        .post(transport.target.clone())
+                        .post(&transport.target)
                         .json(&payload)
                         .send()
                         .await
                         .map_err(|e| Error::HttpError(None, e.to_string()))?;
 
                     let status = res.status();
-                    if status.is_success() {
+                    if res.is_success() {
                         println!("Successfully posted payment");
                         Ok(())
                     } else {
                         let body = res.text().await.unwrap_or_default();
-                        Err(Error::HttpError(Some(status.as_u16()), body))
+                        Err(Error::HttpError(Some(status), body))
                     }
                 }
                 TransportType::InBand => {