Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ac8ced0c06 | |||
| eae677a898 | |||
| 40d31e0cd0 | |||
| 30428ba051 | |||
| ab1fc724ae | |||
| 775250b0ee | |||
| e975051566 | |||
| 8207a9c38a | |||
| bbdf0f158a | |||
| b276a8bc61 | |||
| f31e6f2470 | |||
| ea08345025 | |||
| 3e5537b753 | |||
| 23f86b6c1e | |||
| 8ba58ba11e | |||
| be16fddc75 | |||
| a7d6cba11d | |||
| a67ff33054 | |||
| 61badfdcfe | |||
| 19dfbeb2b4 | |||
| 9f13616c24 | |||
| d8c84cc4d6 | |||
| adbe0392ca | |||
| 3c6567ae64 | |||
| 8384a411df | |||
| c56ebd9ceb | |||
| b081b20a83 | |||
| 866d547745 | |||
| 64e3f066a7 | |||
| 62520c9308 | |||
| e65d455c91 | |||
| 9b9c82a02a | |||
| 1a38a2503e | |||
| 318f293983 | |||
| 5f2aba19c2 | |||
| 814ee45b4d | |||
| 56ed915626 | |||
| 2de8f8bc21 | |||
| f04cb6f6a6 | |||
| 4c67f01efb | |||
| b69c2e1e94 | |||
| d27e3b49db | |||
| ac12455f97 | |||
| 0b92a59f1a | |||
| 474eff67fa | |||
| 1c6db86259 | |||
| 4a1ce8154a | |||
| e126c1f7f1 | |||
| 31772019cd | |||
| aca98ab04f | |||
| f925c6caf0 | |||
| 5369e5eab9 | |||
| 2e634c59a7 | |||
| d7383d74f3 | |||
| e98d60d7ce | |||
| f47650d6c8 |
+3
-3
@@ -415,9 +415,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "5.28.5",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz",
|
||||
"integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==",
|
||||
"version": "5.29.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
|
||||
"integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/busboy": "^2.0.0"
|
||||
|
||||
@@ -13,6 +13,7 @@ on:
|
||||
- 'nym-network-monitor/**'
|
||||
- 'nym-node/**'
|
||||
- 'nym-node-status-api/**'
|
||||
- 'nym-statistics-api/**'
|
||||
- 'nym-outfox/**'
|
||||
- 'nym-validator-rewarder/**'
|
||||
- 'nyx-chain-watcher/**'
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
name: ci-check-nym-stats-api-version
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "nym-statistics-api/**"
|
||||
|
||||
env:
|
||||
WORKING_DIRECTORY: "nym-statistics-api"
|
||||
|
||||
jobs:
|
||||
check-if-tag-exists:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.45.4
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
- name: Check if git tag exists
|
||||
run: |
|
||||
TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
|
||||
if [[ -z "$TAG" ]]; then
|
||||
echo "Tag is empty"
|
||||
exit 1
|
||||
fi
|
||||
git ls-remote --tags origin | awk '{print $2}'
|
||||
if git ls-remote --tags origin | awk '{print $2}' | grep -q "refs/tags/$TAG$" ; then
|
||||
echo "Tag '$TAG' ALREADY EXISTS on the remote"
|
||||
exit 1
|
||||
else
|
||||
echo "Tag '$TAG' does not exist on the remote"
|
||||
fi
|
||||
- name: Check if harbor tag exists
|
||||
run: |
|
||||
TAG=${{ steps.get_version.outputs.result }}
|
||||
registry=https://harbor.nymte.ch
|
||||
repo_name=nym/nym-statistics-api
|
||||
if [[ -z $TAG ]]; then
|
||||
echo "Tag is empty"
|
||||
exit 1
|
||||
fi
|
||||
curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq
|
||||
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' )
|
||||
if [[ $exists = "true" ]]; then
|
||||
echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo"
|
||||
exit 1
|
||||
elif [[ $exists = "false" ]]; then
|
||||
echo "Version '$TAG' doesn't exist on the remote"
|
||||
else
|
||||
echo "Unknown output '$exists'"
|
||||
exit 1
|
||||
fi
|
||||
@@ -56,6 +56,7 @@ jobs:
|
||||
cp contracts/target/wasm32-unknown-unknown/release/cw3_flex_multisig.wasm $OUTPUT_DIR
|
||||
cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR
|
||||
cp contracts/target/wasm32-unknown-unknown/release/nym_ecash.wasm $OUTPUT_DIR
|
||||
cp contracts/target/wasm32-unknown-unknown/release/nym_pool_contract.wasm $OUTPUT_DIR
|
||||
|
||||
- name: Deploy branch to CI www
|
||||
continue-on-error: true
|
||||
|
||||
@@ -20,6 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -27,7 +28,8 @@ jobs:
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
# pinned due to issues building contracts
|
||||
toolchain: 1.86.0
|
||||
target: wasm32-unknown-unknown
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: 1.86.0
|
||||
override: true
|
||||
|
||||
- name: Build all binaries
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Build and upload Nym Statistics API container to harbor.nymte.ch
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
WORKING_DIRECTORY: "nym-statistics-api"
|
||||
CONTAINER_NAME: "nym-statistics-api"
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
password: ${{ secrets.HARBOR_ROBOT_SECRET }}
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
git config --global user.email "lawrence@nymtech.net"
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.45.4
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
- name: Create tag
|
||||
run: |
|
||||
git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}"
|
||||
git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
|
||||
|
||||
- name: BuildAndPushImageOnHarbor
|
||||
run: |
|
||||
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
|
||||
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
|
||||
@@ -4,6 +4,58 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2025.10-brie] (2025-05-27)
|
||||
|
||||
- Backport PR 5779 ([#5801])
|
||||
- Expanded Accept Encoding for `reqwest` ([#5779])
|
||||
- Teach HttpClientError how to report its status code and timeout ([#5770])
|
||||
- Skip refreshing the topology on startup as we already have an initial set ([#5768])
|
||||
- Fetch the topology from the nym-api concurrently ([#5767])
|
||||
- feat: use bincode by default in NymApiClient + remove feature-lock ([#5761])
|
||||
- Instrument create_request ([#5760])
|
||||
- Add node_bonded field to delegations ([#5759])
|
||||
- build(deps): bump mikefarah/yq from 4.45.1 to 4.45.4 ([#5758])
|
||||
- Raw route submissions ([#5756])
|
||||
- feat: expires header for `/active` nym-api responses ([#5755])
|
||||
- Decrease default average packet delay to 15 ms ([#5754])
|
||||
- build(deps): bump the patch-updates group across 1 directory with 12 updates ([#5753])
|
||||
- Remove pretty_env_logger and switch remaining crates to use tracing ([#5749])
|
||||
- Update pretty_env_logger to latest to not depend on unmaintained crate atty ([#5748])
|
||||
- Upgrade prometheus crate to fix security warning ([#5747])
|
||||
- Downgrade deranged crate to 0.4.0 ([#5746])
|
||||
- feat: nym-api bincode + yaml support ([#5745])
|
||||
- fix parallel feature in ecash crate with send + sync ([#5744])
|
||||
- Remove old test directory - Update validator docker ([#5743])
|
||||
- [Feature] `RememberMe` is the new don't `ForgetMe` ([#5742])
|
||||
- build(deps): bump ammonia from 4.0.0 to 4.1.0 ([#5739])
|
||||
- build(deps): bump base-x from 3.0.9 to 3.0.11 in /testnet-faucet ([#5737])
|
||||
- build(deps): bump http-proxy-middleware from 2.0.8 to 2.0.9 ([#5730])
|
||||
|
||||
[#5801]: https://github.com/nymtech/nym/pull/5801
|
||||
[#5779]: https://github.com/nymtech/nym/pull/5779
|
||||
[#5770]: https://github.com/nymtech/nym/pull/5770
|
||||
[#5768]: https://github.com/nymtech/nym/pull/5768
|
||||
[#5767]: https://github.com/nymtech/nym/pull/5767
|
||||
[#5761]: https://github.com/nymtech/nym/pull/5761
|
||||
[#5760]: https://github.com/nymtech/nym/pull/5760
|
||||
[#5759]: https://github.com/nymtech/nym/pull/5759
|
||||
[#5758]: https://github.com/nymtech/nym/pull/5758
|
||||
[#5756]: https://github.com/nymtech/nym/pull/5756
|
||||
[#5755]: https://github.com/nymtech/nym/pull/5755
|
||||
[#5754]: https://github.com/nymtech/nym/pull/5754
|
||||
[#5753]: https://github.com/nymtech/nym/pull/5753
|
||||
[#5749]: https://github.com/nymtech/nym/pull/5749
|
||||
[#5748]: https://github.com/nymtech/nym/pull/5748
|
||||
[#5747]: https://github.com/nymtech/nym/pull/5747
|
||||
[#5746]: https://github.com/nymtech/nym/pull/5746
|
||||
[#5745]: https://github.com/nymtech/nym/pull/5745
|
||||
[#5744]: https://github.com/nymtech/nym/pull/5744
|
||||
[#5743]: https://github.com/nymtech/nym/pull/5743
|
||||
[#5742]: https://github.com/nymtech/nym/pull/5742
|
||||
[#5739]: https://github.com/nymtech/nym/pull/5739
|
||||
[#5737]: https://github.com/nymtech/nym/pull/5737
|
||||
[#5730]: https://github.com/nymtech/nym/pull/5730
|
||||
|
||||
## [2025.9-appenzeller] (2025-05-13)
|
||||
|
||||
- build(deps): bump clap from 4.5.36 to 4.5.37 in the patch-updates group ([#5722])
|
||||
|
||||
Generated
+1373
-569
File diff suppressed because it is too large
Load Diff
+15
-7
@@ -33,11 +33,13 @@ members = [
|
||||
"common/commands",
|
||||
"common/config",
|
||||
"common/cosmwasm-smart-contracts/coconut-dkg",
|
||||
"common/cosmwasm-smart-contracts/contracts-common", "common/cosmwasm-smart-contracts/easy_addr",
|
||||
"common/cosmwasm-smart-contracts/contracts-common",
|
||||
"common/cosmwasm-smart-contracts/easy_addr",
|
||||
"common/cosmwasm-smart-contracts/ecash-contract",
|
||||
"common/cosmwasm-smart-contracts/group-contract",
|
||||
"common/cosmwasm-smart-contracts/mixnet-contract",
|
||||
"common/cosmwasm-smart-contracts/multisig-contract",
|
||||
"common/cosmwasm-smart-contracts/nym-pool-contract",
|
||||
"common/cosmwasm-smart-contracts/vesting-contract",
|
||||
"common/credential-storage",
|
||||
"common/credential-utils",
|
||||
@@ -64,6 +66,8 @@ members = [
|
||||
"common/nym-id",
|
||||
"common/nym-metrics",
|
||||
"common/nym_offline_compact_ecash",
|
||||
"common/nymnoise",
|
||||
"common/nymnoise/keys",
|
||||
"common/nymsphinx",
|
||||
"common/nymsphinx/acknowledgements",
|
||||
"common/nymsphinx/addressing",
|
||||
@@ -112,6 +116,7 @@ members = [
|
||||
"nym-node/nym-node-metrics",
|
||||
"nym-node/nym-node-requests",
|
||||
"nym-outfox",
|
||||
"nym-statistics-api",
|
||||
"nym-validator-rewarder",
|
||||
"nyx-chain-watcher",
|
||||
"sdk/ffi/cpp",
|
||||
@@ -131,7 +136,8 @@ members = [
|
||||
"tools/internal/testnet-manager",
|
||||
"tools/internal/testnet-manager",
|
||||
"tools/internal/testnet-manager/dkg-bypass-contract",
|
||||
"tools/internal/testnet-manager/dkg-bypass-contract", "tools/internal/validator-status-check",
|
||||
"tools/internal/testnet-manager/dkg-bypass-contract",
|
||||
"tools/internal/validator-status-check",
|
||||
"tools/nym-cli",
|
||||
"tools/nym-id-cli",
|
||||
"tools/nym-nr-query",
|
||||
@@ -152,6 +158,7 @@ default-members = [
|
||||
"nym-node",
|
||||
"nym-node-status-api/nym-node-status-agent",
|
||||
"nym-node-status-api/nym-node-status-api",
|
||||
"nym-statistics-api",
|
||||
"nym-validator-rewarder",
|
||||
"nyx-chain-watcher",
|
||||
"service-providers/authenticator",
|
||||
@@ -198,7 +205,7 @@ bloomfilter = "3.0.1"
|
||||
bs58 = "0.5.1"
|
||||
bytecodec = "0.4.15"
|
||||
bytes = "1.10.1"
|
||||
cargo_metadata = "0.18.1"
|
||||
cargo_metadata = "0.19.2"
|
||||
celes = "2.6.0"
|
||||
cfg-if = "1.0.0"
|
||||
chacha20 = "0.9.0"
|
||||
@@ -305,8 +312,9 @@ serde_with = "3.9.0"
|
||||
serde_yaml = "0.9.25"
|
||||
sha2 = "0.10.9"
|
||||
si-scale = "0.2.3"
|
||||
snow = "0.9.6"
|
||||
sphinx-packet = "=0.6.0"
|
||||
sqlx = "0.7.4"
|
||||
sqlx = "0.8.6"
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
subtle-encoding = "0.5"
|
||||
@@ -314,10 +322,10 @@ syn = "1"
|
||||
sysinfo = "0.33.0"
|
||||
tap = "1.0.1"
|
||||
tar = "0.4.44"
|
||||
tempfile = "3.19"
|
||||
tempfile = "3.20"
|
||||
thiserror = "2.0"
|
||||
time = "0.3.41"
|
||||
tokio = "1.44"
|
||||
tokio = "1.45"
|
||||
tokio-postgres = "0.7"
|
||||
tokio-stream = "0.1.17"
|
||||
tokio-test = "0.4.4"
|
||||
@@ -344,7 +352,6 @@ utoipauto = "0.2"
|
||||
uuid = "*"
|
||||
vergen = { version = "=8.3.1", default-features = false }
|
||||
walkdir = "2"
|
||||
wasm-bindgen-test = "0.3.49"
|
||||
x25519-dalek = "2.0.0"
|
||||
zeroize = "1.7.0"
|
||||
|
||||
@@ -392,6 +399,7 @@ serde-wasm-bindgen = "0.6.5"
|
||||
tsify = "0.4.5"
|
||||
wasm-bindgen = "0.2.99"
|
||||
wasm-bindgen-futures = "0.4.49"
|
||||
wasm-bindgen-test = "0.3.49"
|
||||
wasmtimer = "0.4.1"
|
||||
web-sys = "0.3.76"
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ clippy: sdk-wasm-lint
|
||||
# Build contracts ready for deploy
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
CONTRACTS=vesting_contract mixnet_contract nym_ecash cw3_flex_multisig cw4_group nym_coconut_dkg
|
||||
CONTRACTS=vesting_contract mixnet_contract nym_ecash cw3_flex_multisig cw4_group nym_coconut_dkg nym_pool_contract
|
||||
CONTRACTS_WASM=$(addsuffix .wasm, $(CONTRACTS))
|
||||
CONTRACTS_OUT_DIR=contracts/target/wasm32-unknown-unknown/release
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.55"
|
||||
version = "1.1.56"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
@@ -2048,10 +2048,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-middleware": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz",
|
||||
"integrity": "sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==",
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
|
||||
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-proxy": "^1.17.8",
|
||||
"http-proxy": "^1.18.1",
|
||||
@@ -6095,9 +6096,9 @@
|
||||
}
|
||||
},
|
||||
"http-proxy-middleware": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz",
|
||||
"integrity": "sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==",
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
|
||||
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/http-proxy": "^1.17.8",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.55"
|
||||
version = "1.1.56"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||
edition = "2021"
|
||||
|
||||
@@ -108,7 +108,7 @@ impl GatewayClient {
|
||||
#[cfg(feature = "verify")]
|
||||
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
|
||||
// use gateways key as a ref to an x25519_dalek key
|
||||
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
|
||||
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
|
||||
|
||||
// TODO: change that to use our nym_crypto::hmac module instead
|
||||
#[allow(clippy::expect_used)]
|
||||
|
||||
@@ -117,7 +117,7 @@ impl GatewayClient {
|
||||
#[cfg(feature = "verify")]
|
||||
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
|
||||
// use gateways key as a ref to an x25519_dalek key
|
||||
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
|
||||
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
|
||||
|
||||
// TODO: change that to use our nym_crypto::hmac module instead
|
||||
#[allow(clippy::expect_used)]
|
||||
|
||||
@@ -117,7 +117,7 @@ impl GatewayClient {
|
||||
#[cfg(feature = "verify")]
|
||||
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
|
||||
// use gateways key as a ref to an x25519_dalek key
|
||||
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
|
||||
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
|
||||
|
||||
// TODO: change that to use our nym_crypto::hmac module instead
|
||||
#[allow(clippy::expect_used)]
|
||||
|
||||
@@ -169,7 +169,7 @@ impl GatewayClient {
|
||||
#[cfg(feature = "verify")]
|
||||
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
|
||||
// use gateways key as a ref to an x25519_dalek key
|
||||
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
|
||||
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
|
||||
|
||||
// TODO: change that to use our nym_crypto::hmac module instead
|
||||
#[allow(clippy::expect_used)]
|
||||
|
||||
@@ -169,7 +169,7 @@ impl GatewayClient {
|
||||
#[cfg(feature = "verify")]
|
||||
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
|
||||
// use gateways key as a ref to an x25519_dalek key
|
||||
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
|
||||
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
|
||||
|
||||
// TODO: change that to use our nym_crypto::hmac module instead
|
||||
#[allow(clippy::expect_used)]
|
||||
|
||||
@@ -44,7 +44,6 @@ nym-sphinx = { path = "../nymsphinx" }
|
||||
nym-statistics-common = { path = "../statistics" }
|
||||
nym-pemstore = { path = "../pemstore" }
|
||||
nym-topology = { path = "../topology", features = ["persistence"] }
|
||||
nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false }
|
||||
nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
|
||||
nym-task = { path = "../task" }
|
||||
nym-credentials-interface = { path = "../credentials-interface" }
|
||||
@@ -57,6 +56,9 @@ nym-client-core-surb-storage = { path = "./surb-storage" }
|
||||
nym-client-core-gateways-storage = { path = "./gateways-storage" }
|
||||
nym-ecash-time = { path = "../ecash-time" }
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies]
|
||||
nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false }
|
||||
|
||||
### For serving prometheus metrics
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.hyper]
|
||||
workspace = true
|
||||
|
||||
@@ -87,7 +87,7 @@ impl StorageManager {
|
||||
sqlx::query!("SELECT EXISTS (SELECT 1 FROM registered_gateway WHERE gateway_id_bs58 = ?) AS 'exists'", gateway_id)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.await
|
||||
.map(|result| result.exists == Some(1))
|
||||
.map(|result| result.exists == 1)
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_get_registered_gateway(
|
||||
|
||||
@@ -456,7 +456,7 @@ where
|
||||
log::error!("Could not authenticate and start up the gateway connection - {err}");
|
||||
ClientCoreError::GatewayClientError {
|
||||
gateway_id: details.gateway_id.to_base58_string(),
|
||||
source: err,
|
||||
source: Box::new(err),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::client::topology_control::{TopologyAccessor, TopologyReadPermit};
|
||||
use nym_sphinx::acknowledgements::AckKey;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, RepliableMessage, ReplyMessage};
|
||||
use nym_sphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey};
|
||||
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
|
||||
use nym_sphinx::chunking::fragment::{Fragment, FragmentIdentifier};
|
||||
use nym_sphinx::message::NymMessage;
|
||||
use nym_sphinx::params::{PacketSize, PacketType};
|
||||
@@ -44,7 +44,10 @@ pub enum PreparationError {
|
||||
}
|
||||
|
||||
impl PreparationError {
|
||||
fn return_surbs(self, returned_surbs: Vec<ReplySurb>) -> SurbWrappedPreparationError {
|
||||
fn return_surbs(
|
||||
self,
|
||||
returned_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
) -> SurbWrappedPreparationError {
|
||||
SurbWrappedPreparationError {
|
||||
source: self,
|
||||
returned_surbs: Some(returned_surbs),
|
||||
@@ -58,7 +61,7 @@ pub struct SurbWrappedPreparationError {
|
||||
#[source]
|
||||
source: PreparationError,
|
||||
|
||||
returned_surbs: Option<Vec<ReplySurb>>,
|
||||
returned_surbs: Option<Vec<ReplySurbWithKeyRotation>>,
|
||||
}
|
||||
|
||||
impl<T> From<T> for SurbWrappedPreparationError
|
||||
@@ -268,10 +271,10 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
async fn generate_reply_surbs_with_keys(
|
||||
async fn generate_reply_surbs(
|
||||
&mut self,
|
||||
amount: usize,
|
||||
) -> Result<(Vec<ReplySurb>, Vec<SurbEncryptionKey>), PreparationError> {
|
||||
) -> Result<Vec<ReplySurbWithKeyRotation>, PreparationError> {
|
||||
let topology_permit = self.topology_access.get_read_permit().await;
|
||||
let topology = self.get_topology(&topology_permit)?;
|
||||
|
||||
@@ -281,19 +284,14 @@ where
|
||||
topology,
|
||||
)?;
|
||||
|
||||
let reply_keys = reply_surbs
|
||||
.iter()
|
||||
.map(|s| *s.encryption_key())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok((reply_surbs, reply_keys))
|
||||
Ok(reply_surbs)
|
||||
}
|
||||
|
||||
pub(crate) async fn try_send_single_surb_message(
|
||||
&mut self,
|
||||
target: AnonymousSenderTag,
|
||||
message: ReplyMessage,
|
||||
reply_surb: ReplySurb,
|
||||
reply_surb: ReplySurbWithKeyRotation,
|
||||
is_extra_surb_request: bool,
|
||||
) -> Result<(), SurbWrappedPreparationError> {
|
||||
let msg = NymMessage::new_reply(message);
|
||||
@@ -347,7 +345,7 @@ where
|
||||
pub(crate) async fn try_request_additional_reply_surbs(
|
||||
&mut self,
|
||||
from: AnonymousSenderTag,
|
||||
reply_surb: ReplySurb,
|
||||
reply_surb: ReplySurbWithKeyRotation,
|
||||
amount: u32,
|
||||
) -> Result<(), SurbWrappedPreparationError> {
|
||||
debug!("requesting {amount} reply SURBs from {from}");
|
||||
@@ -387,7 +385,7 @@ where
|
||||
&mut self,
|
||||
target: AnonymousSenderTag,
|
||||
fragments: Vec<FragmentWithMaxRetransmissions>,
|
||||
reply_surbs: Vec<ReplySurb>,
|
||||
reply_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
lane: TransmissionLane,
|
||||
) -> Result<(), SurbWrappedPreparationError> {
|
||||
// TODO: technically this is performing an unnecessary cloning, but in the grand scheme of things
|
||||
@@ -404,7 +402,7 @@ where
|
||||
&mut self,
|
||||
target: AnonymousSenderTag,
|
||||
fragments: Vec<(TransmissionLane, FragmentWithMaxRetransmissions)>,
|
||||
reply_surbs: Vec<ReplySurb>,
|
||||
reply_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
) -> Result<(), SurbWrappedPreparationError> {
|
||||
let prepared_fragments = self
|
||||
.prepare_reply_chunks_for_sending(
|
||||
@@ -541,8 +539,12 @@ where
|
||||
) -> Result<(), PreparationError> {
|
||||
debug!("Sending additional reply SURBs with packet type {packet_type}");
|
||||
let sender_tag = self.get_or_create_sender_tag(&recipient);
|
||||
let (reply_surbs, reply_keys) =
|
||||
self.generate_reply_surbs_with_keys(amount as usize).await?;
|
||||
let reply_surbs = self.generate_reply_surbs(amount as usize).await?;
|
||||
|
||||
let reply_keys = reply_surbs
|
||||
.iter()
|
||||
.map(|s| *s.encryption_key())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let message = NymMessage::new_repliable(RepliableMessage::new_additional_surbs(
|
||||
self.config.use_legacy_sphinx_format,
|
||||
@@ -579,9 +581,12 @@ where
|
||||
) -> Result<(), SurbWrappedPreparationError> {
|
||||
debug!("Sending message with reply SURBs with packet type {packet_type}");
|
||||
let sender_tag = self.get_or_create_sender_tag(&recipient);
|
||||
let (reply_surbs, reply_keys) = self
|
||||
.generate_reply_surbs_with_keys(num_reply_surbs as usize)
|
||||
.await?;
|
||||
let reply_surbs = self.generate_reply_surbs(num_reply_surbs as usize).await?;
|
||||
|
||||
let reply_keys = reply_surbs
|
||||
.iter()
|
||||
.map(|s| *s.encryption_key())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let message = NymMessage::new_repliable(RepliableMessage::new_data(
|
||||
self.config.use_legacy_sphinx_format,
|
||||
@@ -629,7 +634,7 @@ where
|
||||
pub(crate) async fn prepare_reply_chunks_for_sending(
|
||||
&mut self,
|
||||
fragments: Vec<Fragment>,
|
||||
reply_surbs: Vec<ReplySurb>,
|
||||
reply_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
) -> Result<Vec<PreparedFragment>, SurbWrappedPreparationError> {
|
||||
debug_assert_eq!(
|
||||
fragments.len(),
|
||||
@@ -665,7 +670,7 @@ where
|
||||
|
||||
pub(crate) async fn try_prepare_single_reply_chunk_for_sending(
|
||||
&mut self,
|
||||
reply_surb: ReplySurb,
|
||||
reply_surb: ReplySurbWithKeyRotation,
|
||||
chunk: Fragment,
|
||||
) -> Result<PreparedFragment, SurbWrappedPreparationError> {
|
||||
let topology_permit = self.topology_access.get_read_permit().await;
|
||||
|
||||
@@ -11,7 +11,7 @@ use futures::StreamExt;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use nym_sphinx::anonymous_replies::ReplySurb;
|
||||
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
|
||||
use nym_sphinx::chunking::fragment::FragmentIdentifier;
|
||||
use nym_task::connections::{ConnectionId, TransmissionLane};
|
||||
use nym_task::TaskClient;
|
||||
@@ -499,7 +499,7 @@ where
|
||||
async fn handle_received_surbs(
|
||||
&mut self,
|
||||
from: AnonymousSenderTag,
|
||||
reply_surbs: Vec<ReplySurb>,
|
||||
reply_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
from_surb_request: bool,
|
||||
) {
|
||||
trace!("handling received surbs");
|
||||
|
||||
@@ -6,7 +6,7 @@ use futures::channel::{mpsc, oneshot};
|
||||
use log::error;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use nym_sphinx::anonymous_replies::ReplySurb;
|
||||
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
|
||||
use nym_task::connections::{ConnectionId, TransmissionLane};
|
||||
use std::sync::Weak;
|
||||
|
||||
@@ -81,7 +81,7 @@ impl ReplyControllerSender {
|
||||
pub(crate) fn send_additional_surbs(
|
||||
&self,
|
||||
sender_tag: AnonymousSenderTag,
|
||||
reply_surbs: Vec<ReplySurb>,
|
||||
reply_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
from_surb_request: bool,
|
||||
) -> Result<(), ReplyControllerSenderError> {
|
||||
self.0
|
||||
@@ -167,7 +167,7 @@ pub enum ReplyControllerMessage {
|
||||
|
||||
AdditionalSurbs {
|
||||
sender_tag: AnonymousSenderTag,
|
||||
reply_surbs: Vec<ReplySurb>,
|
||||
reply_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
from_surb_request: bool,
|
||||
},
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use async_trait::async_trait;
|
||||
use log::{debug, error, warn};
|
||||
use nym_topology::provider_trait::TopologyProvider;
|
||||
use nym_topology::NymTopology;
|
||||
use nym_topology::{NymTopology, NymTopologyMetadata};
|
||||
use nym_validator_client::UserAgent;
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::thread_rng;
|
||||
@@ -89,55 +89,84 @@ impl NymApiTopologyProvider {
|
||||
let rewarded_set_fut = self.validator_client.get_current_rewarded_set();
|
||||
|
||||
let topology = if self.config.use_extended_topology {
|
||||
let all_nodes_fut = self.validator_client.get_all_basic_nodes();
|
||||
let all_nodes_fut = self.validator_client.get_all_basic_nodes_with_metadata();
|
||||
|
||||
// Join rewarded_set_fut and all_nodes_fut concurrently
|
||||
let (rewarded_set, all_nodes) = futures::try_join!(rewarded_set_fut, all_nodes_fut)
|
||||
let (rewarded_set, all_nodes_res) = futures::try_join!(rewarded_set_fut, all_nodes_fut)
|
||||
.inspect_err(|err| error!("failed to get network nodes: {err}"))
|
||||
.ok()?;
|
||||
|
||||
let metadata = all_nodes_res.metadata;
|
||||
let all_nodes = all_nodes_res.nodes;
|
||||
|
||||
debug!(
|
||||
"there are {} nodes on the network (before filtering)",
|
||||
all_nodes.len()
|
||||
);
|
||||
let mut topology = NymTopology::new_empty(rewarded_set);
|
||||
topology.add_additional_nodes(all_nodes.iter().filter(|n| {
|
||||
n.performance.round_to_integer() >= self.config.min_node_performance()
|
||||
}));
|
||||
let nodes_filtered = all_nodes
|
||||
.into_iter()
|
||||
.filter(|n| n.performance.round_to_integer() >= self.config.min_node_performance())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
topology
|
||||
NymTopology::new(
|
||||
NymTopologyMetadata::new(metadata.rotation_id, metadata.absolute_epoch_id),
|
||||
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
|
||||
|
||||
let mixnodes_fut = self
|
||||
.validator_client
|
||||
.get_all_basic_active_mixing_assigned_nodes();
|
||||
.get_all_basic_active_mixing_assigned_nodes_with_metadata();
|
||||
|
||||
// TODO: we really should be getting ACTIVE gateways only
|
||||
let gateways_fut = self.validator_client.get_all_basic_entry_assigned_nodes();
|
||||
let gateways_fut = self
|
||||
.validator_client
|
||||
.get_all_basic_entry_assigned_nodes_v2();
|
||||
|
||||
let (rewarded_set, mixnodes, gateways) =
|
||||
let (rewarded_set, mixnodes_res, gateways_res) =
|
||||
futures::try_join!(rewarded_set_fut, mixnodes_fut, gateways_fut)
|
||||
.inspect_err(|err| {
|
||||
error!("failed to get network nodes: {err}");
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let metadata = mixnodes_res.metadata;
|
||||
let mixnodes = mixnodes_res.nodes;
|
||||
|
||||
if gateways_res.metadata != metadata {
|
||||
warn!("inconsistent nodes metadata between mixnodes and gateways calls! {metadata:?} and {:?}", gateways_res.metadata);
|
||||
return None;
|
||||
}
|
||||
|
||||
let gateways = gateways_res.nodes;
|
||||
|
||||
debug!(
|
||||
"there are {} mixnodes and {} gateways in total (before performance filtering)",
|
||||
mixnodes.len(),
|
||||
gateways.len()
|
||||
);
|
||||
|
||||
let mut topology = NymTopology::new_empty(rewarded_set);
|
||||
topology.add_additional_nodes(mixnodes.iter().filter(|m| {
|
||||
m.performance.round_to_integer() >= self.config.min_mixnode_performance
|
||||
}));
|
||||
topology.add_additional_nodes(gateways.iter().filter(|m| {
|
||||
m.performance.round_to_integer() >= self.config.min_gateway_performance
|
||||
}));
|
||||
let mut nodes = Vec::new();
|
||||
for mix in mixnodes {
|
||||
if mix.performance.round_to_integer() >= self.config.min_mixnode_performance {
|
||||
nodes.push(mix)
|
||||
}
|
||||
}
|
||||
for gateway in gateways {
|
||||
if gateway.performance.round_to_integer() >= self.config.min_gateway_performance {
|
||||
nodes.push(gateway)
|
||||
}
|
||||
}
|
||||
|
||||
topology
|
||||
NymTopology::new(
|
||||
NymTopologyMetadata::new(metadata.rotation_id, metadata.absolute_epoch_id),
|
||||
rewarded_set,
|
||||
Vec::new(),
|
||||
)
|
||||
.with_skimmed_nodes(&nodes)
|
||||
};
|
||||
|
||||
if !topology.is_minimally_routable() {
|
||||
|
||||
@@ -18,7 +18,7 @@ pub enum ClientCoreError {
|
||||
#[error("gateway client error ({gateway_id}): {source}")]
|
||||
GatewayClientError {
|
||||
gateway_id: String,
|
||||
source: GatewayClientError,
|
||||
source: Box<GatewayClientError>,
|
||||
},
|
||||
|
||||
#[error("custom gateway client error: {source}")]
|
||||
@@ -88,10 +88,7 @@ pub enum ClientCoreError {
|
||||
},
|
||||
|
||||
#[error("failed to establish connection to gateway: {source}")]
|
||||
GatewayConnectionFailure {
|
||||
#[from]
|
||||
source: tungstenite::Error,
|
||||
},
|
||||
GatewayConnectionFailure { source: Box<tungstenite::Error> },
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[error("failed to establish gateway connection (wasm)")]
|
||||
@@ -227,6 +224,14 @@ pub enum ClientCoreError {
|
||||
HkdfDerivationError {},
|
||||
}
|
||||
|
||||
impl From<tungstenite::Error> for ClientCoreError {
|
||||
fn from(err: tungstenite::Error) -> ClientCoreError {
|
||||
ClientCoreError::GatewayConnectionFailure {
|
||||
source: Box::new(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set of messages that the client can send to listeners via the task manager
|
||||
#[derive(Debug)]
|
||||
pub enum ClientCoreStatusMessage {
|
||||
|
||||
@@ -107,7 +107,7 @@ pub async fn gateways_for_init<R: Rng>(
|
||||
|
||||
log::debug!("Fetching list of gateways from: {nym_api}");
|
||||
|
||||
let gateways = client.get_all_basic_entry_assigned_nodes().await?;
|
||||
let gateways = client.get_all_basic_entry_assigned_nodes_v2().await?.nodes;
|
||||
info!("nym api reports {} gateways", gateways.len());
|
||||
|
||||
log::trace!("Gateways: {:#?}", gateways);
|
||||
@@ -329,7 +329,7 @@ pub(super) async fn register_with_gateway(
|
||||
log::warn!("Failed to establish connection with gateway!");
|
||||
ClientCoreError::GatewayClientError {
|
||||
gateway_id: gateway_id.to_base58_string(),
|
||||
source: err,
|
||||
source: Box::new(err),
|
||||
}
|
||||
})?;
|
||||
let auth_response = gateway_client
|
||||
@@ -339,7 +339,7 @@ pub(super) async fn register_with_gateway(
|
||||
log::warn!("Failed to register with the gateway {gateway_id}: {err}");
|
||||
ClientCoreError::GatewayClientError {
|
||||
gateway_id: gateway_id.to_base58_string(),
|
||||
source: err,
|
||||
source: Box::new(err),
|
||||
}
|
||||
})?;
|
||||
|
||||
|
||||
@@ -232,7 +232,7 @@ where
|
||||
} => {
|
||||
log::debug!("GatewaySetup::ReuseConnection");
|
||||
Ok(reuse_gateway_connection(
|
||||
authenticated_ephemeral_client,
|
||||
*authenticated_ephemeral_client,
|
||||
*gateway_details,
|
||||
managed_keys,
|
||||
))
|
||||
|
||||
@@ -218,7 +218,7 @@ pub enum GatewaySetup {
|
||||
|
||||
ReuseConnection {
|
||||
/// The authenticated ephemeral client that was created during `init`
|
||||
authenticated_ephemeral_client: InitGatewayClient,
|
||||
authenticated_ephemeral_client: Box<InitGatewayClient>,
|
||||
|
||||
// Details of this pre-initialised client (i.e. gateway and keys)
|
||||
gateway_details: Box<GatewayRegistration>,
|
||||
@@ -261,7 +261,7 @@ impl GatewaySetup {
|
||||
pub fn try_reuse_connection(init_res: InitialisationResult) -> Result<Self, ClientCoreError> {
|
||||
if let Some(authenticated_ephemeral_client) = init_res.authenticated_ephemeral_client {
|
||||
Ok(GatewaySetup::ReuseConnection {
|
||||
authenticated_ephemeral_client,
|
||||
authenticated_ephemeral_client: Box::new(authenticated_ephemeral_client),
|
||||
gateway_details: Box::new(init_res.gateway_registration),
|
||||
client_keys: init_res.client_keys,
|
||||
})
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
-- default value of 0 implies 'unknown' variant
|
||||
ALTER TABLE reply_surb
|
||||
ADD COLUMN encoded_key_rotation TINYINT NOT NULL DEFAULT 0;
|
||||
@@ -205,7 +205,10 @@ impl StorageManager {
|
||||
) -> Result<Vec<StoredReplySurb>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
StoredReplySurb,
|
||||
"SELECT * FROM reply_surb WHERE reply_surb_sender_id = ?",
|
||||
r#"
|
||||
SELECT reply_surb_sender_id, reply_surb, encoded_key_rotation as "encoded_key_rotation: u8" FROM reply_surb
|
||||
WHERE reply_surb_sender_id = ?
|
||||
"#,
|
||||
sender_id
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
@@ -230,10 +233,11 @@ impl StorageManager {
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO reply_surb(reply_surb_sender_id, reply_surb) VALUES (?, ?);
|
||||
INSERT INTO reply_surb(reply_surb_sender_id, reply_surb, encoded_key_rotation) VALUES (?, ?, ?);
|
||||
"#,
|
||||
stored_reply_surb.reply_surb_sender_id,
|
||||
stored_reply_surb.reply_surb
|
||||
stored_reply_surb.reply_surb,
|
||||
stored_reply_surb.encoded_key_rotation
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
|
||||
@@ -8,8 +8,10 @@ use nym_crypto::Digest;
|
||||
use nym_sphinx::addressing::clients::{Recipient, RecipientBytes};
|
||||
use nym_sphinx::anonymous_replies::encryption_key::EncryptionKeyDigest;
|
||||
use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, SENDER_TAG_SIZE};
|
||||
use nym_sphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey, SurbEncryptionKeySize};
|
||||
use nym_sphinx::params::ReplySurbKeyDigestAlgorithm;
|
||||
use nym_sphinx::anonymous_replies::{
|
||||
ReplySurb, ReplySurbWithKeyRotation, SurbEncryptionKey, SurbEncryptionKeySize,
|
||||
};
|
||||
use nym_sphinx::params::{ReplySurbKeyDigestAlgorithm, SphinxKeyRotation};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StoredSenderTag {
|
||||
@@ -146,24 +148,40 @@ impl TryFrom<StoredSurbSender> for (AnonymousSenderTag, i64) {
|
||||
pub struct StoredReplySurb {
|
||||
pub reply_surb_sender_id: i64,
|
||||
pub reply_surb: Vec<u8>,
|
||||
|
||||
// encodes only whether it's 'even', 'odd' or 'unknown' (default)
|
||||
// and not the whole id because that's redundant
|
||||
pub encoded_key_rotation: u8,
|
||||
}
|
||||
|
||||
impl StoredReplySurb {
|
||||
pub fn new(reply_surb_sender_id: i64, reply_surb: &ReplySurb) -> Self {
|
||||
pub fn new(reply_surb_sender_id: i64, reply_surb: &ReplySurbWithKeyRotation) -> Self {
|
||||
StoredReplySurb {
|
||||
reply_surb_sender_id,
|
||||
reply_surb: reply_surb.to_bytes(),
|
||||
reply_surb: reply_surb.inner_reply_surb().to_bytes(),
|
||||
encoded_key_rotation: reply_surb.key_rotation() as u8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<StoredReplySurb> for ReplySurb {
|
||||
impl TryFrom<StoredReplySurb> for ReplySurbWithKeyRotation {
|
||||
type Error = StorageError;
|
||||
|
||||
fn try_from(value: StoredReplySurb) -> Result<Self, Self::Error> {
|
||||
ReplySurb::from_bytes(&value.reply_surb).map_err(|err| StorageError::CorruptedData {
|
||||
details: format!("failed to recover the reply surb: {err}"),
|
||||
})
|
||||
let key_rotation =
|
||||
SphinxKeyRotation::try_from(value.encoded_key_rotation).map_err(|err| {
|
||||
StorageError::CorruptedData {
|
||||
details: format!("stored key rotation was malformed: {err}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
let reply_surb = ReplySurb::from_bytes(&value.reply_surb).map_err(|err| {
|
||||
StorageError::CorruptedData {
|
||||
details: format!("failed to recover the reply surb: {err}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(reply_surb.with_key_rotation(key_rotation))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use dashmap::iter::Iter;
|
||||
use dashmap::DashMap;
|
||||
use log::trace;
|
||||
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use nym_sphinx::anonymous_replies::ReplySurb;
|
||||
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
@@ -134,7 +134,7 @@ impl ReceivedReplySurbsMap {
|
||||
&self,
|
||||
target: &AnonymousSenderTag,
|
||||
amount: usize,
|
||||
) -> (Option<Vec<ReplySurb>>, usize) {
|
||||
) -> (Option<Vec<ReplySurbWithKeyRotation>>, usize) {
|
||||
if let Some(mut entry) = self.inner.data.get_mut(target) {
|
||||
let surbs_left = entry.items_left();
|
||||
if surbs_left < self.min_surb_threshold() + amount {
|
||||
@@ -150,7 +150,7 @@ impl ReceivedReplySurbsMap {
|
||||
pub fn get_reply_surb_ignoring_threshold(
|
||||
&self,
|
||||
target: &AnonymousSenderTag,
|
||||
) -> Option<(Option<ReplySurb>, usize)> {
|
||||
) -> Option<(Option<ReplySurbWithKeyRotation>, usize)> {
|
||||
self.inner
|
||||
.data
|
||||
.get_mut(target)
|
||||
@@ -160,7 +160,7 @@ impl ReceivedReplySurbsMap {
|
||||
pub fn get_reply_surb(
|
||||
&self,
|
||||
target: &AnonymousSenderTag,
|
||||
) -> Option<(Option<ReplySurb>, usize)> {
|
||||
) -> Option<(Option<ReplySurbWithKeyRotation>, usize)> {
|
||||
self.inner.data.get_mut(target).map(|mut entry| {
|
||||
let surbs_left = entry.items_left();
|
||||
if surbs_left < self.min_surb_threshold() {
|
||||
@@ -171,7 +171,7 @@ impl ReceivedReplySurbsMap {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn insert_surbs<I: IntoIterator<Item = ReplySurb>>(
|
||||
pub fn insert_surbs<I: IntoIterator<Item = ReplySurbWithKeyRotation>>(
|
||||
&self,
|
||||
target: &AnonymousSenderTag,
|
||||
surbs: I,
|
||||
@@ -189,14 +189,14 @@ impl ReceivedReplySurbsMap {
|
||||
pub struct ReceivedReplySurbs {
|
||||
// in the future we'd probably want to put extra data here to indicate when the SURBs got received
|
||||
// so we could invalidate entries from the previous key rotations
|
||||
data: VecDeque<ReplySurb>,
|
||||
data: VecDeque<ReplySurbWithKeyRotation>,
|
||||
|
||||
pending_reception: u32,
|
||||
surbs_last_received_at_timestamp: i64,
|
||||
}
|
||||
|
||||
impl ReceivedReplySurbs {
|
||||
fn new(initial_surbs: VecDeque<ReplySurb>) -> Self {
|
||||
fn new(initial_surbs: VecDeque<ReplySurbWithKeyRotation>) -> Self {
|
||||
ReceivedReplySurbs {
|
||||
data: initial_surbs,
|
||||
pending_reception: 0,
|
||||
@@ -206,7 +206,7 @@ impl ReceivedReplySurbs {
|
||||
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
|
||||
pub fn new_retrieved(
|
||||
surbs: Vec<ReplySurb>,
|
||||
surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
surbs_last_received_at_timestamp: i64,
|
||||
) -> ReceivedReplySurbs {
|
||||
ReceivedReplySurbs {
|
||||
@@ -217,7 +217,7 @@ impl ReceivedReplySurbs {
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
|
||||
pub fn surbs_ref(&self) -> &VecDeque<ReplySurb> {
|
||||
pub fn surbs_ref(&self) -> &VecDeque<ReplySurbWithKeyRotation> {
|
||||
&self.data
|
||||
}
|
||||
|
||||
@@ -243,7 +243,10 @@ impl ReceivedReplySurbs {
|
||||
self.pending_reception = 0;
|
||||
}
|
||||
|
||||
pub fn get_reply_surbs(&mut self, amount: usize) -> (Option<Vec<ReplySurb>>, usize) {
|
||||
pub fn get_reply_surbs(
|
||||
&mut self,
|
||||
amount: usize,
|
||||
) -> (Option<Vec<ReplySurbWithKeyRotation>>, usize) {
|
||||
if self.items_left() < amount {
|
||||
(None, self.items_left())
|
||||
} else {
|
||||
@@ -252,11 +255,11 @@ impl ReceivedReplySurbs {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_reply_surb(&mut self) -> (Option<ReplySurb>, usize) {
|
||||
pub fn get_reply_surb(&mut self) -> (Option<ReplySurbWithKeyRotation>, usize) {
|
||||
(self.pop_surb(), self.items_left())
|
||||
}
|
||||
|
||||
fn pop_surb(&mut self) -> Option<ReplySurb> {
|
||||
fn pop_surb(&mut self) -> Option<ReplySurbWithKeyRotation> {
|
||||
self.data.pop_front()
|
||||
}
|
||||
|
||||
@@ -265,7 +268,10 @@ impl ReceivedReplySurbs {
|
||||
}
|
||||
|
||||
// realistically we're always going to be getting multiple surbs at once
|
||||
pub fn insert_reply_surbs<I: IntoIterator<Item = ReplySurb>>(&mut self, surbs: I) {
|
||||
pub fn insert_reply_surbs<I: IntoIterator<Item = ReplySurbWithKeyRotation>>(
|
||||
&mut self,
|
||||
surbs: I,
|
||||
) {
|
||||
let mut v = surbs.into_iter().collect::<VecDeque<_>>();
|
||||
trace!("storing {} surbs in the storage", v.len());
|
||||
self.data.append(&mut v);
|
||||
|
||||
@@ -21,8 +21,8 @@ use nym_crypto::asymmetric::ed25519;
|
||||
use nym_gateway_requests::registration::handshake::client_handshake;
|
||||
use nym_gateway_requests::{
|
||||
BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersionExt,
|
||||
SensitiveServerResponse, ServerResponse, SharedGatewayKey, SharedSymmetricKey,
|
||||
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
|
||||
GatewayRequestsError, SensitiveServerResponse, ServerResponse, SharedGatewayKey,
|
||||
SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
|
||||
};
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use nym_statistics_common::clients::connection::ConnectionStatsEvent;
|
||||
@@ -662,6 +662,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
|
||||
let supports_aes_gcm_siv = gw_protocol.supports_aes256_gcm_siv();
|
||||
let supports_auth_v2 = gw_protocol.supports_authenticate_v2();
|
||||
let supports_key_rotation_info = gw_protocol.supports_key_rotation_packet();
|
||||
|
||||
if !supports_aes_gcm_siv {
|
||||
warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV");
|
||||
@@ -669,6 +670,9 @@ impl<C, St> GatewayClient<C, St> {
|
||||
if !supports_auth_v2 {
|
||||
warn!("this gateway is on an old version that doesn't support authentication v2")
|
||||
}
|
||||
if !supports_key_rotation_info {
|
||||
warn!("this gateway is on an old version that doesn't support key rotation packets")
|
||||
}
|
||||
|
||||
if self.authenticated {
|
||||
debug!("Already authenticated");
|
||||
@@ -849,6 +853,22 @@ impl<C, St> GatewayClient<C, St> {
|
||||
}
|
||||
}
|
||||
|
||||
fn mix_packet_to_ws_message(&self, packet: MixPacket) -> Result<Message, GatewayRequestsError> {
|
||||
// note: into_ws_message encrypts the requests and adds a MAC on it. Perhaps it should
|
||||
// be more explicit in the naming?
|
||||
let req = if self.negotiated_protocol.supports_key_rotation_packet() {
|
||||
BinaryRequest::ForwardSphinxV2 { packet }
|
||||
} else {
|
||||
BinaryRequest::ForwardSphinx { packet }
|
||||
};
|
||||
|
||||
req.into_ws_message(
|
||||
self.shared_key
|
||||
.as_ref()
|
||||
.expect("no shared key present even though we're authenticated!"),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn batch_send_mix_packets(
|
||||
&mut self,
|
||||
packets: Vec<MixPacket>,
|
||||
@@ -877,13 +897,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
|
||||
let messages: Result<Vec<_>, _> = packets
|
||||
.into_iter()
|
||||
.map(|mix_packet| {
|
||||
BinaryRequest::ForwardSphinx { packet: mix_packet }.into_ws_message(
|
||||
self.shared_key
|
||||
.as_ref()
|
||||
.expect("no shared key present even though we're authenticated!"),
|
||||
)
|
||||
})
|
||||
.map(|mix_packet| self.mix_packet_to_ws_message(mix_packet))
|
||||
.collect();
|
||||
|
||||
if let Err(err) = self
|
||||
@@ -949,13 +963,8 @@ impl<C, St> GatewayClient<C, St> {
|
||||
if !self.connection.is_established() {
|
||||
return Err(GatewayClientError::ConnectionNotEstablished);
|
||||
}
|
||||
// note: into_ws_message encrypts the requests and adds a MAC on it. Perhaps it should
|
||||
// be more explicit in the naming?
|
||||
let msg = BinaryRequest::ForwardSphinx { packet: mix_packet }.into_ws_message(
|
||||
self.shared_key
|
||||
.as_ref()
|
||||
.expect("no shared key present even though we're authenticated!"),
|
||||
)?;
|
||||
|
||||
let msg = self.mix_packet_to_ws_message(mix_packet)?;
|
||||
self.send_with_reconnection_on_failure(msg).await
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ pub(crate) async fn connect_async(
|
||||
}
|
||||
.map_err(|err| GatewayClientError::NetworkConnectionFailed {
|
||||
address: endpoint.to_owned(),
|
||||
source: err.into(),
|
||||
source: Box::new(tungstenite::Error::from(err)),
|
||||
})?;
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -72,7 +72,7 @@ pub(crate) async fn connect_async(
|
||||
Err(err) => {
|
||||
stream = Err(GatewayClientError::NetworkConnectionFailed {
|
||||
address: endpoint.to_owned(),
|
||||
source: err.into(),
|
||||
source: Box::new(tungstenite::Error::from(err)),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -83,6 +83,6 @@ pub(crate) async fn connect_async(
|
||||
.await
|
||||
.map_err(|error| GatewayClientError::NetworkConnectionFailed {
|
||||
address: endpoint.to_owned(),
|
||||
source: error,
|
||||
source: Box::new(error),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ pub enum GatewayClientError {
|
||||
RequestError(#[from] GatewayRequestsError),
|
||||
|
||||
#[error("There was a network error: {0}")]
|
||||
NetworkError(#[from] WsError),
|
||||
NetworkError(Box<WsError>),
|
||||
|
||||
#[error("failed to upgrade our shared key - the gateway sent malformed response")]
|
||||
FatalKeyUpgradeFailure,
|
||||
@@ -41,7 +41,10 @@ pub enum GatewayClientError {
|
||||
NetworkErrorWasm(#[from] JsError),
|
||||
|
||||
#[error("connection failed: {address}: {source}")]
|
||||
NetworkConnectionFailed { address: String, source: WsError },
|
||||
NetworkConnectionFailed {
|
||||
address: String,
|
||||
source: Box<WsError>,
|
||||
},
|
||||
|
||||
#[error("no socket address for endpoint: {address}")]
|
||||
NoEndpointForConnection { address: String },
|
||||
@@ -127,10 +130,16 @@ pub enum GatewayClientError {
|
||||
ShutdownInProgress,
|
||||
}
|
||||
|
||||
impl From<WsError> for GatewayClientError {
|
||||
fn from(error: WsError) -> Self {
|
||||
GatewayClientError::NetworkError(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl GatewayClientError {
|
||||
pub fn is_closed_connection(&self) -> bool {
|
||||
match self {
|
||||
GatewayClientError::NetworkError(ws_err) => match ws_err {
|
||||
GatewayClientError::NetworkError(ws_err) => match ws_err.as_ref() {
|
||||
WsError::AlreadyClosed | WsError::ConnectionClosed => true,
|
||||
WsError::Io(io_err) => matches!(
|
||||
io_err.kind(),
|
||||
|
||||
@@ -28,7 +28,7 @@ pub(crate) fn cleanup_socket_message(
|
||||
msg: Option<Result<Message, WsError>>,
|
||||
) -> Result<Message, GatewayClientError> {
|
||||
match msg {
|
||||
Some(msg) => msg.map_err(GatewayClientError::NetworkError),
|
||||
Some(msg) => msg.map_err(GatewayClientError::from),
|
||||
None => Err(GatewayClientError::ConnectionAbruptlyClosed),
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ pub(crate) fn cleanup_socket_messages(
|
||||
match msgs {
|
||||
Some(msgs) => msgs
|
||||
.into_iter()
|
||||
.map(|msg| msg.map_err(GatewayClientError::NetworkError))
|
||||
.map(|msg| msg.map_err(GatewayClientError::from))
|
||||
.collect(),
|
||||
None => Err(GatewayClientError::ConnectionAbruptlyClosed),
|
||||
}
|
||||
|
||||
@@ -16,9 +16,14 @@ tokio-util = { workspace = true, features = ["codec"], optional = true }
|
||||
tokio-stream = { workspace = true }
|
||||
|
||||
# internal
|
||||
nym-noise = { path = "../../nymnoise" }
|
||||
nym-sphinx = { path = "../../nymsphinx" }
|
||||
nym-task = { path = "../../task", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["client"]
|
||||
client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"]
|
||||
client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"]
|
||||
|
||||
[dev-dependencies]
|
||||
nym-crypto = { path = "../../crypto" }
|
||||
rand = { workspace = true }
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
use dashmap::DashMap;
|
||||
use futures::StreamExt;
|
||||
use nym_sphinx::addressing::nodes::NymNodeRoutingAddress;
|
||||
use nym_noise::config::NoiseConfig;
|
||||
use nym_noise::upgrade_noise_initiator;
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use nym_sphinx::framing::codec::NymCodec;
|
||||
use nym_sphinx::framing::packet::FramedNymPacket;
|
||||
use nym_sphinx::params::PacketType;
|
||||
use nym_sphinx::NymPacket;
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::ops::Deref;
|
||||
@@ -49,23 +49,19 @@ impl Config {
|
||||
pub trait SendWithoutResponse {
|
||||
// Without response in this context means we will not listen for anything we might get back (not
|
||||
// that we should get anything), including any possible io errors
|
||||
fn send_without_response(
|
||||
&self,
|
||||
address: NymNodeRoutingAddress,
|
||||
packet: NymPacket,
|
||||
packet_type: PacketType,
|
||||
) -> io::Result<()>;
|
||||
fn send_without_response(&self, packet: MixPacket) -> io::Result<()>;
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
active_connections: ActiveConnections,
|
||||
noise_config: NoiseConfig,
|
||||
connections_count: Arc<AtomicUsize>,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ActiveConnections {
|
||||
inner: Arc<DashMap<NymNodeRoutingAddress, ConnectionSender>>,
|
||||
inner: Arc<DashMap<SocketAddr, ConnectionSender>>,
|
||||
}
|
||||
|
||||
impl ActiveConnections {
|
||||
@@ -82,7 +78,7 @@ impl ActiveConnections {
|
||||
}
|
||||
|
||||
impl Deref for ActiveConnections {
|
||||
type Target = DashMap<NymNodeRoutingAddress, ConnectionSender>;
|
||||
type Target = DashMap<SocketAddr, ConnectionSender>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
@@ -104,6 +100,7 @@ impl ConnectionSender {
|
||||
|
||||
struct ManagedConnection {
|
||||
address: SocketAddr,
|
||||
noise_config: NoiseConfig,
|
||||
message_receiver: ReceiverStream<FramedNymPacket>,
|
||||
connection_timeout: Duration,
|
||||
current_reconnection: Arc<AtomicU32>,
|
||||
@@ -112,12 +109,14 @@ struct ManagedConnection {
|
||||
impl ManagedConnection {
|
||||
fn new(
|
||||
address: SocketAddr,
|
||||
noise_config: NoiseConfig,
|
||||
message_receiver: mpsc::Receiver<FramedNymPacket>,
|
||||
connection_timeout: Duration,
|
||||
current_reconnection: Arc<AtomicU32>,
|
||||
) -> Self {
|
||||
ManagedConnection {
|
||||
address,
|
||||
noise_config,
|
||||
message_receiver: ReceiverStream::new(message_receiver),
|
||||
connection_timeout,
|
||||
current_reconnection,
|
||||
@@ -132,9 +131,21 @@ impl ManagedConnection {
|
||||
Ok(stream_res) => match stream_res {
|
||||
Ok(stream) => {
|
||||
debug!("Managed to establish connection to {}", self.address);
|
||||
// if we managed to connect, reset the reconnection count (whatever it might have been)
|
||||
|
||||
let noise_stream =
|
||||
match upgrade_noise_initiator(stream, &self.noise_config).await {
|
||||
Ok(noise_stream) => noise_stream,
|
||||
Err(err) => {
|
||||
error!("Failed to perform Noise handshake with {address} - {err}");
|
||||
// we failed to finish the noise handshake - increase reconnection attempt
|
||||
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
// if we managed to connect AND do the noise handshake, reset the reconnection count (whatever it might have been)
|
||||
self.current_reconnection.store(0, Ordering::Release);
|
||||
Framed::new(stream, NymCodec)
|
||||
debug!("Noise initiator handshake completed for {:?}", address);
|
||||
Framed::new(noise_stream, NymCodec)
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("failed to establish connection to {address} (err: {err})",);
|
||||
@@ -167,9 +178,14 @@ impl ManagedConnection {
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(config: Config, connections_count: Arc<AtomicUsize>) -> Client {
|
||||
pub fn new(
|
||||
config: Config,
|
||||
noise_config: NoiseConfig,
|
||||
connections_count: Arc<AtomicUsize>,
|
||||
) -> Client {
|
||||
Client {
|
||||
active_connections: Default::default(),
|
||||
noise_config,
|
||||
connections_count,
|
||||
config,
|
||||
}
|
||||
@@ -196,7 +212,7 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_connection(&self, address: NymNodeRoutingAddress, pending_packet: FramedNymPacket) {
|
||||
fn make_connection(&self, address: SocketAddr, pending_packet: FramedNymPacket) {
|
||||
let (sender, receiver) = mpsc::channel(self.config.maximum_connection_buffer_size);
|
||||
|
||||
// this CAN'T fail because we just created the channel which has a non-zero capacity
|
||||
@@ -224,6 +240,7 @@ impl Client {
|
||||
let initial_connection_timeout = self.config.initial_connection_timeout;
|
||||
|
||||
let connections_count = self.connections_count.clone();
|
||||
let noise_config = self.noise_config.clone();
|
||||
tokio::spawn(async move {
|
||||
// before executing the manager, wait for what was specified, if anything
|
||||
if let Some(backoff) = backoff {
|
||||
@@ -233,7 +250,8 @@ impl Client {
|
||||
|
||||
connections_count.fetch_add(1, Ordering::SeqCst);
|
||||
ManagedConnection::new(
|
||||
address.into(),
|
||||
address,
|
||||
noise_config,
|
||||
receiver,
|
||||
initial_connection_timeout,
|
||||
current_reconnection_attempt,
|
||||
@@ -246,18 +264,14 @@ impl Client {
|
||||
}
|
||||
|
||||
impl SendWithoutResponse for Client {
|
||||
fn send_without_response(
|
||||
&self,
|
||||
address: NymNodeRoutingAddress,
|
||||
packet: NymPacket,
|
||||
packet_type: PacketType,
|
||||
) -> io::Result<()> {
|
||||
trace!("Sending packet to {address:?}");
|
||||
let framed_packet = FramedNymPacket::new(packet, packet_type);
|
||||
fn send_without_response(&self, packet: MixPacket) -> io::Result<()> {
|
||||
let address = packet.next_hop_address();
|
||||
trace!("Sending packet to {address}");
|
||||
let framed_packet = FramedNymPacket::from(packet);
|
||||
|
||||
let Some(sender) = self.active_connections.get_mut(&address) else {
|
||||
// there was never a connection to begin with
|
||||
debug!("establishing initial connection to {}", address);
|
||||
debug!("establishing initial connection to {address}");
|
||||
// it's not a 'big' error, but we did not manage to send the packet, but queue the packet
|
||||
// for sending for as soon as the connection is created
|
||||
self.make_connection(address, framed_packet);
|
||||
@@ -302,8 +316,12 @@ impl SendWithoutResponse for Client {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_noise::config::NoiseNetworkView;
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
fn dummy_client() -> Client {
|
||||
let mut rng = OsRng; //for test only, so we don't care if rng source isn't crypto grade
|
||||
Client::new(
|
||||
Config {
|
||||
initial_reconnection_backoff: Duration::from_millis(10_000),
|
||||
@@ -311,6 +329,11 @@ mod tests {
|
||||
initial_connection_timeout: Duration::from_millis(1_500),
|
||||
maximum_connection_buffer_size: 128,
|
||||
},
|
||||
NoiseConfig::new(
|
||||
Arc::new(x25519::KeyPair::new(&mut rng)),
|
||||
NoiseNetworkView::new_empty(),
|
||||
Duration::from_millis(1_500),
|
||||
),
|
||||
Default::default(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@ use nym_api_requests::models::{
|
||||
NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse,
|
||||
};
|
||||
use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated};
|
||||
use nym_api_requests::nym_nodes::{NodesByAddressesResponse, SkimmedNode};
|
||||
use nym_api_requests::nym_nodes::{
|
||||
NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata,
|
||||
};
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_http_api_client::UserAgent;
|
||||
use nym_mixnet_contract_common::EpochRewardedSet;
|
||||
@@ -46,6 +48,46 @@ use crate::rpc::http_client;
|
||||
#[cfg(feature = "http-client")]
|
||||
use crate::{DirectSigningHttpRpcValidatorClient, HttpRpcClient, QueryHttpRpcValidatorClient};
|
||||
|
||||
// a simple helper macro to define to repeatedly call a paged query until a full response is constructed
|
||||
macro_rules! collect_paged_skimmed_v2 {
|
||||
( $self:ident, $f: ident ) => {{
|
||||
// unroll first loop iteration in order to obtain the metadata
|
||||
let mut page = 0;
|
||||
let res = $self
|
||||
.nym_api
|
||||
.$f(false, Some(page), None, $self.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;
|
||||
|
||||
loop {
|
||||
let mut res = $self
|
||||
.nym_api
|
||||
.$f(false, Some(page), None, $self.use_bincode)
|
||||
.await?;
|
||||
|
||||
if metadata != res.metadata {
|
||||
return Err(ValidatorClientError::InconsistentPagedMetadata);
|
||||
}
|
||||
|
||||
nodes.append(&mut res.nodes.data);
|
||||
if nodes.len() < res.nodes.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
|
||||
}};
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
@@ -200,11 +242,11 @@ impl<C, S> Client<C, S> {
|
||||
#[allow(deprecated)]
|
||||
impl<C, S> Client<C, S> {
|
||||
pub fn api_url(&self) -> &Url {
|
||||
self.nym_api.current_url()
|
||||
self.nym_api.current_url().as_ref()
|
||||
}
|
||||
|
||||
pub fn change_nym_api(&mut self, new_endpoint: Url) {
|
||||
self.nym_api.change_base_url(new_endpoint)
|
||||
self.nym_api.change_base_urls(vec![new_endpoint.into()])
|
||||
}
|
||||
|
||||
#[deprecated]
|
||||
@@ -402,11 +444,11 @@ impl NymApiClient {
|
||||
}
|
||||
|
||||
pub fn api_url(&self) -> &Url {
|
||||
self.nym_api.current_url()
|
||||
self.nym_api.current_url().as_ref()
|
||||
}
|
||||
|
||||
pub fn change_nym_api(&mut self, new_endpoint: Url) {
|
||||
self.nym_api.change_base_url(new_endpoint);
|
||||
self.nym_api.change_base_urls(vec![new_endpoint.into()]);
|
||||
}
|
||||
|
||||
#[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes instead")]
|
||||
@@ -425,92 +467,93 @@ impl NymApiClient {
|
||||
|
||||
/// retrieve basic information for nodes are capable of operating as an entry gateway
|
||||
/// this includes legacy gateways and nym-nodes
|
||||
#[deprecated(note = "use get_all_basic_entry_assigned_nodes_with_metadata instead")]
|
||||
pub async fn get_all_basic_entry_assigned_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<SkimmedNode>, 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 nodes = Vec::new();
|
||||
self.get_all_basic_entry_assigned_nodes_v2()
|
||||
.await
|
||||
.map(|res| res.nodes)
|
||||
}
|
||||
|
||||
loop {
|
||||
let mut res = self
|
||||
.nym_api
|
||||
.get_basic_entry_assigned_nodes(false, Some(page), None, self.use_bincode)
|
||||
.await?;
|
||||
|
||||
nodes.append(&mut res.nodes.data);
|
||||
if nodes.len() < res.nodes.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(nodes)
|
||||
pub async fn get_all_basic_entry_assigned_nodes_v2(
|
||||
&self,
|
||||
) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
|
||||
collect_paged_skimmed_v2!(self, get_basic_entry_assigned_nodes_v2)
|
||||
}
|
||||
|
||||
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
|
||||
/// this includes legacy mixnodes and nym-nodes
|
||||
#[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes_with_metadata instead")]
|
||||
pub async fn get_all_basic_active_mixing_assigned_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<SkimmedNode>, 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 nodes = Vec::new();
|
||||
self.get_all_basic_active_mixing_assigned_nodes_with_metadata()
|
||||
.await
|
||||
.map(|res| res.nodes)
|
||||
}
|
||||
|
||||
loop {
|
||||
let mut res = self
|
||||
.nym_api
|
||||
.get_basic_active_mixing_assigned_nodes(false, Some(page), None, self.use_bincode)
|
||||
.await?;
|
||||
|
||||
nodes.append(&mut res.nodes.data);
|
||||
if nodes.len() < res.nodes.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(nodes)
|
||||
pub async fn get_all_basic_active_mixing_assigned_nodes_with_metadata(
|
||||
&self,
|
||||
) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
|
||||
collect_paged_skimmed_v2!(self, get_basic_active_mixing_assigned_nodes_v2)
|
||||
}
|
||||
|
||||
/// retrieve basic information for nodes are capable of operating as a mixnode
|
||||
/// this includes legacy mixnodes and nym-nodes
|
||||
#[deprecated(note = "use get_all_basic_mixing_capable_nodes_with_metadata instead")]
|
||||
pub async fn get_all_basic_mixing_capable_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<SkimmedNode>, 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 nodes = Vec::new();
|
||||
self.get_all_basic_mixing_capable_nodes_with_metadata()
|
||||
.await
|
||||
.map(|res| res.nodes)
|
||||
}
|
||||
|
||||
loop {
|
||||
let mut res = self
|
||||
.nym_api
|
||||
.get_basic_mixing_capable_nodes(false, Some(page), None, self.use_bincode)
|
||||
.await?;
|
||||
|
||||
nodes.append(&mut res.nodes.data);
|
||||
if nodes.len() < res.nodes.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(nodes)
|
||||
pub async fn get_all_basic_mixing_capable_nodes_with_metadata(
|
||||
&self,
|
||||
) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
|
||||
collect_paged_skimmed_v2!(self, get_basic_mixing_capable_nodes_v2)
|
||||
}
|
||||
|
||||
/// retrieve basic information for all bonded nodes on the network
|
||||
#[deprecated(note = "use get_all_basic_nodes_with_metadata instead")]
|
||||
pub async fn get_all_basic_nodes(&self) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
self.get_all_basic_nodes_with_metadata()
|
||||
.await
|
||||
.map(|res| res.nodes)
|
||||
}
|
||||
|
||||
pub async fn get_all_basic_nodes_with_metadata(
|
||||
&self,
|
||||
) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
|
||||
collect_paged_skimmed_v2!(self, get_basic_nodes_v2)
|
||||
}
|
||||
|
||||
/// retrieve expanded information for all bonded nodes on the network
|
||||
pub async fn get_all_expanded_nodes(
|
||||
&self,
|
||||
) -> Result<SemiSkimmedNodesWithMetadata, ValidatorClientError> {
|
||||
// Unroll the first iteration to get the metadata
|
||||
let mut page = 0;
|
||||
let mut nodes = Vec::new();
|
||||
|
||||
let res = self
|
||||
.nym_api
|
||||
.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
|
||||
.nym_api
|
||||
.get_basic_nodes(false, Some(page), None, self.use_bincode)
|
||||
.get_expanded_nodes(false, Some(page), None)
|
||||
.await?;
|
||||
|
||||
nodes.append(&mut res.nodes.data);
|
||||
@@ -521,7 +564,7 @@ impl NymApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(nodes)
|
||||
Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata))
|
||||
}
|
||||
|
||||
pub async fn health(&self) -> Result<ApiHealthResponse, ValidatorClientError> {
|
||||
|
||||
@@ -22,6 +22,9 @@ pub enum ValidatorClientError {
|
||||
#[error("nyxd request failed: {0}")]
|
||||
NyxdError(#[from] crate::nyxd::error::NyxdError),
|
||||
|
||||
#[error("the response metadata has changed between pages")]
|
||||
InconsistentPagedMetadata,
|
||||
|
||||
#[error("No validator API url has been provided")]
|
||||
NoAPIUrlAvailable,
|
||||
}
|
||||
|
||||
@@ -14,11 +14,12 @@ use nym_api_requests::ecash::models::{
|
||||
use nym_api_requests::ecash::VerificationKeyResponse;
|
||||
use nym_api_requests::models::{
|
||||
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainStatusResponse,
|
||||
LegacyDescribedMixNode, NodePerformanceResponse, NodeRefreshBody, NymNodeDescription,
|
||||
PerformanceHistoryResponse, RewardedSetResponse,
|
||||
KeyRotationInfoResponse, LegacyDescribedMixNode, NodePerformanceResponse, NodeRefreshBody,
|
||||
NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
|
||||
};
|
||||
use nym_api_requests::nym_nodes::{
|
||||
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponse,
|
||||
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1,
|
||||
PaginatedCachedNodesResponseV2,
|
||||
};
|
||||
use nym_api_requests::pagination::PaginatedResponse;
|
||||
pub use nym_api_requests::{
|
||||
@@ -34,7 +35,7 @@ pub use nym_api_requests::{
|
||||
MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse,
|
||||
StakeSaturationResponse, UptimeResponse,
|
||||
},
|
||||
nym_nodes::{CachedNodesResponse, SkimmedNode},
|
||||
nym_nodes::{CachedNodesResponse, SemiSkimmedNode, SkimmedNode},
|
||||
NymNetworkDetailsResponse,
|
||||
};
|
||||
use nym_contracts_common::IdentityKey;
|
||||
@@ -62,7 +63,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn health(&self) -> Result<ApiHealthResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::API_STATUS_ROUTES,
|
||||
routes::HEALTH,
|
||||
],
|
||||
@@ -75,7 +76,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn build_information(&self) -> Result<BinaryBuildInformationOwned, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::API_STATUS_ROUTES,
|
||||
routes::BUILD_INFORMATION,
|
||||
],
|
||||
@@ -87,7 +88,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[deprecated]
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
|
||||
self.get_json(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS)
|
||||
self.get_json(&[routes::V1_API_VERSION, routes::MIXNODES], NO_PARAMS)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -96,7 +97,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn get_mixnodes_detailed(&self) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::MIXNODES,
|
||||
routes::DETAILED,
|
||||
@@ -111,7 +112,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn get_gateways_detailed(&self) -> Result<Vec<GatewayBondAnnotated>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::GATEWAYS,
|
||||
routes::DETAILED,
|
||||
@@ -128,7 +129,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<Vec<GatewayBondAnnotated>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::GATEWAYS,
|
||||
routes::DETAILED_UNFILTERED,
|
||||
@@ -145,7 +146,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::MIXNODES,
|
||||
routes::DETAILED_UNFILTERED,
|
||||
@@ -158,7 +159,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[deprecated]
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_gateways(&self) -> Result<Vec<GatewayBond>, NymAPIError> {
|
||||
self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS)
|
||||
self.get_json(&[routes::V1_API_VERSION, routes::GATEWAYS], NO_PARAMS)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -166,7 +167,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_gateways_described(&self) -> Result<Vec<LegacyDescribedGateway>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[routes::API_VERSION, routes::GATEWAYS, routes::DESCRIBED],
|
||||
&[routes::V1_API_VERSION, routes::GATEWAYS, routes::DESCRIBED],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
@@ -176,7 +177,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_mixnodes_described(&self) -> Result<Vec<LegacyDescribedMixNode>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[routes::API_VERSION, routes::MIXNODES, routes::DESCRIBED],
|
||||
&[routes::V1_API_VERSION, routes::MIXNODES, routes::DESCRIBED],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
@@ -201,7 +202,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::NYM_NODES_PERFORMANCE_HISTORY,
|
||||
&*node_id.to_string(),
|
||||
@@ -229,7 +230,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::NYM_NODES_DESCRIBED,
|
||||
],
|
||||
@@ -256,7 +257,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::NYM_NODES_BONDED,
|
||||
],
|
||||
@@ -270,7 +271,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"mixnodes",
|
||||
@@ -286,7 +287,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn get_basic_gateways(&self) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"gateways",
|
||||
@@ -301,7 +302,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn get_rewarded_set(&self) -> Result<RewardedSetResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::NYM_NODES_REWARDED_SET,
|
||||
],
|
||||
@@ -312,6 +313,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
/// retrieve basic information for nodes are capable of operating as an entry gateway
|
||||
/// this includes legacy gateways and nym-nodes
|
||||
#[deprecated(note = "use get_basic_entry_assigned_nodes_v2")]
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_basic_entry_assigned_nodes(
|
||||
&self,
|
||||
@@ -319,7 +321,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
|
||||
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
@@ -340,7 +342,49 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_response(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"skimmed",
|
||||
"entry-gateways",
|
||||
"all",
|
||||
],
|
||||
¶ms,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// retrieve basic information for nodes are capable of operating as an entry gateway
|
||||
/// this includes legacy gateways and nym-nodes
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_basic_entry_assigned_nodes_v2(
|
||||
&self,
|
||||
no_legacy: bool,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
params.push(("no_legacy", "true".to_string()))
|
||||
}
|
||||
|
||||
if let Some(page) = page {
|
||||
params.push(("page", page.to_string()))
|
||||
}
|
||||
|
||||
if let Some(per_page) = per_page {
|
||||
params.push(("per_page", per_page.to_string()))
|
||||
}
|
||||
|
||||
if use_bincode {
|
||||
params.push(("output", "bincode".to_string()))
|
||||
}
|
||||
|
||||
self.get_response(
|
||||
&[
|
||||
routes::V2_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"skimmed",
|
||||
@@ -354,6 +398,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
|
||||
/// this includes legacy mixnodes and nym-nodes
|
||||
#[deprecated(note = "use get_basic_active_mixing_assigned_nodes_v2")]
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_basic_active_mixing_assigned_nodes(
|
||||
&self,
|
||||
@@ -361,7 +406,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
|
||||
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
@@ -382,7 +427,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_response(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"skimmed",
|
||||
@@ -397,13 +442,13 @@ pub trait NymApiClientExt: ApiClient {
|
||||
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
|
||||
/// this includes legacy mixnodes and nym-nodes
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_basic_mixing_capable_nodes(
|
||||
async fn get_basic_active_mixing_assigned_nodes_v2(
|
||||
&self,
|
||||
no_legacy: bool,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
@@ -424,7 +469,50 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_response(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V2_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"skimmed",
|
||||
"mixnodes",
|
||||
"active",
|
||||
],
|
||||
¶ms,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
|
||||
/// this includes legacy mixnodes and nym-nodes
|
||||
#[deprecated(note = "use get_basic_mixing_capable_nodes_v2")]
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_basic_mixing_capable_nodes(
|
||||
&self,
|
||||
no_legacy: bool,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
params.push(("no_legacy", "true".to_string()))
|
||||
}
|
||||
|
||||
if let Some(page) = page {
|
||||
params.push(("page", page.to_string()))
|
||||
}
|
||||
|
||||
if let Some(per_page) = per_page {
|
||||
params.push(("per_page", per_page.to_string()))
|
||||
}
|
||||
|
||||
if use_bincode {
|
||||
params.push(("output", "bincode".to_string()))
|
||||
}
|
||||
|
||||
self.get_response(
|
||||
&[
|
||||
routes::V1_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"skimmed",
|
||||
@@ -436,14 +524,16 @@ pub trait NymApiClientExt: ApiClient {
|
||||
.await
|
||||
}
|
||||
|
||||
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
|
||||
/// this includes legacy mixnodes and nym-nodes
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_basic_nodes(
|
||||
async fn get_basic_mixing_capable_nodes_v2(
|
||||
&self,
|
||||
no_legacy: bool,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
@@ -464,10 +554,122 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_response(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V2_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"skimmed",
|
||||
"mixnodes",
|
||||
"all",
|
||||
],
|
||||
¶ms,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[deprecated(note = "use get_basic_nodes_v2")]
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_basic_nodes(
|
||||
&self,
|
||||
no_legacy: bool,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
params.push(("no_legacy", "true".to_string()))
|
||||
}
|
||||
|
||||
if let Some(page) = page {
|
||||
params.push(("page", page.to_string()))
|
||||
}
|
||||
|
||||
if let Some(per_page) = per_page {
|
||||
params.push(("per_page", per_page.to_string()))
|
||||
}
|
||||
|
||||
if use_bincode {
|
||||
params.push(("output", "bincode".to_string()))
|
||||
}
|
||||
|
||||
self.get_response(
|
||||
&[
|
||||
routes::V1_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"skimmed",
|
||||
],
|
||||
¶ms,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_basic_nodes_v2(
|
||||
&self,
|
||||
no_legacy: bool,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
params.push(("no_legacy", "true".to_string()))
|
||||
}
|
||||
|
||||
if let Some(page) = page {
|
||||
params.push(("page", page.to_string()))
|
||||
}
|
||||
|
||||
if let Some(per_page) = per_page {
|
||||
params.push(("per_page", per_page.to_string()))
|
||||
}
|
||||
|
||||
if use_bincode {
|
||||
params.push(("output", "bincode".to_string()))
|
||||
}
|
||||
|
||||
self.get_response(
|
||||
&[
|
||||
routes::V2_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"skimmed",
|
||||
],
|
||||
¶ms,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_expanded_nodes(
|
||||
&self,
|
||||
no_legacy: bool,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SemiSkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
params.push(("no_legacy", "true".to_string()))
|
||||
}
|
||||
|
||||
if let Some(page) = page {
|
||||
params.push(("page", page.to_string()))
|
||||
}
|
||||
|
||||
if let Some(per_page) = per_page {
|
||||
params.push(("per_page", per_page.to_string()))
|
||||
}
|
||||
|
||||
self.get_json(
|
||||
&[
|
||||
routes::V2_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"semi-skimmed",
|
||||
],
|
||||
¶ms,
|
||||
)
|
||||
@@ -478,7 +680,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE],
|
||||
&[routes::V1_API_VERSION, routes::MIXNODES, routes::ACTIVE],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
@@ -489,7 +691,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn get_active_mixnodes_detailed(&self) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::MIXNODES,
|
||||
routes::ACTIVE,
|
||||
@@ -504,7 +706,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_rewarded_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[routes::API_VERSION, routes::MIXNODES, routes::REWARDED],
|
||||
&[routes::V1_API_VERSION, routes::MIXNODES, routes::REWARDED],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
@@ -518,7 +720,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<MixnodeStatusReportResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -537,7 +739,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<GatewayStatusReportResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::GATEWAY,
|
||||
identity,
|
||||
@@ -556,7 +758,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<MixnodeUptimeHistoryResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -575,7 +777,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<GatewayUptimeHistoryResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::GATEWAY,
|
||||
identity,
|
||||
@@ -593,7 +795,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::MIXNODES,
|
||||
routes::REWARDED,
|
||||
@@ -614,7 +816,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
if let Some(since) = since {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::GATEWAY,
|
||||
identity,
|
||||
@@ -626,7 +828,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
} else {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::GATEWAY,
|
||||
identity,
|
||||
@@ -647,7 +849,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
if let Some(since) = since {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -659,7 +861,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
} else {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -679,7 +881,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<MixnodeStatusResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -698,7 +900,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<RewardEstimationResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -718,7 +920,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<RewardEstimationResponse, NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -738,7 +940,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<StakeSaturationResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -758,7 +960,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<nym_api_requests::models::InclusionProbabilityResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -776,7 +978,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<NodePerformanceResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::NYM_NODES_PERFORMANCE,
|
||||
&node_id.to_string(),
|
||||
@@ -792,7 +994,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<AnnotationResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::NYM_NODES_ANNOTATION,
|
||||
&node_id.to_string(),
|
||||
@@ -806,7 +1008,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn get_mixnode_avg_uptime(&self, mix_id: NodeId) -> Result<UptimeResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -821,7 +1023,11 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_mixnodes_blacklisted(&self) -> Result<Vec<NodeId>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[routes::API_VERSION, routes::MIXNODES, routes::BLACKLISTED],
|
||||
&[
|
||||
routes::V1_API_VERSION,
|
||||
routes::MIXNODES,
|
||||
routes::BLACKLISTED,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
@@ -831,7 +1037,11 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_gateways_blacklisted(&self) -> Result<Vec<IdentityKey>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[routes::API_VERSION, routes::GATEWAYS, routes::BLACKLISTED],
|
||||
&[
|
||||
routes::V1_API_VERSION,
|
||||
routes::GATEWAYS,
|
||||
routes::BLACKLISTED,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
@@ -844,7 +1054,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<BlindedSignatureResponse, NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::ECASH_BLIND_SIGN,
|
||||
],
|
||||
@@ -861,7 +1071,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<EcashTicketVerificationResponse, NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::VERIFY_ECASH_TICKET,
|
||||
],
|
||||
@@ -878,7 +1088,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<EcashBatchTicketRedemptionResponse, NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::BATCH_REDEEM_ECASH_TICKETS,
|
||||
],
|
||||
@@ -903,7 +1113,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::PARTIAL_EXPIRATION_DATE_SIGNATURES,
|
||||
],
|
||||
@@ -924,7 +1134,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::PARTIAL_COIN_INDICES_SIGNATURES,
|
||||
],
|
||||
@@ -948,7 +1158,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::GLOBAL_EXPIRATION_DATE_SIGNATURES,
|
||||
],
|
||||
@@ -969,7 +1179,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::GLOBAL_COIN_INDICES_SIGNATURES,
|
||||
],
|
||||
@@ -989,7 +1199,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
};
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
ecash::MASTER_VERIFICATION_KEY,
|
||||
],
|
||||
@@ -1005,7 +1215,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<(), NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::NYM_NODES_REFRESH_DESCRIBED,
|
||||
],
|
||||
@@ -1022,7 +1232,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<IssuedTicketbooksForResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::ECASH_ISSUED_TICKETBOOKS_FOR,
|
||||
&expiration_date.to_string(),
|
||||
@@ -1039,7 +1249,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<IssuedTicketbooksForCountResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::ECASH_ISSUED_TICKETBOOKS_FOR_COUNT,
|
||||
&expiration_date.to_string(),
|
||||
@@ -1056,7 +1266,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<IssuedTicketbooksChallengeCommitmentResponse, NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::ECASH_ISSUED_TICKETBOOKS_CHALLENGE_COMMITMENT,
|
||||
],
|
||||
@@ -1073,7 +1283,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<IssuedTicketbooksDataResponse, NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::ECASH_ISSUED_TICKETBOOKS_DATA,
|
||||
],
|
||||
@@ -1089,7 +1299,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<NodesByAddressesResponse, NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::nym_nodes::BY_ADDRESSES,
|
||||
@@ -1103,7 +1313,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_network_details(&self) -> Result<NymNetworkDetailsResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[routes::API_VERSION, routes::NETWORK, routes::DETAILS],
|
||||
&[routes::V1_API_VERSION, routes::NETWORK, routes::DETAILS],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
@@ -1112,7 +1322,24 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_chain_status(&self) -> Result<ChainStatusResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[routes::API_VERSION, routes::NETWORK, routes::CHAIN_STATUS],
|
||||
&[
|
||||
routes::V1_API_VERSION,
|
||||
routes::NETWORK,
|
||||
routes::CHAIN_STATUS,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_key_rotation_info(&self) -> Result<KeyRotationInfoResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::V1_API_VERSION,
|
||||
routes::EPOCH,
|
||||
routes::KEY_ROTATION_INFO,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_network_defaults::NYM_API_VERSION;
|
||||
|
||||
pub const API_VERSION: &str = NYM_API_VERSION;
|
||||
pub const V1_API_VERSION: &str = "v1";
|
||||
pub const V2_API_VERSION: &str = "v2";
|
||||
pub const MIXNODES: &str = "mixnodes";
|
||||
pub const GATEWAYS: &str = "gateways";
|
||||
pub const DESCRIBED: &str = "described";
|
||||
@@ -79,3 +78,11 @@ pub const SERVICE_PROVIDERS: &str = "services";
|
||||
pub const DETAILS: &str = "details";
|
||||
pub const CHAIN_STATUS: &str = "chain-status";
|
||||
pub const NETWORK: &str = "network";
|
||||
|
||||
pub const EPOCH: &str = "epoch";
|
||||
|
||||
pub use epoch_routes::*;
|
||||
pub mod epoch_routes {
|
||||
pub const CURRENT: &str = "current";
|
||||
pub const KEY_ROTATION_INFO: &str = "key-rotation-info";
|
||||
}
|
||||
|
||||
+29
-9
@@ -12,8 +12,8 @@ use nym_mixnet_contract_common::gateway::{PreassignedGatewayIdsResponse, Preassi
|
||||
use nym_mixnet_contract_common::nym_node::{
|
||||
EpochAssignmentResponse, NodeDetailsByIdentityResponse, NodeDetailsResponse,
|
||||
NodeOwnershipResponse, NodeRewardingDetailsResponse, PagedNymNodeBondsResponse,
|
||||
PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse,
|
||||
StakeSaturationResponse, UnbondedNodeResponse, UnbondedNymNode,
|
||||
PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, RewardedSetMetadata, Role,
|
||||
RolesMetadataResponse, StakeSaturationResponse, UnbondedNodeResponse, UnbondedNymNode,
|
||||
};
|
||||
use nym_mixnet_contract_common::reward_params::WorkFactor;
|
||||
use nym_mixnet_contract_common::{
|
||||
@@ -28,12 +28,12 @@ use nym_mixnet_contract_common::{
|
||||
ContractBuildInformation, ContractState, ContractStateParams, CurrentIntervalResponse,
|
||||
CurrentNymNodeVersionResponse, Delegation, EpochEventId, EpochRewardedSet, EpochStatus,
|
||||
GatewayBond, GatewayBondResponse, GatewayOwnershipResponse, HistoricalNymNodeVersionEntry,
|
||||
IdentityKey, IdentityKeyRef, IntervalEventId, MixNodeBond, MixNodeDetails,
|
||||
MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeId,
|
||||
NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails, NymNodeVersionHistoryResponse,
|
||||
PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedGatewayResponse,
|
||||
PagedMixnodeBondsResponse, PagedNodeDelegationsResponse, PendingEpochEvent,
|
||||
PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEvent,
|
||||
IdentityKey, IdentityKeyRef, IntervalEventId, KeyRotationIdResponse, KeyRotationState,
|
||||
MixNodeBond, MixNodeDetails, MixOwnershipResponse, MixnodeDetailsByIdentityResponse,
|
||||
MixnodeDetailsResponse, NodeId, NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails,
|
||||
NymNodeVersionHistoryResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse,
|
||||
PagedGatewayResponse, PagedMixnodeBondsResponse, PagedNodeDelegationsResponse,
|
||||
PendingEpochEvent, PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEvent,
|
||||
PendingIntervalEventResponse, PendingIntervalEventsResponse, QueryMsg as MixnetQueryMsg,
|
||||
RewardedSet, UnbondedMixnode,
|
||||
};
|
||||
@@ -546,6 +546,16 @@ pub trait MixnetQueryClient {
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_key_rotation_state(&self) -> Result<KeyRotationState, NyxdError> {
|
||||
self.query_mixnet_contract(MixnetQueryMsg::GetKeyRotationState {})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_key_rotation_id(&self) -> Result<KeyRotationIdResponse, NyxdError> {
|
||||
self.query_mixnet_contract(MixnetQueryMsg::GetKeyRotationId {})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// extension trait to the query client to deal with the paged queries
|
||||
@@ -673,12 +683,20 @@ pub trait MixnetQueryClientExt: MixnetQueryClient {
|
||||
async fn get_rewarded_set(&self) -> Result<EpochRewardedSet, NyxdError> {
|
||||
let error_response = |message| Err(NyxdError::extension_query_failure("mixnet", message));
|
||||
|
||||
// bypass for catch 22 for fresh contracts. we can't refresh cache because there's no rewarded set,
|
||||
// but we can't set the rewarded set because we didn't refresh the cache
|
||||
let metadata = self.get_rewarded_set_metadata().await?;
|
||||
if !metadata.metadata.fully_assigned {
|
||||
|
||||
let is_default = metadata.metadata == RewardedSetMetadata::default();
|
||||
if !metadata.metadata.fully_assigned && !is_default {
|
||||
return error_response("the rewarded set hasn't been fully assigned for this epoch");
|
||||
}
|
||||
let expected_epoch_id = metadata.metadata.epoch_id;
|
||||
|
||||
if is_default {
|
||||
return Ok(Default::default());
|
||||
}
|
||||
|
||||
// if we have to query those things more frequently, we could do it concurrently,
|
||||
// but as it stands now, it happens so infrequently it might as well be sequential
|
||||
let entry = self.get_role_assignment(Role::EntryGateway).await?;
|
||||
@@ -955,6 +973,8 @@ mod tests {
|
||||
QueryMsg::GetNymNodeVersionHistory { limit, start_after } => client
|
||||
.get_nym_node_version_history_paged(start_after, limit)
|
||||
.ignore(),
|
||||
QueryMsg::GetKeyRotationState {} => client.get_key_rotation_state().ignore(),
|
||||
QueryMsg::GetKeyRotationId {} => client.get_key_rotation_id().ignore(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +138,14 @@ impl NyxdClient<HttpClient> {
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn connect_to_default_env<U>(endpoint: U) -> Result<QueryHttpRpcNyxdClient, NyxdError>
|
||||
where
|
||||
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
|
||||
{
|
||||
let config = Config::try_from_nym_network_details(&NymNetworkDetails::new_from_env())?;
|
||||
Self::connect(config, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
impl NyxdClient<ReqwestRpcClient> {
|
||||
|
||||
@@ -157,6 +157,7 @@ pub async fn generate(args: Args) {
|
||||
minimum: args.minimum_interval_operating_cost.amount.into(),
|
||||
maximum: args.maximum_interval_operating_cost.amount.into(),
|
||||
},
|
||||
key_validity_in_epochs: None,
|
||||
};
|
||||
|
||||
debug!("instantiate_msg: {:?}", instantiate_msg);
|
||||
|
||||
@@ -48,8 +48,7 @@ pub mod nym_config {
|
||||
log::trace!("Loading from file: {:#?}", filepath.as_ref().to_owned());
|
||||
let config_contents = fs::read_to_string(filepath)?;
|
||||
|
||||
toml::from_str(&config_contents)
|
||||
.map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err))
|
||||
toml::from_str(&config_contents).map_err(io::Error::other)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,8 +151,7 @@ where
|
||||
let content = fs::read_to_string(path)?;
|
||||
|
||||
// TODO: should we be preserving original error type instead?
|
||||
deserialize_config_from_toml_str(&content)
|
||||
.map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err))
|
||||
deserialize_config_from_toml_str(&content).map_err(io::Error::other)
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -40,3 +40,6 @@ contract-testing = []
|
||||
utoipa = ["dep:utoipa"]
|
||||
schema = ["cw2"]
|
||||
generate-ts = ['ts-rs']
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -193,6 +193,9 @@ pub enum MixnetContractError {
|
||||
#[error("attempted to perform the operation with 0 coins. This is not allowed")]
|
||||
ZeroCoinAmount,
|
||||
|
||||
#[error("key rotation validity below minimum value")]
|
||||
TooShortRotationInterval,
|
||||
|
||||
#[error("this validator ({current_validator}) is not the one responsible for advancing this epoch. It's responsibility of {chosen_validator}.")]
|
||||
RewardingValidatorMismatch {
|
||||
current_validator: Addr,
|
||||
|
||||
@@ -358,7 +358,7 @@ impl Interval {
|
||||
self.total_elapsed_epochs
|
||||
}
|
||||
|
||||
pub const fn current_epoch_absolute_id(&self) -> u32 {
|
||||
pub const fn current_epoch_absolute_id(&self) -> EpochId {
|
||||
// since we count epochs starting from 0, if n epochs have elapsed, the current one has absolute id of n
|
||||
self.total_elapsed_epochs
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::EpochId;
|
||||
use cosmwasm_schema::cw_serde;
|
||||
|
||||
pub type KeyRotationId = u32;
|
||||
|
||||
#[cw_serde]
|
||||
#[derive(Copy)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
pub struct KeyRotationState {
|
||||
/// Defines how long each key rotation is valid for (in terms of epochs)
|
||||
pub validity_epochs: u32,
|
||||
|
||||
/// Records the initial epoch_id when the key rotation has been introduced (0 for fresh contracts).
|
||||
/// It is used for determining when rotation is meant to advance.
|
||||
#[cfg_attr(feature = "utoipa", schema(value_type = u32))]
|
||||
pub initial_epoch_id: EpochId,
|
||||
}
|
||||
|
||||
impl KeyRotationState {
|
||||
pub fn key_rotation_id(&self, current_epoch_id: EpochId) -> KeyRotationId {
|
||||
let diff = current_epoch_id.saturating_sub(self.initial_epoch_id);
|
||||
diff / self.validity_epochs
|
||||
}
|
||||
|
||||
pub fn next_rotation_starting_epoch_id(&self, current_epoch_id: EpochId) -> EpochId {
|
||||
self.current_rotation_starting_epoch_id(current_epoch_id) + self.validity_epochs
|
||||
}
|
||||
|
||||
pub fn current_rotation_starting_epoch_id(&self, current_epoch_id: EpochId) -> EpochId {
|
||||
let current_rotation_id = self.key_rotation_id(current_epoch_id);
|
||||
|
||||
self.initial_epoch_id + self.validity_epochs * current_rotation_id
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct KeyRotationIdResponse {
|
||||
pub rotation_id: KeyRotationId,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn key_rotation_id() {
|
||||
let state = KeyRotationState {
|
||||
validity_epochs: 24,
|
||||
initial_epoch_id: 0,
|
||||
};
|
||||
assert_eq!(0, state.key_rotation_id(0));
|
||||
assert_eq!(0, state.key_rotation_id(23));
|
||||
assert_eq!(1, state.key_rotation_id(24));
|
||||
assert_eq!(1, state.key_rotation_id(47));
|
||||
assert_eq!(2, state.key_rotation_id(48));
|
||||
|
||||
let state = KeyRotationState {
|
||||
validity_epochs: 12,
|
||||
initial_epoch_id: 0,
|
||||
};
|
||||
assert_eq!(0, state.key_rotation_id(0));
|
||||
assert_eq!(0, state.key_rotation_id(11));
|
||||
assert_eq!(1, state.key_rotation_id(12));
|
||||
assert_eq!(1, state.key_rotation_id(23));
|
||||
assert_eq!(2, state.key_rotation_id(24));
|
||||
|
||||
let state = KeyRotationState {
|
||||
validity_epochs: 24,
|
||||
initial_epoch_id: 10000,
|
||||
};
|
||||
assert_eq!(0, state.key_rotation_id(123));
|
||||
assert_eq!(0, state.key_rotation_id(10000));
|
||||
assert_eq!(0, state.key_rotation_id(10001));
|
||||
assert_eq!(0, state.key_rotation_id(10023));
|
||||
assert_eq!(1, state.key_rotation_id(10024));
|
||||
assert_eq!(1, state.key_rotation_id(10047));
|
||||
assert_eq!(2, state.key_rotation_id(10048));
|
||||
assert_eq!(2, state.key_rotation_id(10060));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_rotation_starting_epoch_id() {
|
||||
let state = KeyRotationState {
|
||||
validity_epochs: 24,
|
||||
initial_epoch_id: 0,
|
||||
};
|
||||
assert_eq!(24, state.next_rotation_starting_epoch_id(0));
|
||||
assert_eq!(24, state.next_rotation_starting_epoch_id(23));
|
||||
assert_eq!(48, state.next_rotation_starting_epoch_id(24));
|
||||
assert_eq!(48, state.next_rotation_starting_epoch_id(47));
|
||||
assert_eq!(72, state.next_rotation_starting_epoch_id(48));
|
||||
|
||||
let state = KeyRotationState {
|
||||
validity_epochs: 12,
|
||||
initial_epoch_id: 0,
|
||||
};
|
||||
assert_eq!(12, state.next_rotation_starting_epoch_id(0));
|
||||
assert_eq!(12, state.next_rotation_starting_epoch_id(11));
|
||||
assert_eq!(24, state.next_rotation_starting_epoch_id(12));
|
||||
assert_eq!(24, state.next_rotation_starting_epoch_id(23));
|
||||
assert_eq!(36, state.next_rotation_starting_epoch_id(24));
|
||||
|
||||
let state = KeyRotationState {
|
||||
validity_epochs: 24,
|
||||
initial_epoch_id: 10000,
|
||||
};
|
||||
assert_eq!(10024, state.next_rotation_starting_epoch_id(123));
|
||||
assert_eq!(10024, state.next_rotation_starting_epoch_id(10000));
|
||||
assert_eq!(10024, state.next_rotation_starting_epoch_id(10001));
|
||||
assert_eq!(10024, state.next_rotation_starting_epoch_id(10023));
|
||||
assert_eq!(10048, state.next_rotation_starting_epoch_id(10024));
|
||||
assert_eq!(10048, state.next_rotation_starting_epoch_id(10047));
|
||||
assert_eq!(10072, state.next_rotation_starting_epoch_id(10048));
|
||||
assert_eq!(10072, state.next_rotation_starting_epoch_id(10060));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_rotation_starting_epoch_id() {
|
||||
let state = KeyRotationState {
|
||||
validity_epochs: 24,
|
||||
initial_epoch_id: 0,
|
||||
};
|
||||
assert_eq!(0, state.current_rotation_starting_epoch_id(0));
|
||||
assert_eq!(0, state.current_rotation_starting_epoch_id(23));
|
||||
assert_eq!(24, state.current_rotation_starting_epoch_id(24));
|
||||
assert_eq!(24, state.current_rotation_starting_epoch_id(47));
|
||||
assert_eq!(48, state.current_rotation_starting_epoch_id(48));
|
||||
|
||||
let state = KeyRotationState {
|
||||
validity_epochs: 12,
|
||||
initial_epoch_id: 0,
|
||||
};
|
||||
assert_eq!(0, state.current_rotation_starting_epoch_id(0));
|
||||
assert_eq!(0, state.current_rotation_starting_epoch_id(11));
|
||||
assert_eq!(12, state.current_rotation_starting_epoch_id(12));
|
||||
assert_eq!(12, state.current_rotation_starting_epoch_id(23));
|
||||
assert_eq!(24, state.current_rotation_starting_epoch_id(24));
|
||||
|
||||
let state = KeyRotationState {
|
||||
validity_epochs: 24,
|
||||
initial_epoch_id: 10000,
|
||||
};
|
||||
assert_eq!(10000, state.current_rotation_starting_epoch_id(123));
|
||||
assert_eq!(10000, state.current_rotation_starting_epoch_id(10000));
|
||||
assert_eq!(10000, state.current_rotation_starting_epoch_id(10001));
|
||||
assert_eq!(10000, state.current_rotation_starting_epoch_id(10023));
|
||||
assert_eq!(10024, state.current_rotation_starting_epoch_id(10024));
|
||||
assert_eq!(10024, state.current_rotation_starting_epoch_id(10047));
|
||||
assert_eq!(10048, state.current_rotation_starting_epoch_id(10048));
|
||||
assert_eq!(10048, state.current_rotation_starting_epoch_id(10060));
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#![warn(clippy::expect_used)]
|
||||
#![warn(clippy::unwrap_used)]
|
||||
#![warn(clippy::todo)]
|
||||
|
||||
mod config_score;
|
||||
pub mod constants;
|
||||
pub mod delegation;
|
||||
@@ -13,6 +9,7 @@ pub mod events;
|
||||
pub mod gateway;
|
||||
pub mod helpers;
|
||||
pub mod interval;
|
||||
pub mod key_rotation;
|
||||
pub mod mixnode;
|
||||
pub mod msg;
|
||||
pub mod nym_node;
|
||||
@@ -37,6 +34,7 @@ pub use gateway::{
|
||||
pub use interval::{
|
||||
CurrentIntervalResponse, EpochId, EpochState, EpochStatus, Interval, IntervalId,
|
||||
};
|
||||
pub use key_rotation::*;
|
||||
pub use mixnode::{
|
||||
LegacyMixLayer, MixNode, MixNodeBond, MixNodeConfigUpdate, MixNodeDetails,
|
||||
MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeCostParams,
|
||||
|
||||
@@ -170,6 +170,11 @@ impl NodeRewarding {
|
||||
}
|
||||
}
|
||||
|
||||
// we panic here as opposed to returning an error as this is undefined behaviour,
|
||||
// because the pledge amount has decreased (i.e. slashing has occurred) which
|
||||
// should not be possible under any situation. at this point we don't know how many other things
|
||||
// might have failed so we have to bail
|
||||
#[allow(clippy::panic)]
|
||||
pub fn pending_detailed_operator_reward(&self, original_pledge: &Coin) -> StdResult<Decimal> {
|
||||
let initial_dec = original_pledge.amount.into_base_decimal()?;
|
||||
if initial_dec > self.operator {
|
||||
@@ -189,6 +194,11 @@ impl NodeRewarding {
|
||||
Ok(truncate_reward(delegator_reward, &delegation.amount.denom))
|
||||
}
|
||||
|
||||
// we panic here as opposed to returning an error as this is undefined behaviour,
|
||||
// because the pledge amount has decreased (i.e. slashing has occurred) which
|
||||
// should not be possible under any situation. at this point we don't know how many other things
|
||||
// might have failed so we have to bail
|
||||
#[allow(clippy::panic)]
|
||||
pub fn withdraw_operator_reward(
|
||||
&mut self,
|
||||
original_pledge: &Coin,
|
||||
|
||||
@@ -35,6 +35,7 @@ use crate::{
|
||||
PreassignedGatewayIdsResponse,
|
||||
},
|
||||
interval::{CurrentIntervalResponse, EpochStatus},
|
||||
key_rotation::{KeyRotationIdResponse, KeyRotationState},
|
||||
mixnode::{
|
||||
MixOwnershipResponse, MixStakeSaturationResponse, MixnodeDetailsByIdentityResponse,
|
||||
MixnodeDetailsResponse, MixnodeRewardingDetailsResponse, PagedMixnodeBondsResponse,
|
||||
@@ -81,6 +82,18 @@ pub struct InstantiateMsg {
|
||||
|
||||
#[serde(default)]
|
||||
pub interval_operating_cost: OperatingCostRange,
|
||||
|
||||
#[serde(default)]
|
||||
pub key_validity_in_epochs: Option<u32>,
|
||||
}
|
||||
|
||||
impl InstantiateMsg {
|
||||
// needs to give us enough time to pre-announce key for following epoch
|
||||
// and have an overlap with the preceding epoch
|
||||
pub const MIN_KEY_ROTATION_VALIDITY: u32 = 3;
|
||||
pub fn key_validity_in_epochs(&self) -> u32 {
|
||||
self.key_validity_in_epochs.unwrap_or(24)
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
@@ -857,6 +870,15 @@ pub enum QueryMsg {
|
||||
/// Cosmos address used for the query of the signing nonce.
|
||||
address: String,
|
||||
},
|
||||
|
||||
// sphinx key rotation-related
|
||||
#[cfg_attr(feature = "schema", returns(KeyRotationState))]
|
||||
/// Gets the current state config of the key rotation (i.e. starting epoch id and validity duration)
|
||||
GetKeyRotationState {},
|
||||
|
||||
/// Gets the current key rotation id
|
||||
#[cfg_attr(feature = "schema", returns(KeyRotationIdResponse))]
|
||||
GetKeyRotationId {},
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
|
||||
@@ -151,6 +151,9 @@ impl Simulator {
|
||||
}
|
||||
}
|
||||
|
||||
// this code is not meant to be used in production systems, only in tests
|
||||
// so a panic due to inconsistent arguments is fine
|
||||
#[allow(clippy::panic)]
|
||||
pub fn simulate_epoch(
|
||||
&mut self,
|
||||
node_params: &BTreeMap<NodeId, NodeRewardingParameters>,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "nym-pool-contract-common"
|
||||
version = "0.1.0"
|
||||
description = "Common library for the Nym Pool contract"
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
|
||||
cosmwasm-std = { workspace = true }
|
||||
cosmwasm-schema = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
time = { workspace = true, features = ["macros"] }
|
||||
|
||||
[features]
|
||||
schema = []
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod storage_keys {
|
||||
pub const CONTRACT_ADMIN: &str = "contract-admin";
|
||||
pub const POOL_DENOMINATION: &str = "pool_denom";
|
||||
pub const GRANTERS: &str = "granters";
|
||||
pub const GRANTS: &str = "grants";
|
||||
pub const TOTAL_LOCKED: &str = "total_locked";
|
||||
pub const LOCKED_GRANTEES: &str = "locked_grantees";
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::{Coin, Uint128};
|
||||
use cw_controllers::AdminError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum NymPoolContractError {
|
||||
#[error("could not perform contract migration: {comment}")]
|
||||
FailedMigration { comment: String },
|
||||
|
||||
#[error(transparent)]
|
||||
Admin(#[from] AdminError),
|
||||
|
||||
#[error(transparent)]
|
||||
StdErr(#[from] cosmwasm_std::StdError),
|
||||
|
||||
#[error("this sender is not authorised to revoke this grant. its neither the admin or the original (and still whitelisted) granter")]
|
||||
UnauthorizedGrantRevocation,
|
||||
|
||||
#[error("the specified address is already a whitelisted granter")]
|
||||
AlreadyAGranter,
|
||||
|
||||
#[error("{addr} is not a permitted granter")]
|
||||
InvalidGranter { addr: String },
|
||||
|
||||
#[error("invalid coin denomination. got {got}, but expected {expected}")]
|
||||
InvalidDenom { expected: String, got: String },
|
||||
|
||||
#[error("there already exists an active grant for {grantee}. it was granted by {granter} at block height {created_at_height}")]
|
||||
GrantAlreadyExist {
|
||||
granter: String,
|
||||
grantee: String,
|
||||
created_at_height: u64,
|
||||
},
|
||||
|
||||
#[error("could not find any active grants for {grantee}")]
|
||||
GrantNotFound { grantee: String },
|
||||
|
||||
#[error("the provided timestamp value ({timestamp}) is set in the past. the current block timestamp is {current_block_timestamp}")]
|
||||
TimestampInThePast {
|
||||
timestamp: u64,
|
||||
current_block_timestamp: u64,
|
||||
},
|
||||
|
||||
#[error("there are not enough tokens to process this request. {available} are available, but {required} is needed.")]
|
||||
InsufficientTokens { available: Coin, required: Coin },
|
||||
|
||||
#[error("the period length can't be zero")]
|
||||
ZeroAllowancePeriod,
|
||||
|
||||
#[error("the provided coin value is zero")]
|
||||
ZeroAmount,
|
||||
|
||||
#[error("the periodic spend limit of {periodic} was set to be higher than the total spend limit {total_limit}")]
|
||||
PeriodicGrantOverSpendLimit { periodic: Coin, total_limit: Coin },
|
||||
|
||||
#[error("the accumulation spend limit of {accumulation} was set to be lower than the periodic grant amount of {periodic_grant}")]
|
||||
AccumulationBelowGrantAmount {
|
||||
accumulation: Coin,
|
||||
periodic_grant: Coin,
|
||||
},
|
||||
|
||||
#[error("the accumulation spend limit of {accumulation} was set to be higher than the total spend limit of {total_limit}")]
|
||||
AccumulationOverSpendLimit {
|
||||
accumulation: Coin,
|
||||
total_limit: Coin,
|
||||
},
|
||||
|
||||
#[error("the specified delayed allowance would never be available. it would become active at {available_timestamp} yet it expires at {expiration_timestamp}")]
|
||||
UnattainableDelayedAllowance {
|
||||
expiration_timestamp: u64,
|
||||
available_timestamp: u64,
|
||||
},
|
||||
|
||||
#[error("could not unlock {requested} tokens from {grantee}. it only has {locked} locked")]
|
||||
InsufficientLockedTokens {
|
||||
grantee: String,
|
||||
locked: Uint128,
|
||||
requested: Uint128,
|
||||
},
|
||||
|
||||
#[error("attempted to spend more tokens than permitted by the current allowance")]
|
||||
SpendingAboveAllowance,
|
||||
|
||||
#[error("attempted to send an empty allowance usage request")]
|
||||
EmptyUsageRequest,
|
||||
|
||||
#[error("the associated grant has already expired")]
|
||||
GrantExpired,
|
||||
|
||||
#[error("the associated grant hasn't expired yet")]
|
||||
GrantNotExpired,
|
||||
|
||||
#[error("this grant is not available yet. it will become usable at {available_at_timestamp}")]
|
||||
GrantNotYetAvailable { available_at_timestamp: u64 },
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod constants;
|
||||
pub mod error;
|
||||
pub mod msg;
|
||||
pub mod types;
|
||||
mod utils;
|
||||
|
||||
pub use error::*;
|
||||
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
pub use types::*;
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{Allowance, TransferRecipient};
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::Coin;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
use crate::types::{
|
||||
AvailableTokensResponse, GrantResponse, GranterResponse, GrantersPagedResponse,
|
||||
GrantsPagedResponse, LockedTokensPagedResponse, LockedTokensResponse,
|
||||
TotalLockedTokensResponse,
|
||||
};
|
||||
|
||||
#[cw_serde]
|
||||
pub struct InstantiateMsg {
|
||||
pub pool_denomination: String,
|
||||
|
||||
/// Initial map of grants to be created at instantiation
|
||||
pub grants: HashMap<String, Allowance>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub enum ExecuteMsg {
|
||||
/// Change the admin
|
||||
UpdateAdmin {
|
||||
admin: String,
|
||||
// flag to determine whether old admin should be removed from the granter set
|
||||
// and new one should be included instead
|
||||
// the reason it's provided as an option is to make it possible to skip this field
|
||||
// when creating transaction directly with nyxd
|
||||
update_granter_set: Option<bool>,
|
||||
},
|
||||
|
||||
/// Attempt to grant new allowance to the specified grantee
|
||||
GrantAllowance {
|
||||
grantee: String,
|
||||
allowance: Box<Allowance>,
|
||||
},
|
||||
|
||||
/// Attempt to revoke previously granted allowance
|
||||
RevokeAllowance { grantee: String },
|
||||
|
||||
/// Attempt to use allowance
|
||||
UseAllowance { recipients: Vec<TransferRecipient> },
|
||||
|
||||
/// Attempt to withdraw the specified amount into the grantee's account
|
||||
WithdrawAllowance { amount: Coin },
|
||||
|
||||
/// Attempt to lock part of existing allowance for future use
|
||||
LockAllowance { amount: Coin },
|
||||
|
||||
/// Attempt to unlock previously locked allowance
|
||||
UnlockAllowance { amount: Coin },
|
||||
|
||||
/// Attempt to use part of the locked allowance
|
||||
UseLockedAllowance { recipients: Vec<TransferRecipient> },
|
||||
|
||||
/// Attempt to withdraw the specified amount of locked tokens into the grantee's account
|
||||
WithdrawLockedAllowance { amount: Coin },
|
||||
|
||||
/// Attempt to add a new account to the permitted set of grant granters
|
||||
AddNewGranter { granter: String },
|
||||
|
||||
/// Revoke the provided account from the permitted set of granters
|
||||
RevokeGranter { granter: String },
|
||||
|
||||
/// Attempt to remove expired grant from the storage and unlock (if any) locked tokens
|
||||
RemoveExpiredGrant { grantee: String },
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
|
||||
pub enum QueryMsg {
|
||||
#[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))]
|
||||
Admin {},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(AvailableTokensResponse))]
|
||||
GetAvailableTokens {},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(TotalLockedTokensResponse))]
|
||||
GetTotalLockedTokens {},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(LockedTokensResponse))]
|
||||
GetLockedTokens { grantee: String },
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(GrantResponse))]
|
||||
GetGrant { grantee: String },
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(GranterResponse))]
|
||||
GetGranter { granter: String },
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(LockedTokensPagedResponse))]
|
||||
GetLockedTokensPaged {
|
||||
/// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.
|
||||
limit: Option<u32>,
|
||||
|
||||
/// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.
|
||||
start_after: Option<String>,
|
||||
},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(GrantersPagedResponse))]
|
||||
GetGrantersPaged {
|
||||
/// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.
|
||||
limit: Option<u32>,
|
||||
|
||||
/// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.
|
||||
start_after: Option<String>,
|
||||
},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(GrantsPagedResponse))]
|
||||
GetGrantsPaged {
|
||||
/// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.
|
||||
limit: Option<u32>,
|
||||
|
||||
/// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.
|
||||
start_after: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct MigrateMsg {
|
||||
//
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::NymPoolContractError;
|
||||
use cosmwasm_std::Env;
|
||||
|
||||
pub fn ensure_unix_timestamp_not_in_the_past(
|
||||
unix_timestamp: u64,
|
||||
env: &Env,
|
||||
) -> Result<(), NymPoolContractError> {
|
||||
if unix_timestamp < env.block.time.seconds() {
|
||||
return Err(NymPoolContractError::TimestampInThePast {
|
||||
timestamp: unix_timestamp,
|
||||
current_block_timestamp: env.block.time.seconds(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use cosmwasm_std::Timestamp;
|
||||
use time::macros::datetime;
|
||||
|
||||
#[test]
|
||||
fn ensuring_unix_timestamp_not_in_the_past() {
|
||||
let unix_epoch = 0;
|
||||
|
||||
let date_in_the_past = datetime!(1984-01-02 3:45 UTC);
|
||||
let sane_block_time = datetime!(2025-01-28 12:15 UTC);
|
||||
|
||||
let before_block = datetime!(2025-01-28 12:00 UTC);
|
||||
let after_block = datetime!(2025-01-28 12:30 UTC);
|
||||
|
||||
let mut env = mock_env();
|
||||
env.block.time = Timestamp::from_seconds(sane_block_time.unix_timestamp() as u64);
|
||||
|
||||
let res = ensure_unix_timestamp_not_in_the_past(unix_epoch, &env).unwrap_err();
|
||||
assert_eq!(
|
||||
NymPoolContractError::TimestampInThePast {
|
||||
timestamp: unix_epoch,
|
||||
current_block_timestamp: env.block.time.seconds(),
|
||||
},
|
||||
res
|
||||
);
|
||||
|
||||
let res =
|
||||
ensure_unix_timestamp_not_in_the_past(date_in_the_past.unix_timestamp() as u64, &env)
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
NymPoolContractError::TimestampInThePast {
|
||||
timestamp: date_in_the_past.unix_timestamp() as u64,
|
||||
current_block_timestamp: env.block.time.seconds(),
|
||||
},
|
||||
res
|
||||
);
|
||||
|
||||
let res = ensure_unix_timestamp_not_in_the_past(before_block.unix_timestamp() as u64, &env)
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
NymPoolContractError::TimestampInThePast {
|
||||
timestamp: before_block.unix_timestamp() as u64,
|
||||
current_block_timestamp: env.block.time.seconds(),
|
||||
},
|
||||
res
|
||||
);
|
||||
|
||||
let res =
|
||||
ensure_unix_timestamp_not_in_the_past(sane_block_time.unix_timestamp() as u64, &env);
|
||||
assert!(res.is_ok());
|
||||
|
||||
let res = ensure_unix_timestamp_not_in_the_past(after_block.unix_timestamp() as u64, &env);
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
}
|
||||
@@ -218,6 +218,12 @@ impl From<PublicKey> for x25519_dalek::PublicKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for PublicKey {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Zeroize, ZeroizeOnDrop)]
|
||||
pub struct PrivateKey(x25519_dalek::StaticSecret);
|
||||
|
||||
@@ -248,6 +254,10 @@ impl PrivateKey {
|
||||
PrivateKey(x25519_secret)
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &x25519_dalek::StaticSecret {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> PublicKey {
|
||||
self.into()
|
||||
}
|
||||
@@ -256,6 +266,10 @@ impl PrivateKey {
|
||||
self.0.to_bytes()
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8; PRIVATE_KEY_SIZE] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
|
||||
pub fn from_bytes(b: &[u8]) -> Result<Self, KeyRecoveryError> {
|
||||
if b.len() != PRIVATE_KEY_SIZE {
|
||||
return Err(KeyRecoveryError::InvalidSizePrivateKey {
|
||||
@@ -335,6 +349,12 @@ impl AsRef<x25519_dalek::StaticSecret> for PrivateKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for PrivateKey {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -19,7 +19,7 @@ pub use shared_key::{
|
||||
SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey,
|
||||
};
|
||||
|
||||
pub const CURRENT_PROTOCOL_VERSION: u8 = AUTHENTICATE_V2_PROTOCOL_VERSION;
|
||||
pub const CURRENT_PROTOCOL_VERSION: u8 = EMBEDDED_KEY_ROTATION_INFO_VERSION;
|
||||
|
||||
/// Defines the current version of the communication protocol between gateway and clients.
|
||||
/// It has to be incremented for any breaking change.
|
||||
@@ -28,10 +28,12 @@ pub const CURRENT_PROTOCOL_VERSION: u8 = AUTHENTICATE_V2_PROTOCOL_VERSION;
|
||||
// 2 - changes to client credentials structure
|
||||
// 3 - change to AES-GCM-SIV and non-zero IVs
|
||||
// 4 - introduction of v2 authentication protocol to prevent reply attacks
|
||||
// 5 - add key rotation information to the serialised mix packet
|
||||
pub const INITIAL_PROTOCOL_VERSION: u8 = 1;
|
||||
pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: u8 = 2;
|
||||
pub const AES_GCM_SIV_PROTOCOL_VERSION: u8 = 3;
|
||||
pub const AUTHENTICATE_V2_PROTOCOL_VERSION: u8 = 4;
|
||||
pub const EMBEDDED_KEY_ROTATION_INFO_VERSION: u8 = 5;
|
||||
|
||||
// TODO: could using `Mac` trait here for OutputSize backfire?
|
||||
// Should hmac itself be exposed, imported and used instead?
|
||||
@@ -40,6 +42,7 @@ pub type LegacyGatewayMacSize = <GatewayIntegrityHmacAlgorithm as OutputSizeUser
|
||||
pub trait GatewayProtocolVersionExt {
|
||||
fn supports_aes256_gcm_siv(&self) -> bool;
|
||||
fn supports_authenticate_v2(&self) -> bool;
|
||||
fn supports_key_rotation_packet(&self) -> bool;
|
||||
}
|
||||
|
||||
impl GatewayProtocolVersionExt for Option<u8> {
|
||||
@@ -52,4 +55,9 @@ impl GatewayProtocolVersionExt for Option<u8> {
|
||||
let Some(protocol) = *self else { return false };
|
||||
protocol >= AUTHENTICATE_V2_PROTOCOL_VERSION
|
||||
}
|
||||
|
||||
fn supports_key_rotation_packet(&self) -> bool {
|
||||
let Some(protocol) = *self else { return false };
|
||||
protocol >= EMBEDDED_KEY_ROTATION_INFO_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ use tungstenite::Message;
|
||||
#[non_exhaustive]
|
||||
pub enum BinaryRequest {
|
||||
ForwardSphinx { packet: MixPacket },
|
||||
|
||||
// identical to `ForwardSphinx`, but also contains information about sphinx key rotation used
|
||||
ForwardSphinxV2 { packet: MixPacket },
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
@@ -18,6 +21,9 @@ pub enum BinaryRequest {
|
||||
#[non_exhaustive]
|
||||
pub enum BinaryRequestKind {
|
||||
ForwardSphinx = 1,
|
||||
|
||||
// identical to `ForwardSphinx`, but also contains information about sphinx key rotation used
|
||||
ForwardSphinxV2 = 2,
|
||||
}
|
||||
|
||||
// Right now the only valid `BinaryRequest` is a request to forward a sphinx packet.
|
||||
@@ -29,6 +35,7 @@ impl BinaryRequest {
|
||||
pub fn kind(&self) -> BinaryRequestKind {
|
||||
match self {
|
||||
BinaryRequest::ForwardSphinx { .. } => BinaryRequestKind::ForwardSphinx,
|
||||
BinaryRequest::ForwardSphinxV2 { .. } => BinaryRequestKind::ForwardSphinxV2,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +45,13 @@ impl BinaryRequest {
|
||||
) -> Result<Self, GatewayRequestsError> {
|
||||
match kind {
|
||||
BinaryRequestKind::ForwardSphinx => {
|
||||
let packet = MixPacket::try_from_bytes(plaintext)?;
|
||||
let packet = MixPacket::try_from_v1_bytes(plaintext)?;
|
||||
Ok(BinaryRequest::ForwardSphinx { packet })
|
||||
}
|
||||
BinaryRequestKind::ForwardSphinxV2 => {
|
||||
let packet = MixPacket::try_from_v2_bytes(plaintext)?;
|
||||
Ok(BinaryRequest::ForwardSphinxV2 { packet })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +69,8 @@ impl BinaryRequest {
|
||||
let kind = self.kind();
|
||||
|
||||
let plaintext = match self {
|
||||
BinaryRequest::ForwardSphinx { packet } => packet.into_bytes()?,
|
||||
BinaryRequest::ForwardSphinx { packet } => packet.into_v1_bytes()?,
|
||||
BinaryRequest::ForwardSphinxV2 { packet } => packet.into_v2_bytes()?,
|
||||
};
|
||||
|
||||
BinaryData::make_encrypted_blob(kind as u8, &plaintext, shared_key)
|
||||
@@ -70,7 +82,9 @@ impl BinaryRequest {
|
||||
) -> Result<Message, GatewayRequestsError> {
|
||||
// all variants are currently encrypted
|
||||
let blob = match self {
|
||||
BinaryRequest::ForwardSphinx { .. } => self.into_encrypted_tagged_bytes(shared_key)?,
|
||||
BinaryRequest::ForwardSphinx { .. } | BinaryRequest::ForwardSphinxV2 { .. } => {
|
||||
self.into_encrypted_tagged_bytes(shared_key)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Message::Binary(blob))
|
||||
|
||||
@@ -193,7 +193,7 @@ impl PersistentStatsStorage {
|
||||
pub async fn get_started_sessions_count(
|
||||
&self,
|
||||
start_date: Date,
|
||||
) -> Result<i32, StatsStorageError> {
|
||||
) -> Result<i64, StatsStorageError> {
|
||||
Ok(self
|
||||
.session_manager
|
||||
.get_started_sessions_count(start_date)
|
||||
|
||||
@@ -160,7 +160,7 @@ impl SessionManager {
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_started_sessions_count(&self, start_date: Date) -> Result<i32> {
|
||||
pub(crate) async fn get_started_sessions_count(&self, start_date: Date) -> Result<i64> {
|
||||
Ok(sqlx::query!(
|
||||
"SELECT COUNT(*) as count FROM sessions_active WHERE date(start_time) = ?",
|
||||
start_date
|
||||
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT \n id as \"id!\",\n client_address_bs58 as \"client_address_bs58!\",\n content as \"content!\" \n FROM message_store \n WHERE client_address_bs58 = ? AND id > ?\n ORDER BY id ASC\n LIMIT ?;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "client_address_bs58!",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "content!",
|
||||
"ordinal": 2,
|
||||
"type_info": "Blob"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "03fe56298a6d60cdd5304a2953811a533d59b4f1f0e4efecd32c09256b657e24"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM message_store WHERE timestamp < ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "071fbde5277c0806ed47fa15a9c6288609379049828a94008f854b3daeed21d1"
|
||||
}
|
||||
+5
-5
@@ -16,7 +16,7 @@
|
||||
{
|
||||
"name": "protocol_version",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "endpoint",
|
||||
@@ -31,17 +31,17 @@
|
||||
{
|
||||
"name": "tx_bytes",
|
||||
"ordinal": 5,
|
||||
"type_info": "Int64"
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "rx_bytes",
|
||||
"ordinal": 6,
|
||||
"type_info": "Int64"
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "persistent_keepalive_interval",
|
||||
"ordinal": 7,
|
||||
"type_info": "Int64"
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "allowed_ips",
|
||||
@@ -51,7 +51,7 @@
|
||||
{
|
||||
"name": "client_id",
|
||||
"ordinal": 9,
|
||||
"type_info": "Int64"
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
{
|
||||
"name": "signer_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE shared_keys SET last_used_authentication = ? WHERE client_id = ?;",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "451158af8f5602e30445986a8de22d2e065c4e9dce4e7463a875ca8c21ac97c1"
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT * FROM shared_keys WHERE client_address_bs58 = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "client_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "client_address_bs58",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "derived_aes128_ctr_blake3_hmac_keys_bs58",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "derived_aes256_gcm_siv_key",
|
||||
"ordinal": 3,
|
||||
"type_info": "Blob"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "564c7da81081fab34754b76eeeedd48f3bc18842c03ef5a5c331bbee4c41c71c"
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
{
|
||||
"name": "client_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "client_type: ClientType",
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
{
|
||||
"name": "ticket_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "data!",
|
||||
|
||||
+2
-2
@@ -6,14 +6,14 @@
|
||||
{
|
||||
"name": "exists",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int"
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "7f8af0799d7ae5f751b9964e9566589bf768e7079079f584beb0c1ba16d43a5c"
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
{
|
||||
"name": "signer_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
{
|
||||
"name": "available",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
{
|
||||
"name": "ticket_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "serial_number",
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
{
|
||||
"name": "ticket_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "serial_number",
|
||||
|
||||
+5
-5
@@ -16,7 +16,7 @@
|
||||
{
|
||||
"name": "protocol_version",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "endpoint",
|
||||
@@ -31,17 +31,17 @@
|
||||
{
|
||||
"name": "tx_bytes",
|
||||
"ordinal": 5,
|
||||
"type_info": "Int64"
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "rx_bytes",
|
||||
"ordinal": 6,
|
||||
"type_info": "Int64"
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "persistent_keepalive_interval",
|
||||
"ordinal": 7,
|
||||
"type_info": "Int64"
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "allowed_ips",
|
||||
@@ -51,7 +51,7 @@
|
||||
{
|
||||
"name": "client_id",
|
||||
"ordinal": 9,
|
||||
"type_info": "Int64"
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT \n id as \"id!\",\n client_address_bs58 as \"client_address_bs58!\",\n content as \"content!\"\n FROM message_store\n WHERE client_address_bs58 = ?\n ORDER BY id ASC\n LIMIT ?;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "client_address_bs58!",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "content!",
|
||||
"ordinal": 2,
|
||||
"type_info": "Blob"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e3860c0c31ca03cc0b22ca34cef5f535a94c78d3491d44d7c8bf1b34a840839d"
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
{
|
||||
"name": "proposal_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::time::SystemTime;
|
||||
|
||||
use crate::error::GatewayStorageError;
|
||||
use nym_credentials_interface::{AvailableBandwidth, ClientTicket, CredentialSpendingData};
|
||||
use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey};
|
||||
@@ -113,7 +115,7 @@ pub struct WireguardPeer {
|
||||
pub preshared_key: Option<String>,
|
||||
pub protocol_version: Option<i64>,
|
||||
pub endpoint: Option<String>,
|
||||
pub last_handshake: Option<sqlx::types::chrono::NaiveDateTime>,
|
||||
pub last_handshake: Option<OffsetDateTime>,
|
||||
pub tx_bytes: i64,
|
||||
pub rx_bytes: i64,
|
||||
pub persistent_keepalive_interval: Option<i64>,
|
||||
@@ -128,18 +130,7 @@ impl From<defguard_wireguard_rs::host::Peer> for WireguardPeer {
|
||||
preshared_key: value.preshared_key.as_ref().map(|k| k.to_string()),
|
||||
protocol_version: value.protocol_version.map(|v| v as i64),
|
||||
endpoint: value.endpoint.map(|e| e.to_string()),
|
||||
last_handshake: value.last_handshake.and_then(|t| {
|
||||
if let Ok(d) = t.duration_since(std::time::UNIX_EPOCH) {
|
||||
if let Ok(millis) = d.as_millis().try_into() {
|
||||
sqlx::types::chrono::DateTime::from_timestamp_millis(millis)
|
||||
.map(|d| d.naive_utc())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
last_handshake: value.last_handshake.map(OffsetDateTime::from),
|
||||
tx_bytes: value.tx_bytes as i64,
|
||||
rx_bytes: value.rx_bytes as i64,
|
||||
persistent_keepalive_interval: value.persistent_keepalive_interval.map(|v| v as i64),
|
||||
@@ -180,15 +171,7 @@ impl TryFrom<WireguardPeer> for defguard_wireguard_rs::host::Peer {
|
||||
.map(|e| e.parse())
|
||||
.transpose()
|
||||
.map_err(|e| Self::Error::TypeConversion(format!("endpoint {e}")))?,
|
||||
last_handshake: value.last_handshake.and_then(|t| {
|
||||
let unix_time = std::time::UNIX_EPOCH;
|
||||
if let Ok(millis) = t.and_utc().timestamp_millis().try_into() {
|
||||
let duration = std::time::Duration::from_millis(millis);
|
||||
unix_time.checked_add(duration)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
last_handshake: value.last_handshake.map(SystemTime::from),
|
||||
tx_bytes: value
|
||||
.tx_bytes
|
||||
.try_into()
|
||||
|
||||
@@ -92,7 +92,7 @@ impl TicketStorageManager {
|
||||
)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.await
|
||||
.map(|result| result.exists == Some(1))
|
||||
.map(|result| result.exists == 1)
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_binary_ticket_data(
|
||||
|
||||
@@ -10,10 +10,14 @@ license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
default=["tunneling"]
|
||||
tunneling=[]
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
bincode = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json", "gzip", "deflate", "brotli", "zstd"] }
|
||||
reqwest = { workspace = true, features = ["json", "gzip", "deflate", "brotli", "zstd", "rustls-tls"] }
|
||||
http.workspace = true
|
||||
url = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
@@ -21,6 +25,7 @@ serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
|
||||
# used for decoding text responses (they were already implicitly included)
|
||||
bytes = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Utilities for and implementation of request tunneling
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::ClientBuilder;
|
||||
|
||||
// #[cfg(feature = "tunneling")]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Front {
|
||||
pub(crate) policy: FrontPolicy,
|
||||
enabled: AtomicBool,
|
||||
}
|
||||
|
||||
impl Clone for Front {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
policy: self.policy.clone(),
|
||||
enabled: AtomicBool::new(self.enabled.load(Ordering::Relaxed)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Front {
|
||||
pub(crate) fn new(policy: FrontPolicy) -> Self {
|
||||
Self {
|
||||
enabled: AtomicBool::new(policy == FrontPolicy::Always),
|
||||
policy,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_enabled(&self) -> bool {
|
||||
match self.policy {
|
||||
FrontPolicy::Off => false,
|
||||
FrontPolicy::OnRetry => self.enabled.load(Ordering::Relaxed),
|
||||
FrontPolicy::Always => true,
|
||||
}
|
||||
}
|
||||
|
||||
// Used to indicate that the client hit an error that should trigger the retry policy
|
||||
// to enable fronting.
|
||||
pub(crate) fn retry_enable(&self) {
|
||||
if self.is_enabled() {
|
||||
return;
|
||||
}
|
||||
if matches!(self.policy, FrontPolicy::OnRetry) {
|
||||
self.enabled.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Clone)]
|
||||
#[cfg(feature = "tunneling")]
|
||||
pub enum FrontPolicy {
|
||||
Always,
|
||||
OnRetry,
|
||||
#[default]
|
||||
Off,
|
||||
}
|
||||
|
||||
impl ClientBuilder {
|
||||
/// Enable and configure request tunneling for API requests.
|
||||
#[cfg(feature = "tunneling")]
|
||||
pub fn with_fronting(mut self, policy: FrontPolicy) -> Self {
|
||||
let front = Front::new(policy);
|
||||
|
||||
// Check if any of the supplied urls even support fronting
|
||||
if !self.urls.iter().any(|url| url.has_front()) {
|
||||
warn!("fronting is enabled, but none of the supplied urls have configured fronting domains");
|
||||
}
|
||||
|
||||
self.front = Some(front);
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{ApiClientCore, Url, NO_PARAMS};
|
||||
|
||||
#[tokio::test]
|
||||
async fn nym_api_works() {
|
||||
let url1 = Url::new(
|
||||
"https://validator.global.ssl.fastly.net",
|
||||
Some(vec!["https://yelp.global.ssl.fastly.net"]),
|
||||
)
|
||||
.unwrap(); // fastly
|
||||
|
||||
// let url2 = Url::new(
|
||||
// "https://validator.nymtech.net",
|
||||
// Some(vec!["https://cdn77.com"]),
|
||||
// ).unwrap(); // cdn77
|
||||
|
||||
let client = ClientBuilder::new::<_, &str>(url1)
|
||||
.expect("bad url")
|
||||
.with_fronting(FrontPolicy::Always)
|
||||
.build::<&str>()
|
||||
.expect("failed to build client");
|
||||
|
||||
let response = client
|
||||
.send_request::<_, (), &str, &str, &str>(
|
||||
reqwest::Method::GET,
|
||||
&["api", "v1", "network", "details"],
|
||||
NO_PARAMS,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("failed get request");
|
||||
|
||||
// println!("{response:?}");
|
||||
assert_eq!(response.status(), 200);
|
||||
}
|
||||
}
|
||||
+379
-236
@@ -136,33 +136,40 @@
|
||||
//! ```
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub use reqwest::{IntoUrl, StatusCode};
|
||||
pub use reqwest::StatusCode;
|
||||
|
||||
use crate::path::RequestPath;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use http::header::CONTENT_TYPE;
|
||||
use http::HeaderMap;
|
||||
use itertools::Itertools;
|
||||
use mime::Mime;
|
||||
use reqwest::header::HeaderValue;
|
||||
use reqwest::{RequestBuilder, Response};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, instrument, warn};
|
||||
use url::Url;
|
||||
|
||||
use bytes::Bytes;
|
||||
use http::header::CONTENT_TYPE;
|
||||
use http::HeaderMap;
|
||||
use mime::Mime;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::net::SocketAddr;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
mod fronted;
|
||||
mod url;
|
||||
pub use url::{IntoUrl, Url};
|
||||
mod user_agent;
|
||||
pub use user_agent::UserAgent;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod dns;
|
||||
mod path;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use dns::{HickoryDnsError, HickoryDnsResolver};
|
||||
|
||||
@@ -244,15 +251,121 @@ impl HttpClientError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Core functionality required for types acting as API clients.
|
||||
///
|
||||
/// This trait defines the "skinny waist" of behaviors that are required by an API client. More
|
||||
/// likely downstream libraries should use functions from the [`ApiClient`] interface which provide
|
||||
/// a more ergonomic set of functionalities.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait ApiClientCore {
|
||||
/// Create an HTTP request using the host configured in this client.
|
||||
fn create_request<P, B, K, V>(
|
||||
&self,
|
||||
method: reqwest::Method,
|
||||
path: P,
|
||||
params: Params<'_, K, V>,
|
||||
json_body: Option<&B>,
|
||||
) -> RequestBuilder
|
||||
where
|
||||
P: RequestPath,
|
||||
B: Serialize + ?Sized,
|
||||
K: AsRef<str>,
|
||||
V: AsRef<str>;
|
||||
|
||||
/// Create an HTTP request using the host configured in this client and an API endpoint (i.e.
|
||||
/// `"/api/v1/mixnodes?since=12345"`). If the provided endpoint fails to parse as path (and
|
||||
/// optionally query parameters).
|
||||
///
|
||||
/// Endpoint Examples
|
||||
/// - `"/api/v1/mixnodes?since=12345"`
|
||||
/// - `"/api/v1/mixnodes"`
|
||||
/// - `"/api/v1/mixnodes/img.png"`
|
||||
/// - `"/api/v1/mixnodes/img.png?since=12345"`
|
||||
/// - `"/"`
|
||||
/// - `"/?since=12345"`
|
||||
/// - `""`
|
||||
/// - `"?since=12345"`
|
||||
///
|
||||
/// for more information about URL percent encodings see [`url::Url::set_path()`]
|
||||
fn create_request_endpoint<B, S>(
|
||||
&self,
|
||||
method: reqwest::Method,
|
||||
endpoint: S,
|
||||
json_body: Option<&B>,
|
||||
) -> RequestBuilder
|
||||
where
|
||||
B: Serialize + ?Sized,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
// Use a stand-in url to extract the path and queries from the provided endpoint string
|
||||
// which could potentially fail.
|
||||
//
|
||||
// This parse cannot fail
|
||||
let mut standin_url: Url = "http://example.com".parse().unwrap();
|
||||
|
||||
match endpoint.as_ref().split_once("?") {
|
||||
Some((path, query)) => {
|
||||
standin_url.set_path(path);
|
||||
standin_url.set_query(Some(query));
|
||||
}
|
||||
// There is no query in the provided endpoint
|
||||
None => standin_url.set_path(endpoint.as_ref()),
|
||||
}
|
||||
|
||||
let path: Vec<&str> = match standin_url.path_segments() {
|
||||
Some(segments) => segments.collect(),
|
||||
None => Vec::new(),
|
||||
};
|
||||
let params: Vec<(String, String)> = standin_url.query_pairs().into_owned().collect();
|
||||
|
||||
self.create_request(method, path.as_slice(), ¶ms, json_body)
|
||||
}
|
||||
|
||||
/// Send a created HTTP request.
|
||||
///
|
||||
/// 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<E>(&self, request: RequestBuilder) -> Result<Response, HttpClientError<E>>
|
||||
where
|
||||
E: Display;
|
||||
|
||||
/// Create and send a created HTTP request.
|
||||
async fn send_request<P, B, K, V, E>(
|
||||
&self,
|
||||
method: reqwest::Method,
|
||||
path: P,
|
||||
params: Params<'_, K, V>,
|
||||
json_body: Option<&B>,
|
||||
) -> Result<Response, HttpClientError<E>>
|
||||
where
|
||||
P: RequestPath + Send + Sync,
|
||||
B: Serialize + ?Sized + Sync,
|
||||
K: AsRef<str> + Sync,
|
||||
V: AsRef<str> + Sync,
|
||||
E: Display,
|
||||
{
|
||||
let req = self.create_request(method, path, params, json_body);
|
||||
self.send(req).await
|
||||
}
|
||||
}
|
||||
|
||||
/// A `ClientBuilder` can be used to create a [`Client`] with custom configuration applied consistently
|
||||
/// and state tracked across subsequent requests.
|
||||
pub struct ClientBuilder {
|
||||
url: Url,
|
||||
urls: Vec<Url>,
|
||||
|
||||
timeout: Option<Duration>,
|
||||
custom_user_agent: bool,
|
||||
reqwest_client_builder: reqwest::ClientBuilder,
|
||||
#[allow(dead_code)] // not dead code, just unused in wasm
|
||||
use_secure_dns: bool,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: Option<fronted::Front>,
|
||||
|
||||
retry_limit: usize,
|
||||
}
|
||||
|
||||
impl ClientBuilder {
|
||||
@@ -273,15 +386,14 @@ impl ClientBuilder {
|
||||
// TODO: or should we maybe default to https?
|
||||
Self::new(alt)
|
||||
} else {
|
||||
Ok(Self::new_with_url(url.into_url()?))
|
||||
let url = url.to_url()?;
|
||||
Ok(Self::new_with_urls(vec![url]))
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a new http `ClientBuilder` from a valid url.
|
||||
pub fn new_with_url(url: Url) -> Self {
|
||||
if !url.scheme().starts_with("http") {
|
||||
warn!("the provided url ('{url}') does not use HTTP / HTTPS scheme");
|
||||
}
|
||||
pub fn new_with_urls(urls: Vec<Url>) -> Self {
|
||||
let urls = Self::check_urls(urls);
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let reqwest_client_builder = reqwest::ClientBuilder::new();
|
||||
@@ -303,14 +415,38 @@ impl ClientBuilder {
|
||||
};
|
||||
|
||||
ClientBuilder {
|
||||
url,
|
||||
urls,
|
||||
timeout: None,
|
||||
custom_user_agent: false,
|
||||
reqwest_client_builder,
|
||||
use_secure_dns: true,
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: None,
|
||||
|
||||
retry_limit: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an additional URL to the set usable by this constructed `Client`
|
||||
pub fn add_url(mut self, url: Url) -> Self {
|
||||
self.urls.push(url);
|
||||
self
|
||||
}
|
||||
|
||||
fn check_urls(mut urls: Vec<Url>) -> Vec<Url> {
|
||||
// remove any duplicate URLs
|
||||
urls = urls.into_iter().unique().collect();
|
||||
|
||||
// warn about any invalid URLs
|
||||
urls.iter()
|
||||
.filter(|url| !url.scheme().contains("http") && !url.scheme().contains("https"))
|
||||
.for_each(|url| {
|
||||
warn!("the provided url ('{url}') does not use HTTP / HTTPS scheme");
|
||||
});
|
||||
|
||||
urls
|
||||
}
|
||||
|
||||
/// Enables a total request timeout other than the default.
|
||||
///
|
||||
/// The timeout is applied from when the request starts connecting until the response body has finished. Also considered a total deadline.
|
||||
@@ -321,6 +457,18 @@ impl ClientBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the maximum number of retries for a request. This defaults to 0, indicating no retries.
|
||||
///
|
||||
/// Note that setting a retry limit of 3 (for example) will result in 4 attempts to send the
|
||||
/// request in the case that all are unsuccessful.
|
||||
///
|
||||
/// If multiple urls (or fronting configurations if enabled) are available, retried requests
|
||||
/// will be sent to the next URL in the list.
|
||||
pub fn with_retries(mut self, retry_limit: usize) -> Self {
|
||||
self.retry_limit = retry_limit;
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide a pre-configured [`reqwest::ClientBuilder`]
|
||||
pub fn with_reqwest_builder(mut self, reqwest_builder: reqwest::ClientBuilder) -> Self {
|
||||
self.reqwest_client_builder = reqwest_builder;
|
||||
@@ -378,24 +526,37 @@ impl ClientBuilder {
|
||||
builder.build()?
|
||||
};
|
||||
|
||||
Ok(Client {
|
||||
base_url: self.url,
|
||||
let client = Client {
|
||||
base_urls: self.urls,
|
||||
current_idx: Arc::new(AtomicUsize::new(0)),
|
||||
reqwest_client,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: self.front,
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
request_timeout: self.timeout.unwrap_or(DEFAULT_TIMEOUT),
|
||||
})
|
||||
retry_limit: self.retry_limit,
|
||||
};
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple extendable client wrapper for http request with extra url sanitization.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Client {
|
||||
base_url: Url,
|
||||
base_urls: Vec<Url>,
|
||||
current_idx: Arc<AtomicUsize>,
|
||||
reqwest_client: reqwest::Client,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: Option<fronted::Front>,
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
request_timeout: Duration,
|
||||
|
||||
retry_limit: usize,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
@@ -404,7 +565,7 @@ 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, timeout: Option<Duration>) -> Self {
|
||||
pub fn new(base_url: ::url::Url, timeout: Option<Duration>) -> Self {
|
||||
Self::new_url::<_, String>(base_url, timeout).expect(
|
||||
"we provided valid url and we were unwrapping previous construction errors anyway",
|
||||
)
|
||||
@@ -434,112 +595,119 @@ impl Client {
|
||||
ClientBuilder::new(url)
|
||||
}
|
||||
|
||||
/// Update the host that this client uses when sending API requests.
|
||||
pub fn change_base_url(&mut self, new_url: Url) {
|
||||
self.base_url = new_url
|
||||
/// Update the set of hosts that this client uses when sending API requests.
|
||||
pub fn change_base_urls(&mut self, new_urls: Vec<Url>) {
|
||||
self.current_idx.store(0, Ordering::Relaxed);
|
||||
self.base_urls = new_urls
|
||||
}
|
||||
|
||||
/// Create new instance of `Client` using the provided base url and existing client config
|
||||
pub fn clone_with_new_url(&self, new_url: Url) -> Self {
|
||||
Client {
|
||||
base_urls: vec![new_url],
|
||||
current_idx: Arc::new(Default::default()),
|
||||
reqwest_client: self.reqwest_client.clone(),
|
||||
|
||||
front: self.front.clone(),
|
||||
retry_limit: self.retry_limit,
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
request_timeout: self.request_timeout,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the currently configured host that this client uses when sending API requests.
|
||||
pub fn current_url(&self) -> &Url {
|
||||
&self.base_url
|
||||
&self.base_urls[self.current_idx.load(std::sync::atomic::Ordering::Relaxed)]
|
||||
}
|
||||
}
|
||||
|
||||
/// Core functionality required for types acting as API clients.
|
||||
///
|
||||
/// This trait defines the "skinny waist" of behaviors that are required by an API client. More
|
||||
/// likely downstream libraries should use functions from the [`ApiClient`] interface which provide
|
||||
/// a more ergonomic set of functionalities.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait ApiClientCore {
|
||||
/// Create an HTTP request using the host configured in this client.
|
||||
fn create_request<B, K, V>(
|
||||
&self,
|
||||
method: reqwest::Method,
|
||||
path: PathSegments<'_>,
|
||||
params: Params<'_, K, V>,
|
||||
json_body: Option<&B>,
|
||||
) -> RequestBuilder
|
||||
where
|
||||
B: Serialize + ?Sized,
|
||||
K: AsRef<str>,
|
||||
V: AsRef<str>;
|
||||
/// Get the currently configured host that this client uses when sending API requests.
|
||||
pub fn base_urls(&self) -> &[Url] {
|
||||
&self.base_urls
|
||||
}
|
||||
|
||||
/// Create an HTTP request using the host configured in this client and an API endpoint (i.e.
|
||||
/// `"/api/v1/mixnodes?since=12345"`). If the provided endpoint fails to parse as path (and
|
||||
/// optionally query parameters).
|
||||
///
|
||||
/// Endpoint Examples
|
||||
/// - `"/api/v1/mixnodes?since=12345"`
|
||||
/// - `"/api/v1/mixnodes"`
|
||||
/// - `"/api/v1/mixnodes/img.png"`
|
||||
/// - `"/api/v1/mixnodes/img.png?since=12345"`
|
||||
/// - `"/"`
|
||||
/// - `"/?since=12345"`
|
||||
/// - `""`
|
||||
/// - `"?since=12345"`
|
||||
///
|
||||
/// for more information about URL percent encodings see [`url::Url::set_path()`]
|
||||
fn create_request_endpoint<B, S>(
|
||||
&self,
|
||||
method: reqwest::Method,
|
||||
endpoint: S,
|
||||
json_body: Option<&B>,
|
||||
) -> RequestBuilder
|
||||
where
|
||||
B: Serialize + ?Sized,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
// Use a stand-in url to extract the path and queries from the provided endpoint string
|
||||
// which could potentially fail.
|
||||
//
|
||||
// This parse cannot fail
|
||||
let mut standin_url: Url = "http://example.com".parse().unwrap();
|
||||
/// Get a mutable reference to the hosts that this client uses when sending API requests.
|
||||
pub fn base_urls_mut(&mut self) -> &mut [Url] {
|
||||
&mut self.base_urls
|
||||
}
|
||||
|
||||
match endpoint.as_ref().split_once("?") {
|
||||
Some((path, query)) => {
|
||||
standin_url.set_path(path);
|
||||
standin_url.set_query(Some(query));
|
||||
/// Change the currently configured limit on the number of retries for a request.
|
||||
pub fn change_retry_limit(&mut self, limit: usize) {
|
||||
self.retry_limit = limit;
|
||||
}
|
||||
|
||||
/// If multiple base urls are available rotate to next (e.g. when the current one resulted in an error)
|
||||
fn update_host(&self) {
|
||||
#[cfg(feature = "tunneling")]
|
||||
if let Some(ref front) = self.front {
|
||||
if front.is_enabled() {
|
||||
// if we are using fronting, try updating to the next front
|
||||
let url = self.current_url();
|
||||
|
||||
// try to update the current host to use a next front, if one is available, otherwise
|
||||
// we move on and try the next base url (if one is available)
|
||||
if url.has_front() && !url.update() {
|
||||
// we swapped to the next front for the current host
|
||||
return;
|
||||
}
|
||||
}
|
||||
// There is no query in the provided endpoint
|
||||
None => standin_url.set_path(endpoint.as_ref()),
|
||||
}
|
||||
|
||||
let path: Vec<&str> = match standin_url.path_segments() {
|
||||
Some(segments) => segments.collect(),
|
||||
None => Vec::new(),
|
||||
};
|
||||
let params: Vec<(String, String)> = standin_url.query_pairs().into_owned().collect();
|
||||
if self.base_urls.len() > 1 {
|
||||
let orig = self.current_idx.load(Ordering::Relaxed);
|
||||
let mut next = (orig + 1) % self.base_urls.len();
|
||||
|
||||
self.create_request(method, &path, ¶ms, json_body)
|
||||
// if fronting is enabled we want to update to a host that has fronts configured
|
||||
#[cfg(feature = "tunneling")]
|
||||
if let Some(ref front) = self.front {
|
||||
if front.is_enabled() {
|
||||
while next != orig {
|
||||
if self.base_urls[next].has_front() {
|
||||
// we have a front for the next host, so we can use it
|
||||
break;
|
||||
}
|
||||
|
||||
next = (next + 1) % self.base_urls.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.current_idx.store(next, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a created HTTP request.
|
||||
/// Make modifications to the request to apply the current state of this client i.e. the
|
||||
/// currently configured host. This is required as a caller may use this client to create a
|
||||
/// request, but then have the state of the client change before the caller uses the client to
|
||||
/// send their request.
|
||||
///
|
||||
/// 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<E>(&self, request: RequestBuilder) -> Result<Response, HttpClientError<E>>
|
||||
where
|
||||
E: Display;
|
||||
/// This enures that the outgoing requests benefit from the configured fallback mechanisms, even
|
||||
/// for requests that were created before the state of the client changed.
|
||||
///
|
||||
/// This method assumes that any updates to the state of the client are made before the call to
|
||||
/// 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) {
|
||||
let url = self.current_url();
|
||||
r.url_mut().set_host(url.host_str()).unwrap();
|
||||
|
||||
/// Create and send a created HTTP request.
|
||||
async fn send_request<B, K, V, E>(
|
||||
&self,
|
||||
method: reqwest::Method,
|
||||
path: PathSegments<'_>,
|
||||
params: Params<'_, K, V>,
|
||||
json_body: Option<&B>,
|
||||
) -> Result<Response, HttpClientError<E>>
|
||||
where
|
||||
B: Serialize + ?Sized + Sync,
|
||||
K: AsRef<str> + Sync,
|
||||
V: AsRef<str> + Sync,
|
||||
E: Display,
|
||||
{
|
||||
let req = self.create_request(method, path, params, json_body);
|
||||
self.send(req).await
|
||||
#[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();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,45 +715,90 @@ pub trait ApiClientCore {
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl ApiClientCore for Client {
|
||||
#[instrument(level = "debug", skip_all, fields(path=?path))]
|
||||
fn create_request<B, K, V>(
|
||||
fn create_request<P, B, K, V>(
|
||||
&self,
|
||||
method: reqwest::Method,
|
||||
path: PathSegments<'_>,
|
||||
path: P,
|
||||
params: Params<'_, K, V>,
|
||||
json_body: Option<&B>,
|
||||
) -> RequestBuilder
|
||||
where
|
||||
P: RequestPath,
|
||||
B: Serialize + ?Sized,
|
||||
K: AsRef<str>,
|
||||
V: AsRef<str>,
|
||||
{
|
||||
let url = sanitize_url(&self.base_url, path, params);
|
||||
let url = self.current_url();
|
||||
let url = sanitize_url(url, path, params);
|
||||
|
||||
let mut request = self.reqwest_client.request(method.clone(), url);
|
||||
let mut req = reqwest::Request::new(method, url.into());
|
||||
|
||||
self.apply_hosts_to_req(&mut req);
|
||||
|
||||
let mut rb = RequestBuilder::from_parts(self.reqwest_client.clone(), req);
|
||||
|
||||
if let Some(body) = json_body {
|
||||
request = request.json(body);
|
||||
rb = rb.json(body);
|
||||
}
|
||||
|
||||
request
|
||||
rb
|
||||
}
|
||||
|
||||
async fn send<E>(&self, request: RequestBuilder) -> Result<Response, HttpClientError<E>>
|
||||
where
|
||||
E: Display,
|
||||
{
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
Ok(
|
||||
wasmtimer::tokio::timeout(self.request_timeout, request.send())
|
||||
.await
|
||||
.map_err(|_timeout| HttpClientError::RequestTimeout)??,
|
||||
)
|
||||
}
|
||||
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(),
|
||||
))?;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
Ok(request.send().await?)
|
||||
// apply any changes based on the current state of the client wrt. hosts,
|
||||
// fronting domains, etc.
|
||||
let mut req = r.build()?;
|
||||
self.apply_hosts_to_req(&mut req);
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let response: Result<Response, HttpClientError<E>> = {
|
||||
Ok(wasmtimer::tokio::timeout(
|
||||
self.request_timeout,
|
||||
self.reqwest_client.execute(req),
|
||||
)
|
||||
.await
|
||||
.map_err(|_timeout| HttpClientError::RequestTimeout)??)
|
||||
};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let response = self.reqwest_client.execute(req).await;
|
||||
|
||||
match response {
|
||||
Ok(resp) => return Ok(resp),
|
||||
Err(e) => {
|
||||
// if we have multiple urls, update to the next
|
||||
self.update_host();
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
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.
|
||||
front.retry_enable();
|
||||
}
|
||||
|
||||
if attempts < self.retry_limit {
|
||||
warn!("Retrying request due to http error: {}", e);
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -597,12 +810,9 @@ 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<K, V>(
|
||||
&self,
|
||||
path: PathSegments<'_>,
|
||||
params: Params<'_, K, V>,
|
||||
) -> RequestBuilder
|
||||
fn create_get_request<P, K, V>(&self, path: P, params: Params<'_, K, V>) -> RequestBuilder
|
||||
where
|
||||
P: RequestPath,
|
||||
K: AsRef<str>,
|
||||
V: AsRef<str>,
|
||||
{
|
||||
@@ -610,13 +820,14 @@ pub trait ApiClient: ApiClientCore {
|
||||
}
|
||||
|
||||
/// Create an HTTP POST Request with the provided path, parameters, and json body
|
||||
fn create_post_request<B, K, V>(
|
||||
fn create_post_request<P, B, K, V>(
|
||||
&self,
|
||||
path: PathSegments<'_>,
|
||||
path: P,
|
||||
params: Params<'_, K, V>,
|
||||
json_body: &B,
|
||||
) -> RequestBuilder
|
||||
where
|
||||
P: RequestPath,
|
||||
B: Serialize + ?Sized,
|
||||
K: AsRef<str>,
|
||||
V: AsRef<str>,
|
||||
@@ -625,12 +836,9 @@ pub trait ApiClient: ApiClientCore {
|
||||
}
|
||||
|
||||
/// Create an HTTP DELETE Request with the provided path and parameters
|
||||
fn create_delete_request<K, V>(
|
||||
&self,
|
||||
path: PathSegments<'_>,
|
||||
params: Params<'_, K, V>,
|
||||
) -> RequestBuilder
|
||||
fn create_delete_request<P, K, V>(&self, path: P, params: Params<'_, K, V>) -> RequestBuilder
|
||||
where
|
||||
P: RequestPath,
|
||||
K: AsRef<str>,
|
||||
V: AsRef<str>,
|
||||
{
|
||||
@@ -638,13 +846,14 @@ pub trait ApiClient: ApiClientCore {
|
||||
}
|
||||
|
||||
/// Create an HTTP PATCH Request with the provided path, parameters, and json body
|
||||
fn create_patch_request<B, K, V>(
|
||||
fn create_patch_request<P, B, K, V>(
|
||||
&self,
|
||||
path: PathSegments<'_>,
|
||||
path: P,
|
||||
params: Params<'_, K, V>,
|
||||
json_body: &B,
|
||||
) -> RequestBuilder
|
||||
where
|
||||
P: RequestPath,
|
||||
B: Serialize + ?Sized,
|
||||
K: AsRef<str>,
|
||||
V: AsRef<str>,
|
||||
@@ -654,12 +863,13 @@ 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<K, V, E>(
|
||||
async fn send_get_request<P, K, V, E>(
|
||||
&self,
|
||||
path: PathSegments<'_>,
|
||||
path: P,
|
||||
params: Params<'_, K, V>,
|
||||
) -> Result<Response, HttpClientError<E>>
|
||||
where
|
||||
P: RequestPath + Send + Sync,
|
||||
K: AsRef<str> + Sync,
|
||||
V: AsRef<str> + Sync,
|
||||
E: Display,
|
||||
@@ -669,13 +879,14 @@ pub trait ApiClient: ApiClientCore {
|
||||
}
|
||||
|
||||
/// Create and send an HTTP POST Request with the provided path, parameters, and json data
|
||||
async fn send_post_request<B, K, V, E>(
|
||||
async fn send_post_request<P, B, K, V, E>(
|
||||
&self,
|
||||
path: PathSegments<'_>,
|
||||
path: P,
|
||||
params: Params<'_, K, V>,
|
||||
json_body: &B,
|
||||
) -> Result<Response, HttpClientError<E>>
|
||||
where
|
||||
P: RequestPath + Send + Sync,
|
||||
B: Serialize + ?Sized + Sync,
|
||||
K: AsRef<str> + Sync,
|
||||
V: AsRef<str> + Sync,
|
||||
@@ -686,12 +897,13 @@ pub trait ApiClient: ApiClientCore {
|
||||
}
|
||||
|
||||
/// Create and send an HTTP DELETE Request with the provided path and parameters
|
||||
async fn send_delete_request<K, V, E>(
|
||||
async fn send_delete_request<P, K, V, E>(
|
||||
&self,
|
||||
path: PathSegments<'_>,
|
||||
path: P,
|
||||
params: Params<'_, K, V>,
|
||||
) -> Result<Response, HttpClientError<E>>
|
||||
where
|
||||
P: RequestPath + Send + Sync,
|
||||
K: AsRef<str> + Sync,
|
||||
V: AsRef<str> + Sync,
|
||||
E: Display,
|
||||
@@ -701,13 +913,14 @@ pub trait ApiClient: ApiClientCore {
|
||||
}
|
||||
|
||||
/// Create and send an HTTP PATCH Request with the provided path, parameters, and json data
|
||||
async fn send_patch_request<B, K, V, E>(
|
||||
async fn send_patch_request<P, B, K, V, E>(
|
||||
&self,
|
||||
path: PathSegments<'_>,
|
||||
path: P,
|
||||
params: Params<'_, K, V>,
|
||||
json_body: &B,
|
||||
) -> Result<Response, HttpClientError<E>>
|
||||
where
|
||||
P: RequestPath + Send + Sync,
|
||||
B: Serialize + ?Sized + Sync,
|
||||
K: AsRef<str> + Sync,
|
||||
V: AsRef<str> + Sync,
|
||||
@@ -722,12 +935,13 @@ 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<T, K, V, E>(
|
||||
async fn get_json<P, T, K, V, E>(
|
||||
&self,
|
||||
path: PathSegments<'_>,
|
||||
path: P,
|
||||
params: Params<'_, K, V>,
|
||||
) -> Result<T, HttpClientError<E>>
|
||||
where
|
||||
P: RequestPath + Send + Sync,
|
||||
for<'a> T: Deserialize<'a>,
|
||||
K: AsRef<str> + Sync,
|
||||
V: AsRef<str> + Sync,
|
||||
@@ -739,12 +953,13 @@ 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<T, K, V, E>(
|
||||
async fn get_response<P, T, K, V, E>(
|
||||
&self,
|
||||
path: PathSegments<'_>,
|
||||
path: P,
|
||||
params: Params<'_, K, V>,
|
||||
) -> Result<T, HttpClientError<E>>
|
||||
where
|
||||
P: RequestPath + Send + Sync,
|
||||
for<'a> T: Deserialize<'a>,
|
||||
K: AsRef<str> + Sync,
|
||||
V: AsRef<str> + Sync,
|
||||
@@ -759,13 +974,14 @@ 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<B, T, K, V, E>(
|
||||
async fn post_json<P, B, T, K, V, E>(
|
||||
&self,
|
||||
path: PathSegments<'_>,
|
||||
path: P,
|
||||
params: Params<'_, K, V>,
|
||||
json_body: &B,
|
||||
) -> Result<T, HttpClientError<E>>
|
||||
where
|
||||
P: RequestPath + Send + Sync,
|
||||
B: Serialize + ?Sized + Sync,
|
||||
for<'a> T: Deserialize<'a>,
|
||||
K: AsRef<str> + Sync,
|
||||
@@ -781,12 +997,13 @@ 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<T, K, V, E>(
|
||||
async fn delete_json<P, T, K, V, E>(
|
||||
&self,
|
||||
path: PathSegments<'_>,
|
||||
path: P,
|
||||
params: Params<'_, K, V>,
|
||||
) -> Result<T, HttpClientError<E>>
|
||||
where
|
||||
P: RequestPath + Send + Sync,
|
||||
for<'a> T: Deserialize<'a>,
|
||||
K: AsRef<str> + Sync,
|
||||
V: AsRef<str> + Sync,
|
||||
@@ -801,13 +1018,14 @@ 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<B, T, K, V, E>(
|
||||
async fn patch_json<P, B, T, K, V, E>(
|
||||
&self,
|
||||
path: PathSegments<'_>,
|
||||
path: P,
|
||||
params: Params<'_, K, V>,
|
||||
json_body: &B,
|
||||
) -> Result<T, HttpClientError<E>>
|
||||
where
|
||||
P: RequestPath + Send + Sync,
|
||||
B: Serialize + ?Sized + Sync,
|
||||
for<'a> T: Deserialize<'a>,
|
||||
K: AsRef<str> + Sync,
|
||||
@@ -890,7 +1108,7 @@ impl<C> ApiClient for C where C: ApiClientCore + Sync {}
|
||||
/// utility function that should solve the double slash problem in API urls forever.
|
||||
fn sanitize_url<K: AsRef<str>, V: AsRef<str>>(
|
||||
base: &Url,
|
||||
segments: PathSegments<'_>,
|
||||
request_path: impl RequestPath,
|
||||
params: Params<'_, K, V>,
|
||||
) -> Url {
|
||||
let mut url = base.clone();
|
||||
@@ -900,10 +1118,7 @@ fn sanitize_url<K: AsRef<str>, V: AsRef<str>>(
|
||||
|
||||
path_segments.pop_if_empty();
|
||||
|
||||
for segment in segments {
|
||||
let segment = segment.strip_prefix('/').unwrap_or(segment);
|
||||
let segment = segment.strip_suffix('/').unwrap_or(segment);
|
||||
|
||||
for segment in request_path.to_sanitized_segments() {
|
||||
path_segments.push(segment);
|
||||
}
|
||||
|
||||
@@ -1041,76 +1256,4 @@ fn try_get_mime_type(headers: &HeaderMap) -> Option<Mime> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sanitizing_urls() {
|
||||
let base_url: Url = "http://foomp.com".parse().unwrap();
|
||||
|
||||
// works with 1 segment
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo",
|
||||
sanitize_url(&base_url, &["foo"], NO_PARAMS).as_str()
|
||||
);
|
||||
|
||||
// works with 2 segments
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo/bar",
|
||||
sanitize_url(&base_url, &["foo", "bar"], NO_PARAMS).as_str()
|
||||
);
|
||||
|
||||
// works with leading slash
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo",
|
||||
sanitize_url(&base_url, &["/foo"], NO_PARAMS).as_str()
|
||||
);
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo/bar",
|
||||
sanitize_url(&base_url, &["/foo", "bar"], NO_PARAMS).as_str()
|
||||
);
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo/bar",
|
||||
sanitize_url(&base_url, &["foo", "/bar"], NO_PARAMS).as_str()
|
||||
);
|
||||
|
||||
// works with trailing slash
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo",
|
||||
sanitize_url(&base_url, &["foo/"], NO_PARAMS).as_str()
|
||||
);
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo/bar",
|
||||
sanitize_url(&base_url, &["foo/", "bar"], NO_PARAMS).as_str()
|
||||
);
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo/bar",
|
||||
sanitize_url(&base_url, &["foo", "bar/"], NO_PARAMS).as_str()
|
||||
);
|
||||
|
||||
// works with both leading and trailing slash
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo",
|
||||
sanitize_url(&base_url, &["/foo/"], NO_PARAMS).as_str()
|
||||
);
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo/bar",
|
||||
sanitize_url(&base_url, &["/foo/", "/bar/"], NO_PARAMS).as_str()
|
||||
);
|
||||
|
||||
// adds params
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo/bar?foomp=baz",
|
||||
sanitize_url(&base_url, &["foo", "bar"], &[("foomp", "baz")]).as_str()
|
||||
);
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo/bar?arg1=val1&arg2=val2",
|
||||
sanitize_url(
|
||||
&base_url,
|
||||
&["/foo/", "/bar/"],
|
||||
&[("arg1", "val1"), ("arg2", "val2")]
|
||||
)
|
||||
.as_str()
|
||||
);
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// Collection of URL Path Segments
|
||||
pub type PathSegments<'a> = &'a [&'a str];
|
||||
|
||||
fn sanitize_fragment(segment: &str) -> &str {
|
||||
segment.trim_matches(|c: char| c.is_whitespace() || c == '/')
|
||||
}
|
||||
|
||||
pub trait RequestPath: Debug {
|
||||
fn to_sanitized_segments(&self) -> Vec<&str>;
|
||||
}
|
||||
|
||||
macro_rules! impl_stringified_sanitized_segments {
|
||||
($frag_iter:expr) => {{
|
||||
let mut path_segments = Vec::new();
|
||||
|
||||
for segment in $frag_iter {
|
||||
if !segment.is_empty() {
|
||||
path_segments.push(sanitize_fragment(segment));
|
||||
}
|
||||
}
|
||||
|
||||
path_segments
|
||||
}};
|
||||
}
|
||||
|
||||
impl RequestPath for PathSegments<'_> {
|
||||
fn to_sanitized_segments(&self) -> Vec<&str> {
|
||||
impl_stringified_sanitized_segments!(self.iter())
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> RequestPath for &[&str; N] {
|
||||
fn to_sanitized_segments(&self) -> Vec<&str> {
|
||||
impl_stringified_sanitized_segments!(self.iter())
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestPath for &str {
|
||||
fn to_sanitized_segments(&self) -> Vec<&str> {
|
||||
impl_stringified_sanitized_segments!(self.split('/'))
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestPath for String {
|
||||
fn to_sanitized_segments(&self) -> Vec<&str> {
|
||||
impl_stringified_sanitized_segments!(self.split('/'))
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestPath for &String {
|
||||
fn to_sanitized_segments(&self) -> Vec<&str> {
|
||||
impl_stringified_sanitized_segments!(self.split('/'))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sanitizing_urls() {
|
||||
let base_url: Url = "http://foomp.com".parse().unwrap();
|
||||
|
||||
// works with a full string
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo/bar",
|
||||
sanitize_url(&base_url, "/foo//bar/", NO_PARAMS).as_str()
|
||||
);
|
||||
|
||||
// (and leading slash doesn't matter)
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo/bar",
|
||||
sanitize_url(&base_url, "foo//bar/", NO_PARAMS).as_str()
|
||||
);
|
||||
|
||||
// works with 1 segment
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo",
|
||||
sanitize_url(&base_url, &["foo"], NO_PARAMS).as_str()
|
||||
);
|
||||
|
||||
// works with 2 segments
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo/bar",
|
||||
sanitize_url(&base_url, &["foo", "bar"], NO_PARAMS).as_str()
|
||||
);
|
||||
|
||||
// works with leading slash
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo",
|
||||
sanitize_url(&base_url, &["/foo"], NO_PARAMS).as_str()
|
||||
);
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo/bar",
|
||||
sanitize_url(&base_url, &["/foo", "bar"], NO_PARAMS).as_str()
|
||||
);
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo/bar",
|
||||
sanitize_url(&base_url, &["foo", "/bar"], NO_PARAMS).as_str()
|
||||
);
|
||||
|
||||
// works with trailing slash
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo",
|
||||
sanitize_url(&base_url, &["foo/"], NO_PARAMS).as_str()
|
||||
);
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo/bar",
|
||||
sanitize_url(&base_url, &["foo/", "bar"], NO_PARAMS).as_str()
|
||||
);
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo/bar",
|
||||
sanitize_url(&base_url, &["foo", "bar/"], NO_PARAMS).as_str()
|
||||
);
|
||||
|
||||
// works with both leading and trailing slash
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo",
|
||||
sanitize_url(&base_url, &["/foo/"], NO_PARAMS).as_str()
|
||||
);
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo/bar",
|
||||
sanitize_url(&base_url, &["/foo/", "/bar/"], NO_PARAMS).as_str()
|
||||
);
|
||||
|
||||
// adds params
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo/bar?foomp=baz",
|
||||
sanitize_url(&base_url, &["foo", "bar"], &[("foomp", "baz")]).as_str()
|
||||
);
|
||||
assert_eq!(
|
||||
"http://foomp.com/foo/bar?arg1=val1&arg2=val2",
|
||||
sanitize_url(
|
||||
&base_url,
|
||||
&["/foo/", "/bar/"],
|
||||
&[("arg1", "val1"), ("arg2", "val2")]
|
||||
)
|
||||
.as_str()
|
||||
);
|
||||
}
|
||||
|
||||
// - Do the retries work
|
||||
// - Do we use fallback urls on retry if multiple are provided
|
||||
// - Do we use the next front on retry if multiple are provided
|
||||
// - If we have more retries than urls, do we wrap back to the first one again
|
||||
// - on error without retries is where we have multiple urls, is the url updated?
|
||||
|
||||
#[tokio::test]
|
||||
async fn api_client_retry() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = ClientBuilder::new_with_urls(vec![
|
||||
"http://broken.nym.badurl".parse()?,
|
||||
"http://example.com/".parse()?,
|
||||
])
|
||||
.with_retries(3)
|
||||
.build::<HttpClientError>()?;
|
||||
|
||||
let req = client.create_get_request(&["/"], NO_PARAMS);
|
||||
let resp = client.send::<HttpClientError>(req).await?;
|
||||
|
||||
assert_eq!(resp.status(), 200);
|
||||
|
||||
// check that the url was updated
|
||||
assert_eq!(client.current_url().as_str(), "http://example.com/");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_updating() {
|
||||
let url = Url::new("http://example.com", None).unwrap();
|
||||
let mut client = ClientBuilder::new::<_, &str>(url)
|
||||
.unwrap()
|
||||
.build::<&str>()
|
||||
.unwrap();
|
||||
|
||||
// check that the url is set correctly
|
||||
let current_url = client.current_url();
|
||||
assert_eq!(current_url.as_str(), "http://example.com/");
|
||||
assert_eq!(current_url.front_str(), None);
|
||||
|
||||
// update the url
|
||||
client.update_host();
|
||||
|
||||
// check that the url is still the same since there is one URL
|
||||
assert_eq!(client.current_url().as_str(), "http://example.com/");
|
||||
|
||||
// =======================================
|
||||
// we rotate through urls when available
|
||||
|
||||
let new_urls = vec![
|
||||
Url::new("http://example.com", None).unwrap(),
|
||||
Url::new("http://example.org", None).unwrap(),
|
||||
];
|
||||
client.change_base_urls(new_urls);
|
||||
assert_eq!(client.current_url().as_str(), "http://example.com/");
|
||||
|
||||
client.update_host();
|
||||
|
||||
// check that the url got updated now that there are multiple URLs
|
||||
assert_eq!(client.current_url().as_str(), "http://example.org/");
|
||||
assert_eq!(client.current_url().front_str(), None);
|
||||
|
||||
client.update_host();
|
||||
assert_eq!(client.current_url().as_str(), "http://example.com/");
|
||||
|
||||
// =======================================
|
||||
// we rotate through urls when available if fronting is disabled
|
||||
|
||||
let new_urls = vec![
|
||||
Url::new(
|
||||
"http://example.com",
|
||||
Some(vec!["http://front1.com", "http://front2.com"]),
|
||||
)
|
||||
.unwrap(),
|
||||
Url::new("http://example.org", None).unwrap(),
|
||||
];
|
||||
client.change_base_urls(new_urls);
|
||||
|
||||
assert_eq!(client.current_url().as_str(), "http://example.com/");
|
||||
|
||||
client.update_host();
|
||||
|
||||
// check that the url got updated now that there are multiple URLs
|
||||
assert_eq!(client.current_url().as_str(), "http://example.org/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[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)
|
||||
.unwrap()
|
||||
.with_fronting(crate::fronted::FrontPolicy::Always)
|
||||
.build::<&str>()
|
||||
.unwrap();
|
||||
|
||||
// check that the url is set correctly
|
||||
let current_url = client.current_url();
|
||||
assert_eq!(current_url.as_str(), "http://example.com/");
|
||||
assert_eq!(current_url.front_str(), Some("front1.com"));
|
||||
|
||||
// update the url
|
||||
client.update_host();
|
||||
|
||||
// check that the url is still the same since there is one URL and one front
|
||||
let current_url = client.current_url();
|
||||
assert_eq!(current_url.as_str(), "http://example.com/");
|
||||
assert_eq!(current_url.front_str(), Some("front1.com"));
|
||||
|
||||
// =======================================
|
||||
// we rotate through front urls when available if fronting is enabled
|
||||
|
||||
let new_urls = vec![
|
||||
Url::new(
|
||||
"http://example.com",
|
||||
Some(vec!["http://front1.com", "http://front2.com"]),
|
||||
)
|
||||
.unwrap(),
|
||||
Url::new("http://example.org", None).unwrap(),
|
||||
];
|
||||
client.change_base_urls(new_urls);
|
||||
|
||||
let current_url = client.current_url();
|
||||
assert_eq!(current_url.as_str(), "http://example.com/");
|
||||
assert_eq!(current_url.front_str(), Some("front1.com"));
|
||||
|
||||
// update the url - this should keep the same host but change the front
|
||||
client.update_host();
|
||||
|
||||
let current_url = client.current_url();
|
||||
// check that the url is still the same since there is one URL
|
||||
assert_eq!(current_url.as_str(), "http://example.com/");
|
||||
assert_eq!(current_url.front_str(), Some("front2.com"));
|
||||
|
||||
// update the url - this should wrap around to the first front as the second url is not fronted
|
||||
client.update_host();
|
||||
|
||||
let current_url = client.current_url();
|
||||
assert_eq!(current_url.as_str(), "http://example.com/");
|
||||
assert_eq!(current_url.front_str(), Some("front1.com"));
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
//! Url handling for the HTTP API client.
|
||||
//!
|
||||
//! This module provides a `Url` struct that wraps around the `url::Url` type and adds
|
||||
//! functionality for handling front domains, which are used for reverse proxying.
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use itertools::Itertools;
|
||||
use url::form_urlencoded;
|
||||
pub use url::ParseError;
|
||||
|
||||
/// A trait to try to convert some type into a `Url`.
|
||||
pub trait IntoUrl {
|
||||
/// Parse as a valid `Url`
|
||||
fn to_url(self) -> Result<Url, ParseError>;
|
||||
|
||||
/// Returns the string representation of the URL.
|
||||
fn as_str(&self) -> &str;
|
||||
}
|
||||
|
||||
impl IntoUrl for &str {
|
||||
fn to_url(self) -> Result<Url, ParseError> {
|
||||
let url = url::Url::parse(self)?;
|
||||
Ok(url.into())
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoUrl for String {
|
||||
fn to_url(self) -> Result<Url, ParseError> {
|
||||
let url = url::Url::parse(&self)?;
|
||||
Ok(url.into())
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoUrl for reqwest::Url {
|
||||
fn to_url(self) -> Result<Url, ParseError> {
|
||||
Ok(self.into())
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
/// When configuring fronting, some configurations will require a specific backend host
|
||||
/// to be used for the request to be properly reverse proxied.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Url {
|
||||
url: url::Url,
|
||||
fronts: Option<Vec<url::Url>>,
|
||||
current_front: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl IntoUrl for Url {
|
||||
fn to_url(self) -> Result<Url, ParseError> {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &str {
|
||||
self.url.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Url {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let current = self.current_front.load(Ordering::Relaxed);
|
||||
let other_current = other.current_front.load(Ordering::Relaxed);
|
||||
|
||||
self.fronts == other.fronts && self.url == other.url && current == other_current
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Url {}
|
||||
|
||||
impl std::hash::Hash for Url {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
let current = self.current_front.load(Ordering::Relaxed);
|
||||
self.fronts.hash(state);
|
||||
self.url.hash(state);
|
||||
current.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Url {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.fronts {
|
||||
Some(ref fronts) => {
|
||||
let current = self.current_front.load(Ordering::Relaxed);
|
||||
if let Some(front) = fronts.get(current) {
|
||||
write!(f, "{front}=>{}", self.url)
|
||||
} else {
|
||||
write!(f, "{}", self.url)
|
||||
}
|
||||
}
|
||||
None => write!(f, "{}", self.url),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Url> for url::Url {
|
||||
fn from(val: Url) -> Self {
|
||||
val.url
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Url> for Url {
|
||||
fn from(url: url::Url) -> Self {
|
||||
Self {
|
||||
url,
|
||||
fronts: None,
|
||||
current_front: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<url::Url> for Url {
|
||||
fn as_ref(&self) -> &url::Url {
|
||||
&self.url
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<url::Url> for Url {
|
||||
fn as_mut(&mut self) -> &mut url::Url {
|
||||
&mut self.url
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Url {
|
||||
type Err = url::ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let url = url::Url::parse(s)?;
|
||||
Ok(Self {
|
||||
url,
|
||||
fronts: None,
|
||||
current_front: Arc::new(AtomicUsize::new(0)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Url {
|
||||
/// Create a new `Url` instance with the given something that can be parsed as a URL and
|
||||
/// optional tunneling domains
|
||||
pub fn new<U: reqwest::IntoUrl>(
|
||||
url: U,
|
||||
fronts: Option<Vec<U>>,
|
||||
) -> Result<Self, reqwest::Error> {
|
||||
let mut url = Self {
|
||||
url: url.into_url()?,
|
||||
fronts: None,
|
||||
current_front: Arc::new(AtomicUsize::new(0)),
|
||||
};
|
||||
|
||||
// ensure that the provided URLs are valid
|
||||
if let Some(front_domains) = fronts {
|
||||
let f: Vec<reqwest::Url> = front_domains
|
||||
.into_iter()
|
||||
.map(|front| front.into_url())
|
||||
.try_collect()?;
|
||||
url.fronts = Some(f);
|
||||
}
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
/// Parse an absolute URL from a string.
|
||||
pub fn parse(s: &str) -> Result<Self, ParseError> {
|
||||
let url = url::Url::parse(s)?;
|
||||
Ok(Self {
|
||||
url,
|
||||
fronts: None,
|
||||
current_front: Arc::new(AtomicUsize::new(0)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns true if the URL has a front domain set
|
||||
pub fn has_front(&self) -> bool {
|
||||
if let Some(fronts) = &self.fronts {
|
||||
return !fronts.is_empty();
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Return the string representation of the current front host (domain or IP address) for this
|
||||
/// URL, if any.
|
||||
pub fn front_str(&self) -> Option<&str> {
|
||||
let current = self.current_front.load(Ordering::Relaxed);
|
||||
self.fronts
|
||||
.as_ref()
|
||||
.and_then(|fronts| fronts.get(current))
|
||||
.and_then(|url| url.host_str())
|
||||
}
|
||||
|
||||
/// Return the string representation of the host (domain or IP address) for this URL, if any.
|
||||
pub fn host_str(&self) -> Option<&str> {
|
||||
self.url.host_str()
|
||||
}
|
||||
|
||||
/// Return the serialization of this URL.
|
||||
///
|
||||
/// This is fast since that serialization is already stored in the inner url::Url struct.
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.url.as_str()
|
||||
}
|
||||
|
||||
/// Returns true if updating the front wraps back to the first front, or if no fronts are set
|
||||
pub fn update(&self) -> bool {
|
||||
if let Some(fronts) = &self.fronts {
|
||||
if fronts.len() > 1 {
|
||||
let current = self.current_front.load(Ordering::Relaxed);
|
||||
let next = (current + 1) % fronts.len();
|
||||
self.current_front.store(next, Ordering::Relaxed);
|
||||
return next == 0;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Return the scheme of this URL, lower-cased, as an ASCII string without the ‘:’ delimiter.
|
||||
pub fn scheme(&self) -> &str {
|
||||
self.url.scheme()
|
||||
}
|
||||
|
||||
/// Parse the URL’s query string, if any, as application/x-www-form-urlencoded and return an
|
||||
/// iterator of (key, value) pairs.
|
||||
pub fn query_pairs(&self) -> form_urlencoded::Parse<'_> {
|
||||
self.url.query_pairs()
|
||||
}
|
||||
|
||||
/// Manipulate this URL’s query string, viewed as a sequence of name/value pairs in
|
||||
/// application/x-www-form-urlencoded syntax.
|
||||
pub fn query_pairs_mut(&mut self) -> form_urlencoded::Serializer<'_, ::url::UrlQuery<'_>> {
|
||||
self.url.query_pairs_mut()
|
||||
}
|
||||
|
||||
/// Change this URL’s query string. If `query` is `None`, this URL’s query string will be cleared.
|
||||
pub fn set_query(&mut self, query: Option<&str>) {
|
||||
self.url.set_query(query);
|
||||
}
|
||||
|
||||
/// Change this URL’s path.
|
||||
pub fn set_path(&mut self, path: &str) {
|
||||
self.url.set_path(path);
|
||||
}
|
||||
|
||||
/// Change this URL’s scheme.
|
||||
pub fn set_scheme(&mut self, scheme: &str) {
|
||||
self.url.set_scheme(scheme).unwrap();
|
||||
}
|
||||
|
||||
/// Change this URL’s host.
|
||||
///
|
||||
/// Removing the host (calling this with None) will also remove any username, password, and port number.
|
||||
pub fn set_host(&mut self, host: &str) {
|
||||
self.url.set_host(Some(host)).unwrap();
|
||||
}
|
||||
|
||||
/// Change this URL’s port number.
|
||||
///
|
||||
/// Note that default port numbers are not reflected in the serialization.
|
||||
///
|
||||
/// If this URL is cannot-be-a-base, does not have a host, or has the `file` scheme; do nothing and return `Err`.
|
||||
pub fn set_port(&mut self, port: u16) {
|
||||
self.url.set_port(Some(port)).unwrap();
|
||||
}
|
||||
|
||||
/// Return an object with methods to manipulate this URL’s path segments.
|
||||
///
|
||||
/// Return Err(()) if this URL is cannot-be-a-base.
|
||||
pub fn path_segments(&self) -> Option<std::str::Split<'_, char>> {
|
||||
self.url.path_segments()
|
||||
}
|
||||
|
||||
/// Return an object with methods to manipulate this URL’s path segments.
|
||||
///
|
||||
/// Return Err(()) if this URL is cannot-be-a-base.
|
||||
#[allow(clippy::result_unit_err)]
|
||||
pub fn path_segments_mut(&mut self) -> Result<::url::PathSegmentsMut<'_>, ()> {
|
||||
self.url.path_segments_mut()
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,10 @@ impl<T> Bincode<T> {
|
||||
self.0.headers.insert(name, value.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Bincode<U> {
|
||||
Bincode(self.0.map(op))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoResponse for Bincode<T>
|
||||
|
||||
@@ -32,6 +32,10 @@ impl<T> Json<T> {
|
||||
self.0.headers.insert(name, value.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Json<U> {
|
||||
Json(self.0.map(op))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoResponse for Json<T>
|
||||
|
||||
@@ -14,11 +14,10 @@ pub mod bincode;
|
||||
pub mod json;
|
||||
pub mod yaml;
|
||||
|
||||
pub use bincode::Bincode;
|
||||
pub use json::Json;
|
||||
pub use yaml::Yaml;
|
||||
|
||||
pub use bincode::Bincode;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct ResponseWrapper<T> {
|
||||
data: T,
|
||||
@@ -33,6 +32,13 @@ impl<T> ResponseWrapper<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map<U, F: FnOnce(T) -> U>(self, op: F) -> ResponseWrapper<U> {
|
||||
ResponseWrapper {
|
||||
data: op(self.data),
|
||||
headers: self.headers,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn with_header(
|
||||
mut self,
|
||||
@@ -60,6 +66,14 @@ impl<T> FormattedResponse<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map<U, F: FnOnce(T) -> U>(self, op: F) -> FormattedResponse<U> {
|
||||
match self {
|
||||
FormattedResponse::Json(inner) => FormattedResponse::Json(inner.map(op)),
|
||||
FormattedResponse::Yaml(inner) => FormattedResponse::Yaml(inner.map(op)),
|
||||
FormattedResponse::Bincode(inner) => FormattedResponse::Bincode(inner.map(op)),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_header(
|
||||
self,
|
||||
|
||||
@@ -30,6 +30,10 @@ impl<T> Yaml<T> {
|
||||
self.0.headers.insert(name, value.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Yaml<U> {
|
||||
Yaml(self.0.map(op))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoResponse for Yaml<T>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "nym-noise"
|
||||
version = "0.1.0"
|
||||
authors = ["Simon Wicky <simon@nymtech.net>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
arc-swap = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
pin-project = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
snow = { workspace = true }
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["net", "io-util", "time"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
|
||||
# internal
|
||||
nym-crypto = { path = "../crypto" }
|
||||
nym-noise-keys = { path = "keys" }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
rand_chacha = { workspace = true }
|
||||
nym-crypto = { path = "../crypto", features = ["rand"] }
|
||||
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "nym-noise-keys"
|
||||
version = "0.1.0"
|
||||
authors = ["Simon Wicky <simon@nymtech.net>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
schemars = { workspace = true, features = ["preserve_order"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
utoipa = { workspace = true }
|
||||
|
||||
# internal
|
||||
nym-crypto = { path = "../../crypto", features = ["asymmetric", "serde"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user