From 90e9e3cff86c9807a78efaba760db2a69603582d Mon Sep 17 00:00:00 2001 From: Drazen Urch Date: Mon, 15 Sep 2025 14:32:15 +0200 Subject: [PATCH] Domain fronting integration (#5974) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: unify HTTP client creation and enable domain fronting Enhanced the base nym_http_api_client to reduce fragmentation and enable domain fronting: - Added SerializationFormat enum for explicit JSON/bincode choice (no auto-detection) - Added from_network() method to create clients from NymNetworkDetails with domain fronting - Added with_bincode() builder method for explicit serialization configuration - Set Accept header based on serialization preference - Added deprecation paths for NymApiClient wrapper and nym_api::Client re-export - Enabled domain fronting support via network defaults feature This is part of a broader effort to consolidate HTTP client implementations across the codebase, reducing ~500 lines of wrapper code and providing automatic domain fronting for censorship resistance. * feat: migrate NymApiClient usage to unified HTTP client - Wire up domain fronting configuration in NymNetworkDetails - Implement NymApiClientExt trait for base nym_http_api_client::Client - Migrate direct NymApiClient usage in multiple components: - nym-network-monitor - verloc measurements - connection tester - coconut/ecash client - validator rewarder - Add Copy derive to ApiUrlConst to enable iteration - Update error handling and Display implementations This enables automatic domain fronting for all Nym API calls via the configured CDN front hosts. * fix: resolve all compilation errors after NymApiClient migration - Add missing nym-http-api-client dependencies to multiple crates - Add NymApiClientExt trait imports where needed - Fix type mismatches from NymApiClient to unified Client - Add error conversions for NymAPIError in various error enums - Implement missing trait methods (get_current_rewarded_set, get_all_basic_nodes_with_metadata, get_all_described_nodes) - Fix type conversions for RewardedSetResponse in network monitor - Update all API client instantiation to use new unified HTTP client * feat: complete migration to unified HTTP client and fix all compilation errors - Added missing NymApiClientExt trait methods (get_all_expanded_nodes, change_base_urls) - Fixed all compilation errors across the workspace - Updated nym-node to use unified client instead of deprecated NymApiClient - Fixed type conversions for RewardedSetResponse → EpochRewardedSet - Added nym-http-api-client dependency where needed - Updated all examples and documentation to use new client API * fix: provide all API URLs for automatic failover in endpoint rotation Previously, when rotating API endpoints, only a single URL was provided to the HTTP client, defeating the purpose of having multiple URLs for resilience. Changes: - NymApiTopologyProvider now provides all URLs in rotated order when switching endpoints - NymApisClient similarly provides all URLs starting from the working endpoint - Added clarifying comments for broadcast/exhaustive query methods where single URLs are intentionally used - This enables the HTTP client's built-in failover mechanism while maintaining endpoint rotation behavior The fix ensures that if the primary endpoint fails, the client can automatically failover to alternative endpoints without manual intervention, improving overall network resilience. * Update common/client-core/src/client/base_client/mod.rs Co-authored-by: Jędrzej Stuczyński * Remove error generics, address PR comments * Explicit warning on missing fronting configuration * Assorted CI fixes * Registry proc-macro * Rename macro * Syn workspace version * Where do we need to put inventory * Ergonomics and call sites, incept the builder * fix: Address critical issues in client configuration registry implementation - Fixed HeaderMapInit parsing bug that would cause compilation errors - Added comprehensive documentation with usage examples and DSL reference - Improved error handling with better error messages for invalid headers - Added test coverage for both macro and registry functionality - Added debug inspection capabilities for registered configurations - Fixed module name conflicts in tests by using separate modules All tests now passing: - 7 macro tests validating DSL parsing and code generation - 4 registry tests verifying configuration collection and application * Use default value for the ports until api is deployed * Feature/improved http error (#6025) * use display impl for urls * feat: attempt to add more details to reqwest errors * temporarily restored GenericRequestFailure variant * another restoration * cleanup * Some debug tooling, and default timeout fix * Fix user-agent override * Fix various wasm things --------- Co-authored-by: Jędrzej Stuczyński Co-authored-by: Bogdan-Ștefan Neacşu --- Cargo.lock | 199 +++--- Cargo.toml | 4 +- common/bandwidth-controller/src/utils.rs | 2 +- common/client-core/Cargo.toml | 1 + .../client-core/src/client/base_client/mod.rs | 26 +- .../topology_control/nym_api_provider.rs | 62 +- common/client-core/src/error.rs | 13 +- common/client-core/src/init/helpers.rs | 71 ++- .../validator-client/src/client.rs | 41 +- .../validator-client/src/coconut/mod.rs | 22 +- .../validator-client/src/connection_tester.rs | 20 +- .../client-libs/validator-client/src/error.rs | 11 +- .../client-libs/validator-client/src/lib.rs | 1 - .../validator-client/src/nym_api/error.rs | 3 +- .../validator-client/src/nym_api/mod.rs | 220 ++++++- common/commands/Cargo.toml | 1 + common/commands/src/context/mod.rs | 2 +- .../ts-packages/types/src/types/rust/Role.ts | 2 +- common/credential-proxy/src/error.rs | 13 +- .../credential-proxy/src/shared_state/mod.rs | 1 + .../src/ticketbook_manager/wallet_shares.rs | 1 + .../src/ecash/credential_sender.rs | 4 +- common/credentials/Cargo.toml | 1 + .../src/ecash/bandwidth/issuance.rs | 6 +- common/credentials/src/ecash/utils.rs | 1 + common/credentials/src/error.rs | 11 +- common/ecash-signer-check/Cargo.toml | 1 + common/ecash-signer-check/src/client_check.rs | 56 +- common/ecash-signer-check/src/error.rs | 12 + common/http-api-client-macro/Cargo.toml | 31 + common/http-api-client-macro/build.rs | 13 + common/http-api-client-macro/src/lib.rs | 384 +++++++++++ .../tests/macro_tests.rs | 64 ++ common/http-api-client/Cargo.toml | 8 + .../examples/debug_inventory.rs | 26 + .../http-api-client/examples/test_timeout.rs | 99 +++ common/http-api-client/src/fronted.rs | 10 +- common/http-api-client/src/lib.rs | 598 +++++++++++++----- common/http-api-client/src/registry.rs | 101 +++ common/http-api-client/src/tests.rs | 15 +- .../http-api-client/tests/registry_tests.rs | 70 ++ common/network-defaults/src/network.rs | 11 +- common/types/src/error.rs | 15 +- common/verloc/Cargo.toml | 2 + common/verloc/src/measurements/measurer.rs | 15 +- common/wasm/client-core/Cargo.toml | 1 + common/wasm/client-core/src/error.rs | 6 + common/wasm/client-core/src/helpers.rs | 26 +- common/wasm/client-core/src/lib.rs | 2 +- .../client/src/lib.rs | 13 +- .../tests/src/lib.rs | 4 +- .../tests/src/v0/network.rs | 3 +- contracts/ecash/src/contract/test.rs | 2 +- contracts/ecash/src/support/tests.rs | 4 +- contracts/mixnet/src/delegations/queries.rs | 4 +- .../mixnet/src/delegations/transactions.rs | 2 +- contracts/mixnet/src/mixnodes/queries.rs | 8 +- contracts/mixnet/src/support/tests/mod.rs | 14 +- contracts/performance/src/testing/mod.rs | 6 +- contracts/vesting/src/vesting/mod.rs | 2 +- .../custom-topology/custom-provider.mdx | 10 +- nym-api/Cargo.toml | 1 + .../types/rust/GatewayCoreStatusResponse.ts | 2 +- .../types/rust/MixnodeCoreStatusResponse.ts | 2 +- nym-api/src/ecash/error.rs | 10 + nym-api/src/ecash/state/mod.rs | 1 + nym-api/src/ecash/tests/mod.rs | 7 +- nym-api/src/node_describe_cache/mod.rs | 4 +- .../src/node_describe_cache/query_helpers.rs | 5 +- nym-api/src/node_describe_cache/refresh.rs | 4 +- .../src/client.rs | 33 +- .../src/credentials/ticketbook/mod.rs | 0 nym-network-monitor/Cargo.toml | 2 + nym-network-monitor/src/main.rs | 21 +- .../src/metrics_scraper/error.rs | 23 +- .../src/metrics_scraper/mod.rs | 12 +- .../nym-node-status-api/src/monitor/mod.rs | 26 +- nym-node/nym-node-requests/src/api/client.rs | 3 +- nym-node/src/error.rs | 2 +- nym-node/src/node/http/error.rs | 13 +- nym-node/src/node/nym_apis_client.rs | 41 +- nym-node/src/node/shared_network.rs | 14 +- nym-statistics-api/src/network_view.rs | 18 +- nym-validator-rewarder/Cargo.toml | 1 + .../src/cli/dry_run_check_issuer.rs | 1 + nym-validator-rewarder/src/error.rs | 8 +- .../src/rewarder/nyxd_client.rs | 17 +- .../src/rewarder/ticketbook_issuance/types.rs | 4 +- .../rewarder/ticketbook_issuance/verifier.rs | 1 + nym-wallet/Cargo.lock | 53 ++ nym-wallet/src-tauri/src/error.rs | 19 +- .../src-tauri/src/operations/mixnet/bond.rs | 15 +- sdk/rust/nym-sdk/Cargo.toml | 1 + .../examples/custom_topology_provider.rs | 17 +- tools/internal/testnet-manager/Cargo.toml | 1 + .../src/manager/local_client.rs | 7 +- .../validator-status-check/Cargo.toml | 1 + .../validator-status-check/src/models.rs | 16 +- wasm/zknym-lib/src/error.rs | 4 +- wasm/zknym-lib/src/vpn_api_client/client.rs | 26 +- wasm/zknym-lib/src/vpn_api_client/mod.rs | 5 - 101 files changed, 2248 insertions(+), 570 deletions(-) create mode 100644 common/http-api-client-macro/Cargo.toml create mode 100644 common/http-api-client-macro/build.rs create mode 100644 common/http-api-client-macro/src/lib.rs create mode 100644 common/http-api-client-macro/tests/macro_tests.rs create mode 100644 common/http-api-client/examples/debug_inventory.rs create mode 100644 common/http-api-client/examples/test_timeout.rs create mode 100644 common/http-api-client/src/registry.rs create mode 100644 common/http-api-client/tests/registry_tests.rs create mode 100644 nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs diff --git a/Cargo.lock b/Cargo.lock index c536f31a21..be1bab854c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -408,7 +408,7 @@ dependencies = [ "rustc-hash", "serde", "serde_derive", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -490,7 +490,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -501,7 +501,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -709,7 +709,7 @@ checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1340,7 +1340,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1591,7 +1591,7 @@ checksum = "a782b93fae93e57ca8ad3e9e994e784583f5933aeaaa5c80a545c4b437be2047" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1615,7 +1615,7 @@ checksum = "e01c9214319017f6ebd8e299036e1f717fa9bb6724e758f7d6fb2477599d1a29" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1859,7 +1859,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1963,7 +1963,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2115,7 +2115,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2126,7 +2126,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2179,7 +2179,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2222,7 +2222,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2251,7 +2251,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "unicode-xid", ] @@ -2263,7 +2263,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "unicode-xid", ] @@ -2332,7 +2332,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2385,7 +2385,7 @@ version = "0.1.0" dependencies = [ "cosmwasm-std", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2547,7 +2547,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2677,7 +2677,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2907,7 +2907,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -3784,7 +3784,7 @@ checksum = "0ab604ee7085efba6efc65e4ebca0e9533e3aff6cb501d7d77b211e3a781c6d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -3853,7 +3853,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -3955,9 +3955,9 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "inventory" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" dependencies = [ "rustversion", ] @@ -4103,7 +4103,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4375,7 +4375,7 @@ dependencies = [ "proc-macro2", "quote", "sealed", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4387,7 +4387,7 @@ dependencies = [ "proc-macro2", "quote", "sealed", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4400,7 +4400,7 @@ dependencies = [ "macroific_core", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4428,7 +4428,7 @@ checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4921,6 +4921,7 @@ dependencies = [ "nym-ecash-signer-check", "nym-ecash-time", "nym-gateway-client", + "nym-http-api-client", "nym-http-api-common", "nym-mixnet-contract-common", "nym-node-requests", @@ -5152,6 +5153,7 @@ dependencies = [ "nym-crypto", "nym-ecash-contract-common", "nym-ecash-time", + "nym-http-api-client", "nym-id", "nym-mixnet-contract-common", "nym-multisig-contract-common", @@ -5240,6 +5242,7 @@ dependencies = [ "nym-http-api-client", "nym-id", "nym-mixnet-client", + "nym-mixnet-contract-common", "nym-network-defaults", "nym-nonexhaustive-delayqueue", "nym-pemstore", @@ -5630,6 +5633,7 @@ dependencies = [ "nym-crypto", "nym-ecash-contract-common", "nym-ecash-time", + "nym-http-api-client", "nym-network-defaults", "nym-serde-helpers", "nym-validator-client", @@ -5731,6 +5735,7 @@ version = "0.1.0" dependencies = [ "futures", "nym-ecash-signer-check-types", + "nym-http-api-client", "nym-network-defaults", "nym-validator-client", "semver 1.0.26", @@ -5988,17 +5993,23 @@ dependencies = [ "async-trait", "bincode", "bytes", + "cfg-if", "encoding_rs", "hickory-resolver", "http 1.3.1", + "inventory", "itertools 0.14.0", "mime", "nym-bin-common", + "nym-http-api-client-macro", "nym-http-api-common", + "nym-network-defaults", "once_cell", "reqwest 0.12.22", "serde", "serde_json", + "serde_plain", + "serde_yaml", "thiserror 2.0.12", "tokio", "tracing", @@ -6006,6 +6017,19 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "nym-http-api-client-macro" +version = "0.1.0" +dependencies = [ + "nym-http-api-client", + "proc-macro-crate", + "proc-macro2", + "quote", + "reqwest 0.12.22", + "syn 2.0.106", + "uuid", +] + [[package]] name = "nym-http-api-common" version = "0.1.0" @@ -6269,6 +6293,8 @@ dependencies = [ "nym-client-core", "nym-crypto", "nym-gateway-requests", + "nym-http-api-client", + "nym-mixnet-contract-common", "nym-network-defaults", "nym-sdk", "nym-sphinx", @@ -6737,6 +6763,7 @@ dependencies = [ "nym-credentials-interface", "nym-crypto", "nym-gateway-requests", + "nym-http-api-client", "nym-network-defaults", "nym-ordered-buffer", "nym-service-providers-common", @@ -7350,6 +7377,7 @@ dependencies = [ "nym-credentials-interface", "nym-crypto", "nym-ecash-time", + "nym-http-api-client", "nym-network-defaults", "nym-pemstore", "nym-serde-helpers", @@ -7379,7 +7407,9 @@ dependencies = [ "bytes", "futures", "humantime", + "nym-api-requests", "nym-crypto", + "nym-http-api-client", "nym-task", "nym-validator-client", "rand 0.8.5", @@ -8019,7 +8049,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8082,7 +8112,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8111,7 +8141,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8319,6 +8349,15 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -8338,7 +8377,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8394,7 +8433,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8659,7 +8698,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8909,7 +8948,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.104", + "syn 2.0.106", "walkdir", ] @@ -9213,7 +9252,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals 0.29.1", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9245,7 +9284,7 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9266,7 +9305,7 @@ checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9406,7 +9445,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9417,7 +9456,7 @@ checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9428,7 +9467,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9490,7 +9529,7 @@ checksum = "aafbefbe175fa9bf03ca83ef89beecff7d2a95aaacd5732325b90ac8c3bd7b90" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9503,6 +9542,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -9511,7 +9559,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9564,7 +9612,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9910,7 +9958,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9933,7 +9981,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.104", + "syn 2.0.106", "tokio", "url", ] @@ -10142,7 +10190,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -10198,9 +10246,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -10230,7 +10278,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -10427,7 +10475,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -10451,6 +10499,7 @@ dependencies = [ "nym-crypto", "nym-ecash-contract-common", "nym-group-contract-common", + "nym-http-api-client", "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-pemstore", @@ -10506,7 +10555,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -10517,7 +10566,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -10649,7 +10698,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -10970,7 +11019,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -11140,7 +11189,7 @@ checksum = "0e9d8656589772eeec2cf7a8264d9cda40fb28b9bc53118ceb9e8c07f8f38730" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "termcolor", ] @@ -11167,7 +11216,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals 0.28.0", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -11357,7 +11406,7 @@ dependencies = [ "indexmap 2.10.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -11372,7 +11421,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.104", + "syn 2.0.106", "toml 0.5.11", "uniffi_meta", ] @@ -11499,7 +11548,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.104", + "syn 2.0.106", "uuid", ] @@ -11538,7 +11587,7 @@ checksum = "268d76aaebb80eba79240b805972e52d7d410d4bcc52321b951318b0f440cd60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -11549,7 +11598,7 @@ checksum = "382673bda1d05c85b4550d32fd4192ccd4cffe9a908543a0795d1e7682b36246" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "utoipauto-core", ] @@ -11573,6 +11622,7 @@ dependencies = [ "clap", "comfy-table", "nym-bin-common", + "nym-http-api-client", "nym-network-defaults", "nym-validator-client", "serde", @@ -11726,7 +11776,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-shared", ] @@ -11761,7 +11811,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -11796,7 +11846,7 @@ checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -11812,6 +11862,7 @@ dependencies = [ "nym-credential-storage", "nym-crypto", "nym-gateway-client", + "nym-http-api-client", "nym-sphinx", "nym-sphinx-acknowledgements", "nym-statistics-common", @@ -12067,7 +12118,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -12078,7 +12129,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -12502,7 +12553,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "synstructure", ] @@ -12523,7 +12574,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -12543,7 +12594,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "synstructure", ] @@ -12564,7 +12615,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -12597,7 +12648,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4fa01436df..627a1c29de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ members = [ "common/gateway-requests", "common/gateway-stats-storage", "common/gateway-storage", - "common/http-api-client", + "common/http-api-client", "common/http-api-client-macro", "common/http-api-common", "common/inclusion-probability", "common/ip-packet-requests", @@ -275,6 +275,7 @@ hyper = "1.6.0" hyper-util = "0.1" indicatif = "0.18.0" inquire = "0.6.2" +inventory = "0.3.21" ip_network = "0.4.1" ipnetwork = "0.20" itertools = "0.14.0" @@ -322,6 +323,7 @@ serde_json_path = "0.7.2" serde_repr = "0.1" serde_with = "3.9.0" serde_yaml = "0.9.25" +serde_plain = "1.0.2" sha2 = "0.10.9" si-scale = "0.2.3" snow = "0.9.6" diff --git a/common/bandwidth-controller/src/utils.rs b/common/bandwidth-controller/src/utils.rs index 4502238c91..c353755192 100644 --- a/common/bandwidth-controller/src/utils.rs +++ b/common/bandwidth-controller/src/utils.rs @@ -13,7 +13,7 @@ use nym_credentials_interface::{ }; use nym_ecash_time::Date; use nym_validator_client::coconut::all_ecash_api_clients; -use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nym_api::{EpochId, NymApiClientExt}; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; use nym_validator_client::EcashApiClient; use rand::prelude::SliceRandom; diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index 74663fe807..d3e2409eec 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -53,6 +53,7 @@ nym-client-core-config-types = { path = "./config-types", features = [ nym-client-core-surb-storage = { path = "./surb-storage" } nym-client-core-gateways-storage = { path = "./gateways-storage" } nym-ecash-time = { path = "../ecash-time" } +nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" } [target."cfg(not(target_arch = \"wasm32\"))".dependencies] nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false } diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index 206b682fd1..2b187ca05c 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -57,7 +57,7 @@ use nym_task::{ShutdownManager, ShutdownTracker}; use nym_topology::provider_trait::TopologyProvider; use nym_topology::HardcodedTopologyProvider; use nym_validator_client::nym_api::NymApiClientExt; -use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, NymApiClient, UserAgent}; +use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, UserAgent}; use rand::prelude::SliceRandom; use rand::rngs::OsRng; use rand::thread_rng; @@ -624,7 +624,7 @@ where custom_provider: Option>, config_topology: config::Topology, nym_api_urls: Vec, - nym_api_client: NymApiClient, + nym_api_client: nym_http_api_client::Client, ) -> Box { // if no custom provider was ... provided ..., create one using nym-api custom_provider.unwrap_or_else(|| { @@ -814,21 +814,29 @@ where setup_gateway(setup_method, key_store, details_store).await } - fn construct_nym_api_client(config: &Config, user_agent: Option) -> NymApiClient { + fn construct_nym_api_client( + config: &Config, + user_agent: Option, + ) -> Result { let mut nym_api_urls = config.get_nym_api_endpoints(); nym_api_urls.shuffle(&mut thread_rng()); + let mut builder = nym_http_api_client::Client::builder(nym_api_urls[0].clone()) + .map_err(ClientCoreError::from)?; + if let Some(user_agent) = user_agent { - NymApiClient::new_with_user_agent(nym_api_urls[0].clone(), user_agent) - } else { - NymApiClient::new(nym_api_urls[0].clone()) + builder = builder.with_user_agent(user_agent); } + + builder = builder.with_bincode(); + + builder.build().map_err(ClientCoreError::from) } async fn determine_key_rotation_state( - client: &NymApiClient, + client: &nym_http_api_client::Client, ) -> Result { - Ok(client.nym_api.get_key_rotation_info().await?.into()) + Ok(client.get_key_rotation_info().await?.into()) } pub async fn start_base(mut self) -> Result @@ -891,7 +899,7 @@ where .dkg_query_client .map(|client| BandwidthController::new(credential_store, client)); - let nym_api_client = Self::construct_nym_api_client(&self.config, self.user_agent.clone()); + let nym_api_client = Self::construct_nym_api_client(&self.config, self.user_agent.clone())?; let key_rotation_config = Self::determine_key_rotation_state(&nym_api_client).await?; let topology_provider = Self::setup_topology_provider( diff --git a/common/client-core/src/client/topology_control/nym_api_provider.rs b/common/client-core/src/client/topology_control/nym_api_provider.rs index 339cd12653..a81118665c 100644 --- a/common/client-core/src/client/topology_control/nym_api_provider.rs +++ b/common/client-core/src/client/topology_control/nym_api_provider.rs @@ -2,8 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use async_trait::async_trait; +use nym_mixnet_contract_common::EpochRewardedSet; use nym_topology::provider_trait::{ToTopologyMetadata, TopologyProvider}; use nym_topology::NymTopology; +use nym_validator_client::nym_api::NymApiClientExt; use rand::prelude::SliceRandom; use rand::thread_rng; use std::cmp::min; @@ -39,30 +41,43 @@ impl Config { pub struct NymApiTopologyProvider { config: Config, - validator_client: nym_validator_client::client::NymApiClient, + validator_client: nym_http_api_client::Client, nym_api_urls: Vec, currently_used_api: usize, + use_bincode: bool, } impl NymApiTopologyProvider { pub fn new( config: impl Into, mut nym_api_urls: Vec, - mut validator_client: nym_validator_client::client::NymApiClient, + validator_client: nym_http_api_client::Client, ) -> Self { nym_api_urls.shuffle(&mut thread_rng()); - validator_client.change_nym_api(nym_api_urls[0].clone()); - - NymApiTopologyProvider { + let mut provider = NymApiTopologyProvider { config: config.into(), validator_client, nym_api_urls, currently_used_api: 0, - } + use_bincode: true, + }; + // Set all API URLs - the client will try them in order with automatic failover + provider.validator_client.change_base_urls( + provider + .nym_api_urls + .iter() + .map(|u| u.clone().into()) + .collect(), + ); + provider } pub fn disable_bincode(&mut self) { - self.validator_client.use_bincode = false; + self.use_bincode = false; + // Note: The unified client doesn't support toggling bincode after creation. + // This would require recreating the client without bincode. + // For now, we'll track the preference but it won't take effect. + warn!("Disabling bincode on existing client is not currently supported"); } fn use_next_nym_api(&mut self) { @@ -72,8 +87,19 @@ impl NymApiTopologyProvider { } self.currently_used_api = (self.currently_used_api + 1) % self.nym_api_urls.len(); - self.validator_client - .change_nym_api(self.nym_api_urls[self.currently_used_api].clone()) + + // Provide all URLs starting from the next one in rotation order + // This enables automatic failover to other endpoints + let rotated_urls: Vec<_> = self + .nym_api_urls + .iter() + .cycle() + .skip(self.currently_used_api) + .take(self.nym_api_urls.len()) + .map(|u| u.clone().into()) + .collect(); + + self.validator_client.change_base_urls(rotated_urls) } async fn get_current_compatible_topology(&mut self) -> Option { @@ -99,8 +125,13 @@ impl NymApiTopologyProvider { .filter(|n| n.performance.round_to_integer() >= self.config.min_node_performance()) .collect::>(); - NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new()) - .with_skimmed_nodes(&nodes_filtered) + let epoch_rewarded_set: EpochRewardedSet = rewarded_set.into(); + NymTopology::new( + metadata.to_topology_metadata(), + epoch_rewarded_set, + Vec::new(), + ) + .with_skimmed_nodes(&nodes_filtered) } else { // if we're not using extended topology, we're only getting active set mixnodes and gateways @@ -148,8 +179,13 @@ impl NymApiTopologyProvider { } } - NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new()) - .with_skimmed_nodes(&nodes) + let epoch_rewarded_set: EpochRewardedSet = rewarded_set.into(); + NymTopology::new( + metadata.to_topology_metadata(), + epoch_rewarded_set, + Vec::new(), + ) + .with_skimmed_nodes(&nodes) }; if !topology.is_minimally_routable() { diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index 1c26e96dfb..c76b8a3c88 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -56,10 +56,7 @@ pub enum ClientCoreError { ListOfNymApisIsEmpty, #[error("failed to resolve a query to nym API: {source}")] - NymApiQueryFailure { - #[from] - source: NymAPIError, - }, + NymApiQueryFailure { source: Box }, #[error( "the current network topology seem to be insufficient to route any packets through:\n\t{0}" @@ -255,6 +252,14 @@ impl From for ClientCoreError { } } +impl From for ClientCoreError { + fn from(err: NymAPIError) -> ClientCoreError { + ClientCoreError::NymApiQueryFailure { + source: Box::new(err), + } + } +} + /// Set of messages that the client can send to listeners via the task manager #[derive(Debug)] pub enum ClientCoreStatusMessage { diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index 1ca4b68191..f4e34f5ffd 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -7,7 +7,8 @@ use futures::{SinkExt, StreamExt}; use nym_crypto::asymmetric::ed25519; use nym_gateway_client::GatewayClient; use nym_topology::node::RoutingNode; -use nym_validator_client::client::IdentityKeyRef; +use nym_validator_client::client::{IdentityKeyRef, NymApiClientExt}; +use nym_validator_client::nym_nodes::SkimmedNodesWithMetadata; use nym_validator_client::UserAgent; use rand::{seq::SliceRandom, Rng}; #[cfg(unix)] @@ -83,6 +84,48 @@ struct GatewayWithLatency<'a, G: ConnectableGateway> { latency: Duration, } +// Helper to collect all pages of entry nodes - replicates NymApiClient's convenience method +async fn get_all_basic_entry_nodes_with_metadata( + client: &nym_http_api_client::Client, + use_bincode: bool, +) -> Result { + // Get first page to obtain metadata + let mut page = 0; + let res = client + .get_basic_entry_assigned_nodes_v2(false, Some(page), None, use_bincode) + .await?; + let mut nodes = res.nodes.data; + let metadata = res.metadata; + + if res.nodes.pagination.total == nodes.len() { + return Ok(SkimmedNodesWithMetadata::new(nodes, metadata)); + } + + page += 1; + + // Collect remaining pages + loop { + let mut res = client + .get_basic_entry_assigned_nodes_v2(false, Some(page), None, use_bincode) + .await?; + + if !metadata.consistency_check(&res.metadata) { + return Err(ClientCoreError::ValidatorClientError( + nym_validator_client::ValidatorClientError::InconsistentPagedMetadata, + )); + } + + nodes.append(&mut res.nodes.data); + if nodes.len() < res.nodes.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(SkimmedNodesWithMetadata::new(nodes, metadata)) +} + impl<'a, G: ConnectableGateway> GatewayWithLatency<'a, G> { fn new(gateway: &'a G, latency: Duration) -> Self { GatewayWithLatency { gateway, latency } @@ -99,16 +142,28 @@ pub async fn gateways_for_init( let nym_api = nym_apis .choose(rng) .ok_or(ClientCoreError::ListOfNymApisIsEmpty)?; - let client = if let Some(user_agent) = user_agent { - nym_validator_client::client::NymApiClient::new_with_user_agent(nym_api.clone(), user_agent) - } else { - nym_validator_client::client::NymApiClient::new(nym_api.clone()) - }; + + // Use the unified HTTP client directly with optional user agent + let mut builder = nym_http_api_client::Client::builder(nym_api.clone()) + .map_err(|e| { + ClientCoreError::ValidatorClientError(nym_validator_client::ValidatorClientError::from( + e, + )) + })? + .with_bincode(); // Use bincode for better performance + + if let Some(user_agent) = user_agent { + builder = builder.with_user_agent(user_agent); + } + + let client = builder.build().map_err(|e| { + ClientCoreError::ValidatorClientError(nym_validator_client::ValidatorClientError::from(e)) + })?; tracing::debug!("Fetching list of gateways from: {nym_api}"); - let gateways = client - .get_all_basic_entry_assigned_nodes_with_metadata() + // Use our helper to handle pagination + let gateways = get_all_basic_entry_nodes_with_metadata(&client, true) .await? .nodes; info!("nym api reports {} gateways", gateways.len()); diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 2e6d19d9ea..1a91f5c05b 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -5,8 +5,8 @@ use crate::nyxd::{self, NyxdClient}; use crate::signing::direct_wallet::DirectSecp256k1HdWallet; use crate::signing::signer::{NoSigner, OfflineSigner}; use crate::{ - nym_api, DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient, - ReqwestRpcClient, ValidatorClientError, + DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient, ReqwestRpcClient, + ValidatorClientError, }; use nym_api_requests::ecash::models::{ AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, @@ -153,7 +153,7 @@ impl Config { pub struct Client { // ideally they would have been read-only, but unfortunately rust doesn't have such features // #[deprecated(note = "please use `nym_api_client` instead")] - pub nym_api: nym_api::Client, + pub nym_api: nym_http_api_client::Client, // pub nym_api_client: NymApiClient, pub nyxd: NyxdClient, } @@ -214,7 +214,7 @@ impl Client { impl Client { pub fn new_with_rpc_client(config: Config, rpc_client: C) -> Self { - let nym_api_client = nym_api::Client::new(config.api_url.clone(), None); + let nym_api_client = nym_http_api_client::Client::new(config.api_url.clone(), None); Client { nym_api: nym_api_client, @@ -228,7 +228,7 @@ impl Client { where S: OfflineSigner, { - let nym_api_client = nym_api::Client::new(config.api_url.clone(), None); + let nym_api_client = nym_http_api_client::Client::new(config.api_url.clone(), None); Client { nym_api: nym_api_client, @@ -385,38 +385,25 @@ impl Client { } } +/// DEPRECATED: Use nym_http_api_client::Client with from_network() or with_bincode() instead +#[deprecated( + since = "1.2.0", + note = "Use nym_http_api_client::Client::from_network() or ClientBuilder::with_bincode() instead" +)] #[derive(Clone)] pub struct NymApiClient { pub use_bincode: bool, - pub nym_api: nym_api::Client, + pub nym_api: nym_http_api_client::Client, // TODO: perhaps if we really need it at some (currently I don't see any reasons for it) // we could re-implement the communication with the REST API on port 1317 } -impl From for NymApiClient { - fn from(nym_api: nym_api::Client) -> Self { - NymApiClient { - use_bincode: false, - nym_api, - } - } -} - // we have to allow the use of deprecated method here as they're calling the deprecated trait methods #[allow(deprecated)] impl NymApiClient { - pub fn new(api_url: Url) -> Self { - let nym_api = nym_api::Client::new(api_url, None); - - NymApiClient { - use_bincode: true, - nym_api, - } - } - #[cfg(not(target_arch = "wasm32"))] pub fn new_with_timeout(api_url: Url, timeout: std::time::Duration) -> Self { - let nym_api = nym_api::Client::new(api_url, Some(timeout)); + let nym_api = nym_http_api_client::Client::new(api_url, Some(timeout)); NymApiClient { use_bincode: true, @@ -431,10 +418,10 @@ impl NymApiClient { } pub fn new_with_user_agent(api_url: Url, user_agent: impl Into) -> Self { - let nym_api = nym_api::Client::builder::<_, ValidatorClientError>(api_url) + let nym_api = nym_http_api_client::Client::builder(api_url) .expect("invalid api url") .with_user_agent(user_agent.into()) - .build::() + .build() .expect("failed to build nym api client"); NymApiClient { diff --git a/common/client-libs/validator-client/src/coconut/mod.rs b/common/client-libs/validator-client/src/coconut/mod.rs index 67974c0170..35748dd1de 100644 --- a/common/client-libs/validator-client/src/coconut/mod.rs +++ b/common/client-libs/validator-client/src/coconut/mod.rs @@ -3,7 +3,6 @@ use crate::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient}; use crate::nyxd::error::NyxdError; -use crate::NymApiClient; use nym_coconut_dkg_common::types::{EpochId, NodeIndex}; use nym_coconut_dkg_common::verification_key::ContractVKShare; use nym_compact_ecash::error::CompactEcashError; @@ -15,7 +14,7 @@ use url::Url; // TODO: it really doesn't feel like this should live in this crate. #[derive(Clone)] pub struct EcashApiClient { - pub api_client: NymApiClient, + pub api_client: nym_http_api_client::Client, pub verification_key: VerificationKeyAuth, pub node_id: NodeIndex, pub cosmos_address: cosmrs::AccountId, @@ -25,10 +24,15 @@ impl Display for EcashApiClient { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "[id: {}] {} @ {}", + "[id: {}] {} @ ({})", self.node_id, self.cosmos_address, - self.api_client.api_url() + self.api_client + .base_urls() + .iter() + .map(|url| url.to_string()) + .collect::>() + .join(", ") ) } } @@ -60,6 +64,9 @@ pub enum EcashApiError { source: CompactEcashError, }, + #[error("failed to create API client: {0}")] + ClientError(String), + #[error("the provided account address is malformed: {source}")] MalformedAccountAddress { #[from] @@ -89,8 +96,13 @@ impl TryFrom for EcashApiClient { // In non-client applications this resolver can cause warning logs about H2 connection // failure. This indicates that the long lived https connection was closed by the remote // peer and the resolver will have to reconnect. It should not impact actual functionality + let api_client = nym_http_api_client::Client::builder(url_address) + .map_err(|e| EcashApiError::ClientError(e.to_string()))? + .build() + .map_err(|e| EcashApiError::ClientError(e.to_string()))?; + Ok(EcashApiClient { - api_client: NymApiClient::new(url_address), + api_client, verification_key: VerificationKeyAuth::try_from_bs58(&share.share)?, node_id: share.node_index, cosmos_address: share.owner.as_str().parse()?, diff --git a/common/client-libs/validator-client/src/connection_tester.rs b/common/client-libs/validator-client/src/connection_tester.rs index 3e71a3c000..3cd9e36729 100644 --- a/common/client-libs/validator-client/src/connection_tester.rs +++ b/common/client-libs/validator-client/src/connection_tester.rs @@ -1,7 +1,8 @@ +use crate::nym_api::NymApiClientExt; use crate::nyxd::contract_traits::MixnetQueryClient; use crate::nyxd::error::NyxdError; use crate::nyxd::Config as ClientConfig; -use crate::{NymApiClient, QueryHttpRpcNyxdClient, ValidatorClientError}; +use crate::{QueryHttpRpcNyxdClient, ValidatorClientError}; use colored::Colorize; use core::fmt; use itertools::Itertools; @@ -87,8 +88,17 @@ fn setup_connection_tests( } }); - let api_connection_test_clients = api_urls.map(|(network, url)| { - ClientForConnectionTest::Api(network, url.clone(), NymApiClient::new(url)) + let api_connection_test_clients = api_urls.filter_map(|(network, url)| { + match nym_http_api_client::Client::builder(url.clone()).and_then(|b| b.build()) { + Ok(client) => Some(ClientForConnectionTest::Api(network, url, client)), + Err(err) => { + eprintln!( + "Failed to create API client for {}: {err}", + network.network_name + ); + None + } + } }); nyxd_connection_test_clients.chain(api_connection_test_clients) @@ -160,7 +170,7 @@ async fn test_nyxd_connection( async fn test_nym_api_connection( network: NymNetworkDetails, url: &Url, - client: &NymApiClient, + client: &nym_http_api_client::Client, ) -> ConnectionResult { let result = match timeout( Duration::from_secs(CONNECTION_TEST_TIMEOUT_SEC), @@ -186,7 +196,7 @@ async fn test_nym_api_connection( enum ClientForConnectionTest { Nyxd(NymNetworkDetails, Url, Box), - Api(NymNetworkDetails, Url, NymApiClient), + Api(NymNetworkDetails, Url, nym_http_api_client::Client), } impl ClientForConnectionTest { diff --git a/common/client-libs/validator-client/src/error.rs b/common/client-libs/validator-client/src/error.rs index 6acbc73c78..c3979c0784 100644 --- a/common/client-libs/validator-client/src/error.rs +++ b/common/client-libs/validator-client/src/error.rs @@ -9,8 +9,7 @@ use thiserror::Error; pub enum ValidatorClientError { #[error("nym api request failed: {source}")] NymAPIError { - #[from] - source: nym_api::error::NymAPIError, + source: Box, }, #[error("Tendermint RPC request failure: {0}")] @@ -28,3 +27,11 @@ pub enum ValidatorClientError { #[error("No validator API url has been provided")] NoAPIUrlAvailable, } + +impl From for ValidatorClientError { + fn from(source: nym_api::error::NymAPIError) -> Self { + ValidatorClientError::NymAPIError { + source: Box::new(source), + } + } +} diff --git a/common/client-libs/validator-client/src/lib.rs b/common/client-libs/validator-client/src/lib.rs index 725b86450b..db86658047 100644 --- a/common/client-libs/validator-client/src/lib.rs +++ b/common/client-libs/validator-client/src/lib.rs @@ -14,7 +14,6 @@ pub mod signing; pub use crate::error::ValidatorClientError; pub use crate::rpc::reqwest::ReqwestRpcClient; pub use crate::signing::direct_wallet::DirectSecp256k1HdWallet; -pub use client::NymApiClient; pub use client::{Client, Config, EcashApiClient}; pub use nym_api_requests::*; pub use nym_http_api_client::UserAgent; diff --git a/common/client-libs/validator-client/src/nym_api/error.rs b/common/client-libs/validator-client/src/nym_api/error.rs index 3f4e9c65a4..457e4ecfd7 100644 --- a/common/client-libs/validator-client/src/nym_api/error.rs +++ b/common/client-libs/validator-client/src/nym_api/error.rs @@ -1,7 +1,6 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_api_requests::models::RequestError; use nym_http_api_client::HttpClientError; -pub type NymAPIError = HttpClientError; +pub type NymAPIError = HttpClientError; diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index aa13634503..dcd77c1b18 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -3,6 +3,7 @@ use crate::nym_api::error::NymAPIError; use crate::nym_api::routes::{ecash, CORE_STATUS_COUNT, SINCE_ARG}; +use crate::nym_nodes::SkimmedNodesWithMetadata; use async_trait::async_trait; use nym_api_requests::ecash::models::{ AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, @@ -37,7 +38,7 @@ pub use nym_api_requests::{ MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }, - nym_nodes::{CachedNodesResponse, SemiSkimmedNode, SkimmedNode}, + nym_nodes::{CachedNodesResponse, SemiSkimmedNode, SemiSkimmedNodesWithMetadata, SkimmedNode}, NymNetworkDetailsResponse, }; use nym_contracts_common::IdentityKey; @@ -49,8 +50,8 @@ use time::format_description::BorrowedFormatItem; use time::Date; use tracing::instrument; +use crate::ValidatorClientError; pub use nym_coconut_dkg_common::types::EpochId; -pub use nym_http_api_client::Client; pub mod error; pub mod routes; @@ -62,6 +63,9 @@ pub fn rfc_3339_date() -> Vec> { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait NymApiClientExt: ApiClient { + /// Get the current API URL being used by the client + fn api_url(&self) -> &url::Url; + async fn health(&self) -> Result { self.get_json( &[ @@ -241,6 +245,156 @@ pub trait NymApiClientExt: ApiClient { .await } + async fn get_current_rewarded_set(&self) -> Result { + self.get_rewarded_set().await + } + + async fn get_all_basic_nodes_with_metadata( + &self, + ) -> Result { + // unroll first loop iteration in order to obtain the metadata + let mut page = 0; + let res = self + .get_basic_nodes_v2(false, Some(page), None, true) + .await?; + let mut nodes = res.nodes.data; + let metadata = res.metadata; + + if res.nodes.pagination.total == nodes.len() { + return Ok(SkimmedNodesWithMetadata::new(nodes, metadata)); + } + + page += 1; + + loop { + let mut res = self + .get_basic_nodes_v2(false, Some(page), None, true) + .await?; + + if !metadata.consistency_check(&res.metadata) { + // Create a custom error for inconsistent metadata + return Err(NymAPIError::InternalResponseInconsistency { + url: self.api_url().clone(), + details: "Inconsistent paged metadata".to_string(), + }); + } + + nodes.append(&mut res.nodes.data); + if nodes.len() >= res.nodes.pagination.total { + break; + } else { + page += 1 + } + } + + Ok(SkimmedNodesWithMetadata::new(nodes, metadata)) + } + + async fn get_all_basic_active_mixing_assigned_nodes_with_metadata( + &self, + ) -> Result { + // Get all mixing nodes that are in the active/rewarded set + let mut page = 0; + let res = self + .get_basic_active_mixing_assigned_nodes_v2(false, Some(page), None, false) + .await?; + + let metadata = res.metadata; + let mut nodes = res.nodes.data; + + if res.nodes.pagination.total == nodes.len() { + return Ok(SkimmedNodesWithMetadata::new(nodes, metadata)); + } + + page += 1; + + loop { + let res = self + .get_basic_active_mixing_assigned_nodes_v2(false, Some(page), None, false) + .await?; + + if !metadata.consistency_check(&res.metadata) { + return Err(NymAPIError::InternalResponseInconsistency { + url: self.api_url().clone(), + details: "Inconsistent paged metadata".to_string(), + }); + } + + nodes.append(&mut res.nodes.data.clone()); + + // Check if we've got all nodes + if nodes.len() >= res.nodes.pagination.total { + break; + } else { + page += 1; + } + } + + Ok(SkimmedNodesWithMetadata::new(nodes, metadata)) + } + + async fn get_all_basic_entry_assigned_nodes_with_metadata( + &self, + ) -> Result { + // Get all nodes that can act as entry gateways + let mut page = 0; + let res = self + .get_basic_entry_assigned_nodes_v2(false, Some(page), None, false) + .await?; + + let metadata = res.metadata; + let mut nodes = res.nodes.data; + + if res.nodes.pagination.total == nodes.len() { + return Ok(SkimmedNodesWithMetadata::new(nodes, metadata)); + } + + page += 1; + + loop { + let res = self + .get_basic_entry_assigned_nodes_v2(false, Some(page), None, false) + .await?; + + if !metadata.consistency_check(&res.metadata) { + return Err(NymAPIError::InternalResponseInconsistency { + url: self.api_url().clone(), + details: "Inconsistent paged metadata".to_string(), + }); + } + + nodes.append(&mut res.nodes.data.clone()); + + // Check if we've got all nodes + if nodes.len() >= res.nodes.pagination.total { + break; + } else { + page += 1; + } + } + + Ok(SkimmedNodesWithMetadata::new(nodes, metadata)) + } + + async fn get_all_described_nodes(&self) -> Result, NymAPIError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut descriptions = Vec::new(); + + loop { + let mut res = self.get_nodes_described(Some(page), None).await?; + + descriptions.append(&mut res.data); + if descriptions.len() < res.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(descriptions) + } + #[tracing::instrument(level = "debug", skip_all)] async fn get_nym_nodes( &self, @@ -268,6 +422,25 @@ pub trait NymApiClientExt: ApiClient { .await } + async fn get_all_bonded_nym_nodes(&self) -> Result, ValidatorClientError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut bonds = Vec::new(); + + loop { + let mut res = self.get_nym_nodes(Some(page), None).await?; + + bonds.append(&mut res.data); + if bonds.len() < res.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(bonds) + } + #[deprecated] #[tracing::instrument(level = "debug", skip_all)] async fn get_basic_mixnodes(&self) -> Result, NymAPIError> { @@ -1371,8 +1544,49 @@ pub trait NymApiClientExt: ApiClient { ) .await } + + /// Method to change the base API URLs being used by the client + fn change_base_urls(&mut self, urls: Vec); + + /// Retrieve expanded information for all bonded nodes on the network + async fn get_all_expanded_nodes(&self) -> Result { + // Unroll the first iteration to get the metadata + let mut page = 0; + + let res = self.get_expanded_nodes(false, Some(page), None).await?; + let mut nodes = res.nodes.data; + let metadata = res.metadata; + + if res.nodes.pagination.total == nodes.len() { + return Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata)); + } + + page += 1; + + loop { + let mut res = self.get_expanded_nodes(false, Some(page), None).await?; + + nodes.append(&mut res.nodes.data); + if nodes.len() < res.nodes.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata)) + } } +// Client is already nym_http_api_client::Client (re-exported above), so just one impl needed #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl NymApiClientExt for Client {} +impl NymApiClientExt for nym_http_api_client::Client { + fn api_url(&self) -> &url::Url { + self.current_url().as_ref() + } + + fn change_base_urls(&mut self, urls: Vec) { + self.change_base_urls(urls.into_iter().map(|u| u.into()).collect()); + } +} diff --git a/common/commands/Cargo.toml b/common/commands/Cargo.toml index d382cb696f..4398342d4c 100644 --- a/common/commands/Cargo.toml +++ b/common/commands/Cargo.toml @@ -38,6 +38,7 @@ cosmrs = { workspace = true } cosmwasm-std = { workspace = true } nym-validator-client = { path = "../client-libs/validator-client" } +nym-http-api-client = { path = "../http-api-client" } nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] } nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] } nym-network-defaults = { path = "../network-defaults" } diff --git a/common/commands/src/context/mod.rs b/common/commands/src/context/mod.rs index db20ed0ac3..daca52df77 100644 --- a/common/commands/src/context/mod.rs +++ b/common/commands/src/context/mod.rs @@ -2,12 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use crate::context::errors::ContextError; +pub use nym_http_api_client::Client as NymApiClient; use nym_network_defaults::{ setup_env, var_names::{MIXNET_CONTRACT_ADDRESS, NYM_API, NYXD, VESTING_CONTRACT_ADDRESS}, NymNetworkDetails, }; -pub use nym_validator_client::nym_api::Client as NymApiClient; use nym_validator_client::nyxd::{self, AccountId, NyxdClient}; use nym_validator_client::{ DirectSigningHttpRpcNyxdClient, DirectSigningHttpRpcValidatorClient, QueryHttpRpcNyxdClient, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/bindings/ts-packages/types/src/types/rust/Role.ts b/common/cosmwasm-smart-contracts/mixnet-contract/bindings/ts-packages/types/src/types/rust/Role.ts index 3bac0196bd..ab0a836612 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/bindings/ts-packages/types/src/types/rust/Role.ts +++ b/common/cosmwasm-smart-contracts/mixnet-contract/bindings/ts-packages/types/src/types/rust/Role.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Role = "EntryGateway" | "Layer1" | "Layer2" | "Layer3" | "ExitGateway" | "Standby"; +export type Role = "entry_gateway" | "layer1" | "layer2" | "layer3" | "exit_gateway" | "standby"; diff --git a/common/credential-proxy/src/error.rs b/common/credential-proxy/src/error.rs index 6dff079b09..d50264cdc7 100644 --- a/common/credential-proxy/src/error.rs +++ b/common/credential-proxy/src/error.rs @@ -3,7 +3,7 @@ use nym_ecash_signer_check::SignerCheckError; use nym_validator_client::coconut::EcashApiError; -use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nym_api::{error::NymAPIError, EpochId}; use nym_validator_client::nyxd::error::NyxdError; use std::io; use std::net::SocketAddr; @@ -71,6 +71,9 @@ pub enum CredentialProxyError { source: EcashApiError, }, + #[error("Nym API request failed: {source}")] + NymApiFailure { source: Box }, + #[error("Compact ecash internal error: {0}")] CompactEcashInternalError(#[from] nym_compact_ecash::error::CompactEcashError), @@ -154,6 +157,14 @@ pub enum CredentialProxyError { }, } +impl From for CredentialProxyError { + fn from(source: NymAPIError) -> Self { + CredentialProxyError::NymApiFailure { + source: Box::new(source), + } + } +} + impl CredentialProxyError { pub fn database_inconsistency>(reason: S) -> CredentialProxyError { CredentialProxyError::DatabaseInconsistency { diff --git a/common/credential-proxy/src/shared_state/mod.rs b/common/credential-proxy/src/shared_state/mod.rs index 310b0c6cc5..750b199d52 100644 --- a/common/credential-proxy/src/shared_state/mod.rs +++ b/common/credential-proxy/src/shared_state/mod.rs @@ -23,6 +23,7 @@ use nym_credentials::{ AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, }; use nym_ecash_contract_common::deposit::DepositId; +use nym_validator_client::client::NymApiClientExt; use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::contract_traits::dkg_query_client::Epoch; diff --git a/common/credential-proxy/src/ticketbook_manager/wallet_shares.rs b/common/credential-proxy/src/ticketbook_manager/wallet_shares.rs index 72542090aa..d6508ce2fe 100644 --- a/common/credential-proxy/src/ticketbook_manager/wallet_shares.rs +++ b/common/credential-proxy/src/ticketbook_manager/wallet_shares.rs @@ -11,6 +11,7 @@ use nym_credential_proxy_requests::api::v1::ticketbook::models::{ TicketbookWalletSharesResponse, WalletShare, WebhookTicketbookWalletShares, WebhookTicketbookWalletSharesRequest, }; +use nym_validator_client::client::NymApiClientExt; use nym_validator_client::ecash::BlindSignRequestBody; use std::collections::HashMap; use std::sync::Arc; diff --git a/common/credential-verification/src/ecash/credential_sender.rs b/common/credential-verification/src/ecash/credential_sender.rs index 9169efc61b..98706f37ac 100644 --- a/common/credential-verification/src/ecash/credential_sender.rs +++ b/common/credential-verification/src/ecash/credential_sender.rs @@ -14,7 +14,7 @@ use nym_api_requests::ecash::models::{BatchRedeemTicketsBody, VerifyEcashTicketB use nym_credentials_interface::Bandwidth; use nym_credentials_interface::{ClientTicket, TicketType}; use nym_validator_client::coconut::EcashApiError; -use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nym_api::{EpochId, NymApiClientExt}; use nym_validator_client::nyxd::contract_traits::{ EcashSigningClient, MultisigQueryClient, MultisigSigningClient, PagedMultisigQueryClient, }; @@ -354,7 +354,7 @@ impl CredentialHandler { Err(err) => { error!("failed to send ticket {ticket_id} for verification to ecash signer '{client}': {err}. if we don't reach quorum, we'll retry later"); Err(EcashTicketError::ApiFailure(EcashApiError::NymApi { - source: err, + source: nym_validator_client::ValidatorClientError::from(err), })) } } diff --git a/common/credentials/Cargo.toml b/common/credentials/Cargo.toml index 4240718a37..e3cb06ec40 100644 --- a/common/credentials/Cargo.toml +++ b/common/credentials/Cargo.toml @@ -22,6 +22,7 @@ nym-ecash-time = { path = "../ecash-time", features = ["expiration"] } nym-credentials-interface = { path = "../credentials-interface" } nym-crypto = { path = "../crypto" } nym-api-requests = { path = "../../nym-api/nym-api-requests" } +nym-http-api-client = { path = "../http-api-client" } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } nym-network-defaults = { path = "../network-defaults" } diff --git a/common/credentials/src/ecash/bandwidth/issuance.rs b/common/credentials/src/ecash/bandwidth/issuance.rs index f6de39c259..366eff02f3 100644 --- a/common/credentials/src/ecash/bandwidth/issuance.rs +++ b/common/credentials/src/ecash/bandwidth/issuance.rs @@ -15,7 +15,7 @@ use nym_credentials_interface::{ use nym_crypto::asymmetric::ed25519; use nym_ecash_contract_common::deposit::DepositId; use nym_ecash_time::{ecash_default_expiration_date, ecash_today, EcashTime}; -use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nym_api::{EpochId, NymApiClientExt}; use serde::{Deserialize, Serialize}; use time::Date; @@ -116,7 +116,7 @@ impl IssuanceTicketBook { pub async fn obtain_blinded_credential( &self, - client: &nym_validator_client::client::NymApiClient, + client: &nym_http_api_client::Client, request_body: &BlindSignRequestBody, ) -> Result { let server_response = client.blind_sign(request_body).await?; @@ -179,7 +179,7 @@ impl IssuanceTicketBook { // ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers pub async fn obtain_partial_ticketbook_credential( &self, - client: &nym_validator_client::client::NymApiClient, + client: &nym_http_api_client::Client, signer_index: u64, validator_vk: &VerificationKeyAuth, signing_data: CredentialSigningData, diff --git a/common/credentials/src/ecash/utils.rs b/common/credentials/src/ecash/utils.rs index 305aafd532..497987e3da 100644 --- a/common/credentials/src/ecash/utils.rs +++ b/common/credentials/src/ecash/utils.rs @@ -10,6 +10,7 @@ use nym_credentials_interface::{ VerificationKeyAuth, WalletSignatures, }; use nym_validator_client::client::EcashApiClient; +use nym_validator_client::nym_api::NymApiClientExt; // so we wouldn't break all the existing imports pub use nym_ecash_time::{cred_exp_date, ecash_date_offset, ecash_today, EcashTime}; diff --git a/common/credentials/src/error.rs b/common/credentials/src/error.rs index fa751451dc..daabbcdb1b 100644 --- a/common/credentials/src/error.rs +++ b/common/credentials/src/error.rs @@ -4,7 +4,7 @@ use crate::ecash::bandwidth::issued::CURRENT_SERIALIZATION_REVISION; use nym_credentials_interface::CompactEcashError; use nym_crypto::asymmetric::x25519::KeyRecoveryError; -use nym_validator_client::ValidatorClientError; +use nym_validator_client::{nym_api::error::NymAPIError, ValidatorClientError}; use thiserror::Error; #[derive(Debug, Error)] @@ -37,6 +37,9 @@ pub enum Error { #[error("Ran into a validator client error - {0}")] ValidatorClientError(#[from] ValidatorClientError), + #[error("Nym API request failed - {0}")] + NymAPIError(Box), + #[error("Bandwidth operation overflowed. {0}")] BandwidthOverflow(String), @@ -61,3 +64,9 @@ pub enum Error { #[error("failed to create a secp256k1 signature")] Secp256k1SignFailure, } + +impl From for Error { + fn from(e: NymAPIError) -> Self { + Error::NymAPIError(Box::new(e)) + } +} diff --git a/common/ecash-signer-check/Cargo.toml b/common/ecash-signer-check/Cargo.toml index be22872ad8..cc8fb6ec29 100644 --- a/common/ecash-signer-check/Cargo.toml +++ b/common/ecash-signer-check/Cargo.toml @@ -22,6 +22,7 @@ url = { workspace = true } nym-validator-client = { path = "../client-libs/validator-client" } nym-network-defaults = { path = "../network-defaults" } nym-ecash-signer-check-types = { path = "../ecash-signer-check-types" } +nym-http-api-client = { path = "../http-api-client" } [lints] workspace = true diff --git a/common/ecash-signer-check/src/client_check.rs b/common/ecash-signer-check/src/client_check.rs index b5c367794d..aff2db6725 100644 --- a/common/ecash-signer-check/src/client_check.rs +++ b/common/ecash-signer-check/src/client_check.rs @@ -1,15 +1,14 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{LocalChainStatus, SigningStatus, TypedSignerResult}; +use crate::{LocalChainStatus, SignerCheckError, SigningStatus, TypedSignerResult}; use nym_ecash_signer_check_types::dealer_information::RawDealerInformation; use nym_ecash_signer_check_types::status::{SignerStatus, SignerTestResult}; -use nym_validator_client::client::NymApiClientExt; use nym_validator_client::models::BinaryBuildInformationOwned; +use nym_validator_client::nym_api::NymApiClientExt; use nym_validator_client::nyxd::contract_traits::dkg_query_client::{ ContractVKShare, DealerDetails, }; -use nym_validator_client::NymApiClient; use std::time::Duration; use tracing::{error, warn}; use url::Url; @@ -32,37 +31,38 @@ pub(crate) mod signing_status { } struct ClientUnderTest { - api_client: NymApiClient, + api_client: nym_http_api_client::Client, build_info: Option, } impl ClientUnderTest { - pub(crate) fn new(api_url: &Url) -> Self { - ClientUnderTest { - api_client: NymApiClient::new(api_url.clone()), + pub(crate) fn new(api_url: &Url) -> Result { + // The builder should not fail with a valid URL that's already parsed + // If it does fail, it's an internal error that we can't recover from + let api_client = nym_http_api_client::Client::builder(api_url.clone())?.build()?; + + Ok(ClientUnderTest { + api_client, build_info: None, - } + }) } pub(crate) async fn try_retrieve_build_information(&mut self) -> bool { - match tokio::time::timeout( - Duration::from_secs(5), - self.api_client.nym_api.build_information(), - ) - .await + match tokio::time::timeout(Duration::from_secs(5), self.api_client.build_information()) + .await { Ok(Ok(build_information)) => { self.build_info = Some(build_information); true } Ok(Err(err)) => { - warn!("{}: failed to retrieve build information: {err}. the signer is most likely down", self.api_client.api_url()); + warn!("{}: failed to retrieve build information: {err}. the signer is most likely down", self.api_client.current_url()); false } Err(_timeout) => { warn!( "{}: timed out while attempting to retrieve build information", - self.api_client.api_url() + self.api_client.current_url() ); false } @@ -77,7 +77,7 @@ impl ClientUnderTest { .inspect_err(|err| { error!( "ecash signer '{}' reports invalid version {}: {err}", - self.api_client.api_url(), + self.api_client.current_url(), build_info.build_version ) }) @@ -121,14 +121,14 @@ impl ClientUnderTest { // check if it supports the current query if self.supports_chain_status_query() { - return match self.api_client.nym_api.get_chain_blocks_status().await { + return match self.api_client.get_chain_blocks_status().await { Ok(status) => LocalChainStatus::Reachable { response: Box::new(status), }, Err(err) => { warn!( "{}: failed to retrieve local chain status: {err}", - self.api_client.api_url() + self.api_client.current_url() ); LocalChainStatus::Unreachable } @@ -136,14 +136,14 @@ impl ClientUnderTest { } // fallback to the legacy query - match self.api_client.nym_api.get_chain_status().await { + match self.api_client.get_chain_status().await { Ok(status) => LocalChainStatus::ReachableLegacy { response: Box::new(status), }, Err(err) => { warn!( "{}: failed to retrieve [legacy] local chain status: {err}", - self.api_client.api_url() + self.api_client.current_url() ); LocalChainStatus::Unreachable } @@ -158,14 +158,14 @@ impl ClientUnderTest { // check if it supports the current query if self.supports_signing_status_query() { - return match self.api_client.nym_api.get_signer_status().await { + return match self.api_client.get_signer_status().await { Ok(response) => SigningStatus::Reachable { response: Box::new(response), }, Err(err) => { warn!( "{}: failed to retrieve signer chain status: {err}", - self.api_client.api_url() + self.api_client.current_url() ); SigningStatus::Unreachable } @@ -173,14 +173,14 @@ impl ClientUnderTest { } // fallback to the legacy query - match self.api_client.nym_api.get_signer_information().await { + match self.api_client.get_signer_information().await { Ok(status) => SigningStatus::ReachableLegacy { response: Box::new(status), }, Err(err) => { warn!( "{}: failed to retrieve [legacy] signer chain status: {err}", - self.api_client.api_url() + self.api_client.current_url() ); // NOTE: this might equally mean the signing is disabled SigningStatus::Unreachable @@ -201,7 +201,13 @@ pub(crate) async fn check_client( return SignerStatus::ProvidedInvalidDetails.with_details(dealer_information, dkg_epoch); }; - let mut client = ClientUnderTest::new(&parsed_information.announce_address); + let mut client = match ClientUnderTest::new(&parsed_information.announce_address) { + Ok(client) => client, + Err(err) => { + error!("failed to create client instance: {err}"); + return SignerStatus::Unreachable.with_details(dealer_information, dkg_epoch); + } + }; // 8. check basic connection status - can you retrieve build information? if !client.try_retrieve_build_information().await { diff --git a/common/ecash-signer-check/src/error.rs b/common/ecash-signer-check/src/error.rs index bb7c3c719b..705ce9f3bf 100644 --- a/common/ecash-signer-check/src/error.rs +++ b/common/ecash-signer-check/src/error.rs @@ -1,6 +1,7 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use nym_http_api_client::HttpClientError; use nym_validator_client::nyxd::error::NyxdError; use thiserror::Error; @@ -11,6 +12,17 @@ pub enum SignerCheckError { #[error("failed to query the DKG contract: {source}")] DKGContractQueryFailure { source: NyxdError }, + + #[error("failed to build client: {source}")] + HttpClient { source: Box }, +} + +impl From for SignerCheckError { + fn from(e: HttpClientError) -> Self { + SignerCheckError::HttpClient { + source: Box::new(e), + } + } } impl SignerCheckError { diff --git a/common/http-api-client-macro/Cargo.toml b/common/http-api-client-macro/Cargo.toml new file mode 100644 index 0000000000..ea23bbd23a --- /dev/null +++ b/common/http-api-client-macro/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "nym-http-api-client-macro" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1" +syn = { workspace = true, features = ["full"] } +quote = "1.0.40" +proc-macro-crate = "3" +uuid = { version = "1.0", features = ["v4"] } + +[dev-dependencies] +nym-http-api-client = { path = "../http-api-client" } +reqwest = { workspace = true } + +[features] +debug-inventory = [] + +[lints] +workspace = true diff --git a/common/http-api-client-macro/build.rs b/common/http-api-client-macro/build.rs new file mode 100644 index 0000000000..69b80f56e8 --- /dev/null +++ b/common/http-api-client-macro/build.rs @@ -0,0 +1,13 @@ +use std::env; + +fn main() { + // Enable debug output during build + if env::var("CARGO_FEATURE_DEBUG_INVENTORY").is_ok() || env::var("DEBUG_HTTP_INVENTORY").is_ok() + { + println!("cargo:warning=HTTP Client Inventory Debug Enabled"); + println!("cargo:rustc-cfg=debug_inventory"); + } + + // Force rebuild when this environment variable changes + println!("cargo:rerun-if-env-changed=DEBUG_HTTP_INVENTORY"); +} diff --git a/common/http-api-client-macro/src/lib.rs b/common/http-api-client-macro/src/lib.rs new file mode 100644 index 0000000000..49a9257cea --- /dev/null +++ b/common/http-api-client-macro/src/lib.rs @@ -0,0 +1,384 @@ +//! Proc-macros for configuring HTTP clients globally via the `inventory` crate. +//! +//! This crate provides macros that allow any crate in the workspace to contribute +//! configuration modifications to `reqwest::ClientBuilder` instances through a +//! compile-time registry pattern. +//! +//! # Overview +//! +//! The macros work by: +//! 1. Collecting configuration functions from across all crates at compile time +//! 2. Sorting them by priority (lower numbers run first) +//! 3. Applying them sequentially to build HTTP clients with consistent settings +//! +//! # Examples +//! +//! ## Basic Usage with `client_defaults!` +//! +//! ```ignore +//! use nym_http_api_client_macro::client_defaults; +//! +//! // Register default configurations with priority +//! client_defaults!( +//! priority = 10; // Optional, defaults to 0 +//! timeout = std::time::Duration::from_secs(30), +//! gzip = true, +//! user_agent = "MyApp/1.0" +//! ); +//! ``` +//! +//! ## Using `client_cfg!` for one-off configurations +//! +//! ```ignore +//! use nym_http_api_client_macro::client_cfg; +//! +//! let configure = client_cfg!( +//! timeout = std::time::Duration::from_secs(60), +//! default_headers { +//! "X-Custom-Header" => "value", +//! "Authorization" => auth_token +//! } +//! ); +//! +//! let builder = reqwest::ClientBuilder::new(); +//! let configured = configure(builder); +//! ``` +//! +//! # DSL Reference +//! +//! The macro DSL supports several patterns: +//! - `key = value` - Calls `builder.key(value)` +//! - `key(arg1, arg2)` - Calls `builder.key(arg1, arg2)` +//! - `flag` - Calls `builder.flag()` with no arguments +//! - `default_headers { "name" => "value", ... }` - Sets default headers +//! +//! # Priority System +//! +//! Configurations are applied in priority order (lower numbers first): +//! - Negative priorities: Early configuration (e.g., -100 for base settings) +//! - Zero (default): Standard configuration +//! - Positive priorities: Late configuration (e.g., 100 for overrides) + +use proc_macro::TokenStream; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use proc_macro_crate::{crate_name, FoundCrate}; +use quote::{format_ident, quote}; +use syn::{ + braced, + parse::{Parse, ParseStream}, + parse_macro_input, + punctuated::Punctuated, + token, Expr, Ident, LitInt, Result, Token, +}; + +// ------------------ core crate path resolution ------------------ + +fn core_path() -> TokenStream2 { + match crate_name("nym-http-api-client") { + Ok(FoundCrate::Itself) => quote!(crate), + Ok(FoundCrate::Name(name)) => { + let ident = Ident::new(&name, Span::call_site()); + quote!( ::#ident ) + } + Err(_) => { + // Fallback if the crate is not found by name (unlikely if deps set up correctly) + quote!(::nym_http_api_client) + } + } +} + +// ------------------ DSL parsing ------------------ + +struct Items(Punctuated); +impl Parse for Items { + fn parse(input: ParseStream<'_>) -> Result { + Ok(Self(Punctuated::parse_terminated(input)?)) + } +} + +enum Item { + Assign { + key: Ident, + _eq: Token![=], + value: Expr, + }, // foo = EXPR + Call { + key: Ident, + args: Punctuated, + _p: token::Paren, + }, // foo(a,b) + DefaultHeaders { + _key: Ident, + map: HeaderMapInit, + }, // default_headers { ... } + Flag { + key: Ident, + }, // foo +} + +impl Parse for Item { + fn parse(input: ParseStream<'_>) -> Result { + let key: Ident = input.parse()?; + + if input.peek(Token![=]) { + let _eq: Token![=] = input.parse()?; + let value: Expr = input.parse()?; + return Ok(Self::Assign { key, _eq, value }); + } + if input.peek(token::Paren) { + let content; + let _p = syn::parenthesized!(content in input); + let args = Punctuated::::parse_terminated(&content)?; + return Ok(Self::Call { key, args, _p }); + } + if input.peek(token::Brace) && key == format_ident!("default_headers") { + let map = input.parse::()?; + return Ok(Self::DefaultHeaders { _key: key, map }); + } + Ok(Self::Flag { key }) + } +} + +struct HeaderPair { + k: Expr, + _arrow: Token![=>], + v: Expr, +} +impl Parse for HeaderPair { + fn parse(input: ParseStream<'_>) -> Result { + Ok(Self { + k: input.parse()?, + _arrow: input.parse()?, + v: input.parse()?, + }) + } +} + +struct HeaderMapInit { + _brace: token::Brace, + pairs: Punctuated, +} +impl Parse for HeaderMapInit { + fn parse(input: ParseStream<'_>) -> Result { + let content; + let _brace = braced!(content in input); + let pairs = Punctuated::::parse_terminated(&content)?; + Ok(Self { _brace, pairs }) + } +} + +// Generate statements that mutate a builder named `b` using the resolved core path. +fn to_stmts(items: Items, core: &TokenStream2) -> TokenStream2 { + let mut stmts = Vec::new(); + for it in items.0 { + match it { + Item::Assign { key, value, .. } => { + let m = key; + stmts.push(quote! { b = b.#m(#value); }); + } + Item::Call { key, args, .. } => { + let m = key; + let args = args.iter(); + stmts.push(quote! { b = b.#m( #( #args ),* ); }); + } + Item::DefaultHeaders { map, .. } => { + let (ks, vs): (Vec<_>, Vec<_>) = map.pairs.into_iter().map(|p| (p.k, p.v)).unzip(); + stmts.push(quote! { + let mut __cm = #core::reqwest::header::HeaderMap::new(); + #( + { + use #core::reqwest::header::{HeaderName, HeaderValue}; + let __k = HeaderName::try_from(#ks) + .unwrap_or_else(|e| panic!("Invalid header name: {}", e)); + let __v = HeaderValue::try_from(#vs) + .unwrap_or_else(|e| panic!("Invalid header value: {}", e)); + __cm.insert(__k, __v); + } + )* + b = b.default_headers(__cm); + }); + } + Item::Flag { key } => { + let m = key; + stmts.push(quote! { b = b.#m(); }); + } + } + } + quote! { #(#stmts)* } +} + +// ------------------ client_cfg! ------------------ + +/// Creates a closure that configures a `ReqwestClientBuilder`. +/// +/// This macro generates a closure that can be used to configure a single +/// `reqwest::ClientBuilder` instance without affecting global defaults. +/// +/// # Example +/// +/// ```ignore +/// let config = client_cfg!( +/// timeout = Duration::from_secs(30), +/// gzip = true +/// ); +/// let client = config(reqwest::ClientBuilder::new()).build()?; +/// ``` +#[proc_macro] +pub fn client_cfg(input: TokenStream) -> TokenStream { + let items = parse_macro_input!(input as Items); + let core = core_path(); + let body = to_stmts(items, &core); + let out = quote! { + |mut b: #core::ReqwestClientBuilder| { #body b } + }; + out.into() +} + +// ------------------ client_defaults! with optional priority header ------------------ + +struct MaybePrioritized { + priority: i32, + items: Items, +} +impl Parse for MaybePrioritized { + fn parse(input: ParseStream<'_>) -> Result { + // Optional header: `priority = ;` + let fork = input.fork(); + let mut priority = 0i32; + if fork.peek(Ident) && fork.parse::()? == "priority" && fork.peek(Token![=]) { + // commit + let _ = input.parse::()?; // priority + let _ = input.parse::()?; + let lit: LitInt = input.parse()?; + priority = lit.base10_parse()?; + let _ = input.parse::()?; + } + let items = input.parse::()?; + Ok(Self { priority, items }) + } +} + +/// Registers global default configurations for HTTP clients. +/// +/// This macro submits a configuration record to the global registry that will +/// be applied to all HTTP clients created with `default_builder()`. +/// +/// # Parameters +/// +/// - `priority` (optional): Integer priority for ordering (lower runs first, default: 0) +/// - Configuration items: Any valid `reqwest::ClientBuilder` method calls +/// +/// # Example +/// +/// ```ignore +/// client_defaults!( +/// priority = -50; // Run early in the configuration chain +/// connect_timeout = Duration::from_secs(10), +/// pool_max_idle_per_host = 32, +/// default_headers { +/// "User-Agent" => "MyApp/1.0", +/// "Accept" => "application/json" +/// } +/// ); +/// ``` +#[proc_macro] +pub fn client_defaults(input: TokenStream) -> TokenStream { + let MaybePrioritized { priority, items } = parse_macro_input!(input as MaybePrioritized); + let core = core_path(); + + // Generate a description of what this config does (before consuming items) + let config_description = if cfg!(feature = "debug-inventory") { + let descriptions = items + .0 + .iter() + .map(|item| match item { + Item::Assign { key, value, .. } => { + format!("{}={:?}", quote!(#key), quote!(#value).to_string()) + } + Item::Call { key, args, .. } => { + let args_str = args + .iter() + .map(|a| quote!(#a).to_string()) + .collect::>() + .join(", "); + format!("{}({})", quote!(#key), args_str) + } + Item::Flag { key } => { + format!("{}()", quote!(#key)) + } + Item::DefaultHeaders { .. } => "default_headers{{...}}".to_string(), + }) + .collect::>() + .join(", "); + + quote! { + pub const __CONFIG_DESC: &str = #descriptions; + } + } else { + quote! {} + }; + + // Now consume items to generate the body + let body = to_stmts(items, &core); + + // Generate a unique identifier for this submission + let submission_id = format!("__client_defaults_{}", uuid::Uuid::new_v4().simple()); + let submission_ident = syn::Ident::new(&submission_id, proc_macro2::Span::call_site()); + + // Debug output at compile time if enabled + if std::env::var("DEBUG_HTTP_INVENTORY").is_ok() { + eprintln!( + "cargo:warning=[HTTP-INVENTORY] Registering config with priority={} from {}", + priority, + std::env::var("CARGO_PKG_NAME").unwrap_or_else(|_| "unknown".to_string()) + ); + } + + // Add debug_print_inventory call if the feature is enabled + let debug_call = if cfg!(feature = "debug-inventory") { + quote! { + #config_description + + // Ensure the debug function gets called when config is applied + pub fn __cfg_with_debug( + b: #core::ReqwestClientBuilder + ) -> #core::ReqwestClientBuilder { + eprintln!("[HTTP-INVENTORY] Applying: {} (priority={})", __CONFIG_DESC, #priority); + __cfg(b) + } + } + } else { + quote! {} + }; + + // Use the debug wrapper if feature is enabled + let apply_fn = if cfg!(feature = "debug-inventory") { + quote! { __cfg_with_debug } + } else { + quote! { __cfg } + }; + + let out = quote! { + #[allow(non_snake_case)] + mod #submission_ident { + use super::*; + #[allow(unused)] + pub fn __cfg( + mut b: #core::ReqwestClientBuilder + ) -> #core::ReqwestClientBuilder { + #body + b + } + + #debug_call + + #core::inventory::submit! { + #core::registry::ConfigRecord { + priority: #priority, + apply: #apply_fn, + } + } + } + }; + out.into() +} diff --git a/common/http-api-client-macro/tests/macro_tests.rs b/common/http-api-client-macro/tests/macro_tests.rs new file mode 100644 index 0000000000..2696abb0f8 --- /dev/null +++ b/common/http-api-client-macro/tests/macro_tests.rs @@ -0,0 +1,64 @@ +use nym_http_api_client_macro::{client_cfg, client_defaults}; +use std::time::Duration; + +#[test] +fn test_client_cfg_basic() { + // Test that the macro compiles with basic configuration + let _config = client_cfg!(timeout = Duration::from_secs(30), gzip = true); +} + +#[test] +fn test_client_cfg_with_headers() { + // Test that the macro compiles with default headers + let _config = client_cfg!( + timeout = Duration::from_secs(30), + default_headers { + "User-Agent" => "TestApp/1.0", + "Accept" => "application/json" + } + ); +} + +#[test] +fn test_client_cfg_with_method_calls() { + // Test that the macro compiles with method calls + let _config = client_cfg!( + pool_max_idle_per_host = 32, + tcp_nodelay = true, + danger_accept_invalid_certs = true + ); +} + +#[test] +fn test_client_defaults_with_priority() { + // Test that client_defaults macro compiles with priority + client_defaults!( + priority = -100; + gzip = true, + deflate = true + ); +} + +#[test] +fn test_client_defaults_without_priority() { + // Test that client_defaults macro compiles without priority (defaults to 0) + client_defaults!(brotli = true, zstd = true); +} + +#[test] +fn test_empty_client_cfg() { + // Test that empty configuration compiles + let _config = client_cfg!(); +} + +// Integration test to verify the closure actually works +#[test] +fn test_client_cfg_closure_application() { + let config = client_cfg!(gzip = true); + + // Apply the configuration to a new builder + let builder = reqwest::ClientBuilder::new(); + let _configured_builder = config(builder); + // Note: We can't easily test the internal state of the builder, + // but we verify it compiles and runs without panic +} diff --git a/common/http-api-client/Cargo.toml b/common/http-api-client/Cargo.toml index 853b79b9a5..35da8e0e11 100644 --- a/common/http-api-client/Cargo.toml +++ b/common/http-api-client/Cargo.toml @@ -13,19 +13,25 @@ license.workspace = true [features] default=["tunneling"] tunneling=[] +network-defaults = ["dep:nym-network-defaults"] +debug-inventory = ["nym-http-api-client-macro/debug-inventory"] [dependencies] async-trait = { workspace = true } bincode = { workspace = true } +cfg-if = { workspace = true} reqwest = { workspace = true, features = ["json", "gzip", "deflate", "brotli", "zstd", "rustls-tls"] } http.workspace = true url = { workspace = true } once_cell = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +serde_yaml = { workspace = true} +serde_plain = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } itertools = { workspace = true } +inventory = { workspace = true } # used for decoding text responses (they were already implicitly included) bytes = { workspace = true } @@ -34,6 +40,8 @@ mime = { workspace = true } nym-http-api-common = { path = "../http-api-common", default-features = false } nym-bin-common = { path = "../bin-common" } +nym-network-defaults = { path = "../network-defaults", optional = true } +nym-http-api-client-macro = { path = "../http-api-client-macro" } [target."cfg(not(target_arch = \"wasm32\"))".dependencies] hickory-resolver = { workspace = true, features = ["https-ring", "tls-ring", "webpki-roots"] } diff --git a/common/http-api-client/examples/debug_inventory.rs b/common/http-api-client/examples/debug_inventory.rs new file mode 100644 index 0000000000..e19a9c05dd --- /dev/null +++ b/common/http-api-client/examples/debug_inventory.rs @@ -0,0 +1,26 @@ +use nym_http_api_client::registry; + +fn main() { + println!("Debugging HTTP Client Inventory"); + println!("================================"); + + // Print all registered configurations + registry::debug_print_inventory(); + + // Also print the count + println!( + "\nTotal registered configs: {}", + registry::registered_config_count() + ); + + // Show the detailed breakdown + println!("\nDetailed configuration list:"); + for (i, (priority, ptr)) in registry::inspect_registered_configs().iter().enumerate() { + println!( + " Config #{}: priority={}, function=0x{:x}", + i + 1, + priority, + ptr + ); + } +} diff --git a/common/http-api-client/examples/test_timeout.rs b/common/http-api-client/examples/test_timeout.rs new file mode 100644 index 0000000000..fe3658e34b --- /dev/null +++ b/common/http-api-client/examples/test_timeout.rs @@ -0,0 +1,99 @@ +use nym_http_api_client::registry; +use nym_http_api_client::{inventory, ReqwestClientBuilder}; +use nym_http_api_client_macro::client_defaults; +use std::time::{Duration, Instant}; + +#[tokio::main] +async fn main() { + println!("Testing HTTP Client Timeout Configuration"); + println!("==========================================\n"); + + client_defaults!(timeout = std::time::Duration::from_secs(300),); + + // Build a client using the registry (should have 300s timeout) + let client = registry::build_client().expect("Failed to build client"); + + println!("Testing timeout behavior..."); + println!("The inventory should have set timeout to 300 seconds"); + + // Test 1: Try a request to a slow endpoint that delays for 5 seconds + // This should succeed since timeout is 300s + println!("\nTest 1: Request with 5 second delay (should succeed)"); + let start = Instant::now(); + match client.get("https://httpbin.org/delay/5").send().await { + Ok(_) => { + let elapsed = start.elapsed(); + println!("✓ Request succeeded after {:?}", elapsed); + } + Err(e) => { + let elapsed = start.elapsed(); + if e.is_timeout() { + println!( + "✗ Request timed out after {:?} - timeout might be shorter than expected!", + elapsed + ); + } else { + println!("✗ Request failed after {:?}: {}", elapsed, e); + } + } + } + + // Test 2: Try to inspect the client's actual configuration + println!("\nTest 2: Client debug information"); + println!("Client Debug: {:?}", client); + + // Test 3: Create a client with explicit short timeout to compare behavior + println!("\nTest 3: Control test with 2 second timeout"); + let short_timeout_client = reqwest::Client::builder() + .timeout(Duration::from_secs(2)) + .build() + .expect("Failed to build short timeout client"); + + let start = Instant::now(); + match short_timeout_client + .get("https://httpbin.org/delay/5") + .send() + .await + { + Ok(_) => { + let elapsed = start.elapsed(); + println!( + "✗ Request succeeded after {:?} - timeout not working!", + elapsed + ); + } + Err(e) => { + let elapsed = start.elapsed(); + if e.is_timeout() { + println!("✓ Request timed out as expected after {:?}", elapsed); + } else { + println!("? Request failed after {:?}: {}", elapsed, e); + } + } + } + + // Test 4: Create a client through the registry and verify timeout on a hanging connection + println!("\nTest 4: Testing with a connection that hangs"); + println!("Making request to an endpoint that will hang..."); + + let start = Instant::now(); + // This IP is reserved for documentation and will hang + match client.get("http://192.0.2.1:81").send().await { + Ok(_) => { + println!("✗ Request succeeded - unexpected!"); + } + Err(e) => { + let elapsed = start.elapsed(); + if e.is_timeout() { + println!("✓ Request timed out after {:?}", elapsed); + if elapsed < Duration::from_secs(290) { + println!(" Note: Timeout occurred faster than 300s, might be connection timeout not total timeout"); + } + } else if e.is_connect() { + println!("✓ Connection failed after {:?} (connect timeout)", elapsed); + } else { + println!("? Request failed after {:?}: {}", elapsed, e); + } + } + } +} diff --git a/common/http-api-client/src/fronted.rs b/common/http-api-client/src/fronted.rs index f3ff92b129..f41ce8f3cf 100644 --- a/common/http-api-client/src/fronted.rs +++ b/common/http-api-client/src/fronted.rs @@ -54,10 +54,14 @@ impl Front { #[derive(Debug, Default, PartialEq, Clone)] #[cfg(feature = "tunneling")] +/// Policy for when to use domain fronting for HTTP requests. pub enum FrontPolicy { + /// Always use domain fronting for all requests. Always, + /// Only use domain fronting when retrying failed requests. OnRetry, #[default] + /// Never use domain fronting. Off, } @@ -96,14 +100,14 @@ mod tests { // Some(vec!["https://cdn77.com"]), // ).unwrap(); // cdn77 - let client = ClientBuilder::new::<_, &str>(url1) + let client = ClientBuilder::new(url1) .expect("bad url") .with_fronting(FrontPolicy::Always) - .build::<&str>() + .build() .expect("failed to build client"); let response = client - .send_request::<_, (), &str, &str, &str>( + .send_request::<_, (), &str, &str>( reqwest::Method::GET, &["api", "v1", "network", "details"], NO_PARAMS, diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index 60122efaf1..1e05f514fb 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -15,7 +15,7 @@ //! # use url::Url; //! # use nym_http_api_client::{ApiClient, NO_PARAMS, HttpClientError}; //! -//! # type Err = HttpClientError; +//! # type Err = HttpClientError; //! # async fn run() -> Result<(), Err> { //! let url: Url = "https://nymvpn.com".parse()?; //! let client = nym_http_api_client::Client::new(url, None); @@ -51,7 +51,7 @@ //! Up, //! } //! -//! # type Err = HttpClientError; +//! # type Err = HttpClientError; //! # async fn run() -> Result<(), Err> { //! // This will POST a body of `{"lang":"rust","body":"json"}` //! let mut map = HashMap::new(); @@ -114,7 +114,7 @@ //! } //! } //! -//! pub type SpecificAPIError = HttpClientError; +//! pub type SpecificAPIError = HttpClientError; //! //! #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] //! #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -136,13 +136,18 @@ //! ``` #![warn(missing_docs)] +pub use inventory; +pub use reqwest; pub use reqwest::ClientBuilder as ReqwestClientBuilder; pub use reqwest::StatusCode; +use std::error::Error; + +pub mod registry; use crate::path::RequestPath; use async_trait::async_trait; use bytes::Bytes; -use http::header::CONTENT_TYPE; +use http::header::{ACCEPT, CONTENT_TYPE}; use http::HeaderMap; use itertools::Itertools; use mime::Mime; @@ -162,6 +167,8 @@ use std::sync::Arc; #[cfg(feature = "tunneling")] mod fronted; +#[cfg(feature = "tunneling")] +pub use fronted::FrontPolicy; mod url; pub use url::{IntoUrl, Url}; mod user_agent; @@ -175,8 +182,12 @@ mod path; pub use dns::{HickoryDnsError, HickoryDnsResolver}; // helper for generating user agent based on binary information +#[cfg(not(target_arch = "wasm32"))] +use crate::registry::default_builder; #[doc(hidden)] pub use nym_bin_common::bin_info; +#[cfg(not(target_arch = "wasm32"))] +use nym_http_api_client_macro::client_defaults; /// Default HTTP request connection timeout. /// @@ -184,6 +195,17 @@ pub use nym_bin_common::bin_info; /// high and chatty protocols take a while to complete. pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30); +#[cfg(not(target_arch = "wasm32"))] +client_defaults!( + priority = -100; + gzip = true, + deflate = true, + brotli = true, + zstd = true, + timeout = DEFAULT_TIMEOUT, + user_agent = format!("nym-http-api-client/{}", env!("CARGO_PKG_VERSION")) +); + /// Collection of URL Path Segments pub type PathSegments<'a> = &'a [&'a str]; /// Collection of HTTP Request Parameters @@ -192,53 +214,206 @@ pub type Params<'a, K, V> = &'a [(K, V)]; /// Empty collection of HTTP Request Parameters. pub const NO_PARAMS: Params<'_, &'_ str, &'_ str> = &[]; +/// Serialization format for API requests and responses +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SerializationFormat { + /// Use JSON serialization (default, always works) + Json, + /// Use bincode serialization (must be explicitly opted into) + Bincode, + /// Use YAML serialization + Yaml, + /// Use Text serialization + Text, +} + +impl Display for SerializationFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SerializationFormat::Json => write!(f, "json"), + SerializationFormat::Bincode => write!(f, "bincode"), + SerializationFormat::Yaml => write!(f, "yaml"), + SerializationFormat::Text => write!(f, "text"), + } + } +} + +impl SerializationFormat { + #[allow(missing_docs)] + pub fn content_type(&self) -> String { + match self { + SerializationFormat::Json => "application/json".to_string(), + SerializationFormat::Bincode => "application/bincode".to_string(), + SerializationFormat::Yaml => "application/yaml".to_string(), + SerializationFormat::Text => "text/plain".to_string(), + } + } +} + +#[allow(missing_docs)] +#[derive(Debug)] +pub struct ReqwestErrorWrapper(reqwest::Error); + +impl Display for ReqwestErrorWrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + cfg_if::cfg_if! { + if #[cfg(not(target_arch = "wasm32"))] { + if self.0.is_connect() { + write!(f, "failed to connect: ")?; + } + } + } + + if self.0.is_timeout() { + write!(f, "timed out: ")?; + } + if self.0.is_redirect() { + if let Some(final_stop) = self.0.url() { + write!(f, "redirect loop at {final_stop}: ")?; + } + } + + self.0.fmt(f)?; + if let Some(status_code) = self.0.status() { + write!(f, " status: {status_code}")?; + } else { + write!(f, " unknown status code")?; + } + + if let Some(source) = self.0.source() { + write!(f, " source: {source}")?; + } else { + write!(f, " unknown lower-level error source")?; + } + + Ok(()) + } +} + +impl std::error::Error for ReqwestErrorWrapper {} + /// The Errors that may occur when creating or using an HTTP client. #[derive(Debug, Error)] #[allow(missing_docs)] -pub enum HttpClientError { +pub enum HttpClientError { + #[error("failed to construct inner reqwest client: {source}")] + ReqwestBuildError { + #[source] + source: reqwest::Error, + }, + + #[deprecated( + note = "use another more strongly typed variant - this variant is only left for compatibility reasons" + )] + #[error("request failed with error message: {0}")] + GenericRequestFailure(String), + + #[deprecated( + note = "use another more strongly typed variant - this variant is only left for compatibility reasons" + )] #[error("there was an issue with the REST request: {source}")] ReqwestClientError { #[from] source: reqwest::Error, }, + #[error("failed to parse {raw} as a valid URL: {source}")] + MalformedUrl { + raw: String, + #[source] + source: reqwest::Error, + }, + + #[error("failed to send request for {url}: {source}")] + RequestSendFailure { + url: reqwest::Url, + #[source] + source: ReqwestErrorWrapper, + }, + + #[error("failed to read response body from {url}: {source}")] + ResponseReadFailure { + url: reqwest::Url, + headers: Box, + status: StatusCode, + #[source] + source: ReqwestErrorWrapper, + }, + #[error("failed to deserialize received response: {source}")] ResponseDeserialisationFailure { source: serde_json::Error }, #[error("provided url is malformed: {source}")] - MalformedUrl { + UrlParseFailure { #[from] source: url::ParseError, }, - #[error("the requested resource could not be found")] - NotFound, + #[error("the requested resource could not be found at {url}")] + NotFound { url: reqwest::Url }, - #[error("request failed with error message: {0}")] - GenericRequestFailure(String), + #[error("attempted to use domain fronting and clone a request containing stream data")] + AttemptedToCloneStreamRequest, - #[error("the request failed with status '{status}'. no additional error message provided")] - RequestFailure { status: StatusCode }, + // #[error("request failed with error message: {0}")] + // GenericRequestFailure(String), + // + #[error("the request for {url} failed with status '{status}'. no additional error message provided. response headers: {headers:?}")] + RequestFailure { + url: reqwest::Url, + status: StatusCode, + headers: Box, + }, - #[error("the returned response was empty. status: '{status}'")] - EmptyResponse { status: StatusCode }, + #[error( + "the returned response from {url} was empty. status: '{status}'. response headers: {headers:?}" + )] + EmptyResponse { + url: reqwest::Url, + status: StatusCode, + headers: Box, + }, - #[error("failed to resolve request. status: '{status}', additional error message: {error}")] - EndpointFailure { status: StatusCode, error: E }, + #[error("failed to resolve request for {url}. status: '{status}'. response headers: {headers:?}. additional error message: {error}")] + EndpointFailure { + url: reqwest::Url, + status: StatusCode, + headers: Box, + error: String, + }, #[error("failed to decode response body: {message} from {content}")] ResponseDecodeFailure { message: String, content: String }, + #[error("failed to resolve request to {url} due to data inconsistency: {details}")] + InternalResponseInconsistency { url: ::url::Url, details: String }, + + #[error("Failed to encode bincode: {0}")] + Bincode(#[from] bincode::Error), + + #[error("Failed to json: {0}")] + Json(#[from] serde_json::Error), + + #[error("Failed to yaml: {0}")] + Yaml(#[from] serde_yaml::Error), + + #[error("Failed to plain: {0}")] + Plain(#[from] serde_plain::Error), + #[cfg(target_arch = "wasm32")] #[error("the request has timed out")] RequestTimeout, } +#[allow(missing_docs)] +#[allow(deprecated)] impl HttpClientError { /// Returns true if the error is a timeout. pub fn is_timeout(&self) -> bool { match self { HttpClientError::ReqwestClientError { source } => source.is_timeout(), + HttpClientError::RequestSendFailure { source, .. } => source.0.is_timeout(), + HttpClientError::ResponseReadFailure { source, .. } => source.0.is_timeout(), #[cfg(target_arch = "wasm32")] HttpClientError::RequestTimeout => true, _ => false, @@ -248,12 +423,24 @@ impl HttpClientError { /// Returns the HTTP status code if available. pub fn status_code(&self) -> Option { match self { - HttpClientError::RequestFailure { status } => Some(*status), - HttpClientError::EmptyResponse { status } => Some(*status), + HttpClientError::ResponseReadFailure { status, .. } => Some(*status), + HttpClientError::RequestFailure { status, .. } => Some(*status), + HttpClientError::EmptyResponse { status, .. } => Some(*status), HttpClientError::EndpointFailure { status, .. } => Some(*status), _ => None, } } + + pub fn reqwest_client_build_error(source: reqwest::Error) -> Self { + HttpClientError::ReqwestBuildError { source } + } + + pub fn request_send_error(url: reqwest::Url, source: reqwest::Error) -> Self { + HttpClientError::RequestSendFailure { + url, + source: ReqwestErrorWrapper(source), + } + } } /// Core functionality required for types acting as API clients. @@ -270,8 +457,8 @@ pub trait ApiClientCore { method: reqwest::Method, path: P, params: Params<'_, K, V>, - json_body: Option<&B>, - ) -> RequestBuilder + body: Option<&B>, + ) -> Result where P: RequestPath, B: Serialize + ?Sized, @@ -297,8 +484,8 @@ pub trait ApiClientCore { &self, method: reqwest::Method, endpoint: S, - json_body: Option<&B>, - ) -> RequestBuilder + body: Option<&B>, + ) -> Result where B: Serialize + ?Sized, S: AsRef, @@ -324,7 +511,7 @@ pub trait ApiClientCore { }; let params: Vec<(String, String)> = standin_url.query_pairs().into_owned().collect(); - self.create_request(method, path.as_slice(), ¶ms, json_body) + self.create_request(method, path.as_slice(), ¶ms, body) } /// Send a created HTTP request. @@ -332,26 +519,23 @@ pub trait ApiClientCore { /// A [`RequestBuilder`] can be created with [`ApiClientCore::create_request`] or /// [`ApiClientCore::create_request_endpoint`] or if absolutely necessary, using reqwest /// tooling directly. - async fn send(&self, request: RequestBuilder) -> Result> - where - E: Display; + async fn send(&self, request: RequestBuilder) -> Result; /// Create and send a created HTTP request. - async fn send_request( + async fn send_request( &self, method: reqwest::Method, path: P, params: Params<'_, K, V>, json_body: Option<&B>, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, B: Serialize + ?Sized + Sync, K: AsRef + Sync, V: AsRef + Sync, - E: Display, { - let req = self.create_request(method, path, params, json_body); + let req = self.create_request(method, path, params, json_body)?; self.send(req).await } } @@ -371,16 +555,16 @@ pub struct ClientBuilder { front: Option, retry_limit: usize, + serialization: SerializationFormat, } impl ClientBuilder { /// Constructs a new `ClientBuilder`. /// /// This is the same as `Client::builder()`. - pub fn new(url: U) -> Result> + pub fn new(url: U) -> Result where U: IntoUrl, - E: Display, { let str_url = url.as_str(); @@ -396,6 +580,50 @@ impl ClientBuilder { } } + /// Create a client builder from network details with sensible defaults + #[cfg(feature = "network-defaults")] + pub fn from_network( + network: &nym_network_defaults::NymNetworkDetails, + ) -> Result { + let urls = network + .nym_api_urls + .as_ref() + .ok_or_else(|| { + HttpClientError::GenericRequestFailure( + "No API URLs configured in network details".to_string(), + ) + })? + .iter() + .map(|api_url| { + // Convert ApiUrl to our Url type with fronting support + let mut url = Url::parse(&api_url.url)?; + + // Add fronting domains if available + #[cfg(feature = "tunneling")] + if let Some(ref front_hosts) = api_url.front_hosts { + let fronts: Vec = front_hosts + .iter() + .map(|host| format!("https://{}", host)) + .collect(); + url = Url::new(api_url.url.clone(), Some(fronts)) + .map_err(|e| HttpClientError::GenericRequestFailure(e.to_string()))?; + } + + Ok(url) + }) + .collect::, HttpClientError>>()?; + + let mut builder = Self::new_with_urls(urls); + + // Enable domain fronting by default (on retry) + #[cfg(feature = "tunneling")] + { + builder = builder.with_fronting(FrontPolicy::OnRetry); + } + + Ok(builder) + } + /// Constructs a new http `ClientBuilder` from a valid url. pub fn new_with_urls(urls: Vec) -> Self { let urls = Self::check_urls(urls); @@ -404,20 +632,7 @@ impl ClientBuilder { let reqwest_client_builder = reqwest::ClientBuilder::new(); #[cfg(not(target_arch = "wasm32"))] - let reqwest_client_builder = { - // Note: I believe the manual enable calls for the compression methods are extra - // as the various compression features for `reqwest` crate should be enabled - // just by including the feature which: - // `"Enable[s] auto decompression by checking the Content-Encoding response header."` - // - // I am going to leave these here anyways so that removing a decompression method - // from the features list will throw an error if it is not also removed here. - reqwest::ClientBuilder::new() - .gzip(true) - .deflate(true) - .brotli(true) - .zstd(true) - }; + let reqwest_client_builder = default_builder(); ClientBuilder { urls, @@ -429,6 +644,7 @@ impl ClientBuilder { front: None, retry_limit: 0, + serialization: SerializationFormat::Json, } } @@ -501,11 +717,19 @@ impl ClientBuilder { self } + /// Set the serialization format for API requests and responses + pub fn with_serialization(mut self, format: SerializationFormat) -> Self { + self.serialization = format; + self + } + + /// Configure the client to use bincode serialization + pub fn with_bincode(self) -> Self { + self.with_serialization(SerializationFormat::Bincode) + } + /// Returns a Client that uses this ClientBuilder configuration. - pub fn build(self) -> Result> - where - E: Display, - { + pub fn build(self) -> Result { #[cfg(target_arch = "wasm32")] let reqwest_client = self.reqwest_client_builder.build()?; @@ -513,22 +737,16 @@ impl ClientBuilder { // but that'd break bunch of things due to type changes #[cfg(not(target_arch = "wasm32"))] let reqwest_client = { - let mut builder = self - .reqwest_client_builder - .timeout(self.timeout.unwrap_or(DEFAULT_TIMEOUT)); - - // if no custom user agent was set, use a default - if !self.custom_user_agent { - builder = - builder.user_agent(format!("nym-http-api-client/{}", env!("CARGO_PKG_VERSION"))) - } + let mut builder = self.reqwest_client_builder; // unless explicitly disabled use the DoT/DoH enabled resolver if self.use_secure_dns { builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default())); } - builder.build()? + builder + .build() + .map_err(HttpClientError::reqwest_client_build_error)? }; let client = Client { @@ -542,6 +760,7 @@ impl ClientBuilder { #[cfg(target_arch = "wasm32")] request_timeout: self.timeout.unwrap_or(DEFAULT_TIMEOUT), retry_limit: self.retry_limit, + serialization: self.serialization, }; Ok(client) @@ -562,6 +781,7 @@ pub struct Client { request_timeout: Duration, retry_limit: usize, + serialization: SerializationFormat, } impl Client { @@ -571,16 +791,15 @@ impl Client { // In order to prevent interference in API requests at the DNS phase we default to a resolver // that uses DoT and DoH. pub fn new(base_url: ::url::Url, timeout: Option) -> Self { - Self::new_url::<_, String>(base_url, timeout).expect( + Self::new_url(base_url, timeout).expect( "we provided valid url and we were unwrapping previous construction errors anyway", ) } /// Attempt to create a new http client from a something that can be converted to a URL - pub fn new_url(url: U, timeout: Option) -> Result> + pub fn new_url(url: U, timeout: Option) -> Result where U: IntoUrl, - E: Display, { let builder = Self::builder(url)?; match timeout { @@ -592,10 +811,9 @@ impl Client { /// Creates a [`ClientBuilder`] to configure a [`Client`]. /// /// This is the same as [`ClientBuilder::new()`]. - pub fn builder(url: U) -> Result> + pub fn builder(url: U) -> Result where U: IntoUrl, - E: Display, { ClientBuilder::new(url) } @@ -619,6 +837,7 @@ impl Client { #[cfg(target_arch = "wasm32")] request_timeout: self.request_timeout, + serialization: self.serialization, } } @@ -694,26 +913,42 @@ impl Client { /// this method. For example, if the client is configured to rotate hosts after each error, this /// method should be called after the host has been updated -- i.e. as part of the subsequent /// send. - fn apply_hosts_to_req(&self, r: &mut reqwest::Request) { + fn apply_hosts_to_req(&self, r: &mut reqwest::Request) -> (&str, Option<&str>) { let url = self.current_url(); r.url_mut().set_host(url.host_str()).unwrap(); #[cfg(feature = "tunneling")] if let Some(ref front) = self.front { if front.is_enabled() { - // this should never fail as we are transplanting the host from one url to another - r.url_mut().set_host(url.front_str()).unwrap(); + if let Some(front_host) = url.front_str() { + if let Some(actual_host) = url.host_str() { + tracing::debug!( + "Domain fronting enabled: routing via CDN {} to actual host {}", + front_host, + actual_host + ); - let actual_host: HeaderValue = url - .host_str() - .unwrap_or("") - .parse() - .unwrap_or(HeaderValue::from_static("")); - // If the map did have this key present, the new value is associated with the key - // and all previous values are removed. (reqwest HeaderMap docs) - _ = r.headers_mut().insert(reqwest::header::HOST, actual_host); + // this should never fail as we are transplanting the host from one url to another + r.url_mut().set_host(Some(front_host)).unwrap(); + + let actual_host_header: HeaderValue = + actual_host.parse().unwrap_or(HeaderValue::from_static("")); + // If the map did have this key present, the new value is associated with the key + // and all previous values are removed. (reqwest HeaderMap docs) + _ = r + .headers_mut() + .insert(reqwest::header::HOST, actual_host_header); + + return (url.as_str(), url.front_str()); + } else { + warn!("Domain fronting is enabled, but no host_url is defined! Domain fronting WILL NOT WORK") + } + } else { + warn!("Domain fronting is enabled, but no front_url is defined! Domain fronting WILL NOT WORK") + } } } + (url.as_str(), None) } } @@ -726,8 +961,8 @@ impl ApiClientCore for Client { method: reqwest::Method, path: P, params: Params<'_, K, V>, - json_body: Option<&B>, - ) -> RequestBuilder + body: Option<&B>, + ) -> Result where P: RequestPath, B: Serialize + ?Sized, @@ -743,33 +978,53 @@ impl ApiClientCore for Client { let mut rb = RequestBuilder::from_parts(self.reqwest_client.clone(), req); - if let Some(body) = json_body { - rb = rb.json(body); + rb = rb + .header(ACCEPT, self.serialization.content_type()) + .header(CONTENT_TYPE, self.serialization.content_type()); + + if let Some(body) = body { + match self.serialization { + SerializationFormat::Json => { + rb = rb.json(body); + } + SerializationFormat::Bincode => { + let body = bincode::serialize(body)?; + rb = rb.body(body); + } + SerializationFormat::Yaml => { + let mut body_bytes = Vec::new(); + serde_yaml::to_writer(&mut body_bytes, &body)?; + rb = rb.body(body_bytes); + } + SerializationFormat::Text => { + let body = serde_plain::to_string(&body)?.as_bytes().to_vec(); + rb = rb.body(body); + } + } } - rb + Ok(rb) } - async fn send(&self, request: RequestBuilder) -> Result> - where - E: Display, - { + async fn send(&self, request: RequestBuilder) -> Result { let mut attempts = 0; loop { // try_clone may fail if the body is a stream in which case using retries is not advised. let r = request .try_clone() - .ok_or(HttpClientError::GenericRequestFailure( - "failed to send request".to_string(), - ))?; + .ok_or(HttpClientError::AttemptedToCloneStreamRequest)?; // apply any changes based on the current state of the client wrt. hosts, // fronting domains, etc. - let mut req = r.build()?; + let mut req = r + .build() + .map_err(HttpClientError::reqwest_client_build_error)?; self.apply_hosts_to_req(&mut req); + #[cfg(not(target_arch = "wasm32"))] + let url = req.url().clone(); #[cfg(target_arch = "wasm32")] - let response: Result> = { + let response: Result = { Ok(wasmtimer::tokio::timeout( self.request_timeout, self.reqwest_client.execute(req), @@ -783,7 +1038,7 @@ impl ApiClientCore for Client { match response { Ok(resp) => return Ok(resp), - Err(e) => { + Err(err) => { // if we have multiple urls, update to the next self.update_host(); @@ -791,18 +1046,29 @@ impl ApiClientCore for Client { if let Some(ref front) = self.front { // If fronting is set to be enabled on error, enable domain fronting as we // have encountered an error. + let was_enabled = front.is_enabled(); front.retry_enable(); + if !was_enabled && front.is_enabled() { + tracing::info!( + "Domain fronting activated after connection failure: {err}", + ); + } } if attempts < self.retry_limit { - warn!("Retrying request due to http error: {}", e); + warn!("Retrying request due to http error: {err}"); attempts += 1; continue; } // if we have exhausted our attempts, return the error - #[allow(clippy::useless_conversion)] // conversion considered useless in wasm - return Err(e.into()); + cfg_if::cfg_if! { + if #[cfg(target_arch = "wasm32")] { + return Err(err); + } else { + return Err(HttpClientError::request_send_error(url, err)); + } + } } } } @@ -816,7 +1082,11 @@ impl ApiClientCore for Client { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait ApiClient: ApiClientCore { /// Create an HTTP GET Request with the provided path and parameters - fn create_get_request(&self, path: P, params: Params<'_, K, V>) -> RequestBuilder + fn create_get_request( + &self, + path: P, + params: Params<'_, K, V>, + ) -> Result where P: RequestPath, K: AsRef, @@ -831,7 +1101,7 @@ pub trait ApiClient: ApiClientCore { path: P, params: Params<'_, K, V>, json_body: &B, - ) -> RequestBuilder + ) -> Result where P: RequestPath, B: Serialize + ?Sized, @@ -842,7 +1112,11 @@ pub trait ApiClient: ApiClientCore { } /// Create an HTTP DELETE Request with the provided path and parameters - fn create_delete_request(&self, path: P, params: Params<'_, K, V>) -> RequestBuilder + fn create_delete_request( + &self, + path: P, + params: Params<'_, K, V>, + ) -> Result where P: RequestPath, K: AsRef, @@ -857,7 +1131,7 @@ pub trait ApiClient: ApiClientCore { path: P, params: Params<'_, K, V>, json_body: &B, - ) -> RequestBuilder + ) -> Result where P: RequestPath, B: Serialize + ?Sized, @@ -869,68 +1143,64 @@ pub trait ApiClient: ApiClientCore { /// Create and send an HTTP GET Request with the provided path and parameters #[instrument(level = "debug", skip_all, fields(path=?path))] - async fn send_get_request( + async fn send_get_request( &self, path: P, params: Params<'_, K, V>, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, K: AsRef + Sync, V: AsRef + Sync, - E: Display, { self.send_request(reqwest::Method::GET, path, params, None::<&()>) .await } /// Create and send an HTTP POST Request with the provided path, parameters, and json data - async fn send_post_request( + async fn send_post_request( &self, path: P, params: Params<'_, K, V>, json_body: &B, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, B: Serialize + ?Sized + Sync, K: AsRef + Sync, V: AsRef + Sync, - E: Display, { self.send_request(reqwest::Method::POST, path, params, Some(json_body)) .await } /// Create and send an HTTP DELETE Request with the provided path and parameters - async fn send_delete_request( + async fn send_delete_request( &self, path: P, params: Params<'_, K, V>, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, K: AsRef + Sync, V: AsRef + Sync, - E: Display, { self.send_request(reqwest::Method::DELETE, path, params, None::<&()>) .await } /// Create and send an HTTP PATCH Request with the provided path, parameters, and json data - async fn send_patch_request( + async fn send_patch_request( &self, path: P, params: Params<'_, K, V>, json_body: &B, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, B: Serialize + ?Sized + Sync, K: AsRef + Sync, V: AsRef + Sync, - E: Display, { self.send_request(reqwest::Method::PATCH, path, params, Some(json_body)) .await @@ -941,17 +1211,16 @@ pub trait ApiClient: ApiClientCore { /// into the provided type `T`. #[instrument(level = "debug", skip_all, fields(path=?path))] // TODO: deprecate in favour of get_response that works based on mime type in the response - async fn get_json( + async fn get_json( &self, path: P, params: Params<'_, K, V>, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, for<'a> T: Deserialize<'a>, K: AsRef + Sync, V: AsRef + Sync, - E: Display + DeserializeOwned, { self.get_response(path, params).await } @@ -959,17 +1228,16 @@ pub trait ApiClient: ApiClientCore { /// 'get' data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple /// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response /// into the provided type `T` based on the content type header - async fn get_response( + async fn get_response( &self, path: P, params: Params<'_, K, V>, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, for<'a> T: Deserialize<'a>, K: AsRef + Sync, V: AsRef + Sync, - E: Display + DeserializeOwned, { let res = self .send_request(reqwest::Method::GET, path, params, None::<&()>) @@ -980,19 +1248,18 @@ pub trait ApiClient: ApiClientCore { /// 'post' json data to the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple /// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response /// into the provided type `T`. - async fn post_json( + async fn post_json( &self, path: P, params: Params<'_, K, V>, json_body: &B, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, B: Serialize + ?Sized + Sync, for<'a> T: Deserialize<'a>, K: AsRef + Sync, V: AsRef + Sync, - E: Display + DeserializeOwned, { let res = self .send_request(reqwest::Method::POST, path, params, Some(json_body)) @@ -1003,17 +1270,16 @@ pub trait ApiClient: ApiClientCore { /// 'delete' json data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with /// tuple defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the /// response into the provided type `T`. - async fn delete_json( + async fn delete_json( &self, path: P, params: Params<'_, K, V>, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, for<'a> T: Deserialize<'a>, K: AsRef + Sync, V: AsRef + Sync, - E: Display + DeserializeOwned, { let res = self .send_request(reqwest::Method::DELETE, path, params, None::<&()>) @@ -1024,19 +1290,18 @@ pub trait ApiClient: ApiClientCore { /// 'patch' json data at the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple /// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response /// into the provided type `T`. - async fn patch_json( + async fn patch_json( &self, path: P, params: Params<'_, K, V>, json_body: &B, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, B: Serialize + ?Sized + Sync, for<'a> T: Deserialize<'a>, K: AsRef + Sync, V: AsRef + Sync, - E: Display + DeserializeOwned, { let res = self .send_request(reqwest::Method::PATCH, path, params, Some(json_body)) @@ -1046,62 +1311,59 @@ pub trait ApiClient: ApiClientCore { /// `get` json data from the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`. /// Attempt to parse the response into the provided type `T`. - async fn get_json_from(&self, endpoint: S) -> Result> + async fn get_json_from(&self, endpoint: S) -> Result where for<'a> T: Deserialize<'a>, - E: Display + DeserializeOwned, S: AsRef + Sync + Send, { - let req = self.create_request_endpoint(reqwest::Method::GET, endpoint, None::<&()>); + let req = self.create_request_endpoint(reqwest::Method::GET, endpoint, None::<&()>)?; let res = self.send(req).await?; parse_response(res, false).await } /// `post` json data to the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`. /// Attempt to parse the response into the provided type `T`. - async fn post_json_data_to( + async fn post_json_data_to( &self, endpoint: S, json_body: &B, - ) -> Result> + ) -> Result where B: Serialize + ?Sized + Sync, for<'a> T: Deserialize<'a>, - E: Display + DeserializeOwned, S: AsRef + Sync + Send, { - let req = self.create_request_endpoint(reqwest::Method::POST, endpoint, Some(json_body)); + let req = self.create_request_endpoint(reqwest::Method::POST, endpoint, Some(json_body))?; let res = self.send(req).await?; parse_response(res, false).await } /// `delete` json data from the provided absolute endpoint, e.g. /// `"/api/v1/mixnodes?since=12345"`. Attempt to parse the response into the provided type `T`. - async fn delete_json_from(&self, endpoint: S) -> Result> + async fn delete_json_from(&self, endpoint: S) -> Result where for<'a> T: Deserialize<'a>, - E: Display + DeserializeOwned, S: AsRef + Sync + Send, { - let req = self.create_request_endpoint(reqwest::Method::DELETE, endpoint, None::<&()>); + let req = self.create_request_endpoint(reqwest::Method::DELETE, endpoint, None::<&()>)?; let res = self.send(req).await?; parse_response(res, false).await } /// `patch` json data at the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`. /// Attempt to parse the response into the provided type `T`. - async fn patch_json_data_at( + async fn patch_json_data_at( &self, endpoint: S, json_body: &B, - ) -> Result> + ) -> Result where B: Serialize + ?Sized + Sync, for<'a> T: Deserialize<'a>, - E: Display + DeserializeOwned, S: AsRef + Sync + Send, { - let req = self.create_request_endpoint(reqwest::Method::PATCH, endpoint, Some(json_body)); + let req = + self.create_request_endpoint(reqwest::Method::PATCH, endpoint, Some(json_body))?; let res = self.send(req).await?; parse_response(res, false).await } @@ -1157,49 +1419,63 @@ fn decode_as_text(bytes: &bytes::Bytes, headers: &HeaderMap) -> String { /// Attempt to parse a response object from an HTTP response #[instrument(level = "debug", skip_all)] -pub async fn parse_response(res: Response, allow_empty: bool) -> Result> +pub async fn parse_response(res: Response, allow_empty: bool) -> Result where T: DeserializeOwned, - E: DeserializeOwned + Display, { let status = res.status(); - tracing::trace!("Status: {} (success: {})", &status, status.is_success()); + let headers = res.headers().clone(); + let url = res.url().clone(); + + tracing::trace!("status: {status} (success: {})", status.is_success()); + tracing::trace!("headers: {headers:?}"); if !allow_empty { if let Some(0) = res.content_length() { - return Err(HttpClientError::EmptyResponse { status }); + return Err(HttpClientError::EmptyResponse { + url, + status, + headers: Box::new(headers), + }); } } - let headers = res.headers().clone(); - tracing::trace!("headers: {:?}", headers); if res.status().is_success() { // internally reqwest is first retrieving bytes and then performing parsing via serde_json // (and similarly does the same thing for text()) - let full = res.bytes().await?; + let full = res + .bytes() + .await + .map_err(|source| HttpClientError::ResponseReadFailure { + url, + headers: Box::new(headers.clone()), + status, + source: ReqwestErrorWrapper(source), + })?; decode_raw_response(&headers, full) } else if res.status() == StatusCode::NOT_FOUND { - Err(HttpClientError::NotFound) + Err(HttpClientError::NotFound { url }) } else { let Ok(plaintext) = res.text().await else { - return Err(HttpClientError::RequestFailure { status }); + return Err(HttpClientError::RequestFailure { + url, + status, + headers: Box::new(headers), + }); }; - if let Ok(request_error) = serde_json::from_str(&plaintext) { - Err(HttpClientError::EndpointFailure { - status, - error: request_error, - }) - } else { - Err(HttpClientError::GenericRequestFailure(plaintext)) - } + Err(HttpClientError::EndpointFailure { + url, + status, + headers: Box::new(headers), + error: plaintext, + }) } } -fn decode_as_json(headers: &HeaderMap, content: Bytes) -> Result> +fn decode_as_json(headers: &HeaderMap, content: Bytes) -> Result where T: DeserializeOwned, - E: DeserializeOwned + Display, { match serde_json::from_slice(&content) { Ok(data) => Ok(data), @@ -1213,10 +1489,9 @@ where } } -fn decode_as_bincode(headers: &HeaderMap, content: Bytes) -> Result> +fn decode_as_bincode(headers: &HeaderMap, content: Bytes) -> Result where T: DeserializeOwned, - E: DeserializeOwned + Display, { use bincode::Options; @@ -1233,10 +1508,9 @@ where } } -fn decode_raw_response(headers: &HeaderMap, content: Bytes) -> Result> +fn decode_raw_response(headers: &HeaderMap, content: Bytes) -> Result where T: DeserializeOwned, - E: DeserializeOwned + Display, { // if content type header is missing, fallback to our old default, json let mime = try_get_mime_type(headers).unwrap_or(mime::APPLICATION_JSON); diff --git a/common/http-api-client/src/registry.rs b/common/http-api-client/src/registry.rs new file mode 100644 index 0000000000..05ad9a5daa --- /dev/null +++ b/common/http-api-client/src/registry.rs @@ -0,0 +1,101 @@ +//! Global registry for HTTP client configurations. +//! +//! This module provides a compile-time registry system that allows any crate +//! in the workspace to contribute configuration modifications to HTTP clients. + +use crate::ReqwestClientBuilder; + +/// A configuration record that modifies a `ReqwestClientBuilder`. +/// +/// Records are collected at compile-time via the `inventory` crate and +/// applied in priority order when building HTTP clients. +pub struct ConfigRecord { + /// Lower numbers run earlier. + pub priority: i32, + /// A function that takes a builder and returns a mutated builder. + pub apply: fn(ReqwestClientBuilder) -> ReqwestClientBuilder, +} + +inventory::collect!(ConfigRecord); + +/// Returns the default builder with all registered configurations applied. +pub fn default_builder() -> ReqwestClientBuilder { + let mut b = ReqwestClientBuilder::new(); + let mut records: Vec<&'static ConfigRecord> = + inventory::iter::.into_iter().collect(); + records.sort_by_key(|r| r.priority); // lower runs first + + #[cfg(feature = "debug-inventory")] + { + eprintln!( + "[HTTP-INVENTORY] Building client with {} registered configurations", + records.len() + ); + } + + for r in records { + b = (r.apply)(b); + } + + #[cfg(feature = "debug-inventory")] + { + eprintln!("[HTTP-INVENTORY] Final builder state (Debug):"); + eprintln!("{:#?}", b); + eprintln!( + "[HTTP-INVENTORY] Note: reqwest::ClientBuilder doesn't expose all internal state" + ); + eprintln!("[HTTP-INVENTORY] Building test client to verify configuration..."); + + // Try to build a client to see if it works + match b.try_clone().unwrap().build() { + Ok(client) => { + eprintln!("[HTTP-INVENTORY] ✓ Client built successfully"); + eprintln!("[HTTP-INVENTORY] Client debug info: {:#?}", client); + } + Err(e) => { + eprintln!("[HTTP-INVENTORY] ✗ Failed to build client: {}", e); + } + } + } + + b +} + +/// Builds a client using the default builder with all registered configurations. +pub fn build_client() -> reqwest::Result { + default_builder().build() +} + +/// Debug function to inspect registered configurations. +/// Returns a vector of (priority, function_pointer) tuples for debugging. +pub fn inspect_registered_configs() -> Vec<(i32, usize)> { + let mut configs: Vec<(i32, usize)> = inventory::iter:: + .into_iter() + .map(|record| (record.priority, record.apply as usize)) + .collect(); + configs.sort_by_key(|(priority, _)| *priority); + configs +} + +/// Print all registered configurations to stderr for debugging. +/// This shows the priority and function pointer address of each registered config. +pub fn debug_print_inventory() { + eprintln!("[HTTP-INVENTORY] Registered configurations:"); + let configs = inspect_registered_configs(); + if configs.is_empty() { + eprintln!(" (none)"); + } else { + for (i, (priority, ptr)) in configs.iter().enumerate() { + eprintln!( + " [{:2}] Priority: {:4}, Function: 0x{:016x}", + i, priority, ptr + ); + } + eprintln!(" Total: {} configurations", configs.len()); + } +} + +/// Returns the count of registered configuration records. +pub fn registered_config_count() -> usize { + inventory::iter::.into_iter().count() +} diff --git a/common/http-api-client/src/tests.rs b/common/http-api-client/src/tests.rs index 6f295cd845..98f6fec79f 100644 --- a/common/http-api-client/src/tests.rs +++ b/common/http-api-client/src/tests.rs @@ -95,10 +95,10 @@ async fn api_client_retry() -> Result<(), Box> { "http://example.com/".parse()?, ]) .with_retries(3) - .build::()?; + .build()?; - let req = client.create_get_request(&["/"], NO_PARAMS); - let resp = client.send::(req).await?; + let req = client.create_get_request(&["/"], NO_PARAMS).unwrap(); + let resp = client.send(req).await?; assert_eq!(resp.status(), 200); @@ -111,10 +111,7 @@ async fn api_client_retry() -> Result<(), Box> { #[test] fn host_updating() { let url = Url::new("http://example.com", None).unwrap(); - let mut client = ClientBuilder::new::<_, &str>(url) - .unwrap() - .build::<&str>() - .unwrap(); + let mut client = ClientBuilder::new(url).unwrap().build().unwrap(); // check that the url is set correctly let current_url = client.current_url(); @@ -171,10 +168,10 @@ fn host_updating() { #[cfg(feature = "tunneling")] fn fronted_host_updating() { let url = Url::new("http://example.com", Some(vec!["http://front1.com"])).unwrap(); - let mut client = ClientBuilder::new::<_, &str>(url) + let mut client = ClientBuilder::new(url) .unwrap() .with_fronting(crate::fronted::FrontPolicy::Always) - .build::<&str>() + .build() .unwrap(); // check that the url is set correctly diff --git a/common/http-api-client/tests/registry_tests.rs b/common/http-api-client/tests/registry_tests.rs new file mode 100644 index 0000000000..1f878b9444 --- /dev/null +++ b/common/http-api-client/tests/registry_tests.rs @@ -0,0 +1,70 @@ +use nym_http_api_client::registry; + +// Create separate modules to avoid name conflicts +mod config_early { + use nym_http_api_client_macro::client_defaults; + + client_defaults!( + priority = -200; + tcp_nodelay = true + ); +} + +mod config_late { + use nym_http_api_client_macro::client_defaults; + + client_defaults!( + priority = 100; + pool_idle_timeout = std::time::Duration::from_secs(90) + ); +} + +#[test] +fn test_registry_collects_configs() { + // Verify that configurations are being registered + let count = registry::registered_config_count(); + // Should have at least the ones we registered above plus the default from lib.rs + assert!( + count >= 3, + "Expected at least 3 registered configs, got {}", + count + ); +} + +#[test] +fn test_default_builder_applies_configs() { + // Test that default_builder returns a configured builder + let _builder = registry::default_builder(); + // The builder should have all configurations applied + // We can't easily inspect the internals, but we verify it doesn't panic +} + +#[test] +fn test_build_client_works() { + // Test that we can successfully build a client with all configurations + let result = registry::build_client(); + assert!(result.is_ok(), "Failed to build client: {:?}", result.err()); +} + +#[cfg(debug_assertions)] +#[test] +fn test_inspect_configs() { + // In debug mode, test that we can inspect registered configurations + let configs = registry::inspect_registered_configs(); + + // Verify configs are sorted by priority + for window in configs.windows(2) { + assert!(window[0].0 <= window[1].0, "Configs not sorted by priority"); + } + + // Verify we have configs at different priority levels + let priorities: Vec = configs.iter().map(|(p, _)| *p).collect(); + assert!( + priorities.iter().any(|&p| p < 0), + "Expected negative priority configs" + ); + assert!( + priorities.iter().any(|&p| p >= 0), + "Expected non-negative priority configs" + ); +} diff --git a/common/network-defaults/src/network.rs b/common/network-defaults/src/network.rs index e8fd8fcfa8..3fdf03671e 100644 --- a/common/network-defaults/src/network.rs +++ b/common/network-defaults/src/network.rs @@ -55,6 +55,7 @@ pub struct ApiUrl { pub front_hosts: Option>, } +#[derive(Copy, Clone)] pub struct ApiUrlConst<'a> { pub url: &'a str, pub front_hosts: Option<&'a [&'a str]>, @@ -188,8 +189,14 @@ impl NymNetworkDetails { ), }, nym_vpn_api_url: parse_optional_str(mainnet::NYM_VPN_API), - nym_api_urls: None, - nym_vpn_api_urls: None, + nym_api_urls: Some(mainnet::NYM_APIS.iter().copied().map(Into::into).collect()), + nym_vpn_api_urls: Some( + mainnet::NYM_VPN_APIS + .iter() + .copied() + .map(Into::into) + .collect(), + ), } } diff --git a/common/types/src/error.rs b/common/types/src/error.rs index 823ec8b375..b72a4a18d6 100644 --- a/common/types/src/error.rs +++ b/common/types/src/error.rs @@ -33,10 +33,7 @@ pub enum TypesError { source: eyre::Report, }, #[error("{source}")] - NymApiError { - #[from] - source: NymAPIError, - }, + NymApiError { source: Box }, #[error("{source}")] IOError { #[from] @@ -97,10 +94,18 @@ impl Serialize for TypesError { } } +impl From for TypesError { + fn from(value: NymAPIError) -> Self { + TypesError::NymApiError { + source: Box::new(value), + } + } +} + impl From for TypesError { fn from(e: ValidatorClientError) -> Self { match e { - ValidatorClientError::NymAPIError { source } => source.into(), + ValidatorClientError::NymAPIError { source } => (*source).into(), ValidatorClientError::MalformedUrlProvided(e) => e.into(), ValidatorClientError::NyxdError(e) => e.into(), ValidatorClientError::NoAPIUrlAvailable => TypesError::NoNymApiUrlConfigured, diff --git a/common/verloc/Cargo.toml b/common/verloc/Cargo.toml index e906f72dde..32b81ed1d6 100644 --- a/common/verloc/Cargo.toml +++ b/common/verloc/Cargo.toml @@ -25,3 +25,5 @@ url = { workspace = true } nym-crypto = { path = "../crypto", features = ["asymmetric"] } nym-task = { path = "../task" } nym-validator-client = { path = "../client-libs/validator-client" } +nym-http-api-client = { path = "../http-api-client" } +nym-api-requests = { path = "../../nym-api/nym-api-requests" } diff --git a/common/verloc/src/measurements/measurer.rs b/common/verloc/src/measurements/measurer.rs index 2f5b136485..8451207300 100644 --- a/common/verloc/src/measurements/measurer.rs +++ b/common/verloc/src/measurements/measurer.rs @@ -10,7 +10,7 @@ use futures::StreamExt; use nym_crypto::asymmetric::ed25519; use nym_task::ShutdownToken; use nym_validator_client::models::NymNodeDescription; -use nym_validator_client::NymApiClient; +use nym_validator_client::nym_api::NymApiClientExt; use rand::prelude::SliceRandom; use rand::thread_rng; use std::net::SocketAddr; @@ -136,10 +136,15 @@ impl VerlocMeasurer { let mut api_endpoints = self.config.nym_api_urls.clone(); api_endpoints.shuffle(&mut thread_rng()); for api_endpoint in api_endpoints { - let client = NymApiClient::new_with_user_agent( - api_endpoint.clone(), - self.config.user_agent.clone(), - ); + let client = match nym_http_api_client::Client::builder(api_endpoint.clone()) + .and_then(|b| b.with_user_agent(self.config.user_agent.clone()).build()) + { + Ok(c) => c, + Err(err) => { + warn!("failed to create client for {api_endpoint}: {err}"); + continue; + } + }; match client.get_all_described_nodes().await { Ok(res) => return Some(res), Err(err) => { diff --git a/common/wasm/client-core/Cargo.toml b/common/wasm/client-core/Cargo.toml index d63e31e50a..85668972f9 100644 --- a/common/wasm/client-core/Cargo.toml +++ b/common/wasm/client-core/Cargo.toml @@ -34,6 +34,7 @@ nym-statistics-common = { path = "../../statistics" } nym-task = { path = "../../task" } nym-topology = { path = "../../topology", features = ["wasm-serde-types"] } nym-validator-client = { path = "../../client-libs/validator-client", default-features = false } +nym-http-api-client = { path = "../../http-api-client" } wasm-utils = { path = "../utils" } wasm-storage = { path = "../storage" } diff --git a/common/wasm/client-core/src/error.rs b/common/wasm/client-core/src/error.rs index 8066a9f2bb..ab41b190dd 100644 --- a/common/wasm/client-core/src/error.rs +++ b/common/wasm/client-core/src/error.rs @@ -37,6 +37,12 @@ pub enum WasmCoreError { source: ValidatorClientError, }, + #[error("failed to query nym api: {source}")] + NymApiQueryError { + #[from] + source: nym_validator_client::nym_api::error::NymAPIError, + }, + #[error("The provided wasm topology was invalid: {source}")] WasmTopologyError { #[from] diff --git a/common/wasm/client-core/src/helpers.rs b/common/wasm/client-core/src/helpers.rs index 4cb5f06c55..23aaee4cad 100644 --- a/common/wasm/client-core/src/helpers.rs +++ b/common/wasm/client-core/src/helpers.rs @@ -19,9 +19,9 @@ use nym_client_core::init::{ use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_topology::wasm_helpers::WasmFriendlyNymTopology; -use nym_topology::{NymTopology, RoutingNode}; +use nym_topology::{EpochRewardedSet, NymTopology, RoutingNode}; use nym_validator_client::client::IdentityKey; -use nym_validator_client::{NymApiClient, UserAgent}; +use nym_validator_client::{nym_api::NymApiClientExt, UserAgent}; use rand::thread_rng; use url::Url; use wasm_bindgen::prelude::wasm_bindgen; @@ -72,7 +72,16 @@ pub async fn current_network_topology_async( } }; - let api_client = NymApiClient::new(url); + let api_client = nym_http_api_client::Client::builder(url.clone()) + .map_err(|_err| WasmCoreError::MalformedUrl { + raw: nym_api_url.to_string(), + source: url::ParseError::EmptyHost, + })? + .build() + .map_err(|_err| WasmCoreError::MalformedUrl { + raw: nym_api_url.to_string(), + source: url::ParseError::EmptyHost, + })?; let rewarded_set = api_client.get_current_rewarded_set().await?; let mixnodes_res = api_client .get_all_basic_active_mixing_assigned_nodes_with_metadata() @@ -90,9 +99,14 @@ pub async fn current_network_topology_async( let gateways = gateways_res.nodes; - let topology = NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new()) - .with_skimmed_nodes(&mixnodes) - .with_skimmed_nodes(&gateways); + let epoch_rewarded_set: EpochRewardedSet = rewarded_set.into(); + let topology = NymTopology::new( + metadata.to_topology_metadata(), + epoch_rewarded_set, + Vec::new(), + ) + .with_skimmed_nodes(&mixnodes) + .with_skimmed_nodes(&gateways); Ok(topology.into()) } diff --git a/common/wasm/client-core/src/lib.rs b/common/wasm/client-core/src/lib.rs index 8569dd7ad9..761df16ae4 100644 --- a/common/wasm/client-core/src/lib.rs +++ b/common/wasm/client-core/src/lib.rs @@ -21,6 +21,7 @@ pub use nym_client_core::{ pub use nym_gateway_client::{ error::GatewayClientError, GatewayClient, GatewayClientConfig, GatewayConfig, }; +pub use nym_http_api_client::Client as ApiClient; pub use nym_sphinx::{ addressing::{clients::Recipient, nodes::NodeIdentity}, params::PacketType, @@ -29,7 +30,6 @@ pub use nym_sphinx::{ pub use nym_statistics_common::clients::ClientStatsSender; pub use nym_task; pub use nym_topology::{HardcodedTopologyProvider, MixLayer, NymTopology, TopologyProvider}; -pub use nym_validator_client::nym_api::Client as ApiClient; pub use nym_validator_client::{DirectSigningReqwestRpcNyxdClient, QueryReqwestRpcNyxdClient}; // TODO: that's a very nasty import path. it should come from contracts instead! pub use nym_validator_client::client::IdentityKey; diff --git a/common/wireguard-private-metadata/client/src/lib.rs b/common/wireguard-private-metadata/client/src/lib.rs index 3f683efe5e..fd02fb3847 100644 --- a/common/wireguard-private-metadata/client/src/lib.rs +++ b/common/wireguard-private-metadata/client/src/lib.rs @@ -7,16 +7,14 @@ use tracing::instrument; use nym_http_api_client::{ApiClient, Client, HttpClientError, NO_PARAMS}; use nym_wireguard_private_metadata_shared::{ - routes, Version, {ErrorResponse, Request, Response}, + routes, Version, {Request, Response}, }; -pub type WireguardMetadataApiClientError = HttpClientError; - #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait WireguardMetadataApiClient: ApiClient { #[instrument(level = "debug", skip(self))] - async fn version(&self) -> Result { + async fn version(&self) -> Result { let version: u64 = self .get_json( &[routes::V1_API_VERSION, routes::BANDWIDTH, routes::VERSION], @@ -30,7 +28,7 @@ pub trait WireguardMetadataApiClient: ApiClient { async fn available_bandwidth( &self, request_body: &Request, - ) -> Result { + ) -> Result { self.post_json( &[routes::V1_API_VERSION, routes::BANDWIDTH, routes::AVAILABLE], NO_PARAMS, @@ -40,10 +38,7 @@ pub trait WireguardMetadataApiClient: ApiClient { } #[instrument(level = "debug", skip(self, request_body))] - async fn topup_bandwidth( - &self, - request_body: &Request, - ) -> Result { + async fn topup_bandwidth(&self, request_body: &Request) -> Result { self.post_json( &[routes::V1_API_VERSION, routes::BANDWIDTH, routes::TOPUP], NO_PARAMS, diff --git a/common/wireguard-private-metadata/tests/src/lib.rs b/common/wireguard-private-metadata/tests/src/lib.rs index c1a981e484..1ae9ccdd93 100644 --- a/common/wireguard-private-metadata/tests/src/lib.rs +++ b/common/wireguard-private-metadata/tests/src/lib.rs @@ -13,7 +13,7 @@ mod tests { use nym_wireguard_private_metadata_server::{ AppState, PeerControllerTransceiver, RouterBuilder, }; - use nym_wireguard_private_metadata_shared::{latest, v0, v1, ErrorResponse}; + use nym_wireguard_private_metadata_shared::{latest, v0, v1}; use tokio::{net::TcpListener, sync::mpsc}; pub(crate) const VERIFIER_AVAILABLE_BANDWIDTH: i64 = 42; @@ -140,7 +140,7 @@ mod tests { .await .unwrap(); }); - Client::new_url::<_, ErrorResponse>(addr.to_string(), None).unwrap() + Client::new_url(addr.to_string(), None).unwrap() } #[tokio::test] diff --git a/common/wireguard-private-metadata/tests/src/v0/network.rs b/common/wireguard-private-metadata/tests/src/v0/network.rs index 6a847ecc36..0be32d66dc 100644 --- a/common/wireguard-private-metadata/tests/src/v0/network.rs +++ b/common/wireguard-private-metadata/tests/src/v0/network.rs @@ -15,7 +15,6 @@ pub(crate) mod test { use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_wireguard::{peer_controller::PeerControlRequest, CONTROL_CHANNEL_SIZE}; use nym_wireguard_private_metadata_server::PeerControllerTransceiver; - use nym_wireguard_private_metadata_shared::ErrorResponse; use nym_wireguard_private_metadata_shared::{ v0 as latest, AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, }; @@ -141,6 +140,6 @@ pub(crate) mod test { .await .unwrap(); }); - Client::new_url::<_, ErrorResponse>(addr.to_string(), None).unwrap() + Client::new_url(addr.to_string(), None).unwrap() } } diff --git a/contracts/ecash/src/contract/test.rs b/contracts/ecash/src/contract/test.rs index 09ece6dd4b..77f4dade4c 100644 --- a/contracts/ecash/src/contract/test.rs +++ b/contracts/ecash/src/contract/test.rs @@ -52,7 +52,7 @@ impl TestSetup { } } - pub fn query_ctx(&self) -> QueryCtx { + pub fn query_ctx(&self) -> QueryCtx<'_> { QueryCtx::from((self.deps.as_ref(), self.env.clone())) } } diff --git a/contracts/ecash/src/support/tests.rs b/contracts/ecash/src/support/tests.rs index fb3f3aa579..de31ea4e6d 100644 --- a/contracts/ecash/src/support/tests.rs +++ b/contracts/ecash/src/support/tests.rs @@ -73,13 +73,13 @@ impl TestSetupSimple { message_info(&admin, &[]) } - pub fn execute_ctx(&mut self, sender: MessageInfo) -> ExecCtx { + pub fn execute_ctx(&mut self, sender: MessageInfo) -> ExecCtx<'_> { let env = self.env.clone(); ExecCtx::from((self.deps.as_mut(), env, sender)) } #[allow(dead_code)] - pub fn query_ctx(&self) -> QueryCtx { + pub fn query_ctx(&self) -> QueryCtx<'_> { QueryCtx::from((self.deps.as_ref(), self.env.clone())) } diff --git a/contracts/mixnet/src/delegations/queries.rs b/contracts/mixnet/src/delegations/queries.rs index df2c9befa7..cf970e653e 100644 --- a/contracts/mixnet/src/delegations/queries.rs +++ b/contracts/mixnet/src/delegations/queries.rs @@ -139,9 +139,9 @@ mod tests { fn add_dummy_mixes_with_delegations(test: &mut TestSetup, delegators: usize, mixes: usize) { for i in 0..mixes { - let mix_id = test.add_legacy_mixnode(&test.make_addr(format!("mix-owner{}", i)), None); + let mix_id = test.add_legacy_mixnode(&test.make_addr(format!("mix-owner{i}")), None); for delegator in 0..delegators { - let name = &test.make_addr(format!("delegator{}", delegator)); + let name = &test.make_addr(format!("delegator{delegator}")); test.add_immediate_delegation(name, 100_000_000u32, mix_id) } } diff --git a/contracts/mixnet/src/delegations/transactions.rs b/contracts/mixnet/src/delegations/transactions.rs index 4611f83c20..dff4d46b39 100644 --- a/contracts/mixnet/src/delegations/transactions.rs +++ b/contracts/mixnet/src/delegations/transactions.rs @@ -300,7 +300,7 @@ mod tests { let amount1 = coin(100_000_000, TEST_COIN_DENOM); - let sender1 = message_info(owner, &[amount1.clone()]); + let sender1 = message_info(owner, std::slice::from_ref(&amount1)); try_delegate_to_node(test.deps_mut(), env.clone(), sender1, mix_id).unwrap(); diff --git a/contracts/mixnet/src/mixnodes/queries.rs b/contracts/mixnet/src/mixnodes/queries.rs index bac59ab591..7591b6cba4 100644 --- a/contracts/mixnet/src/mixnodes/queries.rs +++ b/contracts/mixnet/src/mixnodes/queries.rs @@ -503,8 +503,8 @@ pub(crate) mod tests { storage, id, &UnbondedMixnode { - identity_key: format!("dummy{}", id), - owner: Addr::unchecked(format!("dummy{}", id)), + identity_key: format!("dummy{id}"), + owner: Addr::unchecked(format!("dummy{id}")), proxy: None, unbonding_height: 123, }, @@ -570,7 +570,7 @@ pub(crate) mod tests { storage, id, &UnbondedMixnode { - identity_key: format!("dummy{}", id), + identity_key: format!("dummy{id}"), owner: owner.clone(), proxy: None, unbonding_height: 123, @@ -817,7 +817,7 @@ pub(crate) mod tests { id, &UnbondedMixnode { identity_key: identity.to_string(), - owner: Addr::unchecked(format!("dummy{}", id)), + owner: Addr::unchecked(format!("dummy{id}")), proxy: None, unbonding_height: 123, }, diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 29aafaa92e..8ba4a4d3db 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -165,9 +165,9 @@ pub mod test_helpers { #[track_caller] pub fn assert_eq_with_leeway(a: Uint128, b: Uint128, leeway: Uint128) { if a > b { - assert!(a - b <= leeway, "{} != {}", a, b) + assert!(a - b <= leeway, "{a} != {b}") } else { - assert!(b - a <= leeway, "{} != {}", a, b) + assert!(b - a <= leeway, "{a} != {b}") } } @@ -175,9 +175,9 @@ pub mod test_helpers { pub fn assert_decimals(a: Decimal, b: Decimal) { let epsilon = Decimal::from_ratio(1u128, 100_000_000u128); if a > b { - assert!(a - b < epsilon, "{} != {}", a, b) + assert!(a - b < epsilon, "{a} != {b}") } else { - assert!(b - a < epsilon, "{} != {}", a, b) + assert!(b - a < epsilon, "{a} != {b}") } } @@ -1699,7 +1699,7 @@ pub mod test_helpers { deps.branch(), &env, env.block.height, - Addr::unchecked(format!("owner{}", i)), + Addr::unchecked(format!("owner{i}")), mix_id, tests::fixtures::good_mixnode_pledge().pop().unwrap(), ) @@ -1713,7 +1713,7 @@ pub mod test_helpers { n: usize, ) { for i in 0..n { - add_unbonded_mixnode(&mut rng, deps.branch(), None, &addr(format!("owner{}", i))); + add_unbonded_mixnode(&mut rng, deps.branch(), None, &addr(format!("owner{i}"))); } } @@ -1765,7 +1765,7 @@ pub mod test_helpers { id, &UnbondedMixnode { identity_key: identity_key - .unwrap_or(&*format!("identity{}", id)) + .unwrap_or(&*format!("identity{id}")) .to_string(), owner: Addr::unchecked(owner), proxy: None, diff --git a/contracts/performance/src/testing/mod.rs b/contracts/performance/src/testing/mod.rs index 4f8e7cf4bd..614982acc3 100644 --- a/contracts/performance/src/testing/mod.rs +++ b/contracts/performance/src/testing/mod.rs @@ -131,7 +131,7 @@ impl PreInitContract { } } - pub(crate) fn deps(&self) -> Deps { + pub(crate) fn deps(&self) -> Deps<'_> { Deps { storage: &self.storage, api: &self.api, @@ -139,7 +139,7 @@ impl PreInitContract { } } - pub(crate) fn deps_mut(&mut self) -> DepsMut { + pub(crate) fn deps_mut(&mut self) -> DepsMut<'_> { DepsMut { storage: &mut self.storage, api: &self.api, @@ -147,7 +147,7 @@ impl PreInitContract { } } - pub(crate) fn querier(&self) -> QuerierWrapper { + pub(crate) fn querier(&self) -> QuerierWrapper<'_> { self.tester_builder.querier() } diff --git a/contracts/vesting/src/vesting/mod.rs b/contracts/vesting/src/vesting/mod.rs index 48e0f48dc3..8c37ed5eac 100644 --- a/contracts/vesting/src/vesting/mod.rs +++ b/contracts/vesting/src/vesting/mod.rs @@ -137,7 +137,7 @@ mod tests { let response = execute( deps.as_mut(), env.clone(), - message_info(&admin, &[amount.clone()]), + message_info(&admin, std::slice::from_ref(&amount)), msg, ); assert_eq!( diff --git a/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/custom-provider.mdx b/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/custom-provider.mdx index 3f833616be..be32aecb2d 100644 --- a/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/custom-provider.mdx +++ b/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/custom-provider.mdx @@ -20,16 +20,22 @@ use nym_sdk::mixnet; use nym_sdk::mixnet::MixnetMessageSender; use nym_topology::provider_trait::{async_trait, TopologyProvider}; use nym_topology::{nym_topology_from_detailed, NymTopology}; +use nym_validator_client::nym_api::NymApiClientExt; use url::Url; struct MyTopologyProvider { - validator_client: nym_validator_client::client::NymApiClient, + validator_client: nym_http_api_client::Client, } impl MyTopologyProvider { fn new(nym_api_url: Url) -> MyTopologyProvider { + let validator_client = nym_http_api_client::Client::builder::<_, nym_validator_client::models::RequestError>(nym_api_url) + .expect("Failed to create API client builder") + .build::() + .expect("Failed to build API client"); + MyTopologyProvider { - validator_client: nym_validator_client::client::NymApiClient::new(nym_api_url), + validator_client, } } diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 87a8eb9ed2..b055dbe79b 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -93,6 +93,7 @@ nym-task = { path = "../common/task" } nym-topology = { path = "../common/topology" } nym-api-requests = { path = "nym-api-requests" } nym-validator-client = { path = "../common/client-libs/validator-client" } +nym-http-api-client = { path = "../common/http-api-client" } nym-bin-common = { path = "../common/bin-common", features = ["output_format", "openapi", "basic_tracing"] } nym-node-tester-utils = { path = "../common/node-tester-utils" } nym-node-requests = { path = "../nym-node/nym-node-requests" } diff --git a/nym-api/nym-api-requests/bindings/ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts b/nym-api/nym-api-requests/bindings/ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts index 7a7d1ed793..1b60aaeeaf 100644 --- a/nym-api/nym-api-requests/bindings/ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts +++ b/nym-api/nym-api-requests/bindings/ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type GatewayCoreStatusResponse = { identity: string, count: number, }; +export type GatewayCoreStatusResponse = { identity: string, count: bigint, }; diff --git a/nym-api/nym-api-requests/bindings/ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts b/nym-api/nym-api-requests/bindings/ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts index 8883216403..2ad20c4401 100644 --- a/nym-api/nym-api-requests/bindings/ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts +++ b/nym-api/nym-api-requests/bindings/ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type MixnodeCoreStatusResponse = { mix_id: number, count: number, }; +export type MixnodeCoreStatusResponse = { mix_id: number, count: bigint, }; diff --git a/nym-api/src/ecash/error.rs b/nym-api/src/ecash/error.rs index 5e4c0e6e89..cd7a205eae 100644 --- a/nym-api/src/ecash/error.rs +++ b/nym-api/src/ecash/error.rs @@ -13,6 +13,7 @@ use nym_dkg::Threshold; use nym_ecash_contract_common::deposit::DepositId; use nym_ecash_contract_common::redeem_credential::BATCH_REDEMPTION_PROPOSAL_TITLE; use nym_validator_client::coconut::EcashApiError; +use nym_validator_client::nym_api::error::NymAPIError; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::AccountId; use thiserror::Error; @@ -46,6 +47,9 @@ pub enum EcashError { #[error("coconut api query failure: {0}")] CoconutApiError(#[from] EcashApiError), + #[error("nym api query failure: {0}")] + NymApiError(Box), + #[error(transparent)] SerdeJsonError(#[from] serde_json::Error), @@ -161,6 +165,12 @@ pub enum EcashError { MerkleProofGenerationFailure, } +impl From for EcashError { + fn from(e: NymAPIError) -> Self { + EcashError::NymApiError(Box::new(e)) + } +} + // impl<'r, 'o: 'r> Responder<'r, 'o> for EcashError { // fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { // let err_msg = self.to_string(); diff --git a/nym-api/src/ecash/state/mod.rs b/nym-api/src/ecash/state/mod.rs index dd817e4952..d18661e70d 100644 --- a/nym-api/src/ecash/state/mod.rs +++ b/nym-api/src/ecash/state/mod.rs @@ -46,6 +46,7 @@ use nym_ecash_contract_common::redeem_credential::BATCH_REDEMPTION_PROPOSAL_TITL use nym_ecash_time::{ecash_default_expiration_date, ecash_today_date}; use nym_task::ShutdownManager; use nym_ticketbooks_merkle::{IssuedTicketbook, IssuedTicketbooksFullMerkleProof, MerkleLeaf}; +use nym_validator_client::nym_api::NymApiClientExt; use nym_validator_client::nyxd::AccountId; use nym_validator_client::EcashApiClient; use rand::{thread_rng, RngCore}; diff --git a/nym-api/src/ecash/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs index db067962b9..9cd13fd06e 100644 --- a/nym-api/src/ecash/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -67,7 +67,7 @@ use nym_validator_client::nym_api::routes::{ use nym_validator_client::nyxd::cosmwasm_client::logs::Log; use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; use nym_validator_client::nyxd::{AccountId, ExecTxResult, Fee, Hash, TxResponse}; -use nym_validator_client::{EcashApiClient, NymApiClient}; +use nym_validator_client::EcashApiClient; use rand::rngs::OsRng; use rand::RngCore; use std::collections::{BTreeMap, HashMap}; @@ -1148,7 +1148,10 @@ impl DummyCommunicationChannel { cosmos_address: AccountId, ) -> Self { let client = EcashApiClient { - api_client: NymApiClient::new("http://localhost:1234".parse().unwrap()), + api_client: nym_http_api_client::Client::new( + "http://localhost:1234".parse().unwrap(), + None, + ), verification_key: aggregated_verification_key, node_id: 1, cosmos_address, diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs index 096555aa26..b72c1a9103 100644 --- a/nym-api/src/node_describe_cache/mod.rs +++ b/nym-api/src/node_describe_cache/mod.rs @@ -30,7 +30,7 @@ pub enum NodeDescribeCacheError { node_id: NodeId, #[source] - source: NymNodeApiClientError, + source: Box, }, #[error("node {node_id} with host '{host}' doesn't seem to expose its declared http port nor any of the standard API ports, i.e.: 80, 443 or {}", DEFAULT_NYM_NODE_HTTP_PORT)] @@ -41,7 +41,7 @@ pub enum NodeDescribeCacheError { node_id: NodeId, #[source] - source: NymNodeApiClientError, + source: Box, }, // TODO: perhaps include more details here like whether key/signature/payload was malformed diff --git a/nym-api/src/node_describe_cache/query_helpers.rs b/nym-api/src/node_describe_cache/query_helpers.rs index 78611ca624..182257bcc0 100644 --- a/nym-api/src/node_describe_cache/query_helpers.rs +++ b/nym-api/src/node_describe_cache/query_helpers.rs @@ -41,7 +41,10 @@ pub(crate) async fn query_for_described_data( client: &Client, node_id: NodeId, ) -> Result { - let map_query_err = |source| NodeDescribeCacheError::ApiFailure { node_id, source }; + let map_query_err = |source| NodeDescribeCacheError::ApiFailure { + node_id, + source: Box::new(source), + }; // all of those should be happening concurrently. NodeDescribedInfoMegaFuture::new( diff --git a/nym-api/src/node_describe_cache/refresh.rs b/nym-api/src/node_describe_cache/refresh.rs index 6f3949c6c6..8bcd46f7e0 100644 --- a/nym-api/src/node_describe_cache/refresh.rs +++ b/nym-api/src/node_describe_cache/refresh.rs @@ -132,7 +132,7 @@ async fn try_get_client( return Err(NodeDescribeCacheError::MalformedHost { host: host.to_string(), node_id, - source: err, + source: Box::new(err), }); } }; @@ -158,7 +158,7 @@ async fn try_get_description( let map_query_err = |err| NodeDescribeCacheError::ApiFailure { node_id: data.node_id, - source: err, + source: Box::new(err), }; let host_info = client.get_host_information().await.map_err(map_query_err)?; diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs index d9247f15b8..e9b16514f9 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs @@ -22,11 +22,15 @@ pub struct VpnApiClient { bearer_token: String, } +#[allow(clippy::result_large_err)] pub fn new_client( base_url: impl IntoUrl, bearer_token: impl Into, ) -> Result { - let url = base_url.into_url()?; + let raw = base_url.as_str().to_string(); + let url = base_url + .into_url() + .map_err(|source| VpnApiClientError::MalformedUrl { raw, source })?; Ok(VpnApiClient { inner: Client::builder(url)? .with_user_agent(format!( @@ -97,9 +101,14 @@ impl NymVpnApiClient for VpnApiClient { { let req = self .inner - .create_get_request(path, NO_PARAMS) + .create_get_request(path, NO_PARAMS)? .bearer_auth(&self.bearer_token) - .send(); + .build() + .map_err(VpnApiClientError::reqwest_client_build_error)?; + + let url = req.url().clone(); + + let req = reqwest::Client::new().execute(req); // the only reason for that target lock is so that I could call this method from an ephemeral test // running in non-wasm mode (since I wanted to use tokio) @@ -110,8 +119,9 @@ impl NymVpnApiClient for VpnApiClient { .map_err(|_timeout| HttpClientError::RequestTimeout)??; #[cfg(not(target_arch = "wasm32"))] - let res = req.await?; - + let res = req + .await + .map_err(|source| VpnApiClientError::request_send_error(url, source))?; parse_response(res, false).await } @@ -129,9 +139,14 @@ impl NymVpnApiClient for VpnApiClient { { let req = self .inner - .create_post_request(path, params, json_body) + .create_post_request(path, params, json_body)? .bearer_auth(&self.bearer_token) - .send(); + .build() + .map_err(VpnApiClientError::reqwest_client_build_error)?; + + let url = req.url().clone(); + + let req = reqwest::Client::new().execute(req); // the only reason for that target lock is so that I could call this method from an ephemeral test // running in non-wasm mode (since I wanted to use tokio) @@ -142,7 +157,9 @@ impl NymVpnApiClient for VpnApiClient { .map_err(|_timeout| HttpClientError::RequestTimeout)??; #[cfg(not(target_arch = "wasm32"))] - let res = req.await?; + let res = req + .await + .map_err(|source| VpnApiClientError::request_send_error(url, source))?; parse_response(res, false).await } diff --git a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/nym-network-monitor/Cargo.toml b/nym-network-monitor/Cargo.toml index c693cb5b19..a84a5f3c02 100644 --- a/nym-network-monitor/Cargo.toml +++ b/nym-network-monitor/Cargo.toml @@ -40,3 +40,5 @@ nym-sphinx = { path = "../common/nymsphinx" } nym-topology = { path = "../common/topology" } nym-types = { path = "../common/types" } nym-validator-client = { path = "../common/client-libs/validator-client" } +nym-http-api-client = { path = "../common/http-api-client" } +nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" } diff --git a/nym-network-monitor/src/main.rs b/nym-network-monitor/src/main.rs index d0d04d62c6..4dc9980f00 100644 --- a/nym-network-monitor/src/main.rs +++ b/nym-network-monitor/src/main.rs @@ -6,12 +6,15 @@ use log::{info, warn}; use nym_bin_common::bin_info; use nym_client_core::config::ForgetMe; use nym_crypto::asymmetric::ed25519::PrivateKey; +use nym_mixnet_contract_common::EpochRewardedSet; use nym_network_defaults::setup_env; use nym_network_defaults::var_names::NYM_API; use nym_sdk::mixnet::{self, MixnetClient}; use nym_sphinx::chunking::monitoring; use nym_topology::provider_trait::ToTopologyMetadata; use nym_topology::{HardcodedTopologyProvider, NymTopology}; +use nym_validator_client::nym_api::NymApiClientExt; +use nym_validator_client::UserAgent; use std::fs::File; use std::io::Write; use std::sync::LazyLock; @@ -160,10 +163,9 @@ async fn nym_topology_from_env() -> anyhow::Result { let api_url = std::env::var(NYM_API)?; info!("Generating topology from {api_url}"); - let client = nym_validator_client::client::NymApiClient::new_with_user_agent( - api_url.parse()?, - bin_info!(), - ); + let client = nym_http_api_client::Client::builder(api_url)? + .with_user_agent(UserAgent::from(bin_info!())) + .build()?; let rewarded_set = client.get_current_rewarded_set().await?; @@ -172,10 +174,15 @@ async fn nym_topology_from_env() -> anyhow::Result { let nodes = nodes_response.nodes; let metadata = nodes_response.metadata; - Ok( - NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new()) - .with_skimmed_nodes(&nodes), + // Convert RewardedSetResponse to EpochRewardedSet which can then be converted to CachedEpochRewardedSet + let epoch_rewarded_set: EpochRewardedSet = rewarded_set.into(); + + Ok(NymTopology::new( + metadata.to_topology_metadata(), + epoch_rewarded_set, + Vec::new(), ) + .with_skimmed_nodes(&nodes)) } #[tokio::main] diff --git a/nym-node-status-api/nym-node-status-api/src/metrics_scraper/error.rs b/nym-node-status-api/nym-node-status-api/src/metrics_scraper/error.rs index 99cff49a06..86d28d7d08 100644 --- a/nym-node-status-api/nym-node-status-api/src/metrics_scraper/error.rs +++ b/nym-node-status-api/nym-node-status-api/src/metrics_scraper/error.rs @@ -10,7 +10,7 @@ pub enum NodeScraperError { host: String, node_id: NodeId, #[source] - source: NymNodeApiClientError, + source: Box, }, #[error("node {node_id} with host '{host}' doesn't seem to expose its declared http port nor any of the standard API ports, i.e.: 80, 443 or {}", DEFAULT_NYM_NODE_HTTP_PORT)] @@ -22,14 +22,19 @@ mod tests { use super::*; use std::error::Error; + fn dummy_url() -> reqwest::Url { + "http://nym.com".parse().unwrap() + } + #[test] + #[allow(deprecated)] fn test_malformed_host_error() { // Create a generic error to test with let source_error = NymNodeApiClientError::GenericRequestFailure("Invalid URL".to_string()); let error = NodeScraperError::MalformedHost { host: "invalid-host:abc".to_string(), node_id: 42, - source: source_error, + source: Box::new(source_error), }; // Test error message formatting @@ -43,12 +48,13 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_malformed_host_error_edge_cases() { // Test with empty host let error = NodeScraperError::MalformedHost { host: "".to_string(), node_id: 0, - source: NymNodeApiClientError::NotFound, + source: Box::new(NymNodeApiClientError::NotFound { url: dummy_url() }), }; let error_msg = error.to_string(); @@ -59,7 +65,9 @@ mod tests { let error = NodeScraperError::MalformedHost { host: long_host.clone(), node_id: u32::MAX, - source: NymNodeApiClientError::GenericRequestFailure("Too long".to_string()), + source: Box::new(NymNodeApiClientError::GenericRequestFailure( + "Too long".to_string(), + )), }; let error_msg = error.to_string(); @@ -98,20 +106,21 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_error_different_sources() { // Test with different NymNodeApiClientError variants - let not_found_error = NymNodeApiClientError::NotFound; + let not_found_error = NymNodeApiClientError::NotFound { url: dummy_url() }; let error1 = NodeScraperError::MalformedHost { host: "host1".to_string(), node_id: 1, - source: not_found_error, + source: Box::new(not_found_error), }; let generic_error = NymNodeApiClientError::GenericRequestFailure("404 error".to_string()); let error2 = NodeScraperError::MalformedHost { host: "host2".to_string(), node_id: 2, - source: generic_error, + source: Box::new(generic_error), }; // Both should format differently based on their source diff --git a/nym-node-status-api/nym-node-status-api/src/metrics_scraper/mod.rs b/nym-node-status-api/nym-node-status-api/src/metrics_scraper/mod.rs index bc3cd23fe7..8766141737 100644 --- a/nym-node-status-api/nym-node-status-api/src/metrics_scraper/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/metrics_scraper/mod.rs @@ -5,11 +5,11 @@ use nym_node_requests::api::{client::NymNodeApiClientExt, v1::metrics::models::S use nym_validator_client::{ client::{NodeId, NymNodeDetails}, models::{DescribedNodeType, NymNodeDescription}, - NymApiClient, }; use time::OffsetDateTime; use nym_statistics_common::types::SessionType; +use nym_validator_client::client::NymApiClientExt; use std::collections::HashMap; use tokio::time::Duration; use tracing::instrument; @@ -60,13 +60,11 @@ async fn run( let nym_api = nym_http_api_client::ClientBuilder::new_with_urls(vec![default_api_url.into()]) .no_hickory_dns() .with_timeout(nym_api_client_timeout) - .build::<&str>()?; - - let api_client = NymApiClient::from(nym_api); + .build()?; //SW TBC what nodes exactly need to be scraped, the skimmed node endpoint seems to return more nodes - let bonded_nodes = api_client.get_all_bonded_nym_nodes().await?; - let all_nodes = api_client.get_all_described_nodes().await?; //legacy node that did not upgrade the contract bond yet + let bonded_nodes = nym_api.get_all_bonded_nym_nodes().await?; + let all_nodes = nym_api.get_all_described_nodes().await?; //legacy node that did not upgrade the contract bond yet tracing::debug!("Fetched {} total nodes", all_nodes.len()); let mut nodes_to_scrape: HashMap = bonded_nodes @@ -183,7 +181,7 @@ impl MetricsScrapingData { return Err(NodeScraperError::MalformedHost { host: self.host.to_string(), node_id: self.node_id, - source: err, + source: Box::new(err), }); } }; diff --git a/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs index d41d0cedc9..1fec5c0a76 100644 --- a/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs @@ -19,7 +19,7 @@ use nym_validator_client::{ use nym_validator_client::{ nym_nodes::{NodeRole, SkimmedNode}, nyxd::{contract_traits::PagedMixnetQueryClient, AccountId}, - NymApiClient, QueryHttpRpcNyxdClient, + QueryHttpRpcNyxdClient, }; use std::{ collections::{HashMap, HashSet}, @@ -109,11 +109,9 @@ impl Monitor { nym_http_api_client::ClientBuilder::new_with_urls(vec![default_api_url.into()]) .no_hickory_dns() .with_timeout(self.nym_api_client_timeout) - .build::<&str>()?; + .build()?; - let api_client = NymApiClient::from(nym_api); - - let described_nodes = api_client + let described_nodes = nym_api .get_all_described_nodes() .await .log_error("get_all_described_nodes")? @@ -135,7 +133,7 @@ impl Monitor { tracing::info!("🟣 🚪 gateway nodes: {}", gateways.len()); - let bonded_nym_nodes = api_client + let bonded_nym_nodes = nym_api .get_all_bonded_nym_nodes() .await? .into_iter() @@ -146,10 +144,11 @@ impl Monitor { tracing::info!("🟣 bonded_nodes: {}", bonded_nym_nodes.len()); // returns only bonded nodes - let nym_nodes = api_client - .get_all_basic_nodes() + let nym_nodes = nym_api + .get_all_basic_nodes_with_metadata() .await - .log_error("get_all_basic_nodes")?; + .log_error("get_all_basic_nodes")? + .nodes; let nym_node_count = nym_nodes.len(); tracing::info!("🟣 get_all_basic_nodes: {}", nym_node_count); @@ -167,8 +166,7 @@ impl Monitor { self.location_cached(node_description).await; } - let mixnodes_detailed = api_client - .nym_api + let mixnodes_detailed = nym_api .get_mixnodes_detailed_unfiltered() .await .log_error("get_mixnodes_detailed_unfiltered")?; @@ -190,15 +188,13 @@ impl Monitor { }) .collect::>(); - let mixnodes_described = api_client - .nym_api + let mixnodes_described = nym_api .get_mixnodes_described() .await .log_error("get_mixnodes_described")?; tracing::info!("🟣 mixnodes_described: {}", mixnodes_described.len()); - let mixing_assigned_nodes = api_client - .nym_api + let mixing_assigned_nodes = nym_api .get_basic_active_mixing_assigned_nodes(false, None, None, false) .await .log_error("get_basic_active_mixing_assigned_nodes")? diff --git a/nym-node/nym-node-requests/src/api/client.rs b/nym-node/nym-node-requests/src/api/client.rs index 2d21e5477c..e8a5d5205e 100644 --- a/nym-node/nym-node-requests/src/api/client.rs +++ b/nym-node/nym-node-requests/src/api/client.rs @@ -5,7 +5,6 @@ use crate::api::v1::gateway::models::WebSockets; use crate::api::v1::node::models::{ AuxiliaryDetails, NodeDescription, NodeRoles, SignedHostInformation, }; -use crate::api::ErrorResponse; use crate::routes; use async_trait::async_trait; use nym_bin_common::build_information::BinaryBuildInformationOwned; @@ -21,7 +20,7 @@ use crate::api::v1::network_requester::models::NetworkRequester; use crate::api::v1::node_load::models::NodeLoad; pub use nym_http_api_client::Client; -pub type NymNodeApiClientError = HttpClientError; +pub type NymNodeApiClientError = HttpClientError; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] diff --git a/nym-node/src/error.rs b/nym-node/src/error.rs index 37bf7eb59a..e199e3aef1 100644 --- a/nym-node/src/error.rs +++ b/nym-node/src/error.rs @@ -302,6 +302,6 @@ pub enum ServiceProvidersError { impl From for NymNodeError { fn from(value: HttpClientError) -> Self { - Self::HttpFailure(NymNodeHttpError::ClientError { source: value }) + Self::HttpFailure(NymNodeHttpError::from(value)) } } diff --git a/nym-node/src/node/http/error.rs b/nym-node/src/node/http/error.rs index 178e26fdcd..de3d746b48 100644 --- a/nym-node/src/node/http/error.rs +++ b/nym-node/src/node/http/error.rs @@ -27,8 +27,13 @@ pub enum NymNodeHttpError { }, #[error("error building or using HTTP client: {source}")] - ClientError { - #[from] - source: HttpClientError, - }, + ClientError { source: Box }, +} + +impl From for NymNodeHttpError { + fn from(source: HttpClientError) -> Self { + NymNodeHttpError::ClientError { + source: Box::new(source), + } + } } diff --git a/nym-node/src/node/nym_apis_client.rs b/nym-node/src/node/nym_apis_client.rs index 08ef68aff7..869270f163 100644 --- a/nym-node/src/node/nym_apis_client.rs +++ b/nym-node/src/node/nym_apis_client.rs @@ -10,7 +10,6 @@ use nym_task::ShutdownToken; use nym_validator_client::client::NymApiClientExt; use nym_validator_client::models::{KeyRotationInfoResponse, NodeRefreshBody}; use nym_validator_client::nym_api::error::NymAPIError; -use nym_validator_client::NymApiClient; use rand::prelude::SliceRandom; use rand::thread_rng; use std::sync::Arc; @@ -27,7 +26,7 @@ pub struct NymApisClient { struct InnerClient { // NOTE: this was implemented before the internal http client supported multiple URLs - active_client: NymApiClient, + active_client: Client, available_urls: Vec, shutdown_token: ShutdownToken, currently_used_api: usize, @@ -45,7 +44,7 @@ impl NymApisClient { let mut urls = nym_apis.to_vec(); urls.shuffle(&mut thread_rng()); - let active_client = nym_http_api_client::Client::builder(urls[0].clone())? + let active_client = Client::builder(urls[0].clone())? .no_hickory_dns() .with_user_agent(NymNode::user_agent()) .with_timeout(Duration::from_secs(5)) @@ -53,7 +52,7 @@ impl NymApisClient { Ok(NymApisClient { inner: Arc::new(RwLock::new(InnerClient { - active_client: NymApiClient::from(active_client), + active_client: active_client.clone(), available_urls: urls, shutdown_token, currently_used_api: 0, @@ -88,9 +87,19 @@ impl NymApisClient { if guard.currently_used_api != last_working_endpoint { drop(guard); let mut guard = self.inner.write().await; - let next_url = guard.available_urls[last_working_endpoint].clone(); guard.currently_used_api = last_working_endpoint; - guard.active_client.change_nym_api(next_url); + + // Provide all URLs starting from the working endpoint for automatic failover + let rotated_urls: Vec<_> = guard + .available_urls + .iter() + .cycle() + .skip(last_working_endpoint) + .take(guard.available_urls.len()) + .map(|u| u.clone().into()) + .collect(); + + guard.active_client.change_base_urls(rotated_urls); } Ok(res) @@ -123,10 +132,10 @@ impl InnerClient { { let broadcast_fut = stream::iter(self.available_urls.clone()).for_each_concurrent(None, |url| { - let nym_api = self - .active_client - .nym_api - .clone_with_new_url(url.clone().into()); + let mut nym_api = self.active_client.clone(); + // For broadcast, we intentionally set a single URL per client + // to ensure each endpoint receives the request + nym_api.change_base_urls(vec![url.clone().into()]); let req_fut = req(nym_api, request_body); async move { if let Err(err) = req_fut.await { @@ -172,10 +181,10 @@ impl InnerClient { .skip(last_working) .chain(self.available_urls.iter().enumerate().take(last_working)) { - let nym_api = self - .active_client - .nym_api - .clone_with_new_url(url.clone().into()); + let mut nym_api = self.active_client.clone(); + // For exhaustive query, we test each endpoint individually in sequence + // to find a working one - so single URL is correct here + nym_api.change_base_urls(vec![url.clone().into()]); let timeout_fut = sleep(timeout_duration); let query_fut = req(nym_api); @@ -216,8 +225,8 @@ impl InnerClient { } } -impl AsRef for InnerClient { - fn as_ref(&self) -> &NymApiClient { +impl AsRef for InnerClient { + fn as_ref(&self) -> &Client { &self.active_client } } diff --git a/nym-node/src/node/shared_network.rs b/nym-node/src/node/shared_network.rs index f6a8164862..b87475254e 100644 --- a/nym-node/src/node/shared_network.rs +++ b/nym-node/src/node/shared_network.rs @@ -7,6 +7,7 @@ use crate::node::routing_filter::network_filter::NetworkRoutingFilter; use async_trait::async_trait; use nym_crypto::asymmetric::ed25519; use nym_gateway::node::UserAgent; +use nym_http_api_client::Client; use nym_node_metrics::prometheus_wrapper::{PrometheusMetric, PROMETHEUS_METRICS}; use nym_noise::config::NoiseNetworkView; use nym_task::ShutdownToken; @@ -20,7 +21,7 @@ use nym_validator_client::nym_api::NymApiClientExt; use nym_validator_client::nym_nodes::{ NodesByAddressesResponse, SemiSkimmedNode, SemiSkimmedNodesWithMetadata, }; -use nym_validator_client::{NymApiClient, ValidatorClientError}; +use nym_validator_client::ValidatorClientError; use std::collections::{HashMap, HashSet}; use std::net::{IpAddr, SocketAddr}; use std::ops::Deref; @@ -35,7 +36,7 @@ use url::Url; const LOCAL_NODE_ID: NodeId = 1234567890; struct NodesQuerier { - client: NymApiClient, + client: Client, nym_api_urls: Vec, currently_used_api: usize, } @@ -49,7 +50,7 @@ impl NodesQuerier { self.currently_used_api = (self.currently_used_api + 1) % self.nym_api_urls.len(); self.client - .change_nym_api(self.nym_api_urls[self.currently_used_api].clone()) + .change_base_urls(self.nym_api_urls.iter().map(|u| u.clone().into()).collect()) } async fn rewarded_set(&mut self) -> Result { @@ -62,7 +63,7 @@ impl NodesQuerier { if res.is_err() { self.use_next_nym_api() } - res + Ok(res?.into()) } async fn current_nymnodes( @@ -77,7 +78,7 @@ impl NodesQuerier { if res.is_err() { self.use_next_nym_api() } - res + Ok(res?) } async fn query_for_info( @@ -86,7 +87,6 @@ impl NodesQuerier { ) -> Result { let res = self .client - .nym_api .nodes_by_addresses(ips) .await .inspect_err(|err| error!("failed to obtain node information: {err}")); @@ -228,7 +228,7 @@ impl NetworkRefresher { let mut this = NetworkRefresher { querier: NodesQuerier { - client: NymApiClient::from(nym_api), + client: nym_api, nym_api_urls, currently_used_api: 0, }, diff --git a/nym-statistics-api/src/network_view.rs b/nym-statistics-api/src/network_view.rs index c8e41f9623..db3a153de0 100644 --- a/nym-statistics-api/src/network_view.rs +++ b/nym-statistics-api/src/network_view.rs @@ -6,7 +6,6 @@ use nym_task::ShutdownToken; use celes::Country; use nym_validator_client::models::NymNodeDescription; -use nym_validator_client::NymApiClient; use std::collections::HashMap; use std::time::Duration; use std::{net::IpAddr, sync::Arc}; @@ -14,6 +13,8 @@ use tokio::sync::RwLock; use tokio::time::interval; use url::Url; +use nym_http_api_client::Client; +use nym_validator_client::client::NymApiClientExt; use tracing::{error, info, trace, warn}; const NETWORK_CACHE_TTL: Duration = Duration::from_secs(600); @@ -22,7 +23,7 @@ type IpToCountryMap = HashMap>; // SW this should use a proper NS API client once it exists struct NodesQuerier { - client: NymApiClient, + client: Client, } impl NodesQuerier { @@ -99,14 +100,11 @@ impl NetworkRefresher { this } - fn build_http_api_client(url: Url) -> Result { - Ok( - nym_http_api_client::Client::builder::<_, anyhow::Error>(url)? - .no_hickory_dns() - .with_user_agent("node-statistics-api") - .build::()? - .into(), - ) + fn build_http_api_client(url: Url) -> Result { + Ok(Client::builder(url)? + .no_hickory_dns() + .with_user_agent("node-statistics-api") + .build()?) } async fn refresh_network_nodes(&mut self) -> Result<()> { diff --git a/nym-validator-rewarder/Cargo.toml b/nym-validator-rewarder/Cargo.toml index 65e2781fd1..be1ae056dc 100644 --- a/nym-validator-rewarder/Cargo.toml +++ b/nym-validator-rewarder/Cargo.toml @@ -42,6 +42,7 @@ nym-credentials = { path = "../common/credentials" } nym-network-defaults = { path = "../common/network-defaults" } nym-task = { path = "../common/task" } nym-validator-client = { path = "../common/client-libs/validator-client" } +nym-http-api-client = { path = "../common/http-api-client" } nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" } nyxd-scraper = { path = "../common/nyxd-scraper" } nym-ticketbooks-merkle = { path = "../common/ticketbooks-merkle" } diff --git a/nym-validator-rewarder/src/cli/dry_run_check_issuer.rs b/nym-validator-rewarder/src/cli/dry_run_check_issuer.rs index 49d0d250d6..f8974bd601 100644 --- a/nym-validator-rewarder/src/cli/dry_run_check_issuer.rs +++ b/nym-validator-rewarder/src/cli/dry_run_check_issuer.rs @@ -9,6 +9,7 @@ use crate::rewarder::ticketbook_issuance::verifier::TicketbookIssuanceVerifier; use crate::rewarder::Rewarder; use anyhow::bail; use nym_ecash_time::ecash_default_expiration_date; +use nym_validator_client::nym_api::NymApiClientExt; use std::collections::HashSet; use std::path::PathBuf; use time::macros::format_description; diff --git a/nym-validator-rewarder/src/error.rs b/nym-validator-rewarder/src/error.rs index 211d9b0522..31bf710efb 100644 --- a/nym-validator-rewarder/src/error.rs +++ b/nym-validator-rewarder/src/error.rs @@ -130,7 +130,7 @@ pub enum NymRewarderError { }, #[error("failed to resolve nym-api query: {0}")] - ApiQueryFailure(#[from] NymAPIError), + ApiQueryFailure(Box), #[error("operator {runner_account} didn't return all requested credentials! requested {requested} but got only {received}")] IncompleteRequest { @@ -213,6 +213,12 @@ pub enum NymRewarderError { NoBlocksProcessedInEpoch { epoch: Epoch }, } +impl From for NymRewarderError { + fn from(err: NymAPIError) -> Self { + NymRewarderError::ApiQueryFailure(Box::new(err)) + } +} + #[derive(Debug)] pub struct InsufficientBalance { pub daily_budget: Coin, diff --git a/nym-validator-rewarder/src/rewarder/nyxd_client.rs b/nym-validator-rewarder/src/rewarder/nyxd_client.rs index bf2bd9e43c..c5de2c37b1 100644 --- a/nym-validator-rewarder/src/rewarder/nyxd_client.rs +++ b/nym-validator-rewarder/src/rewarder/nyxd_client.rs @@ -15,7 +15,7 @@ use nym_validator_client::nyxd::module_traits::staking::{ use nym_validator_client::nyxd::{ AccountId, Coin, CosmWasmClient, Hash, PageRequest, StakingQueryClient, }; -use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient, NymApiClient}; +use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; use std::collections::HashMap; use std::ops::Deref; use std::sync::Arc; @@ -139,10 +139,23 @@ impl NyxdClient { continue; }; + let api_client = match nym_http_api_client::Client::builder(api_address) + .and_then(|b| b.build()) + { + Ok(client) => client, + Err(err) => { + error!( + "Failed to create API client for issuer {}: {}", + info.assigned_index, err + ); + continue; + } + }; + issuers.push(CredentialIssuer { public_key, operator_account: addr_to_account_id(share.owner), - api_client: NymApiClient::new(api_address), + api_client, verification_key, node_id: info.assigned_index, }) diff --git a/nym-validator-rewarder/src/rewarder/ticketbook_issuance/types.rs b/nym-validator-rewarder/src/rewarder/ticketbook_issuance/types.rs index dc396a7321..f677c0e9bc 100644 --- a/nym-validator-rewarder/src/rewarder/ticketbook_issuance/types.rs +++ b/nym-validator-rewarder/src/rewarder/ticketbook_issuance/types.rs @@ -6,8 +6,8 @@ use cosmwasm_std::{Addr, Decimal, Uint128}; use nym_coconut_dkg_common::types::NodeIndex; use nym_compact_ecash::VerificationKeyAuth; use nym_crypto::asymmetric::ed25519; +use nym_validator_client::nym_api::NymApiClientExt; use nym_validator_client::nyxd::{AccountId, Coin}; -use nym_validator_client::NymApiClient; use std::fmt::{Display, Formatter}; use tracing::info; @@ -68,7 +68,7 @@ impl TicketbookIssuanceResults { pub struct CredentialIssuer { pub public_key: ed25519::PublicKey, pub operator_account: AccountId, - pub api_client: NymApiClient, + pub api_client: nym_http_api_client::Client, pub verification_key: VerificationKeyAuth, pub node_id: NodeIndex, } diff --git a/nym-validator-rewarder/src/rewarder/ticketbook_issuance/verifier.rs b/nym-validator-rewarder/src/rewarder/ticketbook_issuance/verifier.rs index cb0964caf3..dcd7525476 100644 --- a/nym-validator-rewarder/src/rewarder/ticketbook_issuance/verifier.rs +++ b/nym-validator-rewarder/src/rewarder/ticketbook_issuance/verifier.rs @@ -16,6 +16,7 @@ use nym_validator_client::ecash::models::{ IssuedTicketbooksChallengeCommitmentResponse, IssuedTicketbooksDataRequestBody, IssuedTicketbooksDataResponse, IssuedTicketbooksDataResponseBody, IssuedTicketbooksForResponse, }; +use nym_validator_client::nym_api::NymApiClientExt; use nym_validator_client::nyxd::AccountId; use nym_validator_client::signable::{SignableMessageBody, SignedMessage}; use rand::distributions::{Distribution, WeightedIndex}; diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 248771db3f..4603c90e9b 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3384,6 +3384,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inventory" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +dependencies = [ + "rustversion", +] + [[package]] name = "io-uring" version = "0.7.10" @@ -4214,23 +4223,39 @@ dependencies = [ "async-trait", "bincode", "bytes", + "cfg-if", "encoding_rs", "hickory-resolver", "http 1.3.1", + "inventory", "itertools 0.14.0", "mime", "nym-bin-common", + "nym-http-api-client-macro", "nym-http-api-common", "once_cell", "reqwest 0.12.15", "serde", "serde_json", + "serde_plain", + "serde_yaml", "thiserror 2.0.12", "tracing", "url", "wasmtimer", ] +[[package]] +name = "nym-http-api-client-macro" +version = "0.1.0" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.100", + "uuid", +] + [[package]] name = "nym-http-api-common" version = "0.1.0" @@ -6309,6 +6334,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -6371,6 +6405,19 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.8.0", + "itoa 1.0.15", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serdect" version = "0.2.0" @@ -7860,6 +7907,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index 3e3123e088..6f491d4532 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -2,7 +2,6 @@ use nym_contracts_common::signing::SigningAlgorithm; use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError; use nym_node_requests::api::client::NymNodeApiClientError; use nym_types::error::TypesError; -use nym_validator_client::nym_api::error::NymAPIError; use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWalletError; use nym_validator_client::{nyxd::error::NyxdError, ValidatorClientError}; use nym_wallet_types::network::Network; @@ -45,15 +44,7 @@ pub enum BackendError { source: eyre::Report, }, #[error(transparent)] - NymApiError { - #[from] - source: NymAPIError, - }, - #[error(transparent)] - NymNodeApiError { - #[from] - source: NymNodeApiClientError, - }, + NymNodeApiError { source: Box }, #[error(transparent)] IOError { #[from] @@ -214,3 +205,11 @@ impl From for BackendError { TypesError::from(e).into() } } + +impl From for BackendError { + fn from(e: NymNodeApiClientError) -> Self { + BackendError::NymNodeApiError { + source: Box::new(e), + } + } +} diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index 881b54aa3c..3fbf91bbe1 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -15,7 +15,6 @@ use nym_mixnet_contract_common::nym_node::{NodeConfigUpdate, StakeSaturationResp use nym_mixnet_contract_common::{MixNodeConfigUpdate, NodeId, NymNode}; use nym_node_requests::api::client::NymNodeApiClientExt; use nym_node_requests::api::v1::node::models::NodeDescription; -use nym_node_requests::api::ErrorResponse; use nym_types::currency::DecCoin; use nym_types::gateway::GatewayBond; use nym_types::mixnode::{MixNodeDetails, NodeCostParams}; @@ -586,14 +585,12 @@ pub async fn get_nym_node_description( port: u16, ) -> Result { Ok( - nym_node_requests::api::Client::builder::<_, ErrorResponse>(format!( - "http://{host}:{port}" - ))? - .with_timeout(Duration::from_millis(1000)) - .with_user_agent(format!("nym-wallet/{}", env!("CARGO_PKG_VERSION"))) - .build::()? - .get_description() - .await?, + nym_node_requests::api::Client::builder(format!("http://{host}:{port}"))? + .with_timeout(Duration::from_millis(1000)) + .with_user_agent(format!("nym-wallet/{}", env!("CARGO_PKG_VERSION"))) + .build()? + .get_description() + .await?, ) } diff --git a/sdk/rust/nym-sdk/Cargo.toml b/sdk/rust/nym-sdk/Cargo.toml index 9c67bbf898..523e5801c5 100644 --- a/sdk/rust/nym-sdk/Cargo.toml +++ b/sdk/rust/nym-sdk/Cargo.toml @@ -38,6 +38,7 @@ nym-socks5-client-core = { path = "../../../common/socks5-client-core" } nym-validator-client = { path = "../../../common/client-libs/validator-client", features = [ "http-client", ] } +nym-http-api-client = { path = "../../../common/http-api-client" } nym-socks5-requests = { path = "../../../common/socks5/requests" } nym-ordered-buffer = { path = "../../../common/socks5/ordered-buffer" } nym-service-providers-common = { path = "../../../service-providers/common" } diff --git a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs index 7b08e73f24..6f6fe5b730 100644 --- a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs +++ b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs @@ -4,18 +4,22 @@ use nym_sdk::mixnet; use nym_sdk::mixnet::MixnetMessageSender; use nym_topology::provider_trait::{async_trait, ToTopologyMetadata, TopologyProvider}; -use nym_topology::NymTopology; +use nym_topology::{EpochRewardedSet, NymTopology}; +use nym_validator_client::nym_api::NymApiClientExt; use url::Url; struct MyTopologyProvider { - validator_client: nym_validator_client::client::NymApiClient, + validator_client: nym_http_api_client::Client, } impl MyTopologyProvider { fn new(nym_api_url: Url) -> MyTopologyProvider { - MyTopologyProvider { - validator_client: nym_validator_client::client::NymApiClient::new(nym_api_url), - } + let validator_client = nym_http_api_client::Client::builder(nym_api_url) + .expect("Failed to create API client builder") + .build() + .expect("Failed to build API client"); + + MyTopologyProvider { validator_client } } async fn get_topology(&self) -> NymTopology { @@ -33,7 +37,8 @@ impl MyTopologyProvider { let metadata = mixnodes_response.metadata.to_topology_metadata(); - let mut base_topology = NymTopology::new(metadata, rewarded_set, Vec::new()); + let epoch_rewarded_set: EpochRewardedSet = rewarded_set.into(); + let mut base_topology = NymTopology::new(metadata, epoch_rewarded_set, Vec::new()); // in our topology provider only use mixnodes that have node_id divisible by 3 // and has exactly 100 performance score diff --git a/tools/internal/testnet-manager/Cargo.toml b/tools/internal/testnet-manager/Cargo.toml index bea71e6d01..6f799d84a4 100644 --- a/tools/internal/testnet-manager/Cargo.toml +++ b/tools/internal/testnet-manager/Cargo.toml @@ -37,6 +37,7 @@ nym-crypto = { path = "../../../common/crypto", features = ["asymmetric", "rand" nym-config = { path = "../../../common/config" } nym-validator-client = { path = "../../../common/client-libs/validator-client" } nym-compact-ecash = { path = "../../../common/nym_offline_compact_ecash" } +nym-http-api-client = { path = "../../../common/http-api-client" } dkg-bypass-contract = { path = "dkg-bypass-contract", default-features = false } # contracts: diff --git a/tools/internal/testnet-manager/src/manager/local_client.rs b/tools/internal/testnet-manager/src/manager/local_client.rs index 9e1d2025e2..8004c658df 100644 --- a/tools/internal/testnet-manager/src/manager/local_client.rs +++ b/tools/internal/testnet-manager/src/manager/local_client.rs @@ -7,7 +7,7 @@ use crate::manager::network::LoadedNetwork; use crate::manager::NetworkManager; use console::style; use nym_config::{must_get_home, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, NYM_DIR}; -use nym_validator_client::NymApiClient; +use nym_validator_client::nym_api::NymApiClientExt; use rand::{thread_rng, RngCore}; use std::fs; use std::fs::OpenOptions; @@ -91,7 +91,10 @@ impl NetworkManager { "⌛waiting for any gateway to appear in the directory ({api_url})..." )); - let api_client = NymApiClient::new(api_url); + let api_client = nym_http_api_client::Client::builder(api_url.clone()) + .expect("Failed to create API client builder") + .build() + .expect("Failed to build API client"); let wait_fut = async { let inner_fut = async { diff --git a/tools/internal/validator-status-check/Cargo.toml b/tools/internal/validator-status-check/Cargo.toml index 99eb182621..a0d09e336f 100644 --- a/tools/internal/validator-status-check/Cargo.toml +++ b/tools/internal/validator-status-check/Cargo.toml @@ -25,6 +25,7 @@ time = { workspace = true } nym-validator-client = { path = "../../../common/client-libs/validator-client" } nym-bin-common = { path = "../../../common/bin-common", features = ["output_format", "basic_tracing"] } nym-network-defaults = { path = "../../../common/network-defaults" } +nym-http-api-client = { path = "../../../common/http-api-client" } [lints] workspace = true diff --git a/tools/internal/validator-status-check/src/models.rs b/tools/internal/validator-status-check/src/models.rs index 12b8337f5d..11534118d2 100644 --- a/tools/internal/validator-status-check/src/models.rs +++ b/tools/internal/validator-status-check/src/models.rs @@ -1,8 +1,7 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_validator_client::client::NymApiClientExt; -use nym_validator_client::NymApiClient; +use nym_validator_client::nym_api::NymApiClientExt; use serde::{Deserialize, Serialize}; use std::fmt::Display; use strum::{Display, EnumProperty}; @@ -48,8 +47,8 @@ impl SignerStatus { matches!(self.rpc_status, RpcStatus::Up) } - fn build_api_client(&self) -> Option { - let api_endpoint = match self.api_endpoint.as_str().parse() { + fn build_api_client(&self) -> Option { + let api_endpoint: nym_http_api_client::Url = match self.api_endpoint.as_str().parse() { Ok(endpoint) => endpoint, Err(err) => { error!("{} is not a valid api endpoint: {err}", self.api_endpoint); @@ -57,14 +56,17 @@ impl SignerStatus { } }; - Some(NymApiClient::new(api_endpoint)) + nym_http_api_client::Client::builder(api_endpoint) + .ok()? + .build() + .ok() } pub(crate) async fn try_update_api_version(&mut self) { let Some(client) = self.build_api_client() else { return; }; - match client.nym_api.build_information().await { + match client.build_information().await { Ok(build_info) => { self.api_version = ApiVersion::Available { version: build_info.build_version, @@ -84,7 +86,7 @@ impl SignerStatus { return; }; - match client.nym_api.get_chain_status().await { + match client.get_chain_status().await { Ok(chain_status) => { self.used_rpc_endpoint = RpcEndpoint(chain_status.connected_nyxd); let last_block = diff --git a/wasm/zknym-lib/src/error.rs b/wasm/zknym-lib/src/error.rs index f232daddf3..5fe8794262 100644 --- a/wasm/zknym-lib/src/error.rs +++ b/wasm/zknym-lib/src/error.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::vpn_api_client::NymVpnApiClientError; +use nym_http_api_client::HttpClientError; use thiserror::Error; use wasm_utils::wasm_error; @@ -16,7 +16,7 @@ pub enum ZkNymError { #[error("failed to contact the vpn api")] HttpClientFailure { #[from] - source: NymVpnApiClientError, + source: Box, }, #[error("the provided shares and issuers are not from the same epoch! {shares} and {issuers}")] InconsistentEpochId { shares: u64, issuers: u64 }, diff --git a/wasm/zknym-lib/src/vpn_api_client/client.rs b/wasm/zknym-lib/src/vpn_api_client/client.rs index e3a7f983b0..843753b7f0 100644 --- a/wasm/zknym-lib/src/vpn_api_client/client.rs +++ b/wasm/zknym-lib/src/vpn_api_client/client.rs @@ -1,14 +1,14 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 - -use super::NymVpnApiClientError; use crate::error::ZkNymError; use crate::vpn_api_client::types::{ AttributesResponse, MasterVerificationKeyResponse, PartialVerificationKeysResponse, }; use async_trait::async_trait; pub use nym_http_api_client::Client; -use nym_http_api_client::{parse_response, ApiClient, IntoUrl, PathSegments, NO_PARAMS}; +use nym_http_api_client::{ + parse_response, ApiClient, HttpClientError, IntoUrl, PathSegments, NO_PARAMS, +}; use serde::de::DeserializeOwned; #[allow(dead_code)] @@ -23,9 +23,11 @@ pub fn new_client( bearer_token: impl Into, ) -> Result { Ok(VpnApiClient { - inner: Client::builder(base_url)? + inner: Client::builder(base_url) + .map_err(Box::new)? .with_user_agent(format!("nym-wasm-znym-lib/{}", env!("CARGO_PKG_VERSION"))) - .build()?, + .build() + .map_err(Box::new)?, bearer_token: bearer_token.into(), }) } @@ -34,13 +36,11 @@ pub fn new_client( #[allow(dead_code)] #[async_trait(?Send)] pub trait NymVpnApiClient { - async fn simple_get(&self, path: PathSegments<'_>) -> Result + async fn simple_get(&self, path: PathSegments<'_>) -> Result where T: DeserializeOwned; - async fn get_prehashed_public_attributes( - &self, - ) -> Result { + async fn get_prehashed_public_attributes(&self) -> Result { self.simple_get(&[ "/api", "/v1", @@ -52,7 +52,7 @@ pub trait NymVpnApiClient { async fn get_partial_verification_keys( &self, - ) -> Result { + ) -> Result { self.simple_get(&[ "/api", "/v1", @@ -64,7 +64,7 @@ pub trait NymVpnApiClient { async fn get_master_verification_key( &self, - ) -> Result { + ) -> Result { self.simple_get(&[ "/api", "/v1", @@ -77,13 +77,13 @@ pub trait NymVpnApiClient { #[async_trait(?Send)] impl NymVpnApiClient for VpnApiClient { - async fn simple_get(&self, path: PathSegments<'_>) -> Result + async fn simple_get(&self, path: PathSegments<'_>) -> Result where T: DeserializeOwned, { let req = self .inner - .create_get_request(path, NO_PARAMS) + .create_get_request(path, NO_PARAMS)? .bearer_auth(&self.bearer_token) .send(); diff --git a/wasm/zknym-lib/src/vpn_api_client/mod.rs b/wasm/zknym-lib/src/vpn_api_client/mod.rs index 041e9f0bc1..366fa05443 100644 --- a/wasm/zknym-lib/src/vpn_api_client/mod.rs +++ b/wasm/zknym-lib/src/vpn_api_client/mod.rs @@ -1,12 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::vpn_api_client::types::ErrorResponse; -use nym_http_api_client::HttpClientError; - #[cfg(test)] pub(crate) mod client; pub mod types; - -pub type NymVpnApiClientError = HttpClientError;