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;