Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d7f51f9f2 | |||
| 483bb6f477 | |||
| a68355a75a | |||
| 1572d8e5c2 | |||
| fd76c5ca4d | |||
| f94589c2d1 | |||
| 1c40499829 | |||
| f8a4d5f1ff | |||
| 42807890af | |||
| 5aa576b596 | |||
| 0215ad9294 | |||
| 227e6a10e1 | |||
| d3b6a270de | |||
| e12ada0105 | |||
| 71d50d79c2 | |||
| a21a01cf1a | |||
| 362f84b5f6 | |||
| daed9cd15b | |||
| a53ca71bd2 | |||
| a70e68c7bd | |||
| fdebed7c38 | |||
| f576a4ee2d | |||
| a9aafd785e | |||
| 0f7dbb94a8 | |||
| 2d72b1b201 | |||
| 412657f773 |
@@ -25,14 +25,14 @@ jobs:
|
||||
echo "file2=$(ls nym-vpn*.deb)" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload nym-repo-setup
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ env.file1 }}
|
||||
path: ppa/packages/nym-repo-setup*.deb
|
||||
retention-days: 10
|
||||
|
||||
- name: Upload nym-vpn
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ env.file2 }}
|
||||
path: ppa/packages/nym-vpn*.deb
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
run: sudo apt-get install -y rsync
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.2.0
|
||||
uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
version: 9
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -37,6 +37,9 @@ jobs:
|
||||
command: build
|
||||
args: --workspace --release
|
||||
|
||||
- name: Verify doc versions
|
||||
run: ${{ github.workspace }}/documentation/scripts/verify-doc-versions.sh
|
||||
working-directory: ${{ github.workspace }}
|
||||
- name: Install project dependencies
|
||||
run: pnpm i
|
||||
- name: Generate llms-full.txt
|
||||
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
|
||||
- name: Upload Artifact
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nym-binaries-artifacts
|
||||
path: |
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
name: ci-build-upload-network-monitor-agent
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-upload:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [arc-ubuntu-22.04]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Prepare build output directory
|
||||
shell: bash
|
||||
env:
|
||||
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
|
||||
run: |
|
||||
rm -rf ci-builds || true
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get -y install libudev-dev
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
|
||||
|
||||
- name: Build nym-network-monitor-agent
|
||||
shell: bash
|
||||
run: cargo build -p nym-network-monitor-agent --release
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: nym-network-monitor-agent
|
||||
path: target/release/nym-network-monitor-agent
|
||||
retention-days: 30
|
||||
|
||||
- name: Prepare build output
|
||||
shell: bash
|
||||
env:
|
||||
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
|
||||
run: cp target/release/nym-network-monitor-agent "$OUTPUT_DIR"
|
||||
|
||||
- name: Deploy to CI www
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-avzr"
|
||||
SOURCE: "ci-builds/"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
@@ -0,0 +1,19 @@
|
||||
name: ci-crates-preflight
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'Cargo.toml'
|
||||
- '**/Cargo.toml'
|
||||
- 'tools/internal/check_publish_preflight.py'
|
||||
- '.github/workflows/ci-crates-preflight.yml'
|
||||
|
||||
jobs:
|
||||
preflight:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Preflight publish checks
|
||||
run: python3 tools/internal/check_publish_preflight.py
|
||||
@@ -57,7 +57,8 @@ jobs:
|
||||
|
||||
- name: Update workspace dependencies
|
||||
run: |
|
||||
sed -i '/path = /s/version = "${{ steps.current_version.outputs.version }}"/version = "${{ inputs.version }}"/g' Cargo.toml
|
||||
# Match any semver version on lines with `path = `, not just the current workspace version.
|
||||
sed -i '/path = /s/version = "[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"/version = "${{ inputs.version }}"/g' Cargo.toml
|
||||
|
||||
- name: Bump versions (local only)
|
||||
run: |
|
||||
|
||||
@@ -33,7 +33,11 @@ jobs:
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
# `--publish-as-is` skips version bumping since that's done in a separate CI job.
|
||||
- name: Preflight publish checks
|
||||
run: |
|
||||
python3 tools/internal/check_publish_preflight.py
|
||||
|
||||
# --publish-as-is skips version bumping since that's done in a separate CI job.
|
||||
- name: Publish
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
|
||||
@@ -59,7 +59,9 @@ jobs:
|
||||
|
||||
- name: Update workspace dependencies
|
||||
run: |
|
||||
sed -i '/path = /s/version = "${{ steps.current_version.outputs.version }}"/version = "${{ inputs.version }}"/g' Cargo.toml
|
||||
# Match any semver version on lines with `path = `, not just the current workspace version.
|
||||
# This catches entries whose version has drifted (e.g. nym-sqlx-pool-guard at 1.2.0).
|
||||
sed -i '/path = /s/version = "[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"/version = "${{ inputs.version }}"/g' Cargo.toml
|
||||
|
||||
- name: Bump versions
|
||||
run: |
|
||||
@@ -67,20 +69,35 @@ jobs:
|
||||
--no-git-commit \
|
||||
--yes
|
||||
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: "chore/bump-version-${{ inputs.version }}"
|
||||
base: ${{ github.ref_name }}
|
||||
commit-message: "crates release: bump version to ${{ inputs.version }}"
|
||||
title: "chore: bump crate versions to ${{ inputs.version }}"
|
||||
body: |
|
||||
Automated version bump from `${{ steps.current_version.outputs.version }}` → `${{ inputs.version }}`.
|
||||
- name: Commit and push version bump
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE_BRANCH="${GITHUB_REF_NAME}"
|
||||
PR_BRANCH="ci/crates-version-bump-${{ inputs.version }}-${GITHUB_RUN_ID}"
|
||||
|
||||
Triggered by @${{ github.actor }} via workflow dispatch.
|
||||
labels: "automated, crates-version-bump"
|
||||
delete-branch: true
|
||||
git checkout -b "$PR_BRANCH"
|
||||
git add -A
|
||||
git commit -m "crates release: bump version to ${{ inputs.version }}"
|
||||
git push -u origin "$PR_BRANCH"
|
||||
|
||||
cat > /tmp/crates-version-bump-pr-body.md <<'EOF'
|
||||
This PR was created by CI because direct pushes to the release branch are blocked by branch protection rules.
|
||||
|
||||
## Summary
|
||||
- Bump workspace crate versions to the requested release version.
|
||||
- Update workspace dependency versions accordingly.
|
||||
|
||||
## Notes
|
||||
- Merge this PR to proceed with crates.io publishing.
|
||||
EOF
|
||||
|
||||
gh pr create \
|
||||
--base "$BASE_BRANCH" \
|
||||
--head "$PR_BRANCH" \
|
||||
--title "crates release: bump version to ${{ inputs.version }}" \
|
||||
--body-file /tmp/crates-version-bump-pr-body.md
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Show package versions
|
||||
run: cargo workspaces list --long
|
||||
run: cargo workspaces list --long
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
run: sudo apt-get install -y rsync
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.2.0
|
||||
uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
version: 9
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -61,6 +61,9 @@ jobs:
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/sdk && typedoc --skipErrorChecking
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/mix-fetch && typedoc --skipErrorChecking
|
||||
|
||||
- name: Verify doc versions
|
||||
run: ${{ github.workspace }}/documentation/scripts/verify-doc-versions.sh
|
||||
working-directory: ${{ github.workspace }}
|
||||
- name: Install project dependencies
|
||||
run: pnpm i
|
||||
- name: Generate llms-full.txt
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
find . -name Cargo.toml -exec cargo deny --manifest-path {} check \
|
||||
advisories -A advisory-not-detected --hide-inclusion-graph \; &> \
|
||||
>(uniq &> .github/workflows/support-files/notifications/deny.message )
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: report
|
||||
path: .github/workflows/support-files/notifications/deny.message
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
args: --workspace --release ${{ env.CARGO_FEATURES }}
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: my-artifact
|
||||
path: |
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
|
||||
- id: create-release
|
||||
name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda
|
||||
if: github.event_name == 'release'
|
||||
with:
|
||||
files: |
|
||||
|
||||
@@ -27,21 +27,21 @@ jobs:
|
||||
run: make contracts
|
||||
|
||||
- name: Upload Mixnet Contract Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: mixnet_contract.wasm
|
||||
path: contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm
|
||||
retention-days: 5
|
||||
|
||||
- name: Upload Vesting Contract Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: vesting_contract.wasm
|
||||
path: contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm
|
||||
retention-days: 5
|
||||
|
||||
- name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: github.event_name == 'release'
|
||||
with:
|
||||
files: |
|
||||
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
cd -
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nym-wallet.app.tar.gz
|
||||
path: nym-wallet/target/x86_64-apple-darwin/release/bundle/macos/nym-wallet.app.tar.gz
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
|
||||
- id: create-release
|
||||
name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: github.event_name == 'release'
|
||||
with:
|
||||
files: |
|
||||
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nym-wallet-appimage.tar.gz
|
||||
path: |
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
|
||||
- id: create-release
|
||||
name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: github.event_name == 'release'
|
||||
with:
|
||||
files: |
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
|
||||
|
||||
- name: Setup MSBuild.exe
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
uses: microsoft/setup-msbuild@v3
|
||||
|
||||
# No cache:yarn here: setup-node needs yarn on PATH to populate the cache, but this runner
|
||||
# only gets yarn from the step below.
|
||||
@@ -165,7 +165,7 @@ jobs:
|
||||
find . -name "*.msi" -type f
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nym-wallet.msi
|
||||
path: |
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
|
||||
- id: create-release
|
||||
name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: github.event_name == 'release'
|
||||
with:
|
||||
files: |
|
||||
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
apk/nyms5-arch64-release.apk
|
||||
|
||||
- name: Upload APKs
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nyms5-apk-arch64
|
||||
path: |
|
||||
@@ -91,12 +91,12 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: Download binary artifact
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: nyms5-apk-arch64
|
||||
path: apk
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
files: |
|
||||
apk/nyms5-arch64-debug.apk
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
with:
|
||||
release-tag-or-name-or-id: ${{ inputs.release_tag }}
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: Asset Hashes
|
||||
path: hashes.json
|
||||
|
||||
@@ -78,3 +78,4 @@ CLAUDE.md
|
||||
|
||||
/notes
|
||||
/target-otel
|
||||
test-tutorials/
|
||||
|
||||
@@ -4,52 +4,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2026.10-waterloo] (2026-05-27)
|
||||
|
||||
- Re-order default API urls for network details - Waterloo release ([#6799])
|
||||
- [bugfix] IPR v8<->v9 mismatch on Waterloo ([#6772])
|
||||
- Migrate to hickory 0.26.1 ([#6751])
|
||||
- add workflows for NM3 ([#6729])
|
||||
- credential proxy pool ([#6726])
|
||||
- chore: made sphinx version threshold assertion a compile time check ([#6718])
|
||||
- Feat/nmv3 updated performance calculation ([#6714])
|
||||
- feat: NMv3: submission of stress testing result into nym-api ([#6709])
|
||||
- feat: NMv3: Prometheus metrics for network monitor ([#6693])
|
||||
- feat: NMv3: add read-only results API to orchestrator ([#6689])
|
||||
- feat: NMv3: Eviction of stale testrun data ([#6685])
|
||||
- feat: NMv3: Wire up testrun assignment and result submission flow ([#6680])
|
||||
- feat: NMv3: Support multiple network monitor agents per host ([#6679])
|
||||
- Feat/nmv3 agent announcement ([#6673])
|
||||
- add node refresher for periodic scraping of bonded nym-node details ([#6626])
|
||||
- Feat/nmv3 orchestrator queue ([#6597])
|
||||
- feat: network monitor agent - standalone node stress-testing ([#6582])
|
||||
- [feat] propagate NM agent noise keys to nym-node routing ([#6577])
|
||||
- start mix stress testing topic branch ([#6575])
|
||||
- Feat/nmv3 agents subscription ([#6567])
|
||||
- Feat/nmv3 agents contract ([#6555])
|
||||
|
||||
[#6799]: https://github.com/nymtech/nym/pull/6799
|
||||
[#6772]: https://github.com/nymtech/nym/pull/6772
|
||||
[#6751]: https://github.com/nymtech/nym/pull/6751
|
||||
[#6729]: https://github.com/nymtech/nym/pull/6729
|
||||
[#6726]: https://github.com/nymtech/nym/pull/6726
|
||||
[#6718]: https://github.com/nymtech/nym/pull/6718
|
||||
[#6714]: https://github.com/nymtech/nym/pull/6714
|
||||
[#6709]: https://github.com/nymtech/nym/pull/6709
|
||||
[#6693]: https://github.com/nymtech/nym/pull/6693
|
||||
[#6689]: https://github.com/nymtech/nym/pull/6689
|
||||
[#6685]: https://github.com/nymtech/nym/pull/6685
|
||||
[#6680]: https://github.com/nymtech/nym/pull/6680
|
||||
[#6679]: https://github.com/nymtech/nym/pull/6679
|
||||
[#6673]: https://github.com/nymtech/nym/pull/6673
|
||||
[#6626]: https://github.com/nymtech/nym/pull/6626
|
||||
[#6597]: https://github.com/nymtech/nym/pull/6597
|
||||
[#6582]: https://github.com/nymtech/nym/pull/6582
|
||||
[#6577]: https://github.com/nymtech/nym/pull/6577
|
||||
[#6575]: https://github.com/nymtech/nym/pull/6575
|
||||
[#6567]: https://github.com/nymtech/nym/pull/6567
|
||||
[#6555]: https://github.com/nymtech/nym/pull/6555
|
||||
|
||||
## [2026.9-venaco] (2026-05-06)
|
||||
|
||||
- Fix for v9 IPR ([#6710])
|
||||
|
||||
Generated
+153
-231
File diff suppressed because it is too large
Load Diff
+125
-131
@@ -31,7 +31,6 @@ members = [
|
||||
"common/client-libs/mixnet-client",
|
||||
"common/client-libs/validator-client",
|
||||
"common/commands",
|
||||
"common/nym-common",
|
||||
"common/config",
|
||||
"common/cosmwasm-smart-contracts/coconut-dkg",
|
||||
"common/cosmwasm-smart-contracts/contracts-common",
|
||||
@@ -41,10 +40,10 @@ members = [
|
||||
"common/cosmwasm-smart-contracts/group-contract",
|
||||
"common/cosmwasm-smart-contracts/mixnet-contract",
|
||||
"common/cosmwasm-smart-contracts/multisig-contract",
|
||||
"common/cosmwasm-smart-contracts/node-families-contract",
|
||||
"common/cosmwasm-smart-contracts/nym-performance-contract",
|
||||
"common/cosmwasm-smart-contracts/nym-pool-contract",
|
||||
"common/cosmwasm-smart-contracts/vesting-contract",
|
||||
"common/cosmwasm-smart-contracts/network-monitors-contract",
|
||||
"common/credential-proxy",
|
||||
"common/credential-storage",
|
||||
"common/credential-utils",
|
||||
@@ -71,11 +70,14 @@ members = [
|
||||
"common/node-tester-utils",
|
||||
"common/nonexhaustive-delayqueue",
|
||||
"common/nym-cache",
|
||||
"common/nym-common",
|
||||
"common/nym-connection-monitor",
|
||||
"common/nym-id",
|
||||
"common/nym-kcp",
|
||||
"common/nym-lp",
|
||||
"common/nym-kkt",
|
||||
"common/nym-kkt-ciphersuite",
|
||||
"common/nym-kkt-context",
|
||||
"common/nym-lp",
|
||||
"common/nym-metrics",
|
||||
"common/nym_offline_compact_ecash",
|
||||
"common/nymnoise",
|
||||
@@ -91,9 +93,9 @@ members = [
|
||||
"common/nymsphinx/params",
|
||||
"common/nymsphinx/routing",
|
||||
"common/nymsphinx/types",
|
||||
"common/nyxd-scraper-sqlite",
|
||||
"common/nyxd-scraper-psql",
|
||||
"common/nyxd-scraper-shared",
|
||||
"common/nyxd-scraper-sqlite",
|
||||
"common/pemstore",
|
||||
"common/registration",
|
||||
"common/serde-helpers",
|
||||
@@ -123,6 +125,7 @@ members = [
|
||||
"common/zulip-client",
|
||||
"documentation/autodoc",
|
||||
"gateway",
|
||||
"integration-tests",
|
||||
"nym-api",
|
||||
"nym-api/nym-api-requests",
|
||||
"nym-authenticator-client",
|
||||
@@ -130,6 +133,7 @@ members = [
|
||||
"nym-credential-proxy/nym-credential-proxy",
|
||||
"nym-credential-proxy/nym-credential-proxy-requests",
|
||||
"nym-data-observatory",
|
||||
"nym-gateway-probe",
|
||||
"nym-ip-packet-client",
|
||||
"nym-network-monitor",
|
||||
"nym-node",
|
||||
@@ -141,6 +145,7 @@ members = [
|
||||
"nym-outfox",
|
||||
"nym-registration-client",
|
||||
"nym-signers-monitor",
|
||||
"nym-sqlx-pool-guard",
|
||||
"nym-statistics-api",
|
||||
"nym-validator-rewarder",
|
||||
"nyx-chain-watcher",
|
||||
@@ -148,19 +153,18 @@ members = [
|
||||
"sdk/ffi/go",
|
||||
"sdk/ffi/shared",
|
||||
"sdk/rust/nym-sdk",
|
||||
"smolmix/core",
|
||||
"service-providers/common",
|
||||
"service-providers/ip-packet-router",
|
||||
"service-providers/network-requester",
|
||||
"nym-sqlx-pool-guard",
|
||||
"smolmix/core",
|
||||
"tools/echo-server",
|
||||
"tools/internal/contract-state-importer/importer-cli",
|
||||
"tools/internal/contract-state-importer/importer-contract",
|
||||
"tools/internal/localnet-orchestrator",
|
||||
"tools/internal/localnet-orchestrator/dkg-bypass-contract",
|
||||
"tools/internal/mixnet-connectivity-check",
|
||||
# "tools/internal/sdk-version-bump",
|
||||
"tools/internal/ssl-inject",
|
||||
"tools/internal/localnet-orchestrator",
|
||||
"tools/internal/localnet-orchestrator/dkg-bypass-contract",
|
||||
"tools/internal/validator-status-check",
|
||||
"tools/nym-cli",
|
||||
"tools/nym-id-cli",
|
||||
@@ -173,31 +177,23 @@ members = [
|
||||
"wasm/mix-fetch",
|
||||
"wasm/node-tester",
|
||||
"wasm/zknym-lib",
|
||||
"nym-gateway-probe",
|
||||
"integration-tests",
|
||||
"common/nym-kkt-ciphersuite",
|
||||
"common/nym-kkt-context",
|
||||
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
|
||||
"nym-network-monitor-v3/nym-network-monitor-agent", "nym-network-monitor-v3/nym-network-monitor-orchestrator-requests",
|
||||
]
|
||||
|
||||
default-members = [
|
||||
"clients/native",
|
||||
"clients/socks5",
|
||||
"nym-authenticator-client",
|
||||
"nym-api",
|
||||
"nym-authenticator-client",
|
||||
"nym-credential-proxy/nym-credential-proxy",
|
||||
"nym-node",
|
||||
"nym-registration-client",
|
||||
"nym-statistics-api",
|
||||
"nym-validator-rewarder",
|
||||
"nyx-chain-watcher",
|
||||
"service-providers/ip-packet-router",
|
||||
"service-providers/network-requester",
|
||||
"tools/internal/localnet-orchestrator",
|
||||
"tools/nymvisor",
|
||||
"nym-registration-client",
|
||||
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
|
||||
"nym-network-monitor-v3/nym-network-monitor-agent",
|
||||
"tools/internal/localnet-orchestrator"
|
||||
]
|
||||
|
||||
exclude = ["contracts", "nym-wallet", "cpu-cycles"]
|
||||
@@ -211,7 +207,7 @@ edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
rust-version = "1.87.0"
|
||||
readme = "README.md"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
addr = "0.15.6"
|
||||
@@ -234,7 +230,7 @@ base85rs = "0.1.3"
|
||||
bincode = "1.3.3"
|
||||
bip39 = { version = "2.0.0", features = ["zeroize"] }
|
||||
bitvec = "1.0.0"
|
||||
blake3 = "1.7.0"
|
||||
blake3 = ">=1.7, <1.8.4" # blake3 1.8.4+ requires digest 0.11; workspace is on 0.10
|
||||
bloomfilter = "3.0.1"
|
||||
bs58 = "0.5.1"
|
||||
bytecodec = "0.4.15"
|
||||
@@ -330,7 +326,7 @@ pnet_packet = "0.35.0"
|
||||
publicsuffix = "2.3.0"
|
||||
proc_pidinfo = "0.1.3"
|
||||
quote = "1"
|
||||
rand = "0.8.5"
|
||||
rand = "0.8.6"
|
||||
rand09 = { package = "rand", version = "=0.9.2" }
|
||||
rand_chacha = "0.3"
|
||||
rand_chacha09 = { package = "rand_chacha", version = "=0.9.0" }
|
||||
@@ -354,7 +350,6 @@ serde_yaml = "0.9.25"
|
||||
serde_plain = "1.0.2"
|
||||
sha2 = "0.10.3"
|
||||
si-scale = "0.2.3"
|
||||
smolmix = { version = "1.21.1", path = "smolmix/core" }
|
||||
smoltcp = "0.12"
|
||||
snow = "0.9.6"
|
||||
sphinx-packet = "=0.6.0"
|
||||
@@ -365,7 +360,7 @@ subtle-encoding = "0.5"
|
||||
syn = "2"
|
||||
sysinfo = "0.37.0"
|
||||
tap = "1.0.1"
|
||||
tar = "0.4.44"
|
||||
tar = "0.4.45"
|
||||
test-with = { version = "0.15.4", default-features = false }
|
||||
tempfile = "3.20"
|
||||
thiserror = "2.0"
|
||||
@@ -407,10 +402,6 @@ zeroize = "1.7.0"
|
||||
|
||||
prometheus = { version = "0.14.0" }
|
||||
|
||||
# recreating lioness
|
||||
# we don't care about particular versions - just pull whatever is used by sphinx
|
||||
lioness = "*"
|
||||
arrayref = "*"
|
||||
|
||||
# libcrux
|
||||
libcrux-kem = "0.0.7"
|
||||
@@ -423,111 +414,113 @@ libcrux-sha3 = "0.0.8"
|
||||
libcrux-traits = "0.0.6"
|
||||
|
||||
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
|
||||
nym-api-requests = { version = "1.21.1", path = "nym-api/nym-api-requests" }
|
||||
nym-authenticator-requests = { version = "1.21.1", path = "common/authenticator-requests" }
|
||||
nym-async-file-watcher = { version = "1.21.1", path = "common/async-file-watcher" }
|
||||
nym-authenticator-client = { version = "1.21.1", path = "nym-authenticator-client" }
|
||||
nym-bandwidth-controller = { version = "1.21.1", path = "common/bandwidth-controller" }
|
||||
nym-bin-common = { version = "1.21.1", path = "common/bin-common" }
|
||||
nym-cache = { version = "1.21.1", path = "common/nym-cache" }
|
||||
nym-client-core = { version = "1.21.1", path = "common/client-core", default-features = false }
|
||||
nym-client-core-config-types = { version = "1.21.1", path = "common/client-core/config-types" }
|
||||
nym-client-core-gateways-storage = { version = "1.21.1", path = "common/client-core/gateways-storage" }
|
||||
nym-client-core-surb-storage = { version = "1.21.1", path = "common/client-core/surb-storage" }
|
||||
nym-client-websocket-requests = { version = "1.21.1", path = "clients/native/websocket-requests" }
|
||||
nym-common = { version = "1.21.1", path = "common/nym-common" }
|
||||
nym-compact-ecash = { version = "1.21.1", path = "common/nym_offline_compact_ecash" }
|
||||
nym-config = { version = "1.21.1", path = "common/config" }
|
||||
nym-contracts-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-coconut-dkg-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-credential-storage = { version = "1.21.1", path = "common/credential-storage" }
|
||||
nym-credential-utils = { version = "1.21.1", path = "common/credential-utils" }
|
||||
nym-credential-proxy-lib = { version = "1.21.1", path = "common/credential-proxy" }
|
||||
nym-credentials = { version = "1.21.1", path = "common/credentials", default-features = false }
|
||||
nym-credentials-interface = { version = "1.21.1", path = "common/credentials-interface" }
|
||||
nym-credential-proxy-requests = { version = "1.21.1", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
|
||||
nym-credential-verification = { version = "1.21.1", path = "common/credential-verification" }
|
||||
nym-crypto = { version = "1.21.1", path = "common/crypto", default-features = false }
|
||||
nym-dkg = { version = "1.21.1", path = "common/dkg" }
|
||||
nym-ecash-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-ecash-signer-check = { version = "1.21.1", path = "common/ecash-signer-check" }
|
||||
nym-ecash-signer-check-types = { version = "1.21.1", path = "common/ecash-signer-check-types" }
|
||||
nym-ecash-time = { version = "1.21.1", path = "common/ecash-time" }
|
||||
nym-exit-policy = { version = "1.21.1", path = "common/exit-policy" }
|
||||
nym-ffi-shared = { version = "1.21.1", path = "sdk/ffi/shared" }
|
||||
nym-gateway-client = { version = "1.21.1", path = "common/client-libs/gateway-client", default-features = false }
|
||||
nym-api-requests = { version = "1.21.0", path = "nym-api/nym-api-requests" }
|
||||
nym-authenticator-requests = { version = "1.21.0", path = "common/authenticator-requests" }
|
||||
nym-async-file-watcher = { version = "1.21.0", path = "common/async-file-watcher" }
|
||||
nym-authenticator-client = { version = "1.21.0", path = "nym-authenticator-client" }
|
||||
nym-bandwidth-controller = { version = "1.21.0", path = "common/bandwidth-controller" }
|
||||
nym-bin-common = { version = "1.21.0", path = "common/bin-common" }
|
||||
nym-cache = { version = "1.21.0", path = "common/nym-cache" }
|
||||
nym-client-core = { version = "1.21.0", path = "common/client-core", default-features = false }
|
||||
nym-client-core-config-types = { version = "1.21.0", path = "common/client-core/config-types" }
|
||||
nym-client-core-gateways-storage = { version = "1.21.0", path = "common/client-core/gateways-storage" }
|
||||
nym-client-core-surb-storage = { version = "1.21.0", path = "common/client-core/surb-storage" }
|
||||
nym-client-websocket-requests = { version = "1.21.0", path = "clients/native/websocket-requests" }
|
||||
nym-common = { version = "1.21.0", path = "common/nym-common" }
|
||||
nym-compact-ecash = { version = "1.21.0", path = "common/nym_offline_compact_ecash" }
|
||||
nym-config = { version = "1.21.0", path = "common/config" }
|
||||
nym-contracts-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-coconut-dkg-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-credential-storage = { version = "1.21.0", path = "common/credential-storage" }
|
||||
nym-credential-utils = { version = "1.21.0", path = "common/credential-utils" }
|
||||
nym-credential-proxy-lib = { version = "1.21.0", path = "common/credential-proxy" }
|
||||
nym-credentials = { version = "1.21.0", path = "common/credentials", default-features = false }
|
||||
nym-credentials-interface = { version = "1.21.0", path = "common/credentials-interface" }
|
||||
nym-credential-proxy-requests = { version = "1.21.0", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
|
||||
nym-credential-verification = { version = "1.21.0", path = "common/credential-verification" }
|
||||
nym-crypto = { version = "1.21.0", path = "common/crypto", default-features = false }
|
||||
nym-dkg = { version = "1.21.0", path = "common/dkg" }
|
||||
nym-ecash-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-ecash-signer-check = { version = "1.21.0", path = "common/ecash-signer-check" }
|
||||
nym-ecash-signer-check-types = { version = "1.21.0", path = "common/ecash-signer-check-types" }
|
||||
nym-ecash-time = { version = "1.21.0", path = "common/ecash-time" }
|
||||
nym-exit-policy = { version = "1.21.0", path = "common/exit-policy" }
|
||||
nym-ffi-shared = { version = "1.21.0", path = "sdk/ffi/shared" }
|
||||
nym-gateway-client = { version = "1.21.0", path = "common/client-libs/gateway-client", default-features = false }
|
||||
nym-gateway-probe = { version = "1.18.0", path = "nym-gateway-probe" }
|
||||
nym-gateway-requests = { version = "1.21.1", path = "common/gateway-requests" }
|
||||
nym-gateway-storage = { version = "1.21.1", path = "common/gateway-storage" }
|
||||
nym-gateway-stats-storage = { version = "1.21.1", path = "common/gateway-stats-storage" }
|
||||
nym-group-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-http-api-client = { version = "1.21.1", path = "common/http-api-client" }
|
||||
nym-http-api-client-macro = { version = "1.21.1", path = "common/http-api-client-macro" }
|
||||
nym-http-api-common = { version = "1.21.1", path = "common/http-api-common", default-features = false }
|
||||
nym-id = { version = "1.21.1", path = "common/nym-id" }
|
||||
nym-ip-packet-client = { version = "1.21.1", path = "nym-ip-packet-client" }
|
||||
nym-ip-packet-requests = { version = "1.21.1", path = "common/ip-packet-requests" }
|
||||
nym-lp = { version = "1.21.1", path = "common/nym-lp" }
|
||||
nym-kkt = { version = "1.21.1", path = "common/nym-kkt" }
|
||||
nym-kkt-ciphersuite = { version = "1.21.1", path = "common/nym-kkt-ciphersuite" }
|
||||
nym-kkt-context = { version = "1.21.1", path = "common/nym-kkt-context" }
|
||||
nym-metrics = { version = "1.21.1", path = "common/nym-metrics" }
|
||||
nym-mixnet-client = { version = "1.21.1", path = "common/client-libs/mixnet-client" }
|
||||
nym-mixnet-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-multisig-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-network-defaults = { version = "1.21.1", path = "common/network-defaults" }
|
||||
nym-node-tester-utils = { version = "1.21.1", path = "common/node-tester-utils" }
|
||||
nym-noise = { version = "1.21.1", path = "common/nymnoise" }
|
||||
nym-noise-keys = { version = "1.21.1", path = "common/nymnoise/keys" }
|
||||
nym-nonexhaustive-delayqueue = { version = "1.21.1", path = "common/nonexhaustive-delayqueue" }
|
||||
nym-node-requests = { version = "1.21.1", path = "nym-node/nym-node-requests", default-features = false }
|
||||
nym-node-metrics = { version = "1.21.1", path = "nym-node/nym-node-metrics" }
|
||||
nym-ordered-buffer = { version = "1.21.1", path = "common/socks5/ordered-buffer" }
|
||||
nym-outfox = { version = "1.21.1", path = "nym-outfox" }
|
||||
nym-registration-common = { version = "1.21.1", path = "common/registration" }
|
||||
nym-pemstore = { version = "1.21.1", path = "common/pemstore" }
|
||||
nym-performance-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-sdk = { version = "1.21.1", path = "sdk/rust/nym-sdk" }
|
||||
nym-serde-helpers = { version = "1.21.1", path = "common/serde-helpers" }
|
||||
nym-service-providers-common = { version = "1.21.1", path = "service-providers/common" }
|
||||
nym-service-provider-requests-common = { version = "1.21.1", path = "common/service-provider-requests-common" }
|
||||
nym-socks5-client-core = { version = "1.21.1", path = "common/socks5-client-core" }
|
||||
nym-socks5-proxy-helpers = { version = "1.21.1", path = "common/socks5/proxy-helpers" }
|
||||
nym-socks5-requests = { version = "1.21.1", path = "common/socks5/requests" }
|
||||
nym-sphinx = { version = "1.21.1", path = "common/nymsphinx" }
|
||||
nym-sphinx-acknowledgements = { version = "1.21.1", path = "common/nymsphinx/acknowledgements" }
|
||||
nym-sphinx-addressing = { version = "1.21.1", path = "common/nymsphinx/addressing" }
|
||||
nym-sphinx-anonymous-replies = { version = "1.21.1", path = "common/nymsphinx/anonymous-replies" }
|
||||
nym-sphinx-chunking = { version = "1.21.1", path = "common/nymsphinx/chunking" }
|
||||
nym-sphinx-cover = { version = "1.21.1", path = "common/nymsphinx/cover" }
|
||||
nym-sphinx-forwarding = { version = "1.21.1", path = "common/nymsphinx/forwarding" }
|
||||
nym-sphinx-framing = { version = "1.21.1", path = "common/nymsphinx/framing" }
|
||||
nym-sphinx-params = { version = "1.21.1", path = "common/nymsphinx/params" }
|
||||
nym-sphinx-routing = { version = "1.21.1", path = "common/nymsphinx/routing" }
|
||||
nym-sphinx-types = { version = "1.21.1", path = "common/nymsphinx/types" }
|
||||
nym-statistics-common = { version = "1.21.1", path = "common/statistics" }
|
||||
nym-store-cipher = { version = "1.21.1", path = "common/store-cipher" }
|
||||
nym-task = { version = "1.21.1", path = "common/task" }
|
||||
nym-tun = { version = "1.21.1", path = "common/tun" }
|
||||
nym-test-utils = { version = "1.21.1", path = "common/test-utils" }
|
||||
nym-ticketbooks-merkle = { version = "1.21.1", path = "common/ticketbooks-merkle" }
|
||||
nym-topology = { version = "1.21.1", path = "common/topology" }
|
||||
nym-types = { version = "1.21.1", path = "common/types" }
|
||||
nym-upgrade-mode-check = { version = "1.21.1", path = "common/upgrade-mode-check" }
|
||||
nym-validator-client = { version = "1.21.1", path = "common/client-libs/validator-client", default-features = false }
|
||||
nym-vesting-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
nym-network-monitors-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/network-monitors-contract" }
|
||||
nym-verloc = { version = "1.21.1", path = "common/verloc" }
|
||||
nym-wireguard = { version = "1.21.1", path = "common/wireguard" }
|
||||
nym-wireguard-types = { version = "1.21.1", path = "common/wireguard-types" }
|
||||
nym-wireguard-private-metadata-shared = { version = "1.21.1", path = "common/wireguard-private-metadata/shared" }
|
||||
nym-wireguard-private-metadata-client = { version = "1.21.1", path = "common/wireguard-private-metadata/client" }
|
||||
nym-wireguard-private-metadata-server = { version = "1.21.1", path = "common/wireguard-private-metadata/server" }
|
||||
nym-gateway-requests = { version = "1.21.0", path = "common/gateway-requests" }
|
||||
nym-gateway-storage = { version = "1.21.0", path = "common/gateway-storage" }
|
||||
nym-gateway-stats-storage = { version = "1.21.0", path = "common/gateway-stats-storage" }
|
||||
nym-group-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-http-api-client = { version = "1.21.0", path = "common/http-api-client" }
|
||||
nym-http-api-client-macro = { version = "1.21.0", path = "common/http-api-client-macro" }
|
||||
nym-http-api-common = { version = "1.21.0", path = "common/http-api-common", default-features = false }
|
||||
nym-id = { version = "1.21.0", path = "common/nym-id" }
|
||||
nym-ip-packet-client = { version = "1.21.0", path = "nym-ip-packet-client" }
|
||||
nym-ip-packet-requests = { version = "1.21.0", path = "common/ip-packet-requests" }
|
||||
nym-lp = { version = "1.21.0", path = "common/nym-lp" }
|
||||
nym-kkt = { version = "1.21.0", path = "common/nym-kkt" }
|
||||
nym-kkt-ciphersuite = { version = "1.21.0", path = "common/nym-kkt-ciphersuite" }
|
||||
nym-kkt-context = { version = "1.21.0", path = "common/nym-kkt-context" }
|
||||
nym-metrics = { version = "1.21.0", path = "common/nym-metrics" }
|
||||
nym-mixnet-client = { version = "1.21.0", path = "common/client-libs/mixnet-client" }
|
||||
nym-mixnet-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-multisig-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-network-defaults = { version = "1.21.0", path = "common/network-defaults" }
|
||||
nym-node-tester-utils = { version = "1.21.0", path = "common/node-tester-utils" }
|
||||
nym-noise = { version = "1.21.0", path = "common/nymnoise" }
|
||||
nym-noise-keys = { version = "1.21.0", path = "common/nymnoise/keys" }
|
||||
nym-nonexhaustive-delayqueue = { version = "1.21.0", path = "common/nonexhaustive-delayqueue" }
|
||||
nym-node-requests = { version = "1.21.0", path = "nym-node/nym-node-requests", default-features = false }
|
||||
nym-node-metrics = { version = "1.21.0", path = "nym-node/nym-node-metrics" }
|
||||
nym-node-families-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/node-families-contract" }
|
||||
nym-ordered-buffer = { version = "1.21.0", path = "common/socks5/ordered-buffer" }
|
||||
nym-outfox = { version = "1.21.0", path = "nym-outfox" }
|
||||
nym-registration-common = { version = "1.21.0", path = "common/registration" }
|
||||
nym-pemstore = { version = "1.21.0", path = "common/pemstore" }
|
||||
nym-performance-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-sdk = { version = "1.21.0", path = "sdk/rust/nym-sdk" }
|
||||
nym-serde-helpers = { version = "1.21.0", path = "common/serde-helpers" }
|
||||
nym-service-providers-common = { version = "1.21.0", path = "service-providers/common" }
|
||||
nym-service-provider-requests-common = { version = "1.21.0", path = "common/service-provider-requests-common" }
|
||||
nym-socks5-client-core = { version = "1.21.0", path = "common/socks5-client-core" }
|
||||
nym-socks5-proxy-helpers = { version = "1.21.0", path = "common/socks5/proxy-helpers" }
|
||||
nym-socks5-requests = { version = "1.21.0", path = "common/socks5/requests" }
|
||||
nym-sphinx = { version = "1.21.0", path = "common/nymsphinx" }
|
||||
nym-sphinx-acknowledgements = { version = "1.21.0", path = "common/nymsphinx/acknowledgements" }
|
||||
nym-sphinx-addressing = { version = "1.21.0", path = "common/nymsphinx/addressing" }
|
||||
nym-sphinx-anonymous-replies = { version = "1.21.0", path = "common/nymsphinx/anonymous-replies" }
|
||||
nym-sphinx-chunking = { version = "1.21.0", path = "common/nymsphinx/chunking" }
|
||||
nym-sphinx-cover = { version = "1.21.0", path = "common/nymsphinx/cover" }
|
||||
nym-sphinx-forwarding = { version = "1.21.0", path = "common/nymsphinx/forwarding" }
|
||||
nym-sphinx-framing = { version = "1.21.0", path = "common/nymsphinx/framing" }
|
||||
nym-sphinx-params = { version = "1.21.0", path = "common/nymsphinx/params" }
|
||||
nym-sphinx-routing = { version = "1.21.0", path = "common/nymsphinx/routing" }
|
||||
nym-sphinx-types = { version = "1.21.0", path = "common/nymsphinx/types" }
|
||||
nym-statistics-common = { version = "1.21.0", path = "common/statistics" }
|
||||
nym-store-cipher = { version = "1.21.0", path = "common/store-cipher" }
|
||||
nym-task = { version = "1.21.0", path = "common/task" }
|
||||
nym-tun = { version = "1.21.0", path = "common/tun" }
|
||||
nym-test-utils = { version = "1.21.0", path = "common/test-utils" }
|
||||
nym-ticketbooks-merkle = { version = "1.21.0", path = "common/ticketbooks-merkle" }
|
||||
nym-topology = { version = "1.21.0", path = "common/topology" }
|
||||
nym-types = { version = "1.21.0", path = "common/types" }
|
||||
nym-upgrade-mode-check = { version = "1.21.0", path = "common/upgrade-mode-check" }
|
||||
nym-validator-client = { version = "1.21.0", path = "common/client-libs/validator-client", default-features = false }
|
||||
nym-vesting-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
nym-verloc = { version = "1.21.0", path = "common/verloc" }
|
||||
nym-wireguard = { version = "1.21.0", path = "common/wireguard" }
|
||||
nym-wireguard-types = { version = "1.21.0", path = "common/wireguard-types" }
|
||||
nym-wireguard-private-metadata-shared = { version = "1.21.0", path = "common/wireguard-private-metadata/shared" }
|
||||
nym-wireguard-private-metadata-client = { version = "1.21.0", path = "common/wireguard-private-metadata/client" }
|
||||
nym-wireguard-private-metadata-server = { version = "1.21.0", path = "common/wireguard-private-metadata/server" }
|
||||
nym-sqlx-pool-guard = { version = "1.2.0", path = "nym-sqlx-pool-guard" }
|
||||
nym-wasm-client-core = { version = "1.21.1", path = "common/wasm/client-core" }
|
||||
nym-wasm-storage = { version = "1.21.1", path = "common/wasm/storage" }
|
||||
nym-wasm-utils = { version = "1.21.1", path = "common/wasm/utils", default-features = false }
|
||||
nyxd-scraper-shared = { version = "1.21.1", path = "common/nyxd-scraper-shared" }
|
||||
nym-wasm-client-core = { version = "1.21.0", path = "common/wasm/client-core" }
|
||||
nym-wasm-storage = { version = "1.21.0", path = "common/wasm/storage" }
|
||||
nym-wasm-utils = { version = "1.21.0", path = "common/wasm/utils", default-features = false }
|
||||
nyxd-scraper-shared = { version = "1.21.0", path = "common/nyxd-scraper-shared" }
|
||||
|
||||
smolmix = { version = "1.21.0", path = "smolmix/core" }
|
||||
|
||||
# coconut/DKG related
|
||||
# unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork
|
||||
@@ -626,3 +619,4 @@ exit = "deny"
|
||||
panic = "deny"
|
||||
unimplemented = "deny"
|
||||
unreachable = "deny"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
description = "Implementation of the Nym Client"
|
||||
version = "1.1.77"
|
||||
version = "1.1.76"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
@@ -472,6 +472,7 @@ impl Handler {
|
||||
fn prepare_reconstructed_binary(
|
||||
reconstructed_messages: Vec<ReconstructedMessage>,
|
||||
) -> Vec<Result<WsMessage, WsError>> {
|
||||
#[allow(clippy::result_large_err)] // TODO : remove this once tungstenite is updated
|
||||
reconstructed_messages
|
||||
.into_iter()
|
||||
.map(ServerResponse::Received)
|
||||
@@ -484,6 +485,7 @@ fn prepare_reconstructed_binary(
|
||||
fn prepare_reconstructed_text(
|
||||
reconstructed_messages: Vec<ReconstructedMessage>,
|
||||
) -> Vec<Result<WsMessage, WsError>> {
|
||||
#[allow(clippy::result_large_err)] // TODO : remove this once tungstenite is updated
|
||||
reconstructed_messages
|
||||
.into_iter()
|
||||
.map(ServerResponse::Received)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||
version = "1.1.77"
|
||||
version = "1.1.76"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
@@ -240,7 +240,7 @@ mod nonwasm_sealed {
|
||||
impl GatewaySender for LocalGateway {
|
||||
async fn send_mix_packet(&mut self, packet: MixPacket) -> Result<(), ErasedGatewayError> {
|
||||
self.packet_forwarder
|
||||
.forward_client_packet_without_delay(packet)
|
||||
.forward_packet(packet)
|
||||
.map_err(erase_err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,7 +439,7 @@ where
|
||||
let mut pending_acks = Vec::with_capacity(fragments.len());
|
||||
let mut to_forward: HashMap<_, Vec<_>> = HashMap::new();
|
||||
|
||||
for (raw, prepared) in fragments.into_iter().zip(prepared_fragments.into_iter()) {
|
||||
for (raw, prepared) in fragments.into_iter().zip(prepared_fragments) {
|
||||
let lane = raw.0;
|
||||
let FragmentWithMaxRetransmissions {
|
||||
fragment,
|
||||
@@ -670,7 +670,7 @@ where
|
||||
|
||||
Ok(fragments
|
||||
.into_iter()
|
||||
.zip(reply_surbs.into_iter())
|
||||
.zip(reply_surbs)
|
||||
.map(|(fragment, reply_surb)| {
|
||||
// unwrap here is fine as we know we have a valid topology
|
||||
#[allow(clippy::unwrap_used)]
|
||||
|
||||
@@ -34,4 +34,3 @@ client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"]
|
||||
[dev-dependencies]
|
||||
nym-crypto = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "io-util", "rt", "rt-multi-thread"] }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use dashmap::DashMap;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use futures::StreamExt;
|
||||
use nym_noise::config::NoiseConfig;
|
||||
use nym_noise::upgrade_noise_initiator;
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
@@ -14,7 +14,6 @@ use std::ops::Deref;
|
||||
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::mpsc::error::TrySendError;
|
||||
@@ -91,17 +90,13 @@ impl Deref for ActiveConnections {
|
||||
pub struct ConnectionSender {
|
||||
channel: mpsc::Sender<FramedNymPacket>,
|
||||
current_reconnection_attempt: Arc<AtomicU32>,
|
||||
// Identifies the `ManagedConnection` task currently owning this entry; used
|
||||
// to ensure drop-time eviction only fires on the still-owning task.
|
||||
handle_token: Arc<()>,
|
||||
}
|
||||
|
||||
impl ConnectionSender {
|
||||
fn new(channel: mpsc::Sender<FramedNymPacket>, handle_token: Arc<()>) -> Self {
|
||||
fn new(channel: mpsc::Sender<FramedNymPacket>) -> Self {
|
||||
ConnectionSender {
|
||||
channel,
|
||||
current_reconnection_attempt: Arc::new(AtomicU32::new(0)),
|
||||
handle_token,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,31 +107,6 @@ struct ManagedConnection {
|
||||
message_receiver: ReceiverStream<FramedNymPacket>,
|
||||
connection_timeout: Duration,
|
||||
current_reconnection: Arc<AtomicU32>,
|
||||
active_connections: ActiveConnections,
|
||||
handle_token: Arc<()>,
|
||||
}
|
||||
|
||||
// Evicts the cache entry on task exit (only if still owned by this task).
|
||||
// Without this, a stale `ConnectionSender` survives after the peer disconnects
|
||||
// and the next outbound packet is silently swallowed by the dead TCP.
|
||||
struct EvictOnDrop {
|
||||
active_connections: ActiveConnections,
|
||||
address: SocketAddr,
|
||||
handle_token: Arc<()>,
|
||||
}
|
||||
|
||||
impl Drop for EvictOnDrop {
|
||||
fn drop(&mut self) {
|
||||
let address = self.address;
|
||||
let handle_token = &self.handle_token;
|
||||
self.active_connections.remove_if(&address, |_, sender| {
|
||||
Arc::ptr_eq(&sender.handle_token, handle_token)
|
||||
});
|
||||
trace!(
|
||||
peer = %address,
|
||||
"managed connection task exited; evicted owning cache entry"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl ManagedConnection {
|
||||
@@ -146,8 +116,6 @@ impl ManagedConnection {
|
||||
message_receiver: mpsc::Receiver<FramedNymPacket>,
|
||||
connection_timeout: Duration,
|
||||
current_reconnection: Arc<AtomicU32>,
|
||||
active_connections: ActiveConnections,
|
||||
handle_token: Arc<()>,
|
||||
) -> Self {
|
||||
ManagedConnection {
|
||||
address,
|
||||
@@ -155,30 +123,72 @@ impl ManagedConnection {
|
||||
message_receiver: ReceiverStream::new(message_receiver),
|
||||
connection_timeout,
|
||||
current_reconnection,
|
||||
active_connections,
|
||||
handle_token,
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(self) {
|
||||
let address = self.address;
|
||||
let _evict_guard = EvictOnDrop {
|
||||
active_connections: self.active_connections,
|
||||
address,
|
||||
handle_token: self.handle_token,
|
||||
};
|
||||
|
||||
let reconnection_attempt = self.current_reconnection.load(Ordering::Acquire);
|
||||
let connect_start = tokio::time::Instant::now();
|
||||
let connection_fut = TcpStream::connect(address);
|
||||
|
||||
// 1. attempt to establish the connection with timeout
|
||||
let maybe_stream = match tokio::time::timeout(self.connection_timeout, connection_fut).await
|
||||
{
|
||||
Ok(stream) => stream,
|
||||
let conn = match tokio::time::timeout(self.connection_timeout, connection_fut).await {
|
||||
Ok(stream_res) => match stream_res {
|
||||
Ok(stream) => {
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
"Managed to establish connection to {}", self.address
|
||||
);
|
||||
|
||||
let noise_start = tokio::time::Instant::now();
|
||||
let noise_stream =
|
||||
match upgrade_noise_initiator(stream, &self.noise_config).await {
|
||||
Ok(noise_stream) => noise_stream,
|
||||
Err(err) => {
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
event = "connection.failed.noise",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "noise_error",
|
||||
"Failed to perform Noise initiator handshake with {address}"
|
||||
);
|
||||
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
self.current_reconnection.store(0, Ordering::Release);
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
"Noise initiator handshake completed for {:?}", address
|
||||
);
|
||||
Framed::new(noise_stream, NymCodec)
|
||||
}
|
||||
Err(err) => {
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
event = "connection.failed.connect",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "connect_error",
|
||||
"failed to establish connection to {address}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
warn!(
|
||||
event = "connection.failed.timeout",
|
||||
peer = %address,
|
||||
timeout_ms = self.connection_timeout.as_millis() as u64,
|
||||
@@ -193,133 +203,21 @@ impl ManagedConnection {
|
||||
}
|
||||
};
|
||||
|
||||
// 2. check if it actually succeeded
|
||||
let stream = match maybe_stream {
|
||||
Ok(stream) => stream,
|
||||
Err(err) => {
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
event = "connection.failed.connect",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "connect_error",
|
||||
"failed to establish connection to {address}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
"Managed to establish connection to {}", self.address
|
||||
);
|
||||
|
||||
// 3. perform noise handshake (if applicable)
|
||||
let noise_start = tokio::time::Instant::now();
|
||||
let noise_stream = match upgrade_noise_initiator(stream, &self.noise_config).await {
|
||||
Ok(noise_stream) => noise_stream,
|
||||
Err(err) => {
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
event = "connection.failed.noise",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "noise_error",
|
||||
"Failed to perform Noise initiator handshake with {address}"
|
||||
);
|
||||
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
self.current_reconnection.store(0, Ordering::Release);
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
"Noise initiator handshake completed for {:?}", address
|
||||
);
|
||||
let conn = Framed::new(noise_stream, NymCodec);
|
||||
|
||||
// 4. start handling the framed stream
|
||||
run_io_loop(conn, self.message_receiver, address).await;
|
||||
}
|
||||
}
|
||||
|
||||
// The connection is unidirectional (send-only); we read from it solely to
|
||||
// notice peer FIN/RST while idle so we can evict the cache entry before the
|
||||
// next outbound send finds it stale.
|
||||
async fn run_io_loop<T>(
|
||||
conn: Framed<T, NymCodec>,
|
||||
mut receiver: ReceiverStream<FramedNymPacket>,
|
||||
address: SocketAddr,
|
||||
) where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let (mut sink, mut stream) = conn.split();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = stream.next() => {
|
||||
match msg {
|
||||
None => {
|
||||
debug!(
|
||||
peer = %address,
|
||||
exit_reason = "peer_closed",
|
||||
"peer closed mixnet connection to {address}"
|
||||
);
|
||||
break;
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
debug!(
|
||||
event = "connection.read_error",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
exit_reason = "read_error",
|
||||
"read error on mixnet connection to {address}: {err}"
|
||||
);
|
||||
break;
|
||||
}
|
||||
Some(Ok(_)) => {
|
||||
trace!(
|
||||
peer = %address,
|
||||
"unexpected inbound packet on mixnet connection to {address}; discarding"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
outgoing = receiver.next() => {
|
||||
match outgoing {
|
||||
None => {
|
||||
debug!(
|
||||
peer = %address,
|
||||
exit_reason = "sender_dropped",
|
||||
"connection manager to {address} finished"
|
||||
);
|
||||
break;
|
||||
}
|
||||
Some(packet) => {
|
||||
if let Err(err) = sink.send(packet).await {
|
||||
debug!(
|
||||
event = "connection.forward_error",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
exit_reason = "forward_error",
|
||||
"Failed to forward packet to {address}: {err}"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Err(err) = self.message_receiver.map(Ok).forward(conn).await {
|
||||
warn!(
|
||||
event = "connection.forward_error",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
exit_reason = "forward_error",
|
||||
"Failed to forward packets to {address}: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
debug!(
|
||||
peer = %address,
|
||||
exit_reason = "sender_dropped",
|
||||
"connection manager to {address} finished"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,18 +264,13 @@ impl Client {
|
||||
sender.try_send(pending_packet).unwrap();
|
||||
}
|
||||
|
||||
// Ownership token for the task we're about to spawn; lets it tell
|
||||
// on exit whether the cache entry still names it.
|
||||
let handle_token = Arc::new(());
|
||||
|
||||
// if we already tried to connect to `address` before, grab the current attempt count
|
||||
let current_reconnection_attempt =
|
||||
if let Some(mut existing) = self.active_connections.get_mut(&address) {
|
||||
existing.channel = sender;
|
||||
existing.handle_token = Arc::clone(&handle_token);
|
||||
Arc::clone(&existing.current_reconnection_attempt)
|
||||
} else {
|
||||
let new_entry = ConnectionSender::new(sender, Arc::clone(&handle_token));
|
||||
let new_entry = ConnectionSender::new(sender);
|
||||
let current_attempt = Arc::clone(&new_entry.current_reconnection_attempt);
|
||||
self.active_connections.insert(address, new_entry);
|
||||
current_attempt
|
||||
@@ -392,7 +285,6 @@ impl Client {
|
||||
|
||||
let connections_count = self.connections_count.clone();
|
||||
let noise_config = self.noise_config.clone();
|
||||
let active_connections = self.active_connections.clone();
|
||||
tokio::spawn(async move {
|
||||
// before executing the manager, wait for what was specified, if anything
|
||||
if let Some(backoff) = backoff {
|
||||
@@ -407,8 +299,6 @@ impl Client {
|
||||
receiver,
|
||||
initial_connection_timeout,
|
||||
current_reconnection_attempt,
|
||||
active_connections,
|
||||
handle_token,
|
||||
)
|
||||
.run()
|
||||
.await;
|
||||
@@ -538,102 +428,4 @@ mod tests {
|
||||
client.config.maximum_reconnection_backoff
|
||||
);
|
||||
}
|
||||
|
||||
fn test_addr() -> SocketAddr {
|
||||
"127.0.0.1:1".parse().unwrap()
|
||||
}
|
||||
|
||||
fn insert_with_token(
|
||||
active: &ActiveConnections,
|
||||
addr: SocketAddr,
|
||||
token: Arc<()>,
|
||||
) -> mpsc::Receiver<FramedNymPacket> {
|
||||
let (tx, rx) = mpsc::channel(1);
|
||||
active.insert(addr, ConnectionSender::new(tx, token));
|
||||
rx
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evict_on_drop_removes_entry_when_token_still_matches() {
|
||||
let active = ActiveConnections::default();
|
||||
let addr = test_addr();
|
||||
let token = Arc::new(());
|
||||
let _rx = insert_with_token(&active, addr, Arc::clone(&token));
|
||||
|
||||
assert!(active.get(&addr).is_some());
|
||||
|
||||
{
|
||||
let _guard = EvictOnDrop {
|
||||
active_connections: active.clone(),
|
||||
address: addr,
|
||||
handle_token: token,
|
||||
};
|
||||
}
|
||||
|
||||
assert!(
|
||||
active.get(&addr).is_none(),
|
||||
"owning task's drop should evict the entry"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evict_on_drop_preserves_entry_replaced_by_newer_make_connection() {
|
||||
// Simulates the race: old task's run() has returned, but before its
|
||||
// drop guard fires, a concurrent `make_connection` replaced the
|
||||
// entry's channel + handle_token with a fresh task's token.
|
||||
let active = ActiveConnections::default();
|
||||
let addr = test_addr();
|
||||
let old_token = Arc::new(());
|
||||
let new_token = Arc::new(());
|
||||
let _rx_new = insert_with_token(&active, addr, Arc::clone(&new_token));
|
||||
|
||||
{
|
||||
let _guard = EvictOnDrop {
|
||||
active_connections: active.clone(),
|
||||
address: addr,
|
||||
handle_token: old_token,
|
||||
};
|
||||
}
|
||||
|
||||
assert!(
|
||||
active.get(&addr).is_some(),
|
||||
"old task's drop must not clobber the newer entry"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn io_loop_exits_when_peer_closes_idle_connection() {
|
||||
// The fix's second half: while no packets are flowing, peer FIN/RST
|
||||
// must still be observed so the cache entry can be evicted before the
|
||||
// next send finds it stale.
|
||||
let (a, b) = tokio::io::duplex(64);
|
||||
let conn = Framed::new(a, NymCodec);
|
||||
let (_tx, rx) = mpsc::channel(1);
|
||||
|
||||
let task = tokio::spawn(run_io_loop(conn, ReceiverStream::new(rx), test_addr()));
|
||||
|
||||
// Simulate peer closing both directions of the connection.
|
||||
drop(b);
|
||||
|
||||
tokio::time::timeout(Duration::from_secs(1), task)
|
||||
.await
|
||||
.expect("io_loop must notice peer close while idle")
|
||||
.expect("io_loop task must not panic");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn io_loop_exits_when_sender_dropped() {
|
||||
let (a, _b) = tokio::io::duplex(64);
|
||||
let conn = Framed::new(a, NymCodec);
|
||||
let (tx, rx) = mpsc::channel(1);
|
||||
|
||||
let task = tokio::spawn(run_io_loop(conn, ReceiverStream::new(rx), test_addr()));
|
||||
|
||||
drop(tx);
|
||||
|
||||
tokio::time::timeout(Duration::from_secs(1), task)
|
||||
.await
|
||||
.expect("io_loop must exit when the upstream sender is dropped")
|
||||
.expect("io_loop task must not panic");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,16 +21,12 @@ impl From<mpsc::UnboundedSender<PacketToForward>> for MixForwardingSender {
|
||||
}
|
||||
|
||||
impl MixForwardingSender {
|
||||
pub fn forward_packet(&self, packet: PacketToForward) -> Result<(), SendError> {
|
||||
pub fn forward_packet(&self, packet: impl Into<PacketToForward>) -> Result<(), SendError> {
|
||||
self.0
|
||||
.unbounded_send(packet)
|
||||
.unbounded_send(packet.into())
|
||||
.map_err(|err| err.into_send_error())
|
||||
}
|
||||
|
||||
pub fn forward_client_packet_without_delay(&self, packet: MixPacket) -> Result<(), SendError> {
|
||||
self.forward_packet(PacketToForward::client_packet_without_delay(packet))
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
@@ -42,23 +38,35 @@ pub type MixForwardingReceiver = mpsc::UnboundedReceiver<PacketToForward>;
|
||||
pub struct PacketToForward {
|
||||
pub packet: MixPacket,
|
||||
pub forward_delay_target: Option<Instant>,
|
||||
pub network_monitor_packet: bool,
|
||||
}
|
||||
|
||||
impl From<MixPacket> for PacketToForward {
|
||||
fn from(packet: MixPacket) -> Self {
|
||||
PacketToForward::new_no_delay(packet)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(MixPacket, Option<Instant>)> for PacketToForward {
|
||||
fn from((packet, delay_until): (MixPacket, Option<Instant>)) -> Self {
|
||||
PacketToForward::new(packet, delay_until)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(MixPacket, Instant)> for PacketToForward {
|
||||
fn from((packet, delay_until): (MixPacket, Instant)) -> Self {
|
||||
PacketToForward::new(packet, Some(delay_until))
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketToForward {
|
||||
pub fn new(
|
||||
packet: MixPacket,
|
||||
forward_delay_target: Option<Instant>,
|
||||
network_monitor_packet: bool,
|
||||
) -> Self {
|
||||
pub fn new(packet: MixPacket, forward_delay_target: Option<Instant>) -> Self {
|
||||
PacketToForward {
|
||||
packet,
|
||||
forward_delay_target,
|
||||
network_monitor_packet,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client_packet_without_delay(packet: MixPacket) -> Self {
|
||||
Self::new(packet, None, false)
|
||||
pub fn new_no_delay(packet: MixPacket) -> Self {
|
||||
Self::new(packet, None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ nym-ecash-contract-common = { workspace = true }
|
||||
nym-multisig-contract-common = { workspace = true }
|
||||
nym-group-contract-common = { workspace = true }
|
||||
nym-performance-contract-common = { workspace = true }
|
||||
nym-network-monitors-contract-common = { workspace = true }
|
||||
nym-node-families-contract-common = { workspace = true }
|
||||
nym-serde-helpers = { workspace = true, features = ["hex", "base64"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -104,14 +104,6 @@ impl TryFrom<NymNetworkDetails> for Config {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new(nyxd_url: Url, api_url: Url, nyxd_config: nyxd::Config) -> Self {
|
||||
Config {
|
||||
api_url,
|
||||
nyxd_url,
|
||||
nyxd_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_nym_network_details(
|
||||
details: &NymNetworkDetails,
|
||||
) -> Result<Self, ValidatorClientError> {
|
||||
@@ -122,15 +114,6 @@ impl Config {
|
||||
.map(|url| Url::parse(url))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
if let Some(nym_api_urls) = details.nym_api_urls.as_ref() {
|
||||
api_url.extend(
|
||||
nym_api_urls
|
||||
.iter()
|
||||
.map(|url| url.url.parse())
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
);
|
||||
}
|
||||
|
||||
if api_url.is_empty() {
|
||||
return Err(ValidatorClientError::NoAPIUrlAvailable);
|
||||
}
|
||||
|
||||
@@ -15,15 +15,12 @@ use nym_api_requests::ecash::models::{
|
||||
VerifyEcashTicketBody,
|
||||
};
|
||||
use nym_api_requests::ecash::VerificationKeyResponse;
|
||||
use nym_api_requests::models::network_monitor::{
|
||||
KnownNetworkMonitorResponse, StressTestBatchSubmission,
|
||||
};
|
||||
use nym_api_requests::models::node_families::NodeFamily;
|
||||
use nym_api_requests::models::{
|
||||
AnnotationResponseV1, ApiHealthResponse, BinaryBuildInformationOwned,
|
||||
ChainBlocksStatusResponse, ChainStatusResponse, KeyRotationInfoResponse,
|
||||
NodePerformanceResponse, NodeRefreshBody, NymNodeDescriptionV1, NymNodeDescriptionV2,
|
||||
PerformanceHistoryResponse, RewardedSetResponse, SignerInformationResponse,
|
||||
StressTestBatchSubmissionResponse,
|
||||
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
|
||||
ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
|
||||
NymNodeDescriptionV1, NymNodeDescriptionV2, PerformanceHistoryResponse, RewardedSetResponse,
|
||||
SignerInformationResponse,
|
||||
};
|
||||
use nym_api_requests::pagination::PaginatedResponse;
|
||||
use nym_http_api_client::{ApiClient, NO_PARAMS};
|
||||
@@ -393,6 +390,45 @@ pub trait NymApiClientExt: ApiClient {
|
||||
Ok(bonds)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn get_node_families(
|
||||
&self,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
) -> Result<PaginatedResponse<NodeFamily>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
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::V1_API_VERSION, routes::NODE_FAMILIES_ROUTES],
|
||||
¶ms,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_node_families(&self) -> Result<Vec<NodeFamily>, NymAPIError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut families = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut res = self.get_node_families(Some(page), None).await?;
|
||||
|
||||
families.append(&mut res.data);
|
||||
if families.len() < res.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(families)
|
||||
}
|
||||
|
||||
#[deprecated]
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNodeV1>, NymAPIError> {
|
||||
@@ -980,7 +1016,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn get_node_annotation(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<AnnotationResponseV1, NymAPIError> {
|
||||
) -> Result<AnnotationResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::V1_API_VERSION,
|
||||
@@ -1363,53 +1399,6 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata))
|
||||
}
|
||||
|
||||
/// Queries the nym-api for whether a particular ed25519 identity key is currently recognised
|
||||
/// as an authorised network monitor permitted to submit stress testing results.
|
||||
///
|
||||
/// `identity_key` is expected to be the base58-encoded form of the ed25519 public key.
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_known_network_monitor(
|
||||
&self,
|
||||
identity_key: IdentityKeyRef<'_>,
|
||||
) -> Result<KnownNetworkMonitorResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::V3_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::STRESS_TESTING,
|
||||
routes::STRESS_TESTING_KNOWN_MONITORS,
|
||||
identity_key,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Submit a signed batch of stress-testing results to nym-api on behalf of a network monitor
|
||||
/// orchestrator.
|
||||
///
|
||||
/// The caller is expected to have produced `request` via
|
||||
/// `StressTestBatchSubmissionContent::new(...)` and signed it with the orchestrator's ed25519
|
||||
/// key; nym-api will reject submissions that are stale, replayed, unauthorised, or whose
|
||||
/// signature fails to verify.
|
||||
#[instrument(level = "debug", skip(self, request))]
|
||||
async fn submit_stress_testing_results(
|
||||
&self,
|
||||
request: &StressTestBatchSubmission,
|
||||
) -> Result<StressTestBatchSubmissionResponse, NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::V3_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::STRESS_TESTING,
|
||||
routes::STRESS_TESTING_BATCH_SUBMIT,
|
||||
],
|
||||
NO_PARAMS,
|
||||
request,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// Client is already nym_http_api_client::Client (re-exported above), so just one impl needed
|
||||
|
||||
@@ -38,6 +38,7 @@ pub mod ecash {
|
||||
}
|
||||
|
||||
pub const NYM_NODES_ROUTES: &str = "nym-nodes";
|
||||
pub const NODE_FAMILIES_ROUTES: &str = "node-families";
|
||||
|
||||
pub use nym_nodes::*;
|
||||
pub mod nym_nodes {
|
||||
@@ -49,9 +50,6 @@ pub mod nym_nodes {
|
||||
pub const NYM_NODES_REWARDED_SET: &str = "rewarded-set";
|
||||
pub const NYM_NODES_REFRESH_DESCRIBED: &str = "refresh-described";
|
||||
pub const BY_ADDRESSES: &str = "by-addresses";
|
||||
pub const STRESS_TESTING: &str = "stress-testing";
|
||||
pub const STRESS_TESTING_KNOWN_MONITORS: &str = "known-monitors";
|
||||
pub const STRESS_TESTING_BATCH_SUBMIT: &str = "batch-submit";
|
||||
}
|
||||
|
||||
pub const STATUS_ROUTES: &str = "status";
|
||||
|
||||
@@ -867,6 +867,10 @@ mod tests {
|
||||
MixnetExecuteMsg::TestingResolveAllPendingEvents { .. } => {
|
||||
client.testing_resolve_all_pending_events(None).ignore()
|
||||
}
|
||||
// not expected to be exposed by the client
|
||||
ExecuteMsg::AdminMigrateVestedMixNode { .. }
|
||||
| ExecuteMsg::AdminMigrateVestedDelegation { .. }
|
||||
| ExecuteMsg::AdminBatchMigrateVestedDelegations { .. } => ().ignore(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ pub mod ecash_query_client;
|
||||
pub mod group_query_client;
|
||||
pub mod mixnet_query_client;
|
||||
pub mod multisig_query_client;
|
||||
pub mod network_monitors_query_client;
|
||||
pub mod node_families_query_client;
|
||||
pub mod performance_query_client;
|
||||
pub mod vesting_query_client;
|
||||
|
||||
@@ -23,7 +23,7 @@ pub mod ecash_signing_client;
|
||||
pub mod group_signing_client;
|
||||
pub mod mixnet_signing_client;
|
||||
pub mod multisig_signing_client;
|
||||
pub mod network_monitors_signing_client;
|
||||
pub mod node_families_signing_client;
|
||||
pub mod performance_signing_client;
|
||||
pub mod vesting_signing_client;
|
||||
|
||||
@@ -33,9 +33,7 @@ pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient};
|
||||
pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient};
|
||||
pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient};
|
||||
pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient};
|
||||
pub use network_monitors_query_client::{
|
||||
NetworkMonitorsQueryClient, PagedNetworkMonitorsQueryClient,
|
||||
};
|
||||
pub use node_families_query_client::{NodeFamiliesQueryClient, PagedNodeFamiliesQueryClient};
|
||||
pub use performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient};
|
||||
pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient};
|
||||
|
||||
@@ -45,7 +43,7 @@ pub use ecash_signing_client::EcashSigningClient;
|
||||
pub use group_signing_client::GroupSigningClient;
|
||||
pub use mixnet_signing_client::MixnetSigningClient;
|
||||
pub use multisig_signing_client::MultisigSigningClient;
|
||||
pub use network_monitors_signing_client::NetworkMonitorsSigningClient;
|
||||
pub use node_families_signing_client::NodeFamiliesSigningClient;
|
||||
pub use performance_signing_client::PerformanceSigningClient;
|
||||
pub use vesting_signing_client::VestingSigningClient;
|
||||
|
||||
@@ -55,7 +53,7 @@ pub trait NymContractsProvider {
|
||||
fn mixnet_contract_address(&self) -> Option<&AccountId>;
|
||||
fn vesting_contract_address(&self) -> Option<&AccountId>;
|
||||
fn performance_contract_address(&self) -> Option<&AccountId>;
|
||||
fn network_monitors_contract_address(&self) -> Option<&AccountId>;
|
||||
fn node_families_contract_address(&self) -> Option<&AccountId>;
|
||||
|
||||
// coconut-related
|
||||
fn ecash_contract_address(&self) -> Option<&AccountId>;
|
||||
@@ -69,7 +67,7 @@ pub struct TypedNymContracts {
|
||||
pub mixnet_contract_address: Option<AccountId>,
|
||||
pub vesting_contract_address: Option<AccountId>,
|
||||
pub performance_contract_address: Option<AccountId>,
|
||||
pub network_monitors_contract_address: Option<AccountId>,
|
||||
pub node_families_contract_address: Option<AccountId>,
|
||||
|
||||
pub ecash_contract_address: Option<AccountId>,
|
||||
pub group_contract_address: Option<AccountId>,
|
||||
@@ -94,8 +92,8 @@ impl TryFrom<NymContracts> for TypedNymContracts {
|
||||
.performance_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
.transpose()?,
|
||||
network_monitors_contract_address: value
|
||||
.network_monitors_contract_address
|
||||
node_families_contract_address: value
|
||||
.node_families_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
.transpose()?,
|
||||
ecash_contract_address: value
|
||||
|
||||
-107
@@ -1,107 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::collect_paged;
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::CosmWasmClient;
|
||||
use async_trait::async_trait;
|
||||
use nym_network_monitors_contract_common::{
|
||||
AuthorisedNetworkMonitor, AuthorisedNetworkMonitorOrchestratorsResponse,
|
||||
AuthorisedNetworkMonitorsPagedResponse, QueryMsg as NetworkMonitorsQueryMsg,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait NetworkMonitorsQueryClient {
|
||||
async fn query_network_monitors_contract<T>(
|
||||
&self,
|
||||
query: NetworkMonitorsQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>;
|
||||
|
||||
async fn get_admin(&self) -> Result<cw_controllers::AdminResponse, NyxdError> {
|
||||
self.query_network_monitors_contract(NetworkMonitorsQueryMsg::Admin {})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_network_monitor_orchestrators(
|
||||
&self,
|
||||
) -> Result<AuthorisedNetworkMonitorOrchestratorsResponse, NyxdError> {
|
||||
self.query_network_monitors_contract(
|
||||
NetworkMonitorsQueryMsg::NetworkMonitorOrchestrators {},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_network_monitor_agents_paged(
|
||||
&self,
|
||||
start_next_after: Option<SocketAddr>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<AuthorisedNetworkMonitorsPagedResponse, NyxdError> {
|
||||
self.query_network_monitors_contract(NetworkMonitorsQueryMsg::NetworkMonitorAgents {
|
||||
start_next_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait PagedNetworkMonitorsQueryClient: NetworkMonitorsQueryClient {
|
||||
async fn get_all_network_monitor_agents(
|
||||
&self,
|
||||
) -> Result<Vec<AuthorisedNetworkMonitor>, NyxdError> {
|
||||
collect_paged!(self, get_network_monitor_agents_paged, authorised)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> PagedNetworkMonitorsQueryClient for T where T: NetworkMonitorsQueryClient {}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> NetworkMonitorsQueryClient for C
|
||||
where
|
||||
C: CosmWasmClient + NymContractsProvider + Send + Sync,
|
||||
{
|
||||
async fn query_network_monitors_contract<T>(
|
||||
&self,
|
||||
query: NetworkMonitorsQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
let contract_address = &self
|
||||
.network_monitors_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("network monitors contract"))?;
|
||||
self.query_contract_smart(contract_address, &query).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_query_variants_are_covered<C: NetworkMonitorsQueryClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: NetworkMonitorsQueryMsg,
|
||||
) {
|
||||
match msg {
|
||||
NetworkMonitorsQueryMsg::Admin {} => client.get_admin().ignore(),
|
||||
NetworkMonitorsQueryMsg::NetworkMonitorOrchestrators {} => {
|
||||
client.get_network_monitor_orchestrators().ignore()
|
||||
}
|
||||
NetworkMonitorsQueryMsg::NetworkMonitorAgents { .. } => {
|
||||
client.get_network_monitor_agents_paged(None, None).ignore()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
-205
@@ -1,205 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::{Coin, Fee, SigningCosmWasmClient};
|
||||
use crate::signing::signer::OfflineSigner;
|
||||
use async_trait::async_trait;
|
||||
use nym_network_monitors_contract_common::ExecuteMsg as NetworkMonitorsExecuteMsg;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait NetworkMonitorsSigningClient {
|
||||
async fn execute_network_monitors_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: NetworkMonitorsExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError>;
|
||||
|
||||
async fn update_admin(
|
||||
&self,
|
||||
admin: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::UpdateAdmin { admin };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::UpdateAdmin".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn authorise_network_monitor_orchestrator(
|
||||
&self,
|
||||
address: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitorOrchestrator { address };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitorOrchestrator".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Announce (or rotate) the ed25519 identity key of the calling network monitor orchestrator.
|
||||
///
|
||||
/// The caller must already be an authorised orchestrator; the contract validates that
|
||||
/// `identity_key` is a well-formed base-58 encoding of a 32-byte ed25519 public key.
|
||||
async fn update_orchestrator_identity_key(
|
||||
&self,
|
||||
identity_key: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::UpdateOrchestratorIdentityKey { key: identity_key };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::UpdateOrchestratorIdentityKey".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn revoke_network_monitor_orchestrator(
|
||||
&self,
|
||||
address: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::RevokeNetworkMonitorOrchestrator { address };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::RevokeNetworkMonitorOrchestrator".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn authorise_network_monitor(
|
||||
&self,
|
||||
mixnet_address: SocketAddr,
|
||||
bs58_x25519_noise: String,
|
||||
noise_version: u8,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitor {
|
||||
mixnet_address,
|
||||
bs58_x25519_noise,
|
||||
noise_version,
|
||||
};
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitor".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn revoke_network_monitor(
|
||||
&self,
|
||||
address: SocketAddr,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::RevokeNetworkMonitor { address };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::RevokeNetworkMonitor".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn revoke_all_network_monitors(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::RevokeAllNetworkMonitors;
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::RevokeAllNetworkMonitors".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> NetworkMonitorsSigningClient for C
|
||||
where
|
||||
C: SigningCosmWasmClient + NymContractsProvider + Sync,
|
||||
NyxdError: From<<Self as OfflineSigner>::Error>,
|
||||
{
|
||||
async fn execute_network_monitors_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: NetworkMonitorsExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let contract_address = &self
|
||||
.network_monitors_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("network monitors contract"))?;
|
||||
|
||||
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
|
||||
|
||||
let signer_address = &self.signer_addresses()[0];
|
||||
self.execute(signer_address, contract_address, &msg, fee, memo, funds)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
use nym_network_monitors_contract_common::ExecuteMsg;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_execute_variants_are_covered<C: NetworkMonitorsSigningClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: NetworkMonitorsExecuteMsg,
|
||||
) {
|
||||
match msg {
|
||||
NetworkMonitorsExecuteMsg::UpdateAdmin { admin } => {
|
||||
client.update_admin(admin, None).ignore()
|
||||
}
|
||||
ExecuteMsg::AuthoriseNetworkMonitorOrchestrator { address } => client
|
||||
.authorise_network_monitor_orchestrator(address, None)
|
||||
.ignore(),
|
||||
ExecuteMsg::UpdateOrchestratorIdentityKey { key } => {
|
||||
client.update_orchestrator_identity_key(key, None).ignore()
|
||||
}
|
||||
ExecuteMsg::RevokeNetworkMonitorOrchestrator { address } => client
|
||||
.revoke_network_monitor_orchestrator(address, None)
|
||||
.ignore(),
|
||||
ExecuteMsg::AuthoriseNetworkMonitor {
|
||||
mixnet_address: address,
|
||||
bs58_x25519_noise,
|
||||
noise_version,
|
||||
} => client
|
||||
.authorise_network_monitor(address, bs58_x25519_noise, noise_version, None)
|
||||
.ignore(),
|
||||
ExecuteMsg::RevokeNetworkMonitor { address } => {
|
||||
client.revoke_network_monitor(address, None).ignore()
|
||||
}
|
||||
ExecuteMsg::RevokeAllNetworkMonitors => {
|
||||
client.revoke_all_network_monitors(None).ignore()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+441
@@ -0,0 +1,441 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::collect_paged;
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::CosmWasmClient;
|
||||
use async_trait::async_trait;
|
||||
use cosmrs::AccountId;
|
||||
use serde::Deserialize;
|
||||
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
pub use nym_node_families_contract_common::{
|
||||
msg::QueryMsg as NodeFamiliesQueryMsg, AllFamilyMembersPagedResponse,
|
||||
AllPastFamilyInvitationsPagedResponse, FamiliesPagedResponse, FamilyMemberRecord,
|
||||
FamilyMembersPagedResponse, GlobalPastFamilyInvitationCursor, NodeFamily,
|
||||
NodeFamilyByNameResponse, NodeFamilyByOwnerResponse, NodeFamilyId,
|
||||
NodeFamilyMembershipResponse, NodeFamilyResponse, PastFamilyInvitation,
|
||||
PastFamilyInvitationCursor, PastFamilyInvitationForNodeCursor,
|
||||
PastFamilyInvitationsForNodePagedResponse, PastFamilyInvitationsPagedResponse,
|
||||
PastFamilyMember, PastFamilyMemberCursor, PastFamilyMemberForNodeCursor,
|
||||
PastFamilyMembersForNodePagedResponse, PastFamilyMembersPagedResponse,
|
||||
PendingFamilyInvitationDetails, PendingFamilyInvitationResponse,
|
||||
PendingFamilyInvitationsPagedResponse, PendingInvitationsForNodePagedResponse,
|
||||
PendingInvitationsPagedResponse,
|
||||
};
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait NodeFamiliesQueryClient {
|
||||
async fn query_node_families_contract<T>(
|
||||
&self,
|
||||
query: NodeFamiliesQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>;
|
||||
|
||||
async fn get_family_by_id(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<NodeFamilyResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyById { family_id })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_family_by_owner(
|
||||
&self,
|
||||
owner: &AccountId,
|
||||
) -> Result<NodeFamilyByOwnerResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyByOwner {
|
||||
owner: owner.to_string(),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_family_by_name(
|
||||
&self,
|
||||
name: String,
|
||||
) -> Result<NodeFamilyByNameResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyByName { name })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_families_paged(
|
||||
&self,
|
||||
start_after: Option<NodeFamilyId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<FamiliesPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamiliesPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_family_membership(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<NodeFamilyMembershipResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyMembership { node_id })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_family_members_paged(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<FamilyMembersPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyMembersPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_family_members_paged(
|
||||
&self,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<AllFamilyMembersPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetAllFamilyMembersPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_pending_invitation(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
) -> Result<PendingFamilyInvitationResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPendingInvitation {
|
||||
family_id,
|
||||
node_id,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_pending_invitations_for_family_paged(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PendingFamilyInvitationsPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(
|
||||
NodeFamiliesQueryMsg::GetPendingInvitationsForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_pending_invitations_for_node_paged(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
start_after: Option<NodeFamilyId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PendingInvitationsForNodePagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPendingInvitationsForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_pending_invitations_paged(
|
||||
&self,
|
||||
start_after: Option<(NodeFamilyId, NodeId)>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PendingInvitationsPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetAllPendingInvitationsPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_past_invitations_for_family_paged(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<PastFamilyInvitationCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PastFamilyInvitationsPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastInvitationsForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_past_invitations_for_node_paged(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
start_after: Option<PastFamilyInvitationForNodeCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PastFamilyInvitationsForNodePagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastInvitationsForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_past_invitations_paged(
|
||||
&self,
|
||||
start_after: Option<GlobalPastFamilyInvitationCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<AllPastFamilyInvitationsPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetAllPastInvitationsPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_past_members_for_family_paged(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<PastFamilyMemberCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PastFamilyMembersPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastMembersForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_past_members_for_node_paged(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
start_after: Option<PastFamilyMemberForNodeCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PastFamilyMembersForNodePagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastMembersForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// extension trait to the query client to deal with the paged queries
|
||||
// (it didn't feel appropriate to combine it with the existing trait)
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait PagedNodeFamiliesQueryClient: NodeFamiliesQueryClient {
|
||||
async fn get_all_families(&self) -> Result<Vec<NodeFamily>, NyxdError> {
|
||||
collect_paged!(self, get_families_paged, families)
|
||||
}
|
||||
|
||||
async fn get_all_family_members_for_family(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<Vec<FamilyMemberRecord>, NyxdError> {
|
||||
collect_paged!(self, get_family_members_paged, members, family_id)
|
||||
}
|
||||
|
||||
async fn get_all_family_members(&self) -> Result<Vec<FamilyMemberRecord>, NyxdError> {
|
||||
collect_paged!(self, get_all_family_members_paged, members)
|
||||
}
|
||||
|
||||
async fn get_all_pending_invitations_for_family(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<Vec<PendingFamilyInvitationDetails>, NyxdError> {
|
||||
collect_paged!(
|
||||
self,
|
||||
get_pending_invitations_for_family_paged,
|
||||
invitations,
|
||||
family_id
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_all_pending_invitations_for_node(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<Vec<PendingFamilyInvitationDetails>, NyxdError> {
|
||||
collect_paged!(
|
||||
self,
|
||||
get_pending_invitations_for_node_paged,
|
||||
invitations,
|
||||
node_id
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_all_pending_invitations(
|
||||
&self,
|
||||
) -> Result<Vec<PendingFamilyInvitationDetails>, NyxdError> {
|
||||
collect_paged!(self, get_all_pending_invitations_paged, invitations)
|
||||
}
|
||||
|
||||
async fn get_all_past_invitations_for_family(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<Vec<PastFamilyInvitation>, NyxdError> {
|
||||
collect_paged!(
|
||||
self,
|
||||
get_past_invitations_for_family_paged,
|
||||
invitations,
|
||||
family_id
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_all_past_invitations_for_node(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<Vec<PastFamilyInvitation>, NyxdError> {
|
||||
collect_paged!(
|
||||
self,
|
||||
get_past_invitations_for_node_paged,
|
||||
invitations,
|
||||
node_id
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_all_past_invitations(&self) -> Result<Vec<PastFamilyInvitation>, NyxdError> {
|
||||
collect_paged!(self, get_all_past_invitations_paged, invitations)
|
||||
}
|
||||
|
||||
async fn get_all_past_members_for_family(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<Vec<PastFamilyMember>, NyxdError> {
|
||||
collect_paged!(self, get_past_members_for_family_paged, members, family_id)
|
||||
}
|
||||
|
||||
async fn get_all_past_members_for_node(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<Vec<PastFamilyMember>, NyxdError> {
|
||||
collect_paged!(self, get_past_members_for_node_paged, members, node_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> PagedNodeFamiliesQueryClient for T where T: NodeFamiliesQueryClient {}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> NodeFamiliesQueryClient for C
|
||||
where
|
||||
C: CosmWasmClient + NymContractsProvider + Send + Sync,
|
||||
{
|
||||
async fn query_node_families_contract<T>(
|
||||
&self,
|
||||
query: NodeFamiliesQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
let node_families_contract_address = &self
|
||||
.node_families_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("node families contract"))?;
|
||||
self.query_contract_smart(node_families_contract_address, &query)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
use nym_node_families_contract_common::QueryMsg;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_query_variants_are_covered<C: NodeFamiliesQueryClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: NodeFamiliesQueryMsg,
|
||||
) {
|
||||
match msg {
|
||||
NodeFamiliesQueryMsg::GetFamilyById { family_id } => {
|
||||
client.get_family_by_id(family_id).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamilyByOwner { owner } => {
|
||||
client.get_family_by_owner(&owner.parse().unwrap()).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamilyByName { name } => {
|
||||
client.get_family_by_name(name).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamiliesPaged { start_after, limit } => {
|
||||
client.get_families_paged(start_after, limit).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamilyMembership { node_id } => {
|
||||
client.get_family_membership(node_id).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamilyMembersPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_family_members_paged(family_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetAllFamilyMembersPaged { start_after, limit } => client
|
||||
.get_all_family_members_paged(start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPendingInvitation { family_id, node_id } => {
|
||||
client.get_pending_invitation(family_id, node_id).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetPendingInvitationsForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_pending_invitations_for_family_paged(family_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPendingInvitationsForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_pending_invitations_for_node_paged(node_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetAllPendingInvitationsPaged { start_after, limit } => client
|
||||
.get_all_pending_invitations_paged(start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPastInvitationsForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_past_invitations_for_family_paged(family_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPastInvitationsForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_past_invitations_for_node_paged(node_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetAllPastInvitationsPaged { start_after, limit } => client
|
||||
.get_all_past_invitations_paged(start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPastMembersForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_past_members_for_family_paged(family_id, start_after, limit)
|
||||
.ignore(),
|
||||
QueryMsg::GetPastMembersForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_past_members_for_node_paged(node_id, start_after, limit)
|
||||
.ignore(),
|
||||
};
|
||||
}
|
||||
}
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::nyxd::coin::Coin;
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::{Fee, SigningCosmWasmClient};
|
||||
use crate::signing::signer::OfflineSigner;
|
||||
use async_trait::async_trait;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_node_families_contract_common::{
|
||||
Config, ExecuteMsg as NodeFamiliesExecuteMsg, NodeFamilyId,
|
||||
};
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait NodeFamiliesSigningClient {
|
||||
async fn execute_node_families_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: NodeFamiliesExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError>;
|
||||
|
||||
async fn update_node_families_config(
|
||||
&self,
|
||||
config: Config,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::UpdateConfig { config },
|
||||
"NodeFamiliesContract::UpdateConfig".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn create_family(
|
||||
&self,
|
||||
name: String,
|
||||
description: String,
|
||||
fee: Option<Fee>,
|
||||
creation_fee: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::CreateFamily { name, description },
|
||||
"NodeFamiliesContract::CreateFamily".to_string(),
|
||||
creation_fee,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn disband_family(&self, fee: Option<Fee>) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::DisbandFamily {},
|
||||
"NodeFamiliesContract::DisbandFamily".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn invite_to_family(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
validity_secs: Option<u64>,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::InviteToFamily {
|
||||
node_id,
|
||||
validity_secs,
|
||||
},
|
||||
"NodeFamiliesContract::InviteToFamily".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn revoke_family_invitation(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::RevokeFamilyInvitation { node_id },
|
||||
"NodeFamiliesContract::RevokeFamilyInvitation".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn accept_family_invitation(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::AcceptFamilyInvitation { family_id, node_id },
|
||||
"NodeFamiliesContract::AcceptFamilyInvitation".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn reject_family_invitation(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::RejectFamilyInvitation { family_id, node_id },
|
||||
"NodeFamiliesContract::RejectFamilyInvitation".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn leave_family(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::LeaveFamily { node_id },
|
||||
"NodeFamiliesContract::LeaveFamily".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn kick_from_family(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::KickFromFamily { node_id },
|
||||
"NodeFamiliesContract::KickFromFamily".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Cross-contract callback fired by the mixnet contract on node unbonding.
|
||||
/// Exposed for completeness; the families contract rejects this call from
|
||||
/// any sender other than the configured mixnet contract address.
|
||||
async fn on_nym_node_unbond(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::OnNymNodeUnbond { node_id },
|
||||
"NodeFamiliesContract::OnNymNodeUnbond".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> NodeFamiliesSigningClient for C
|
||||
where
|
||||
C: SigningCosmWasmClient + NymContractsProvider + Sync,
|
||||
NyxdError: From<<Self as OfflineSigner>::Error>,
|
||||
{
|
||||
async fn execute_node_families_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: NodeFamiliesExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let node_families_contract_address = &self
|
||||
.node_families_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("node families contract"))?;
|
||||
|
||||
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
|
||||
|
||||
let signer_address = &self.signer_addresses()[0];
|
||||
self.execute(
|
||||
signer_address,
|
||||
node_families_contract_address,
|
||||
&msg,
|
||||
fee,
|
||||
memo,
|
||||
funds,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
use nym_node_families_contract_common::ExecuteMsg;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_execute_variants_are_covered<C: NodeFamiliesSigningClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: NodeFamiliesExecuteMsg,
|
||||
) {
|
||||
match msg {
|
||||
NodeFamiliesExecuteMsg::UpdateConfig { config } => {
|
||||
client.update_node_families_config(config, None).ignore()
|
||||
}
|
||||
NodeFamiliesExecuteMsg::CreateFamily { name, description } => client
|
||||
.create_family(name, description, None, vec![])
|
||||
.ignore(),
|
||||
NodeFamiliesExecuteMsg::DisbandFamily {} => client.disband_family(None).ignore(),
|
||||
NodeFamiliesExecuteMsg::InviteToFamily {
|
||||
node_id,
|
||||
validity_secs,
|
||||
} => client
|
||||
.invite_to_family(node_id, validity_secs, None)
|
||||
.ignore(),
|
||||
NodeFamiliesExecuteMsg::RevokeFamilyInvitation { node_id } => {
|
||||
client.revoke_family_invitation(node_id, None).ignore()
|
||||
}
|
||||
NodeFamiliesExecuteMsg::AcceptFamilyInvitation { family_id, node_id } => client
|
||||
.accept_family_invitation(family_id, node_id, None)
|
||||
.ignore(),
|
||||
NodeFamiliesExecuteMsg::RejectFamilyInvitation { family_id, node_id } => client
|
||||
.reject_family_invitation(family_id, node_id, None)
|
||||
.ignore(),
|
||||
NodeFamiliesExecuteMsg::LeaveFamily { node_id } => {
|
||||
client.leave_family(node_id, None).ignore()
|
||||
}
|
||||
NodeFamiliesExecuteMsg::KickFromFamily { node_id } => {
|
||||
client.kick_from_family(node_id, None).ignore()
|
||||
}
|
||||
ExecuteMsg::OnNymNodeUnbond { node_id } => {
|
||||
client.on_nym_node_unbond(node_id, None).ignore()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ pub mod logs;
|
||||
pub mod module_traits;
|
||||
pub mod types;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SigningClientOptions {
|
||||
gas_price: GasPrice,
|
||||
simulated_gas_multiplier: f32,
|
||||
@@ -80,17 +80,6 @@ impl<C, S> MaybeSigningClient<C, S> {
|
||||
opts,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clone_query_client(&self) -> MaybeSigningClient<C, NoSigner>
|
||||
where
|
||||
C: Clone,
|
||||
{
|
||||
MaybeSigningClient {
|
||||
client: self.client.clone(),
|
||||
signer: Default::default(),
|
||||
opts: self.opts.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "http-client")]
|
||||
|
||||
@@ -24,8 +24,6 @@ use async_trait::async_trait;
|
||||
use cosmrs::tendermint::{abci, evidence::Evidence, Genesis};
|
||||
use cosmrs::tx::{Raw, SignDoc};
|
||||
use cosmwasm_std::Addr;
|
||||
use nym_contracts_common::build_information::CONTRACT_BUILD_INFO_STORAGE_KEY;
|
||||
use nym_contracts_common::ContractBuildInformation;
|
||||
use nym_network_defaults::{ChainDetails, NymNetworkDetails};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::fmt::Debug;
|
||||
@@ -42,7 +40,6 @@ pub use crate::nyxd::{
|
||||
fee::Fee,
|
||||
};
|
||||
pub use crate::rpc::TendermintRpcClient;
|
||||
pub use bip39;
|
||||
pub use coin::Coin;
|
||||
pub use cosmrs::{
|
||||
bank::MsgSend,
|
||||
@@ -73,19 +70,14 @@ pub use tendermint_rpc::{
|
||||
Paging, Request, Response, SimpleRequest,
|
||||
};
|
||||
|
||||
pub use nym_ecash_contract_common;
|
||||
pub use nym_mixnet_contract_common;
|
||||
pub use nym_multisig_contract_common;
|
||||
pub use nym_network_monitors_contract_common;
|
||||
pub use nym_performance_contract_common;
|
||||
pub use nym_vesting_contract_common;
|
||||
|
||||
#[cfg(feature = "http-client")]
|
||||
use crate::http_client;
|
||||
#[cfg(feature = "http-client")]
|
||||
use crate::{DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient};
|
||||
#[cfg(feature = "http-client")]
|
||||
use cosmrs::rpc::{HttpClient, HttpClientUrl};
|
||||
use nym_contracts_common::build_information::CONTRACT_BUILD_INFO_STORAGE_KEY;
|
||||
use nym_contracts_common::ContractBuildInformation;
|
||||
|
||||
pub mod coin;
|
||||
pub mod contract_traits;
|
||||
@@ -270,16 +262,6 @@ impl<C, S> NyxdClient<C, S> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clone_query_client(&self) -> NyxdClient<C>
|
||||
where
|
||||
C: Clone,
|
||||
{
|
||||
NyxdClient {
|
||||
client: self.client.clone_query_client(),
|
||||
config: self.config.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_config(&self) -> &Config {
|
||||
&self.config
|
||||
}
|
||||
@@ -304,12 +286,12 @@ impl<C, S> NyxdClient<C, S> {
|
||||
self.config.contracts.multisig_contract_address = Some(address);
|
||||
}
|
||||
|
||||
pub fn set_simulated_gas_multiplier(&mut self, multiplier: f32) {
|
||||
self.config.simulated_gas_multiplier = multiplier;
|
||||
pub fn set_node_families_contract_address(&mut self, address: AccountId) {
|
||||
self.config.contracts.node_families_contract_address = Some(address);
|
||||
}
|
||||
|
||||
pub fn get_nym_contracts(&self) -> TypedNymContracts {
|
||||
self.config.contracts.clone()
|
||||
pub fn set_simulated_gas_multiplier(&mut self, multiplier: f32) {
|
||||
self.config.simulated_gas_multiplier = multiplier;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,10 +307,11 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
|
||||
fn performance_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config.contracts.performance_contract_address.as_ref()
|
||||
}
|
||||
fn network_monitors_contract_address(&self) -> Option<&AccountId> {
|
||||
|
||||
fn node_families_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config
|
||||
.contracts
|
||||
.network_monitors_contract_address
|
||||
.node_families_contract_address
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ pub struct Args {
|
||||
#[clap(long)]
|
||||
pub vesting_contract_address: Option<AccountId>,
|
||||
|
||||
#[clap(long)]
|
||||
pub node_families_contract_address: Option<AccountId>,
|
||||
|
||||
#[clap(long)]
|
||||
pub rewarding_denom: Option<String>,
|
||||
|
||||
@@ -130,6 +133,14 @@ pub async fn generate(args: Args) {
|
||||
.expect("Failed converting vesting contract address to AccountId")
|
||||
});
|
||||
|
||||
let node_families_contract_address = args.node_families_contract_address.unwrap_or_else(|| {
|
||||
let address =
|
||||
std::env::var(nym_network_defaults::var_names::NODE_FAMILIES_CONTRACT_ADDRESS)
|
||||
.expect("node families contract address has to be set");
|
||||
AccountId::from_str(address.as_str())
|
||||
.expect("Failed converting node families contract address to AccountId")
|
||||
});
|
||||
|
||||
let rewarding_denom = args.rewarding_denom.unwrap_or_else(|| {
|
||||
std::env::var(nym_network_defaults::var_names::MIX_DENOM)
|
||||
.expect("Rewarding (mix) denom has to be set")
|
||||
@@ -142,6 +153,7 @@ pub async fn generate(args: Args) {
|
||||
let instantiate_msg = InstantiateMsg {
|
||||
rewarding_validator_address: rewarding_validator_address.to_string(),
|
||||
vesting_contract_address: vesting_contract_address.to_string(),
|
||||
node_families_contract_address: node_families_contract_address.to_string(),
|
||||
rewarding_denom,
|
||||
epochs_in_interval: args.epochs_in_interval,
|
||||
epoch_duration: Duration::from_secs(args.epoch_duration),
|
||||
|
||||
@@ -26,6 +26,14 @@ pub trait ContractOpts {
|
||||
|
||||
fn addr_make(&self, input: &str) -> Addr;
|
||||
|
||||
fn make_sender_with_funds(&self, input: &str, funds: &[Coin]) -> MessageInfo {
|
||||
message_info(&self.addr_make(input), funds)
|
||||
}
|
||||
|
||||
fn make_sender(&self, input: &str) -> MessageInfo {
|
||||
self.make_sender_with_funds(input, &[])
|
||||
}
|
||||
|
||||
fn deps_mut_env(&mut self) -> (DepsMut<'_>, Env) {
|
||||
let env = self.env().clone();
|
||||
(self.deps_mut(), env)
|
||||
|
||||
@@ -3,12 +3,121 @@
|
||||
|
||||
use crate::error::MixnetContractError;
|
||||
use crate::mixnode::PendingMixNodeChanges;
|
||||
use crate::nym_node::NodeOwnershipResponse;
|
||||
use crate::{
|
||||
EpochEventId, IntervalEventId, MixNodeBond, MixNodeDetails, NodeId, NodeRewarding, NymNodeBond,
|
||||
NymNodeDetails, PendingNodeChanges,
|
||||
EpochEventId, EpochId, Interval, IntervalEventId, MixNodeBond, MixNodeDetails, NodeId,
|
||||
NodeRewarding, NymNodeBond, NymNodeDetails, PendingNodeChanges, QueryMsg,
|
||||
};
|
||||
use cosmwasm_std::{Coin, Decimal, StdError, StdResult, Uint128};
|
||||
use cosmwasm_std::{
|
||||
Addr, Binary, Coin, CustomQuery, Decimal, QuerierWrapper, StdError, StdResult, Uint128,
|
||||
from_json,
|
||||
};
|
||||
use cw_storage_plus::{Key, Namespace, Path, PrimaryKey};
|
||||
use nym_contracts_common::IdentityKeyRef;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::ops::Deref;
|
||||
|
||||
pub trait MixnetContractQuerier {
|
||||
#[allow(dead_code)]
|
||||
fn query_mixnet_contract<T: DeserializeOwned>(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
msg: &QueryMsg,
|
||||
) -> StdResult<T>;
|
||||
|
||||
fn query_mixnet_contract_storage(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl Into<Binary>,
|
||||
) -> StdResult<Option<Vec<u8>>>;
|
||||
|
||||
fn query_mixnet_contract_storage_value<T: DeserializeOwned>(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl Into<Binary>,
|
||||
) -> StdResult<Option<T>> {
|
||||
match self.query_mixnet_contract_storage(address, key)? {
|
||||
None => Ok(None),
|
||||
Some(value) => Ok(Some(from_json(&value)?)),
|
||||
}
|
||||
}
|
||||
|
||||
fn query_current_mixnet_interval(&self, address: impl Into<String>) -> StdResult<Interval> {
|
||||
self.query_mixnet_contract_storage_value(address, b"ci")?
|
||||
.ok_or(StdError::not_found(
|
||||
"unable to retrieve interval information from the mixnet contract storage",
|
||||
))
|
||||
}
|
||||
|
||||
fn query_current_absolute_mixnet_epoch_id(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
) -> StdResult<EpochId> {
|
||||
self.query_current_mixnet_interval(address)
|
||||
.map(|interval| interval.current_epoch_absolute_id())
|
||||
}
|
||||
|
||||
fn check_node_existence(&self, address: impl Into<String>, node_id: NodeId) -> StdResult<bool> {
|
||||
let mixnet_contract_address = address.into();
|
||||
|
||||
if let Some(nym_node) = self.query_nymnode_bond(mixnet_contract_address.clone(), node_id)? {
|
||||
return Ok(!nym_node.is_unbonding);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn query_nymnode_bond(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
node_id: NodeId,
|
||||
) -> StdResult<Option<NymNodeBond>> {
|
||||
// construct proper map key
|
||||
let pk_namespace = "nn";
|
||||
let path: Path<NymNodeBond> = Path::new(
|
||||
Namespace::from_static_str(pk_namespace).as_slice(),
|
||||
&node_id.key().iter().map(Key::as_ref).collect::<Vec<_>>(),
|
||||
);
|
||||
let storage_key = path.deref();
|
||||
|
||||
self.query_mixnet_contract_storage_value(address, storage_key)
|
||||
}
|
||||
|
||||
fn query_nymnode_ownership(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
owner: &Addr,
|
||||
) -> StdResult<Option<NymNodeBond>> {
|
||||
let resp: NodeOwnershipResponse = self.query_mixnet_contract(
|
||||
address,
|
||||
&QueryMsg::GetOwnedNymNode {
|
||||
address: owner.to_string(),
|
||||
},
|
||||
)?;
|
||||
Ok(resp.details.map(|d| d.bond_information))
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> MixnetContractQuerier for QuerierWrapper<'_, C>
|
||||
where
|
||||
C: CustomQuery,
|
||||
{
|
||||
fn query_mixnet_contract<T: DeserializeOwned>(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
msg: &QueryMsg,
|
||||
) -> StdResult<T> {
|
||||
self.query_wasm_smart(address, msg)
|
||||
}
|
||||
|
||||
fn query_mixnet_contract_storage(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl Into<Binary>,
|
||||
) -> StdResult<Option<Vec<u8>>> {
|
||||
self.query_wasm_raw(address, key)
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option<Decimal>) {
|
||||
|
||||
@@ -30,6 +30,7 @@ pub use gateway::{
|
||||
Gateway, GatewayBond, GatewayBondResponse, GatewayConfigUpdate, GatewayOwnershipResponse,
|
||||
PagedGatewayResponse,
|
||||
};
|
||||
pub use helpers::MixnetContractQuerier;
|
||||
pub use interval::{
|
||||
CurrentIntervalResponse, EpochId, EpochState, EpochStatus, Interval, IntervalId,
|
||||
};
|
||||
|
||||
@@ -190,6 +190,10 @@ impl NodeRewarding {
|
||||
truncate_reward(self.operator, denom)
|
||||
}
|
||||
|
||||
pub fn delegations_with_reward(&self, denom: impl Into<String>) -> Coin {
|
||||
truncate_reward(self.delegates, denom)
|
||||
}
|
||||
|
||||
pub fn pending_delegator_reward(&self, delegation: &Delegation) -> StdResult<Coin> {
|
||||
let delegator_reward = self.determine_delegation_reward(delegation)?;
|
||||
Ok(truncate_reward(delegator_reward, &delegation.amount.denom))
|
||||
|
||||
@@ -63,6 +63,7 @@ use nym_contracts_common::{ContractBuildInformation, signing::Nonce};
|
||||
pub struct InstantiateMsg {
|
||||
pub rewarding_validator_address: String,
|
||||
pub vesting_contract_address: String,
|
||||
pub node_families_contract_address: String,
|
||||
|
||||
pub rewarding_denom: String,
|
||||
pub epochs_in_interval: u32,
|
||||
@@ -305,6 +306,22 @@ pub enum ExecuteMsg {
|
||||
MigrateVestedDelegation {
|
||||
mix_id: NodeId,
|
||||
},
|
||||
/// Admin-only: forcibly migrate the vested mixnode owned by `owner`.
|
||||
/// Used to drain the last vested entries so the mixnet contract can drop its dependency on the vesting contract.
|
||||
AdminMigrateVestedMixNode {
|
||||
owner: String,
|
||||
},
|
||||
/// Admin-only: forcibly migrate the vested delegation `(mix_id, owner)`.
|
||||
/// Used to drain the last vested entries so the mixnet contract can drop its dependency on the vesting contract.
|
||||
AdminMigrateVestedDelegation {
|
||||
mix_id: NodeId,
|
||||
owner: String,
|
||||
},
|
||||
/// Admin-only: batch variant of [`ExecuteMsg::AdminMigrateVestedDelegation`].
|
||||
/// Reverts the entire batch on the first error, so callers should treat it as all-or-nothing.
|
||||
AdminBatchMigrateVestedDelegations {
|
||||
entries: Vec<VestedDelegationMigrationEntry>,
|
||||
},
|
||||
|
||||
// testing-only
|
||||
#[cfg(feature = "contract-testing")]
|
||||
@@ -394,6 +411,15 @@ impl ExecuteMsg {
|
||||
}
|
||||
ExecuteMsg::MigrateVestedMixNode { .. } => "migrate vested mixnode".into(),
|
||||
ExecuteMsg::MigrateVestedDelegation { .. } => "migrate vested delegation".to_string(),
|
||||
ExecuteMsg::AdminMigrateVestedMixNode { owner } => {
|
||||
format!("admin migrating vested mixnode of {owner}")
|
||||
}
|
||||
ExecuteMsg::AdminMigrateVestedDelegation { mix_id, owner } => {
|
||||
format!("admin migrating vested delegation of {owner} on mixnode {mix_id}")
|
||||
}
|
||||
ExecuteMsg::AdminBatchMigrateVestedDelegations { entries } => {
|
||||
format!("admin batch migrating {} vested delegations", entries.len())
|
||||
}
|
||||
ExecuteMsg::AssignRoles { .. } => "assigning epoch roles".into(),
|
||||
ExecuteMsg::MigrateMixnode { .. } => "migrating legacy mixnode".into(),
|
||||
ExecuteMsg::MigrateGateway { .. } => "migrating legacy gateway".into(),
|
||||
@@ -881,8 +907,15 @@ pub enum QueryMsg {
|
||||
GetKeyRotationId {},
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct VestedDelegationMigrationEntry {
|
||||
pub mix_id: NodeId,
|
||||
pub owner: String,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct MigrateMsg {
|
||||
pub unsafe_skip_state_updates: Option<bool>,
|
||||
pub vesting_contract_address: Option<String>,
|
||||
pub node_families_contract_address: String,
|
||||
}
|
||||
|
||||
@@ -212,6 +212,10 @@ pub struct ContractState {
|
||||
/// track-related messages.
|
||||
pub vesting_contract_address: Addr,
|
||||
|
||||
/// Address of the node families contract. It is called whenever nym-node unbonds
|
||||
/// so that it could be removed from any family it belongs to.
|
||||
pub node_families_contract_address: Addr,
|
||||
|
||||
/// The expected denom used for rewarding (and realistically any other operation).
|
||||
/// Default: `unym`
|
||||
pub rewarding_denom: String,
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright 2026 - 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 AUTHORISED_ORCHESTRATORS: &str = "authorised-orchestrators";
|
||||
pub const AUTHORISED_NETWORK_MONITORS: &str = "authorised-network-monitors";
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::Addr;
|
||||
use cw_controllers::AdminError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum NetworkMonitorsContractError {
|
||||
#[error("could not perform contract migration: {comment}")]
|
||||
FailedMigration { comment: String },
|
||||
|
||||
#[error(transparent)]
|
||||
Admin(#[from] AdminError),
|
||||
|
||||
#[error("unauthorised")]
|
||||
Unauthorized,
|
||||
|
||||
#[error("address {addr} is not an authorised orchestrator")]
|
||||
NotAnOrchestrator { addr: Addr },
|
||||
|
||||
#[error("Failed to recover x25519 public key from its base58 representation: {0}")]
|
||||
MalformedX25519AgentNoiseKey(String),
|
||||
|
||||
#[error("Failed to recover ed25519 public key from its base58 representation: {0}")]
|
||||
MalformedEd25519OrchestratorIdentityKey(String),
|
||||
|
||||
#[error(transparent)]
|
||||
StdErr(#[from] cosmwasm_std::StdError),
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod constants;
|
||||
pub mod error;
|
||||
pub mod msg;
|
||||
pub mod types;
|
||||
|
||||
pub use error::*;
|
||||
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
pub use types::*;
|
||||
@@ -1,78 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
use crate::{
|
||||
AuthorisedNetworkMonitorOrchestratorsResponse, AuthorisedNetworkMonitorsPagedResponse,
|
||||
};
|
||||
|
||||
#[cw_serde]
|
||||
pub struct InstantiateMsg {
|
||||
/// Address of the initial network monitor orchestrator.
|
||||
pub orchestrator_address: String,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub enum ExecuteMsg {
|
||||
/// Change the admin
|
||||
UpdateAdmin { admin: String },
|
||||
|
||||
/// Authorise new network monitor orchestrator
|
||||
AuthoriseNetworkMonitorOrchestrator { address: String },
|
||||
|
||||
/// Attempt to update the announced identity key of this orchestrator
|
||||
UpdateOrchestratorIdentityKey { key: String },
|
||||
|
||||
/// Revoke network monitor orchestrator authorisation.
|
||||
RevokeNetworkMonitorOrchestrator { address: String },
|
||||
|
||||
/// Authorise new network monitor (or renew authorisation)
|
||||
/// granting additional privileges when sending mixnet packets to Nym nodes.
|
||||
AuthoriseNetworkMonitor {
|
||||
/// Mixnet address of the agent.
|
||||
/// The underlying ip address is going to be used as ingress to the nodes,
|
||||
/// and the full socket address announces the egress and the association with the noise key
|
||||
mixnet_address: SocketAddr,
|
||||
|
||||
/// Base-58 encoded noise key of the agent.
|
||||
bs58_x25519_noise: String,
|
||||
|
||||
/// Version of the noise protocol used by the agent.
|
||||
noise_version: u8,
|
||||
},
|
||||
|
||||
/// Revoke network monitor authorisation.
|
||||
RevokeNetworkMonitor { address: SocketAddr },
|
||||
|
||||
/// Revoke all network monitor authorisations.
|
||||
RevokeAllNetworkMonitors,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
|
||||
pub enum QueryMsg {
|
||||
#[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))]
|
||||
Admin {},
|
||||
|
||||
// no need for pagination as we don't expect even a double digit of those
|
||||
#[cfg_attr(
|
||||
feature = "schema",
|
||||
returns(AuthorisedNetworkMonitorOrchestratorsResponse)
|
||||
)]
|
||||
NetworkMonitorOrchestrators {},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(AuthorisedNetworkMonitorsPagedResponse))]
|
||||
NetworkMonitorAgents {
|
||||
/// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.
|
||||
start_next_after: Option<SocketAddr>,
|
||||
|
||||
/// 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>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct MigrateMsg {}
|
||||
@@ -1,53 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{Addr, Timestamp};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
pub type OrchestratorAddress = Addr;
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AuthorisedNetworkMonitorOrchestrator {
|
||||
/// The address associated with the network monitor orchestrator.
|
||||
pub address: Addr,
|
||||
|
||||
/// Base-58 encoded identity key of the orchestrator, announced by the orchestrator itself
|
||||
/// on startup.
|
||||
pub identity_key: Option<String>,
|
||||
|
||||
/// Timestamp of when the network monitor was authorised.
|
||||
pub authorised_at: Timestamp,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AuthorisedNetworkMonitor {
|
||||
/// Mixnet address of the agent.
|
||||
/// The underlying ip address is going to be used as ingress to the nodes,
|
||||
/// and the full socket address announces the egress and the association with the noise key
|
||||
pub mixnet_address: SocketAddr,
|
||||
|
||||
/// The address of the orchestrator that authorised the network monitor agent.
|
||||
pub authorised_by: OrchestratorAddress,
|
||||
|
||||
/// Timestamp of when the network monitor was authorised.
|
||||
pub authorised_at: Timestamp,
|
||||
|
||||
/// Base-58 encoded noise key of the agent.
|
||||
pub bs58_x25519_noise: String,
|
||||
|
||||
/// Version of the noise protocol used by the agent.
|
||||
pub noise_version: u8,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AuthorisedNetworkMonitorOrchestratorsResponse {
|
||||
pub authorised: Vec<AuthorisedNetworkMonitorOrchestrator>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AuthorisedNetworkMonitorsPagedResponse {
|
||||
pub authorised: Vec<AuthorisedNetworkMonitor>,
|
||||
|
||||
pub start_next_after: Option<SocketAddr>,
|
||||
}
|
||||
+11
-5
@@ -1,14 +1,16 @@
|
||||
[package]
|
||||
name = "nym-network-monitors-contract-common"
|
||||
description = "Common library for the Nym Network Monitors contract"
|
||||
name = "nym-node-families-contract-common"
|
||||
description = "Common crate for Nym's node families contract"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version = "1.85"
|
||||
readme.workspace = true
|
||||
version.workspace = true
|
||||
publish = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
@@ -18,6 +20,10 @@ schemars = { workspace = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
cosmwasm-schema = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
cw-utils = { workspace = true }
|
||||
|
||||
nym-contracts-common = { workspace = true }
|
||||
nym-mixnet-contract-common = { workspace = true }
|
||||
|
||||
[features]
|
||||
schema = []
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
/// Storage key constants used by the node families contract.
|
||||
///
|
||||
/// They are kept in the common crate so that off-chain tooling (indexers, migration
|
||||
/// scripts) can reference them without depending on the contract crate itself.
|
||||
/// Changing any of these values is a breaking change for already-deployed contracts.
|
||||
pub mod storage_keys {
|
||||
/// `Item<Addr>`: address of the mixnet contract used to validate node existence.
|
||||
pub const MIXNET_CONTRACT_ADDRESS: &str = "mixnet-contract-address";
|
||||
|
||||
/// `Item<Config>`: runtime configuration (fees, length limits) set at instantiation.
|
||||
pub const CONFIG: &str = "config";
|
||||
|
||||
/// `Admin` (cw-controllers): admin allowed to perform privileged operations.
|
||||
pub const CONTRACT_ADMIN: &str = "contract-admin";
|
||||
/// `Item<NodeFamilyId>`: monotonically increasing id counter for new families.
|
||||
pub const NODE_FAMILY_ID_COUNTER: &str = "node-family-id-counter";
|
||||
/// Primary namespace for the current family-members `IndexedMap`,
|
||||
/// keyed by `NodeId` with value [`crate::FamilyMembership`].
|
||||
pub const NODE_FAMILY_MEMBERS: &str = "node-family-members";
|
||||
/// Multi-index over current family members keyed by family id —
|
||||
/// enables paginated listing of all nodes in a given family.
|
||||
pub const NODE_FAMILY_MEMBERS_FAMILY_IDX_NAMESPACE: &str = "node-family-members__family";
|
||||
|
||||
/// Primary namespace for the families `IndexedMap`.
|
||||
pub const FAMILIES_NAMESPACE: &str = "families";
|
||||
/// Secondary unique index keyed by `owner` (one family per owner).
|
||||
pub const FAMILIES_OWNER_IDX_NAMESPACE: &str = "families__owner";
|
||||
/// Secondary unique index keyed by `name` (family names are globally unique).
|
||||
pub const FAMILIES_NAME_IDX_NAMESPACE: &str = "families__name";
|
||||
|
||||
/// Primary namespace for the pending invitations `IndexedMap`.
|
||||
pub const INVITATIONS_NAMESPACE: &str = "invitations";
|
||||
/// Multi-index over pending invitations keyed by family id.
|
||||
pub const INVITATIONS_FAMILY_IDX_NAMESPACE: &str = "invitations__family";
|
||||
/// Multi-index over pending invitations keyed by node id
|
||||
/// (a node can be invited to multiple families simultaneously).
|
||||
pub const INVITATIONS_NODE_IDX_NAMESPACE: &str = "invitations__node";
|
||||
|
||||
/// Primary namespace for the archived (accepted/rejected/revoked) invitations `IndexedMap`.
|
||||
pub const PAST_INVITATIONS_NAMESPACE: &str = "past-invitations";
|
||||
/// Multi-index over past invitations keyed by family id.
|
||||
pub const PAST_INVITATIONS_FAMILY_IDX_NAMESPACE: &str = "past-invitations__family";
|
||||
/// Multi-index over past invitations keyed by node id.
|
||||
pub const PAST_INVITATIONS_NODE_IDX_NAMESPACE: &str = "past-invitations__node";
|
||||
/// `Map<(NodeFamilyId, NodeId), u64>`: per-`(family, node)` counter used to
|
||||
/// disambiguate repeat archive entries (a node can be invited and have the
|
||||
/// invitation reach a terminal state more than once).
|
||||
pub const PAST_INVITATIONS_COUNTER_NAMESPACE: &str = "past-invitations-counter";
|
||||
|
||||
/// Primary namespace for the past-members `IndexedMap`.
|
||||
pub const PAST_FAMILY_MEMBER_NAMESPACE: &str = "past-family-member";
|
||||
/// Multi-index over past members keyed by family id.
|
||||
pub const PAST_FAMILY_MEMBER_FAMILY_IDX_NAMESPACE: &str = "past-family-member__family";
|
||||
/// Multi-index over past members keyed by node id.
|
||||
pub const PAST_FAMILY_MEMBER_NODE_IDX_NAMESPACE: &str = "past-family-member__node";
|
||||
/// `Map<(NodeFamilyId, NodeId), u64>`: per-`(family, node)` counter used to
|
||||
/// disambiguate repeat past-membership entries (a node can join and leave
|
||||
/// the same family more than once).
|
||||
pub const PAST_FAMILY_MEMBER_COUNTER_NAMESPACE: &str = "past-family-member-counter";
|
||||
}
|
||||
|
||||
pub mod events {
|
||||
pub const FAMILY_CREATION_EVENT_NAME: &str = "family_creation";
|
||||
pub const FAMILY_CREATION_EVENT_FAMILY_NAME: &str = "family_name";
|
||||
pub const FAMILY_CREATION_EVENT_OWNER_ADDRESS: &str = "owner_address";
|
||||
pub const FAMILY_CREATION_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_CREATION_EVENT_PAID_FEE: &str = "paid_fee";
|
||||
|
||||
pub const FAMILY_DISBAND_EVENT_NAME: &str = "family_disband";
|
||||
pub const FAMILY_DISBAND_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_DISBAND_EVENT_OWNER_ADDRESS: &str = "owner_address";
|
||||
pub const FAMILY_DISBAND_EVENT_REFUNDED_FEE: &str = "refunded_fee";
|
||||
|
||||
pub const FAMILY_INVITATION_EVENT_NAME: &str = "family_invitation";
|
||||
pub const FAMILY_INVITATION_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_INVITATION_EVENT_NODE_ID: &str = "node_id";
|
||||
pub const FAMILY_INVITATION_EVENT_EXPIRES_AT: &str = "expires_at";
|
||||
|
||||
pub const FAMILY_INVITATION_REVOKED_EVENT_NAME: &str = "family_invitation_revoked";
|
||||
pub const FAMILY_INVITATION_REVOKED_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_INVITATION_REVOKED_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const FAMILY_INVITATION_ACCEPTED_EVENT_NAME: &str = "family_invitation_accepted";
|
||||
pub const FAMILY_INVITATION_ACCEPTED_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_INVITATION_ACCEPTED_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const FAMILY_INVITATION_REJECTED_EVENT_NAME: &str = "family_invitation_rejected";
|
||||
pub const FAMILY_INVITATION_REJECTED_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_INVITATION_REJECTED_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const FAMILY_MEMBER_LEFT_EVENT_NAME: &str = "family_member_left";
|
||||
pub const FAMILY_MEMBER_LEFT_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_MEMBER_LEFT_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const FAMILY_MEMBER_KICKED_EVENT_NAME: &str = "family_member_kicked";
|
||||
pub const FAMILY_MEMBER_KICKED_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_MEMBER_KICKED_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const NODE_UNBOND_CLEANUP_EVENT_NAME: &str = "family_node_unbond_cleanup";
|
||||
pub const NODE_UNBOND_CLEANUP_EVENT_NODE_ID: &str = "node_id";
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::NodeFamilyId;
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use cw_controllers::AdminError;
|
||||
use cw_utils::PaymentError;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors returned from any entry point of the node families contract.
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum NodeFamiliesContractError {
|
||||
/// Returned from `migrate` when the on-chain state cannot be brought forward
|
||||
/// to the current contract version (e.g. unsupported source version, malformed
|
||||
/// stored data).
|
||||
#[error("could not perform contract migration: {comment}")]
|
||||
FailedMigration { comment: String },
|
||||
|
||||
/// The referenced family does not exist (or no longer exists).
|
||||
#[error("family with id {family_id} does not exist")]
|
||||
FamilyNotFound { family_id: NodeFamilyId },
|
||||
|
||||
/// Disbanding was requested on a family that still has members.
|
||||
#[error("family {family_id} cannot be disbanded: it still has {members} member(s)")]
|
||||
FamilyNotEmpty {
|
||||
family_id: NodeFamilyId,
|
||||
members: u64,
|
||||
},
|
||||
|
||||
/// The given node is not currently a member of any family.
|
||||
#[error("node {node_id} is not currently a member of any family")]
|
||||
NodeNotInFamily { node_id: NodeId },
|
||||
|
||||
/// The given node is a member of a different family than the one the
|
||||
/// caller is acting on. Distinct from [`NodeNotInFamily`] (which means the
|
||||
/// node has no membership at all) — surfaces when, e.g., a family owner
|
||||
/// tries to kick a node that belongs to someone else's family.
|
||||
#[error("node {node_id} is not a member of family {family_id}")]
|
||||
NodeNotMemberOfFamily {
|
||||
node_id: NodeId,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// A cross-contract callback (e.g. `OnNymNodeUnbond`) was received from a
|
||||
/// sender that is not the configured mixnet contract address.
|
||||
#[error("address {sender} is not authorised to invoke the mixnet-contract callback")]
|
||||
UnauthorisedMixnetCallback { sender: Addr },
|
||||
|
||||
/// No pending invitation exists for the given `(family, node)` pair.
|
||||
#[error("no pending invitation for node {node_id} from family {family_id}")]
|
||||
InvitationNotFound {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// A pending invitation for the given `(family, node)` pair already exists;
|
||||
/// issuing a new one would silently overwrite it.
|
||||
#[error("a pending invitation for node {node_id} from family {family_id} already exists")]
|
||||
PendingInvitationAlreadyExists {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// The invitation exists but its `expires_at` is at or before the current
|
||||
/// block time, so it can no longer be acted on.
|
||||
#[error(
|
||||
"invitation for node {node_id} from family {family_id} expired at {expires_at} (now: {now})"
|
||||
)]
|
||||
InvitationExpired {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
expires_at: u64,
|
||||
now: u64,
|
||||
},
|
||||
|
||||
/// The funds attached to a paid execution failed `cw_utils` payment
|
||||
/// validation (no funds, wrong/extra denom).
|
||||
#[error("invalid fee provided: {0}")]
|
||||
InvalidDeposit(#[from] PaymentError),
|
||||
|
||||
/// The funds attached to a `CreateFamily` execution don't match the
|
||||
/// configured `create_family_fee`.
|
||||
#[error("expected exactly {expected} as family creation fee; received {received:?}")]
|
||||
InvalidFamilyCreationFee { expected: Coin, received: Vec<Coin> },
|
||||
|
||||
/// The submitted family name normalised to the empty string (i.e. it
|
||||
/// contained no ASCII alphanumeric characters).
|
||||
#[error("family name cannot be empty after normalisation")]
|
||||
EmptyFamilyName,
|
||||
|
||||
/// The submitted family name exceeds the configured length limit.
|
||||
#[error("family name length {length} exceeds the configured limit of {limit}")]
|
||||
FamilyNameTooLong { length: usize, limit: usize },
|
||||
|
||||
/// The submitted family description exceeds the configured length limit.
|
||||
#[error("family description length {length} exceeds the configured limit of {limit}")]
|
||||
FamilyDescriptionTooLong { length: usize, limit: usize },
|
||||
|
||||
/// The transaction sender already owns a family.
|
||||
#[error("address {address} already owns family {family_id}")]
|
||||
SenderAlreadyOwnsAFamily {
|
||||
address: Addr,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// The transaction sender does not currently own any family - emitted by
|
||||
/// owner-gated operations like `disband_family` when the sender has
|
||||
/// nothing to act on.
|
||||
#[error("address {address} does not currently own any family")]
|
||||
SenderDoesntOwnAFamily { address: Addr },
|
||||
|
||||
/// The transaction sender is not the controller of the bonded node
|
||||
/// referenced by the message. Covers all of: sender controls no bonded
|
||||
/// node, sender controls a different node id, and sender's node has
|
||||
/// entered the unbonding state.
|
||||
#[error("address {address} is not the controller of bonded node {node_id}")]
|
||||
SenderDoesntControlNode { address: Addr, node_id: NodeId },
|
||||
|
||||
/// A family with the requested (normalised) name already exists.
|
||||
#[error("a family with name {name:?} already exists (id {family_id})")]
|
||||
FamilyNameAlreadyTaken {
|
||||
name: String,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// A node controlled by the address is currently a member of a family,
|
||||
/// so the address cannot also become a family owner or join another family.
|
||||
#[error("address {address} controls node {node_id} which is currently in family {family_id}")]
|
||||
AlreadyInFamily {
|
||||
address: Addr,
|
||||
node_id: NodeId,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// The node referenced by an invitation does not exist as a bonded node
|
||||
/// in the mixnet contract (or has already unbonded).
|
||||
#[error("node {node_id} is not a bonded node in the mixnet contract")]
|
||||
NodeDoesntExist { node_id: NodeId },
|
||||
|
||||
/// The node referenced by an invitation is already a member of a family,
|
||||
/// so it cannot be invited to another one until it leaves / is removed.
|
||||
#[error("node {node_id} is already a member of family {family_id}")]
|
||||
NodeAlreadyInFamily {
|
||||
node_id: NodeId,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// The sender supplied a `validity_secs` of `0` for an invitation, which
|
||||
/// would create one that is already expired at the moment it is stored.
|
||||
#[error("invitation validity must be strictly positive")]
|
||||
ZeroInvitationValidity,
|
||||
|
||||
/// Wraps errors raised by `cw-controllers::Admin` (e.g. caller is not admin).
|
||||
#[error(transparent)]
|
||||
Admin(#[from] AdminError),
|
||||
|
||||
/// Wraps any underlying `cosmwasm_std::StdError` (storage, serialization, etc.).
|
||||
#[error(transparent)]
|
||||
StdErr(#[from] cosmwasm_std::StdError),
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Common types, messages, errors and storage-key constants shared between the
|
||||
//! node families contract and any off-chain client.
|
||||
//!
|
||||
//! Keeping these in a separate crate allows clients to depend on the contract's
|
||||
//! public surface without pulling in `cw-storage-plus` and other on-chain-only
|
||||
//! dependencies.
|
||||
|
||||
/// Storage-key string constants. See [`constants::storage_keys`].
|
||||
pub mod constants;
|
||||
/// Contract-level error type.
|
||||
pub mod error;
|
||||
/// `InstantiateMsg`, `ExecuteMsg`, `QueryMsg`, `MigrateMsg` definitions.
|
||||
pub mod msg;
|
||||
/// Domain types stored in / returned by the contract.
|
||||
pub mod types;
|
||||
|
||||
pub use error::*;
|
||||
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
pub use types::*;
|
||||
@@ -0,0 +1,211 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::{
|
||||
Config, GlobalPastFamilyInvitationCursor, NodeFamilyId, PastFamilyInvitationCursor,
|
||||
PastFamilyInvitationForNodeCursor, PastFamilyMemberCursor, PastFamilyMemberForNodeCursor,
|
||||
};
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
use crate::{
|
||||
AllFamilyMembersPagedResponse, AllPastFamilyInvitationsPagedResponse, FamiliesPagedResponse,
|
||||
FamilyMembersPagedResponse, NodeFamilyByNameResponse, NodeFamilyByOwnerResponse,
|
||||
NodeFamilyMembershipResponse, NodeFamilyResponse, PastFamilyInvitationsForNodePagedResponse,
|
||||
PastFamilyInvitationsPagedResponse, PastFamilyMembersForNodePagedResponse,
|
||||
PastFamilyMembersPagedResponse, PendingFamilyInvitationResponse,
|
||||
PendingFamilyInvitationsPagedResponse, PendingInvitationsForNodePagedResponse,
|
||||
PendingInvitationsPagedResponse,
|
||||
};
|
||||
|
||||
/// Message used to instantiate the node families contract.
|
||||
#[cw_serde]
|
||||
pub struct InstantiateMsg {
|
||||
pub config: Config,
|
||||
|
||||
pub mixnet_contract_address: String,
|
||||
}
|
||||
|
||||
/// Execute messages accepted by the contract.
|
||||
#[cw_serde]
|
||||
pub enum ExecuteMsg {
|
||||
/// Replace the contract's runtime [`Config`]. Restricted to the contract
|
||||
/// admin.
|
||||
UpdateConfig { config: Config },
|
||||
|
||||
/// Create a new family owned by the message sender. The configured
|
||||
/// `create_family_fee` must be attached as funds.
|
||||
CreateFamily { name: String, description: String },
|
||||
|
||||
/// Disband the family owned by the message sender. The family must have
|
||||
/// no current members; any still-pending invitations are revoked.
|
||||
DisbandFamily {},
|
||||
|
||||
/// Invite a node to the family owned by the message sender. If
|
||||
/// `validity_secs` is omitted the invitation expires
|
||||
/// `default_invitation_validity_secs` seconds (from [`Config`]) after the
|
||||
/// current block time.
|
||||
InviteToFamily {
|
||||
node_id: NodeId,
|
||||
validity_secs: Option<u64>,
|
||||
},
|
||||
|
||||
/// Revoke a still-pending invitation previously issued by the sender's
|
||||
/// family.
|
||||
RevokeFamilyInvitation { node_id: NodeId },
|
||||
|
||||
/// Accept a pending invitation. The sender must control `node_id`.
|
||||
AcceptFamilyInvitation {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// Reject a pending invitation. The sender must control `node_id`.
|
||||
RejectFamilyInvitation {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// Leave the family `node_id` currently belongs to. The sender must
|
||||
/// control `node_id`.
|
||||
LeaveFamily { node_id: NodeId },
|
||||
|
||||
/// Remove `node_id` from the family owned by the message sender.
|
||||
KickFromFamily { node_id: NodeId },
|
||||
|
||||
/// Cross-contract callback fired by the mixnet contract the moment
|
||||
/// node with `node_id` initiates unbonding.
|
||||
/// Removes the node from any family it currently
|
||||
/// belongs to and rejects every pending invitation issued to it.
|
||||
/// Sender must be the configured mixnet contract address.
|
||||
OnNymNodeUnbond { node_id: NodeId },
|
||||
}
|
||||
|
||||
/// Query messages accepted by the contract.
|
||||
#[cw_serde]
|
||||
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
|
||||
pub enum QueryMsg {
|
||||
/// Look up a single family by its id.
|
||||
#[cfg_attr(feature = "schema", returns(NodeFamilyResponse))]
|
||||
GetFamilyById { family_id: NodeFamilyId },
|
||||
|
||||
/// Look up the (at most one) family owned by a given address.
|
||||
#[cfg_attr(feature = "schema", returns(NodeFamilyByOwnerResponse))]
|
||||
GetFamilyByOwner { owner: String },
|
||||
|
||||
/// Look up a single family by its name. The lookup is normalised
|
||||
/// contract-side (lowercased, non-alphanumerics stripped), so equivalent
|
||||
/// inputs resolve to the same family.
|
||||
#[cfg_attr(feature = "schema", returns(NodeFamilyByNameResponse))]
|
||||
GetFamilyByName { name: String },
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(FamiliesPagedResponse))]
|
||||
GetFamiliesPaged {
|
||||
start_after: Option<NodeFamilyId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Look up which family — if any — a node currently belongs to.
|
||||
#[cfg_attr(feature = "schema", returns(NodeFamilyMembershipResponse))]
|
||||
GetFamilyMembership { node_id: NodeId },
|
||||
|
||||
/// Page through every node currently in a given family.
|
||||
#[cfg_attr(feature = "schema", returns(FamilyMembersPagedResponse))]
|
||||
GetFamilyMembersPaged {
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every current family member across all families, in
|
||||
/// ascending [`NodeId`] order. Each entry carries the membership record
|
||||
/// (which in turn names the family the node belongs to).
|
||||
#[cfg_attr(feature = "schema", returns(AllFamilyMembersPagedResponse))]
|
||||
GetAllFamilyMembersPaged {
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Look up the pending invitation for a specific `(family_id, node_id)`
|
||||
/// pair.
|
||||
#[cfg_attr(feature = "schema", returns(PendingFamilyInvitationResponse))]
|
||||
GetPendingInvitation {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// Page through every pending invitation issued by a given family.
|
||||
#[cfg_attr(feature = "schema", returns(PendingFamilyInvitationsPagedResponse))]
|
||||
GetPendingInvitationsForFamilyPaged {
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every pending invitation issued for a given node.
|
||||
#[cfg_attr(feature = "schema", returns(PendingInvitationsForNodePagedResponse))]
|
||||
GetPendingInvitationsForNodePaged {
|
||||
node_id: NodeId,
|
||||
start_after: Option<NodeFamilyId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every pending invitation across all families.
|
||||
#[cfg_attr(feature = "schema", returns(PendingInvitationsPagedResponse))]
|
||||
GetAllPendingInvitationsPaged {
|
||||
start_after: Option<(NodeFamilyId, NodeId)>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived (terminal-state) invitation issued by a
|
||||
/// given family.
|
||||
#[cfg_attr(feature = "schema", returns(PastFamilyInvitationsPagedResponse))]
|
||||
GetPastInvitationsForFamilyPaged {
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<PastFamilyInvitationCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived (terminal-state) invitation issued to a
|
||||
/// given node.
|
||||
#[cfg_attr(feature = "schema", returns(PastFamilyInvitationsForNodePagedResponse))]
|
||||
GetPastInvitationsForNodePaged {
|
||||
node_id: NodeId,
|
||||
start_after: Option<PastFamilyInvitationForNodeCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived (terminal-state) invitation across all
|
||||
/// families.
|
||||
#[cfg_attr(feature = "schema", returns(AllPastFamilyInvitationsPagedResponse))]
|
||||
GetAllPastInvitationsPaged {
|
||||
start_after: Option<GlobalPastFamilyInvitationCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived membership record for a given family
|
||||
/// (nodes that used to belong to it but have since been removed).
|
||||
#[cfg_attr(feature = "schema", returns(PastFamilyMembersPagedResponse))]
|
||||
GetPastMembersForFamilyPaged {
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<PastFamilyMemberCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived membership record for a given node
|
||||
/// (every family the node used to belong to but has since been removed
|
||||
/// from), across all families.
|
||||
#[cfg_attr(feature = "schema", returns(PastFamilyMembersForNodePagedResponse))]
|
||||
GetPastMembersForNodePaged {
|
||||
node_id: NodeId,
|
||||
start_after: Option<PastFamilyMemberForNodeCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Message passed to the contract's `migrate` entry point.
|
||||
#[cw_serde]
|
||||
pub struct MigrateMsg {
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
|
||||
/// Identifier of a node family.
|
||||
///
|
||||
/// Issued sequentially by the contract on family creation; never reused even if the
|
||||
/// family is later disbanded.
|
||||
pub type NodeFamilyId = u32;
|
||||
|
||||
/// Runtime configuration of the node families contract.
|
||||
#[cw_serde]
|
||||
pub struct Config {
|
||||
/// Fee charged on each successful `create_family` execution.
|
||||
pub create_family_fee: Coin,
|
||||
|
||||
/// Maximum allowed length, in characters, of a family name.
|
||||
pub family_name_length_limit: usize,
|
||||
|
||||
/// Maximum allowed length, in characters, of a family description.
|
||||
pub family_description_length_limit: usize,
|
||||
|
||||
/// Default lifetime, in seconds, used by `invite_to_family` when the
|
||||
/// sender doesn't supply an explicit value. Senders may override this
|
||||
/// per-invitation via the optional `validity_secs` argument.
|
||||
pub default_invitation_validity_secs: u64,
|
||||
}
|
||||
|
||||
/// On-chain representation of a node family.
|
||||
#[cw_serde]
|
||||
pub struct NodeFamily {
|
||||
/// The id of the node family
|
||||
pub id: NodeFamilyId,
|
||||
|
||||
/// The name of the node family
|
||||
pub name: String,
|
||||
|
||||
/// Normalised name of the node family used for uniqueness checks
|
||||
pub normalised_name: String,
|
||||
|
||||
/// The optional description of the node family
|
||||
pub description: String,
|
||||
|
||||
/// The owner of the node family
|
||||
pub owner: Addr,
|
||||
|
||||
/// Records the fee paid when the family was created,
|
||||
/// so that the appropriate amount could be returned upon it getting disbanded.
|
||||
pub paid_fee: Coin,
|
||||
|
||||
/// Memoized value of the current number of members in the node family
|
||||
/// Used to detect if the family is empty
|
||||
pub members: u64,
|
||||
|
||||
/// Timestamp of the creation of the node family
|
||||
pub created_at: u64,
|
||||
}
|
||||
|
||||
/// A pending invitation for a node to join a particular family.
|
||||
///
|
||||
/// Invitations are stored until they are accepted, rejected, revoked, or until the
|
||||
/// chain advances past `expires_at` (in which case they remain in storage but are
|
||||
/// treated as inert — there is no background process clearing expired invitations).
|
||||
#[cw_serde]
|
||||
pub struct FamilyInvitation {
|
||||
/// The family that issued the invitation.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The node being invited.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// Block timestamp (unix seconds) after which the invitation is no longer valid.
|
||||
pub expires_at: u64,
|
||||
}
|
||||
|
||||
/// On-chain record of a node's current family membership.
|
||||
///
|
||||
/// A node belongs to at most one family at a time, so this is keyed by
|
||||
/// `NodeId` alone — `family_id` is carried in the value to support reverse
|
||||
/// lookups (all nodes in a given family) via a secondary index.
|
||||
#[cw_serde]
|
||||
pub struct FamilyMembership {
|
||||
/// The family the node is currently a member of.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// Block timestamp (unix seconds) at which the node accepted its
|
||||
/// invitation and joined the family.
|
||||
pub joined_at: u64,
|
||||
}
|
||||
|
||||
/// Historical record of a node that used to be part of a family but has since been
|
||||
/// removed (kicked, left voluntarily, or because the family was disbanded).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyMember {
|
||||
/// The family the node used to belong to.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The node that was removed.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// Block timestamp (unix seconds) at which the membership was terminated.
|
||||
pub removed_at: u64,
|
||||
}
|
||||
|
||||
/// Terminal status for an invitation that has been moved out of the pending set.
|
||||
///
|
||||
/// Note: timed-out invitations are not represented here — they are simply left in
|
||||
/// the pending set (see `FamilyInvitation::expires_at`).
|
||||
#[cw_serde]
|
||||
pub enum FamilyInvitationStatus {
|
||||
/// Still awaiting a response. Recorded with a timestamp for completeness even
|
||||
/// though pending invitations live in a separate map.
|
||||
Pending { at: u64 },
|
||||
/// The invitee accepted and joined the family at the given timestamp.
|
||||
Accepted { at: u64 },
|
||||
/// The invitee explicitly rejected the invitation at the given timestamp.
|
||||
Rejected { at: u64 },
|
||||
/// The family revoked the invitation at the given timestamp before it could
|
||||
/// be accepted or rejected.
|
||||
Revoked { at: u64 },
|
||||
}
|
||||
|
||||
/// Historical record of an invitation that has reached a terminal state
|
||||
/// (`Accepted`, `Rejected`, or `Revoked`). Timed-out invitations are **not**
|
||||
/// archived here — they remain in the pending map until explicitly cleared.
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyInvitation {
|
||||
/// The original invitation as it was issued.
|
||||
pub invitation: FamilyInvitation,
|
||||
|
||||
/// What ultimately happened to it.
|
||||
pub status: FamilyInvitationStatus,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyById`](crate::QueryMsg::GetFamilyById).
|
||||
#[cw_serde]
|
||||
pub struct NodeFamilyResponse {
|
||||
/// The id that was queried, echoed back so paginated callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The matching family, or `None` if no family with `family_id` exists.
|
||||
pub family: Option<NodeFamily>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyByOwner`](crate::QueryMsg::GetFamilyByOwner).
|
||||
#[cw_serde]
|
||||
pub struct NodeFamilyByOwnerResponse {
|
||||
/// The (validated) owner address that was queried, echoed back so callers
|
||||
/// can correlate.
|
||||
pub owner: Addr,
|
||||
|
||||
/// The matching family, or `None` if `owner` does not currently own one.
|
||||
pub family: Option<NodeFamily>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyByName`](crate::QueryMsg::GetFamilyByName).
|
||||
#[cw_serde]
|
||||
pub struct NodeFamilyByNameResponse {
|
||||
/// The name that was queried, echoed back so callers can correlate.
|
||||
pub name: String,
|
||||
|
||||
/// The matching family, or `None` if no family with that name exists.
|
||||
pub family: Option<NodeFamily>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyMembership`](crate::QueryMsg::GetFamilyMembership).
|
||||
#[cw_serde]
|
||||
pub struct NodeFamilyMembershipResponse {
|
||||
/// The node that was queried.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The id of the family the node currently belongs to, or `None` if the
|
||||
/// node is not currently a member of any family.
|
||||
pub family_id: Option<NodeFamilyId>,
|
||||
}
|
||||
|
||||
/// A pending [`FamilyInvitation`] paired with whether it has already timed
|
||||
/// out at the time the query was served.
|
||||
#[cw_serde]
|
||||
pub struct PendingFamilyInvitationDetails {
|
||||
/// The stored invitation as it was issued.
|
||||
pub invitation: FamilyInvitation,
|
||||
|
||||
/// `true` iff `now >= invitation.expires_at` at query time, i.e. the
|
||||
/// invitation is still in the pending map but can no longer be acted on.
|
||||
pub expired: bool,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPendingInvitation`](crate::QueryMsg::GetPendingInvitation).
|
||||
#[cw_serde]
|
||||
pub struct PendingFamilyInvitationResponse {
|
||||
/// The family component of the queried `(family_id, node_id)` key.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The node component of the queried `(family_id, node_id)` key.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The matching pending invitation along with an explicit expiry flag,
|
||||
/// or `None` if no such invitation exists.
|
||||
pub invitation: Option<PendingFamilyInvitationDetails>,
|
||||
}
|
||||
|
||||
/// One entry in a [`FamilyMembersPagedResponse`] page — pairs a node id with
|
||||
/// its [`FamilyMembership`] record (notably its `joined_at` timestamp).
|
||||
#[cw_serde]
|
||||
pub struct FamilyMemberRecord {
|
||||
/// The node currently in the family.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The membership record (carries `family_id` and `joined_at`).
|
||||
pub membership: FamilyMembership,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyMembersPaged`](crate::QueryMsg::GetFamilyMembersPaged).
|
||||
#[cw_serde]
|
||||
pub struct FamilyMembersPagedResponse {
|
||||
/// The family whose members were queried, echoed back so paginated
|
||||
/// callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The members on this page, in ascending [`NodeId`] order.
|
||||
pub members: Vec<FamilyMemberRecord>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (which the caller should treat as end-of-list).
|
||||
pub start_next_after: Option<NodeId>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetAllFamilyMembersPaged`](crate::QueryMsg::GetAllFamilyMembersPaged).
|
||||
#[cw_serde]
|
||||
pub struct AllFamilyMembersPagedResponse {
|
||||
/// The members on this page, in ascending [`NodeId`] order across every
|
||||
/// family.
|
||||
pub members: Vec<FamilyMemberRecord>,
|
||||
|
||||
/// Cursor (last `node_id`) to pass as `start_after` on the next call,
|
||||
/// or `None` if this page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<NodeId>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPendingInvitationsForFamilyPaged`](crate::QueryMsg::GetPendingInvitationsForFamilyPaged).
|
||||
#[cw_serde]
|
||||
pub struct PendingFamilyInvitationsPagedResponse {
|
||||
/// The family whose pending invitations were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The pending invitations on this page, in ascending invitee
|
||||
/// [`NodeId`] order, each stamped with whether it had already timed out
|
||||
/// at the time the query was served.
|
||||
pub invitations: Vec<PendingFamilyInvitationDetails>,
|
||||
|
||||
/// Cursor (last invitee node id) to pass as `start_after` on the next
|
||||
/// call, or `None` if this page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<NodeId>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPendingInvitationsForNodePaged`](crate::QueryMsg::GetPendingInvitationsForNodePaged).
|
||||
#[cw_serde]
|
||||
pub struct PendingInvitationsForNodePagedResponse {
|
||||
/// The node whose pending invitations were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The pending invitations addressed to this node on this page, in
|
||||
/// ascending [`NodeFamilyId`] order, each stamped with whether it had
|
||||
/// already timed out at the time the query was served.
|
||||
pub invitations: Vec<PendingFamilyInvitationDetails>,
|
||||
|
||||
/// Cursor (last issuing family id) to pass as `start_after` on the
|
||||
/// next call, or `None` if this page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<NodeFamilyId>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetAllPendingInvitationsPaged`](crate::QueryMsg::GetAllPendingInvitationsPaged).
|
||||
#[cw_serde]
|
||||
pub struct PendingInvitationsPagedResponse {
|
||||
/// The pending invitations on this page, in ascending
|
||||
/// `(family_id, node_id)` order, each stamped with whether it had
|
||||
/// already timed out at the time the query was served.
|
||||
pub invitations: Vec<PendingFamilyInvitationDetails>,
|
||||
|
||||
/// Cursor (last `(family_id, node_id)` pair) to pass as `start_after`
|
||||
/// on the next call, or `None` if this page is empty (treat as
|
||||
/// end-of-list).
|
||||
pub start_next_after: Option<(NodeFamilyId, NodeId)>,
|
||||
}
|
||||
|
||||
/// Cursor for paginating per-family past-invitation listings: identifies a
|
||||
/// single archive entry within a family by `(node_id, counter)`. The
|
||||
/// `counter` is the per-`(family, node)` archive slot — multiple archived
|
||||
/// invitations can exist for the same `(family, node)` pair (a node may be
|
||||
/// invited and have the invitation reach a terminal state more than once).
|
||||
pub type PastFamilyInvitationCursor = (NodeId, u64);
|
||||
|
||||
/// Cursor for paginating per-node past-invitation listings: identifies a
|
||||
/// single archive entry addressed to a fixed node by `(family_id, counter)`.
|
||||
pub type PastFamilyInvitationForNodeCursor = (NodeFamilyId, u64);
|
||||
|
||||
/// Cursor for paginating global past-invitation listings: identifies a
|
||||
/// single archive entry across all families by `((family_id, node_id), counter)`.
|
||||
pub type GlobalPastFamilyInvitationCursor = ((NodeFamilyId, NodeId), u64);
|
||||
|
||||
/// Response to [`QueryMsg::GetPastInvitationsForFamilyPaged`](crate::QueryMsg::GetPastInvitationsForFamilyPaged).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyInvitationsPagedResponse {
|
||||
/// The family whose archived invitations were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The archived invitations on this page, in ascending
|
||||
/// `(node_id, counter)` order across all terminal statuses.
|
||||
pub invitations: Vec<PastFamilyInvitation>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<PastFamilyInvitationCursor>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPastInvitationsForNodePaged`](crate::QueryMsg::GetPastInvitationsForNodePaged).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyInvitationsForNodePagedResponse {
|
||||
/// The node whose past invitations were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The archived invitations addressed to this node on this page, in
|
||||
/// ascending `(family_id, counter)` order across all terminal statuses.
|
||||
pub invitations: Vec<PastFamilyInvitation>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<PastFamilyInvitationForNodeCursor>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetAllPastInvitationsPaged`](crate::QueryMsg::GetAllPastInvitationsPaged).
|
||||
#[cw_serde]
|
||||
pub struct AllPastFamilyInvitationsPagedResponse {
|
||||
/// The archived invitations on this page, in ascending
|
||||
/// `((family_id, node_id), counter)` order across all terminal statuses.
|
||||
pub invitations: Vec<PastFamilyInvitation>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<GlobalPastFamilyInvitationCursor>,
|
||||
}
|
||||
|
||||
/// Cursor for paginating per-family past-member listings: identifies a single
|
||||
/// archive entry within a family by `(node_id, counter)`. The `counter` is the
|
||||
/// per-`(family, node)` archive slot — multiple archived membership entries
|
||||
/// can exist for the same `(family, node)` pair (a node may join, leave, and
|
||||
/// re-join the same family more than once).
|
||||
pub type PastFamilyMemberCursor = (NodeId, u64);
|
||||
|
||||
/// Cursor for paginating per-node past-member listings: identifies a single
|
||||
/// archive entry for a fixed node by `(family_id, counter)`.
|
||||
pub type PastFamilyMemberForNodeCursor = (NodeFamilyId, u64);
|
||||
|
||||
/// Response to [`QueryMsg::GetPastMembersForFamilyPaged`](crate::QueryMsg::GetPastMembersForFamilyPaged).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyMembersPagedResponse {
|
||||
/// The family whose archived memberships were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The archived membership records on this page, in ascending
|
||||
/// `(node_id, counter)` order.
|
||||
pub members: Vec<PastFamilyMember>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<PastFamilyMemberCursor>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPastMembersForNodePaged`](crate::QueryMsg::GetPastMembersForNodePaged).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyMembersForNodePagedResponse {
|
||||
/// The node whose archived memberships were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The archived membership records for this node on this page, in
|
||||
/// ascending `(family_id, counter)` order.
|
||||
pub members: Vec<PastFamilyMember>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<PastFamilyMemberForNodeCursor>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamiliesPaged`](crate::QueryMsg::GetFamiliesPaged).
|
||||
#[cw_serde]
|
||||
pub struct FamiliesPagedResponse {
|
||||
/// The families on this page, in ascending [`NodeFamilyId`] order.
|
||||
pub families: Vec<NodeFamily>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (which the caller should treat as end-of-list).
|
||||
pub start_next_after: Option<NodeFamilyId>,
|
||||
}
|
||||
@@ -57,7 +57,6 @@ impl QuorumStateChecker {
|
||||
}
|
||||
|
||||
async fn check_quorum_state(&self) -> Result<bool, CredentialProxyError> {
|
||||
info!("checking the current quorum state");
|
||||
let client_guard = self.client.query_chain().await;
|
||||
|
||||
// split the operation as we only need to hold the reference to chain client for the first part
|
||||
@@ -66,7 +65,6 @@ impl QuorumStateChecker {
|
||||
drop(client_guard);
|
||||
|
||||
let res = check_known_dealers(dkg_details).await?;
|
||||
info!("there are {} known DKG dealers", res.results.len());
|
||||
|
||||
let Some(signing_threshold) = res.threshold else {
|
||||
warn!(
|
||||
@@ -78,33 +76,12 @@ impl QuorumStateChecker {
|
||||
let mut working_issuer = 0;
|
||||
|
||||
for result in res.results {
|
||||
let dealer = &result.information;
|
||||
let info = format!("[id: {}] @ {}", dealer.node_index, dealer.announce_address);
|
||||
if result.chain_available() && result.signing_available() {
|
||||
info!("✅ {info} is fully available");
|
||||
working_issuer += 1;
|
||||
} else if !result.chain_available() && !result.signing_available() {
|
||||
warn!("❌ {info} is not available for both chain and signing");
|
||||
} else if !result.chain_available() {
|
||||
warn!("❌ {info} is not available for chain");
|
||||
} else {
|
||||
warn!("❌ {info} is not available for signing");
|
||||
}
|
||||
}
|
||||
|
||||
let available = (working_issuer as u64) >= signing_threshold;
|
||||
|
||||
if available {
|
||||
info!(
|
||||
"✅ Quorum state is available with {working_issuer} out of {signing_threshold} issuers"
|
||||
)
|
||||
} else {
|
||||
error!(
|
||||
"❌ Quorum state is not available with {working_issuer} out of {signing_threshold} issuers"
|
||||
)
|
||||
}
|
||||
|
||||
Ok(available)
|
||||
Ok((working_issuer as u64) >= signing_threshold)
|
||||
}
|
||||
|
||||
pub async fn run_forever(self) {
|
||||
|
||||
@@ -8,7 +8,9 @@ license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version.workspace = true
|
||||
# pinned (not inherited from workspace) because this crate is imported by the ecash contract,
|
||||
# and the contracts workspace cannot be built with rustc more recent than 1.86
|
||||
rust-version = "1.86.0"
|
||||
readme.workspace = true
|
||||
publish = true
|
||||
|
||||
|
||||
@@ -31,5 +31,3 @@ pub use aes_gcm_siv::{Aes128GcmSiv, Aes256GcmSiv};
|
||||
pub use blake3;
|
||||
#[cfg(feature = "stream_cipher")]
|
||||
pub use ctr;
|
||||
#[cfg(feature = "hashing")]
|
||||
pub use sha2;
|
||||
|
||||
@@ -6,7 +6,7 @@ use nym_coconut_dkg_common::verification_key::VerificationKeyShare;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::warn;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
pub trait Verifiable {
|
||||
fn verify_signature(&self, pub_key: &ed25519::PublicKey) -> bool;
|
||||
@@ -36,7 +36,6 @@ pub trait ChainResponse: Verifiable + TimestampedResponse {
|
||||
|
||||
// we rely on information provided from the api itself AS LONG AS it's not too outdated
|
||||
if self.timestamp() + stale_response_threshold < now {
|
||||
warn!("chain status response is stale");
|
||||
return false;
|
||||
}
|
||||
self.chain_synced()
|
||||
@@ -97,27 +96,26 @@ pub trait SignerResponse: Verifiable + TimestampedResponse {
|
||||
|
||||
// we rely on information provided from the api itself AS LONG AS it's not too outdated
|
||||
if self.timestamp() + stale_response_threshold < now {
|
||||
warn!("stale signer response");
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.has_signing_keys() {
|
||||
warn!("missing signing keys");
|
||||
debug!("missing signing keys");
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.signer_disabled() {
|
||||
warn!("signer functionalities are explicitly disabled");
|
||||
debug!("signer functionalities explicitly disabled");
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.is_ecash_signer() {
|
||||
warn!("signer doesn't recognise it's a signer for this epoch");
|
||||
debug!("signer doesn't recognise it's a signer for this epoch");
|
||||
return false;
|
||||
}
|
||||
|
||||
if dkg_epoch_id != self.dkg_ecash_epoch_id() {
|
||||
warn!(
|
||||
debug!(
|
||||
"mismatched dkg epoch id. current: {dkg_epoch_id}, signer's: {}",
|
||||
self.dkg_ecash_epoch_id()
|
||||
);
|
||||
|
||||
@@ -11,11 +11,10 @@ use nym_crypto::asymmetric::ed25519;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::warn;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(10 * 60);
|
||||
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(10 * 60);
|
||||
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60);
|
||||
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60);
|
||||
|
||||
// the reason for generics is not to remove duplication of code,
|
||||
// but because without them, we'd be having problems with circular dependencies,
|
||||
@@ -189,7 +188,6 @@ where
|
||||
};
|
||||
|
||||
let SignerStatus::Tested { result } = &self.status else {
|
||||
warn!("no valid chain response");
|
||||
return false;
|
||||
};
|
||||
result
|
||||
@@ -241,7 +239,6 @@ where
|
||||
};
|
||||
|
||||
let SignerStatus::Tested { result } = &self.status else {
|
||||
warn!("no valid signer response");
|
||||
return false;
|
||||
};
|
||||
result.signing_status.signing_available(
|
||||
|
||||
@@ -1173,7 +1173,16 @@ impl ApiClientCore for Client {
|
||||
};
|
||||
|
||||
match response {
|
||||
Ok(resp) => return Ok(resp),
|
||||
Ok(resp) => {
|
||||
// Check if the response includes a rate limit error from the vercel API
|
||||
if is_http_rate_limit_err(&resp) {
|
||||
warn!("encountered vercel rate limit error for {}", url.as_str());
|
||||
// if we have multiple urls, update to the next
|
||||
self.maybe_rotate_hosts(Some(url.clone()));
|
||||
}
|
||||
|
||||
return Ok(resp);
|
||||
}
|
||||
Err(err) => {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let is_network_err = err.is_timeout();
|
||||
@@ -1226,17 +1235,39 @@ impl ApiClientCore for Client {
|
||||
}
|
||||
}
|
||||
|
||||
const VERCEL_CHALLENGE_HEADER: &str = "x-vercel-mitigated";
|
||||
const VERCEL_CHALLENGE_VALUE: &[u8] = b"challenge";
|
||||
|
||||
/// Check for Rate Limit challenge response from the vercel API
|
||||
pub(crate) fn is_http_rate_limit_err(resp: &Response) -> bool {
|
||||
let status = resp.status() == StatusCode::FORBIDDEN;
|
||||
let header = resp
|
||||
.headers()
|
||||
.get(VERCEL_CHALLENGE_HEADER)
|
||||
.is_some_and(|v| v.as_bytes() == VERCEL_CHALLENGE_VALUE);
|
||||
let content_type = resp
|
||||
.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.parse::<Mime>().ok())
|
||||
.is_some_and(|mime_type| {
|
||||
mime_type.type_() == mime::TEXT && mime_type.subtype() == mime::HTML
|
||||
});
|
||||
|
||||
status && header && content_type
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const MAX_ERR_SOURCE_ITERATIONS: usize = 4;
|
||||
|
||||
/// This functions attempts to check the error returned by reqwest to see if
|
||||
/// rotating host informtion (for clients with mutliple hosts defined) could be
|
||||
/// helpful. This looks for situations where the error could plausibly be caused
|
||||
/// by a network adversary, or where rotating to an equival hostname might help.
|
||||
/// This functions attempts to check the error returned by reqwest to see if rotating host
|
||||
/// information (for clients with multiple hosts defined) could be helpful. This looks for
|
||||
/// situations where the error could plausibly be caused by a network adversary, or where rotating
|
||||
/// to an equivalent hostname might help.
|
||||
///
|
||||
/// For example --> NetworkUnreachable will not be helped by rotating domains,
|
||||
/// but ConnectionReset might be caused by a network adversary blocking by SNI
|
||||
/// which could possibly benefit from rotating domains.
|
||||
/// For example --> NetworkUnreachable will not be helped by rotating domains, but ConnectionReset
|
||||
/// might be caused by a network adversary blocking by SNI which could possibly benefit from
|
||||
/// rotating domains.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
|
||||
if err.is_timeout() {
|
||||
@@ -1697,6 +1728,13 @@ where
|
||||
decode_raw_response(&headers, full)
|
||||
} else if res.status() == StatusCode::NOT_FOUND {
|
||||
Err(HttpClientError::NotFound { url: Box::new(url) })
|
||||
} else if is_http_rate_limit_err(&res) {
|
||||
Err(HttpClientError::EndpointFailure {
|
||||
url: Box::new(url),
|
||||
status,
|
||||
headers: Box::new(headers),
|
||||
error: String::from("received vercel rate limit challenge response"),
|
||||
})
|
||||
} else {
|
||||
let Ok(plaintext) = res.text().await else {
|
||||
return Err(HttpClientError::RequestFailure {
|
||||
|
||||
@@ -10,9 +10,7 @@ fn sanitize_fragment(segment: &str) -> &str {
|
||||
segment.trim_matches(|c: char| c.is_whitespace() || c == '/')
|
||||
}
|
||||
|
||||
/// Defines a path that can be used to make a request to an API.
|
||||
pub trait RequestPath: Debug {
|
||||
/// Sanitise the request path by removing empty segments and trimming whitespace and slashes
|
||||
fn to_sanitized_segments(&self) -> Vec<&str>;
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,41 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum OutputV2 {
|
||||
#[default]
|
||||
Json,
|
||||
Yaml,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::IntoParams, utoipa::ToSchema))]
|
||||
#[serde(default)]
|
||||
pub struct OutputParamsV2 {
|
||||
pub output: Option<OutputV2>,
|
||||
}
|
||||
|
||||
impl OutputParamsV2 {
|
||||
pub fn get_output(&self) -> OutputV2 {
|
||||
self.output.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn to_response<T: Serialize>(self, data: T) -> FormattedResponse<T> {
|
||||
self.get_output().to_response(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputV2 {
|
||||
pub fn to_response<T: Serialize>(self, data: T) -> FormattedResponse<T> {
|
||||
match self {
|
||||
OutputV2::Json => FormattedResponse::Json(Json::from(data)),
|
||||
OutputV2::Yaml => FormattedResponse::Yaml(Yaml::from(data)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
|
||||
@@ -20,6 +20,22 @@ pub const MAX_NON_STREAM_VERSION: u8 = v8::VERSION;
|
||||
/// mixnet sends, matching the node-side enforcement in `ip-packet-router`.
|
||||
pub const SPHINX_STREAM_VERSION_THRESHOLD: u8 = v9::VERSION;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const _: () = {
|
||||
assert!(SPHINX_STREAM_VERSION_THRESHOLD > MAX_NON_STREAM_VERSION);
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn stream_transport_threshold_is_consistent() {
|
||||
assert_eq!(MAX_NON_STREAM_VERSION, 8);
|
||||
assert_eq!(SPHINX_STREAM_VERSION_THRESHOLD, 9);
|
||||
const _: () = assert!(SPHINX_STREAM_VERSION_THRESHOLD > MAX_NON_STREAM_VERSION);
|
||||
}
|
||||
}
|
||||
|
||||
// version 3: initial version
|
||||
// version 4: IPv6 support
|
||||
// version 5: Add severity level to info response
|
||||
|
||||
@@ -8,7 +8,9 @@ license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version.workspace = true
|
||||
# pinned (not inherited from workspace) because this crate is imported by the ecash contract,
|
||||
# and the contracts workspace cannot be built with rustc more recent than 1.86
|
||||
rust-version = "1.86.0"
|
||||
readme.workspace = true
|
||||
publish = true
|
||||
# Exclude build.rs from published crate - it's only used for dev-time sync
|
||||
@@ -17,10 +19,11 @@ exclude = ["build.rs"]
|
||||
|
||||
[dependencies]
|
||||
dotenvy = { workspace = true, optional = true }
|
||||
log = { workspace = true, optional = true }
|
||||
schemars = { workspace = true, features = ["preserve_order"], optional = true }
|
||||
serde = { workspace = true, features = ["derive"], optional = true }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
tracing = { workspace = true, optional = true }
|
||||
serde_json = {workspace = true, optional = true }
|
||||
tracing = {workspace = true, optional = true }
|
||||
url = { workspace = true, optional = true }
|
||||
utoipa = { workspace = true, optional = true }
|
||||
|
||||
@@ -29,9 +32,9 @@ utoipa = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
default = ["env", "network"]
|
||||
env = ["dotenvy", "serde_json", "tracing"]
|
||||
env = ["dotenvy", "log", "serde_json", "tracing"]
|
||||
network = ["schemars", "serde", "url"]
|
||||
utoipa = ["dep:utoipa"]
|
||||
utoipa = [ "dep:utoipa" ]
|
||||
|
||||
[build-dependencies]
|
||||
regex = { workspace = true }
|
||||
|
||||
@@ -27,20 +27,16 @@ fn print_env_vars_with_keys_in_file<P: AsRef<Path> + Copy>(config_env_file: P) {
|
||||
.expect("Invalid path to environment configuration file");
|
||||
for item in items {
|
||||
let (key, val) = item.expect("Invalid item in environment configuration file");
|
||||
tracing::debug!("{key}: {val}");
|
||||
log::debug!("{key}: {val}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn env_configured() -> bool {
|
||||
std::env::var(var_names::CONFIGURED).is_ok()
|
||||
}
|
||||
|
||||
pub fn setup_env<P: AsRef<Path>>(config_env_file: Option<P>) {
|
||||
match std::env::var(var_names::CONFIGURED) {
|
||||
// if the configuration is not already set in the env vars
|
||||
Err(std::env::VarError::NotPresent) => {
|
||||
if let Some(config_env_file) = &config_env_file {
|
||||
tracing::debug!(
|
||||
log::debug!(
|
||||
"Loading environment variables from {:?}",
|
||||
config_env_file.as_ref()
|
||||
);
|
||||
@@ -51,12 +47,12 @@ pub fn setup_env<P: AsRef<Path>>(config_env_file: Option<P>) {
|
||||
// if nothing is set, the use mainnet defaults
|
||||
// if the user has not set `CONFIGURED`, then even if they set any of the env variables,
|
||||
// overwrite them
|
||||
tracing::debug!("Loading mainnet defaults");
|
||||
log::debug!("Loading mainnet defaults");
|
||||
crate::mainnet::export_to_env();
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::debug!("Environment variables already set. Using them");
|
||||
log::debug!("Environment variables already set. Using them");
|
||||
crate::mainnet::export_to_env()
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -22,8 +22,10 @@ pub const VESTING_CONTRACT_ADDRESS: &str =
|
||||
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "";
|
||||
// /\ TODO: this has to be updated once the contract is deployed
|
||||
|
||||
pub const NETWORK_MONITORS_CONTRACT_ADDRESS: &str =
|
||||
"n1m3a2ltkjqud8mkmrpqvgllrtv2p4r6js6qwl7p8cqkzrq8jg6e2qwqgl8z";
|
||||
// \/ TODO: this has to be updated once the contract is deployed
|
||||
pub const NODE_FAMILIES_CONTRACT_ADDRESS: &str = "";
|
||||
// /\ TODO: this has to be updated once the contract is deployed
|
||||
|
||||
pub const ECASH_CONTRACT_ADDRESS: &str =
|
||||
"n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun";
|
||||
pub const GROUP_CONTRACT_ADDRESS: &str =
|
||||
@@ -38,10 +40,6 @@ pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772
|
||||
pub const NYXD_URL: &str = "https://rpc.nymtech.net";
|
||||
pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket";
|
||||
|
||||
// cluster of lite rpc nodes (not part of consensus, aggressive pruning, no archival state)
|
||||
pub const NYXD_QUERY_LITE: &str = "https://blockstream.nymtech.net";
|
||||
pub const NYXD_WS_LITE: &str = "wss://blockstream.nymtech.net/websocket";
|
||||
|
||||
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
|
||||
#[cfg(feature = "network")]
|
||||
pub const NYM_APIS: &[ApiUrlConst] = &[
|
||||
@@ -139,11 +137,6 @@ pub fn read_parsed_var_if_not_default<T: std::str::FromStr>(
|
||||
.map(std::str::FromStr::from_str)
|
||||
}
|
||||
|
||||
#[cfg(feature = "env")]
|
||||
pub fn read_parsed_var<T: std::str::FromStr>(var: &str) -> Result<T, T::Err> {
|
||||
std::env::var(var).unwrap_or_default().parse()
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "env", feature = "network"))]
|
||||
pub fn export_to_env() {
|
||||
use crate::var_names;
|
||||
@@ -174,14 +167,6 @@ pub fn export_to_env() {
|
||||
var_names::COCONUT_DKG_CONTRACT_ADDRESS,
|
||||
COCONUT_DKG_CONTRACT_ADDRESS,
|
||||
);
|
||||
set_var_to_default(
|
||||
var_names::PERFORMANCE_CONTRACT_ADDRESS,
|
||||
PERFORMANCE_CONTRACT_ADDRESS,
|
||||
);
|
||||
set_var_to_default(
|
||||
var_names::NETWORK_MONITORS_CONTRACT_ADDRESS,
|
||||
NETWORK_MONITORS_CONTRACT_ADDRESS,
|
||||
);
|
||||
set_var_to_default(
|
||||
var_names::REWARDING_VALIDATOR_ADDRESS,
|
||||
REWARDING_VALIDATOR_ADDRESS,
|
||||
@@ -201,8 +186,6 @@ pub fn export_to_env() {
|
||||
var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
);
|
||||
set_var_to_default(var_names::NYXD_QUERY_LITE, NYXD_QUERY_LITE);
|
||||
set_var_to_default(var_names::NYXD_WS_LITE, NYXD_WS_LITE);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "env", feature = "network"))]
|
||||
@@ -254,6 +237,4 @@ pub fn export_to_env_if_not_set() {
|
||||
var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
);
|
||||
set_var_conditionally_to_default(var_names::NYXD_QUERY_LITE, NYXD_QUERY_LITE);
|
||||
set_var_conditionally_to_default(var_names::NYXD_WS_LITE, NYXD_WS_LITE);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ pub struct NymContracts {
|
||||
#[serde(default)]
|
||||
pub performance_contract_address: Option<String>,
|
||||
#[serde(default)]
|
||||
pub network_monitors_contract_address: Option<String>,
|
||||
pub node_families_contract_address: Option<String>,
|
||||
pub ecash_contract_address: Option<String>,
|
||||
pub group_contract_address: Option<String>,
|
||||
pub multisig_contract_address: Option<String>,
|
||||
@@ -74,15 +74,6 @@ pub struct ApiUrl {
|
||||
pub front_hosts: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl From<Url> for ApiUrl {
|
||||
fn from(value: Url) -> Self {
|
||||
ApiUrl {
|
||||
url: value.to_string(),
|
||||
front_hosts: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize)]
|
||||
pub struct ApiUrlConst<'a> {
|
||||
pub url: &'a str,
|
||||
@@ -185,14 +176,13 @@ impl NymNetworkDetails {
|
||||
))
|
||||
.with_mixnet_contract(get_optional_env(var_names::MIXNET_CONTRACT_ADDRESS))
|
||||
.with_vesting_contract(get_optional_env(var_names::VESTING_CONTRACT_ADDRESS))
|
||||
.with_node_families_contract(get_optional_env(
|
||||
var_names::NODE_FAMILIES_CONTRACT_ADDRESS,
|
||||
))
|
||||
.with_ecash_contract(get_optional_env(var_names::ECASH_CONTRACT_ADDRESS))
|
||||
.with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS))
|
||||
.with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
|
||||
.with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS))
|
||||
.with_performance_contract(get_optional_env(var_names::PERFORMANCE_CONTRACT_ADDRESS))
|
||||
.with_network_monitors_contract(get_optional_env(
|
||||
var_names::NETWORK_MONITORS_CONTRACT_ADDRESS,
|
||||
))
|
||||
.with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API))
|
||||
.with_nym_vpn_api_urls(nym_vpn_api_urls)
|
||||
.with_nym_api_urls(nym_api_urls)
|
||||
@@ -214,8 +204,8 @@ impl NymNetworkDetails {
|
||||
performance_contract_address: parse_optional_str(
|
||||
mainnet::PERFORMANCE_CONTRACT_ADDRESS,
|
||||
),
|
||||
network_monitors_contract_address: parse_optional_str(
|
||||
mainnet::NETWORK_MONITORS_CONTRACT_ADDRESS,
|
||||
node_families_contract_address: parse_optional_str(
|
||||
mainnet::NODE_FAMILIES_CONTRACT_ADDRESS,
|
||||
),
|
||||
ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS),
|
||||
group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS),
|
||||
@@ -244,7 +234,7 @@ impl NymNetworkDetails {
|
||||
|
||||
fn set_optional_var(var_name: &str, value: Option<String>) {
|
||||
if let Some(value) = value {
|
||||
unsafe { set_var(var_name, value) }
|
||||
unsafe {set_var(var_name, value)}
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
@@ -270,6 +260,7 @@ impl NymNetworkDetails {
|
||||
|
||||
set_optional_var(var_names::MIXNET_CONTRACT_ADDRESS, self.contracts.mixnet_contract_address);
|
||||
set_optional_var(var_names::VESTING_CONTRACT_ADDRESS, self.contracts.vesting_contract_address);
|
||||
set_optional_var(var_names::NODE_FAMILIES_CONTRACT_ADDRESS, self.contracts.node_families_contract_address);
|
||||
set_optional_var(var_names::ECASH_CONTRACT_ADDRESS, self.contracts.ecash_contract_address);
|
||||
set_optional_var(var_names::GROUP_CONTRACT_ADDRESS, self.contracts.group_contract_address);
|
||||
set_optional_var(var_names::MULTISIG_CONTRACT_ADDRESS, self.contracts.multisig_contract_address);
|
||||
@@ -358,6 +349,12 @@ impl NymNetworkDetails {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_node_families_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
|
||||
self.contracts.node_families_contract_address = contract.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_ecash_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
|
||||
self.contracts.ecash_contract_address = contract.map(Into::into);
|
||||
@@ -382,31 +379,15 @@ impl NymNetworkDetails {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_performance_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
|
||||
self.contracts.performance_contract_address = contract.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_network_monitors_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
|
||||
self.contracts.network_monitors_contract_address = contract.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_nym_vpn_api_url<S: Into<String>>(mut self, endpoint: Option<S>) -> Self {
|
||||
self.nym_vpn_api_url = endpoint.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_nym_api_urls<U: Into<ApiUrl>>(&mut self, urls: Vec<U>) {
|
||||
self.nym_api_urls = Some(urls.into_iter().map(Into::into).collect());
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_nym_api_urls<U: Into<ApiUrl>>(mut self, urls: Vec<U>) -> Self {
|
||||
self.set_nym_api_urls(urls);
|
||||
pub fn with_nym_api_urls(mut self, urls: Vec<ApiUrl>) -> Self {
|
||||
self.nym_api_urls = Some(urls);
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@@ -17,16 +17,13 @@ pub const VESTING_CONTRACT_ADDRESS: &str = "VESTING_CONTRACT_ADDRESS";
|
||||
pub const ECASH_CONTRACT_ADDRESS: &str = "ECASH_CONTRACT_ADDRESS";
|
||||
pub const GROUP_CONTRACT_ADDRESS: &str = "GROUP_CONTRACT_ADDRESS";
|
||||
pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_CONTRACT_ADDRESS";
|
||||
pub const NODE_FAMILIES_CONTRACT_ADDRESS: &str = "NODE_FAMILIES_CONTRACT_ADDRESS";
|
||||
pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS";
|
||||
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "PERFORMANCE_CONTRACT_ADDRESS";
|
||||
pub const NETWORK_MONITORS_CONTRACT_ADDRESS: &str = "NETWORK_MONITORS_CONTRACT_ADDRESS";
|
||||
pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS";
|
||||
pub const NYXD: &str = "NYXD";
|
||||
pub const NYM_API: &str = "NYM_API";
|
||||
pub const NYM_APIS: &str = "NYM_APIS";
|
||||
pub const NYXD_WEBSOCKET: &str = "NYXD_WS";
|
||||
pub const NYXD_QUERY_LITE: &str = "NYXD_LITE";
|
||||
pub const NYXD_WS_LITE: &str = "NYXD_WS_LITE";
|
||||
pub const EXIT_POLICY_URL: &str = "EXIT_POLICY";
|
||||
pub const NYM_VPN_API: &str = "NYM_VPN_API";
|
||||
pub const NYM_VPN_APIS: &str = "NYM_VPN_APIS";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-kkt"
|
||||
description = "Key transport protocol for the Nym network"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
authors = ["Georgio Nicolas <georgio@nymtech.net>"]
|
||||
edition = { workspace = true }
|
||||
license.workspace = true
|
||||
|
||||
@@ -31,9 +31,7 @@ where
|
||||
// instead we could maybe use the `from_bytes` variant and adding some suffix
|
||||
// when computing the digest until we produce a valid scalar.
|
||||
let mut bytes = [0u8; 64];
|
||||
let pad_size = 64usize
|
||||
.checked_sub(D::OutputSize::to_usize())
|
||||
.unwrap_or_default();
|
||||
let pad_size = 64usize.saturating_sub(D::OutputSize::to_usize());
|
||||
|
||||
bytes[pad_size..].copy_from_slice(&digest);
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ impl SecretKeyAuth {
|
||||
let ys_len = self.ys.len();
|
||||
let mut bytes = Vec::with_capacity(8 + (ys_len + 1) * 32);
|
||||
bytes.extend_from_slice(&self.x.to_bytes());
|
||||
bytes.extend_from_slice(&ys_len.to_le_bytes());
|
||||
bytes.extend_from_slice(&(ys_len as u64).to_le_bytes());
|
||||
for y in self.ys.iter() {
|
||||
bytes.extend_from_slice(&y.to_bytes())
|
||||
}
|
||||
@@ -337,7 +337,7 @@ impl VerificationKeyAuth {
|
||||
|
||||
bytes.extend_from_slice(&self.alpha.to_affine().to_compressed());
|
||||
|
||||
bytes.extend_from_slice(&beta_g1_len.to_le_bytes());
|
||||
bytes.extend_from_slice(&(beta_g1_len as u64).to_le_bytes());
|
||||
|
||||
for beta_g1 in self.beta_g1.iter() {
|
||||
bytes.extend_from_slice(&beta_g1.to_affine().to_compressed())
|
||||
|
||||
+42
-383
@@ -1,19 +1,19 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use snow::params::NoiseParams;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use strum_macros::{EnumIter, FromRepr};
|
||||
use tokio::sync::{Mutex, MutexGuard};
|
||||
|
||||
pub use nym_noise_keys::{NoiseVersion, VersionedNoiseKeyV1};
|
||||
use arc_swap::ArcSwap;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_noise_keys::{NoiseVersion, VersionedNoiseKeyV1};
|
||||
use snow::params::NoiseParams;
|
||||
|
||||
use strum_macros::{EnumIter, FromRepr};
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, EnumIter, FromRepr, Eq, PartialEq)]
|
||||
#[repr(u8)]
|
||||
@@ -53,125 +53,38 @@ impl NoisePattern {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Default)]
|
||||
struct SocketAddrToKey {
|
||||
inner: ArcSwap<HashMap<SocketAddr, VersionedNoiseKeyV1>>,
|
||||
}
|
||||
|
||||
// SW NOTE : Only for phased upgrade. To remove once we decide all nodes have to support Noise
|
||||
#[derive(Debug, Default)]
|
||||
struct IpAddrToVersion {
|
||||
inner: ArcSwap<HashMap<IpAddr, NoiseVersion>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct NoiseNetworkView {
|
||||
inner: Arc<NoiseNetworkViewInner>,
|
||||
}
|
||||
|
||||
/// Inner state of [`NoiseNetworkView`], shared behind an `Arc`.
|
||||
///
|
||||
/// # Concurrency model
|
||||
///
|
||||
/// Reads (on the packet-processing hot path) use `ArcSwap` and are fully lock-free.
|
||||
/// Writers must first acquire `update_lock` to serialise concurrent updates, then call
|
||||
/// `swap_view` to atomically publish the new map. The lock is intentionally *not* wrapping
|
||||
/// the map itself so that readers are never blocked.
|
||||
#[derive(Debug)]
|
||||
struct NoiseNetworkViewInner {
|
||||
update_lock: Mutex<()>,
|
||||
nodes: ArcSwap<HashMap<IpAddr, NoiseNode>>,
|
||||
}
|
||||
|
||||
/// A node in the noise network map, keyed by IP address.
|
||||
///
|
||||
/// A single IP can correspond to either one nym-node (which has a single noise key)
|
||||
/// or one-or-more network monitor agents (each with its own port and noise key).
|
||||
/// The two variants have independent lifecycles: nym-node entries come from the
|
||||
/// nym-api topology refresher, while agent entries come from blockchain events.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum NoiseNode {
|
||||
NymNode { key: VersionedNoiseKeyV1 },
|
||||
// due to the structure of network monitor agents,
|
||||
// it is possible to have multiple destinations with the same host ip address,
|
||||
// but a different noise key.
|
||||
// however, we are also guaranteed all of those are going to have a unique port.
|
||||
// note: we're not storing it in a map, since at maximum we might have maybe 20 or so
|
||||
// entries under a single ip address and linear look-up of a vec is faster than the overhead of a hashmap
|
||||
NetworkMonitorAgent { nodes: Vec<NetworkMonitorAgentNode> },
|
||||
}
|
||||
|
||||
impl NoiseNode {
|
||||
pub fn new_nym_node(key: VersionedNoiseKeyV1) -> Self {
|
||||
NoiseNode::NymNode { key }
|
||||
}
|
||||
|
||||
pub fn new_agent(socket_addr: SocketAddr, key: VersionedNoiseKeyV1) -> Self {
|
||||
NoiseNode::NetworkMonitorAgent {
|
||||
nodes: vec![NetworkMonitorAgentNode {
|
||||
port: socket_addr.port(),
|
||||
key,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_nym_node(&self) -> bool {
|
||||
matches!(self, NoiseNode::NymNode { .. })
|
||||
}
|
||||
}
|
||||
|
||||
/// A single network monitor agent identified by its port on a shared host.
|
||||
///
|
||||
/// Multiple agents may share an IP address but are guaranteed to have unique ports.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetworkMonitorAgentNode {
|
||||
pub port: u16,
|
||||
pub key: VersionedNoiseKeyV1,
|
||||
keys: Arc<SocketAddrToKey>,
|
||||
support: Arc<IpAddrToVersion>,
|
||||
}
|
||||
|
||||
impl NoiseNetworkView {
|
||||
pub fn new(nodes: HashMap<IpAddr, NoiseNode>) -> Self {
|
||||
// ensure we're always storing canonical IPs
|
||||
NoiseNetworkView {
|
||||
inner: Arc::new(NoiseNetworkViewInner {
|
||||
update_lock: Mutex::new(()),
|
||||
nodes: ArcSwap::from_pointee(
|
||||
nodes
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_canonical(), v))
|
||||
.collect(),
|
||||
),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self::new(Default::default())
|
||||
}
|
||||
|
||||
/// Build a noise view pre-populated with network monitor agents (used at startup).
|
||||
pub fn new_with_agents(agents: HashMap<IpAddr, Vec<NetworkMonitorAgentNode>>) -> Self {
|
||||
let mut nodes = HashMap::new();
|
||||
for (ip, agent_nodes) in agents {
|
||||
nodes.insert(ip, NoiseNode::NetworkMonitorAgent { nodes: agent_nodes });
|
||||
NoiseNetworkView {
|
||||
keys: Default::default(),
|
||||
support: Default::default(),
|
||||
}
|
||||
Self::new(nodes)
|
||||
}
|
||||
|
||||
pub async fn get_update_permit(&self) -> MutexGuard<'_, ()> {
|
||||
self.inner.update_lock.lock().await
|
||||
}
|
||||
|
||||
/// Atomically replace the noise key map.
|
||||
///
|
||||
/// # Precondition
|
||||
///
|
||||
/// The caller **must** hold the permit returned by [`NoiseNetworkView::get_update_permit`].
|
||||
/// Passing the `MutexGuard` by value enforces this at the type level — the guard is dropped
|
||||
/// (releasing the lock) only after the swap completes, preventing torn writes from concurrent
|
||||
/// update calls.
|
||||
pub fn swap_view(&self, _permit: MutexGuard<'_, ()>, new: HashMap<IpAddr, NoiseNode>) {
|
||||
// defensive: ensure stored keys are always canonical so lookups (which canonicalise)
|
||||
// always match. callers should still canonicalise before assembling `new` to keep
|
||||
// collision resolution deterministic.
|
||||
let canonical = new
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_canonical(), v))
|
||||
.collect();
|
||||
self.inner.nodes.store(Arc::new(canonical));
|
||||
}
|
||||
|
||||
pub fn all_nodes(&self) -> HashMap<IpAddr, NoiseNode> {
|
||||
self.inner.nodes.load().as_ref().clone()
|
||||
pub fn swap_view(&self, new: HashMap<SocketAddr, VersionedNoiseKeyV1>) {
|
||||
let noise_support = new
|
||||
.iter()
|
||||
.map(|(s_addr, key)| (s_addr.ip(), key.supported_version))
|
||||
.collect::<HashMap<_, _>>();
|
||||
self.keys.inner.store(Arc::new(new));
|
||||
self.support.inner.store(Arc::new(noise_support));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,38 +126,20 @@ impl NoiseConfig {
|
||||
self
|
||||
}
|
||||
|
||||
/// Look up the noise key for a specific remote socket address.
|
||||
///
|
||||
/// Used on the **initiator** path where we need the responder's public key
|
||||
/// to start the handshake. For nym-nodes the port is ignored (one key per IP);
|
||||
/// for network monitor agents, the port disambiguates which agent's key to use.
|
||||
pub(crate) fn get_noise_key(&self, address: SocketAddr) -> Option<VersionedNoiseKeyV1> {
|
||||
let ip_to_check = address.ip().to_canonical();
|
||||
let nodes = self.network.inner.nodes.load();
|
||||
|
||||
// Resolve the noise key for `address` from a loaded snapshot of the node map.
|
||||
// For [`NoiseNode::NymNode`] entries the port is irrelevant — only the IP is matched.
|
||||
// For [`NoiseNode::NetworkMonitorAgent`] entries the port selects the specific agent.
|
||||
match nodes.get(&ip_to_check)? {
|
||||
NoiseNode::NymNode { key } => Some(*key),
|
||||
NoiseNode::NetworkMonitorAgent { nodes } => {
|
||||
let port = address.port();
|
||||
nodes.iter().find(|n| n.port == port).map(|n| n.key)
|
||||
}
|
||||
}
|
||||
pub(crate) fn get_noise_key(&self, s_address: &SocketAddr) -> Option<VersionedNoiseKeyV1> {
|
||||
self.network.keys.inner.load().get(s_address).copied()
|
||||
}
|
||||
|
||||
/// Check whether a remote IP is known to support noise.
|
||||
/// Used on the responder path where we don't need the remote's key
|
||||
/// (the initiator sends it during the handshake).
|
||||
// note: in the case of network monitor agents, it must hold
|
||||
// that ALL agents on given host support it (or don't support it)
|
||||
pub(crate) fn supports_noise(&self, ip_addr: IpAddr) -> bool {
|
||||
self.network
|
||||
.inner
|
||||
.nodes
|
||||
.load()
|
||||
.contains_key(&ip_addr.to_canonical())
|
||||
// Only for phased update
|
||||
//SW This can lead to some troubles if two nodes share the same IP and one support Noise but not the other.
|
||||
// This in only for the progressive update though and there is no workaround
|
||||
pub(crate) fn get_noise_support(&self, ip_addr: IpAddr) -> Option<NoiseVersion> {
|
||||
let plain_ip_support = self.network.support.inner.load().get(&ip_addr).copied();
|
||||
|
||||
// SW default bind address being [::]:1789, it can happen that a responder sees the ipv6-mapped address of the initiator, this check for that
|
||||
let canonical_ip = &ip_addr.to_canonical();
|
||||
let canonical_ip_support = self.network.support.inner.load().get(canonical_ip).copied();
|
||||
plain_ip_support.or(canonical_ip_support)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,240 +169,4 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod noise_key_lookup {
|
||||
use super::super::*;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_noise_keys::NoiseVersion;
|
||||
use nym_test_utils::helpers::deterministic_rng;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
fn dummy_key(seed: u8) -> VersionedNoiseKeyV1 {
|
||||
VersionedNoiseKeyV1 {
|
||||
supported_version: NoiseVersion::V1,
|
||||
x25519_pubkey: x25519::PublicKey::from([seed; 32]),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_config(nodes: HashMap<IpAddr, NoiseNode>) -> NoiseConfig {
|
||||
NoiseConfig::new(
|
||||
Arc::new(x25519::KeyPair::new(&mut deterministic_rng())),
|
||||
NoiseNetworkView::new(nodes),
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
}
|
||||
|
||||
// -- get_noise_key tests --
|
||||
|
||||
#[test]
|
||||
fn nym_node_key_returned_regardless_of_port() {
|
||||
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
|
||||
let key = dummy_key(1);
|
||||
let config = make_config(HashMap::from([(ip, NoiseNode::new_nym_node(key))]));
|
||||
|
||||
// any port should resolve to the same key
|
||||
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 1000)), Some(key));
|
||||
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 9999)), Some(key));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_key_resolved_by_port() {
|
||||
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
|
||||
let key_a = dummy_key(1);
|
||||
let key_b = dummy_key(2);
|
||||
|
||||
let node = NoiseNode::NetworkMonitorAgent {
|
||||
nodes: vec![
|
||||
NetworkMonitorAgentNode {
|
||||
port: 1000,
|
||||
key: key_a,
|
||||
},
|
||||
NetworkMonitorAgentNode {
|
||||
port: 2000,
|
||||
key: key_b,
|
||||
},
|
||||
],
|
||||
};
|
||||
let config = make_config(HashMap::from([(ip, node)]));
|
||||
|
||||
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 1000)), Some(key_a));
|
||||
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 2000)), Some(key_b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_unknown_port_returns_none() {
|
||||
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
|
||||
let node = NoiseNode::NetworkMonitorAgent {
|
||||
nodes: vec![NetworkMonitorAgentNode {
|
||||
port: 1000,
|
||||
key: dummy_key(1),
|
||||
}],
|
||||
};
|
||||
let config = make_config(HashMap::from([(ip, node)]));
|
||||
|
||||
assert!(config.get_noise_key(SocketAddr::new(ip, 9999)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completely_unknown_address_returns_none() {
|
||||
let config = make_config(HashMap::new());
|
||||
|
||||
assert!(config
|
||||
.get_noise_key(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 80))
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_ipv6_fallback_for_nym_node() {
|
||||
// register under the plain IPv4 address
|
||||
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
|
||||
let key = dummy_key(1);
|
||||
let config = make_config(HashMap::from([(v4, NoiseNode::new_nym_node(key))]));
|
||||
|
||||
// query with the IPv4-mapped IPv6 form (::ffff:1.2.3.4)
|
||||
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
|
||||
assert_eq!(
|
||||
config.get_noise_key(SocketAddr::new(v6_mapped, 1789)),
|
||||
Some(key)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_ipv6_fallback_for_agent() {
|
||||
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
|
||||
let key = dummy_key(1);
|
||||
let node = NoiseNode::NetworkMonitorAgent {
|
||||
nodes: vec![NetworkMonitorAgentNode { port: 1000, key }],
|
||||
};
|
||||
let config = make_config(HashMap::from([(v4, node)]));
|
||||
|
||||
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
|
||||
assert_eq!(
|
||||
config.get_noise_key(SocketAddr::new(v6_mapped, 1000)),
|
||||
Some(key)
|
||||
);
|
||||
// wrong port still returns None even with the fallback
|
||||
assert!(config
|
||||
.get_noise_key(SocketAddr::new(v6_mapped, 9999))
|
||||
.is_none());
|
||||
}
|
||||
|
||||
// -- supports_noise tests --
|
||||
|
||||
#[test]
|
||||
fn supports_noise_true_for_nym_node() {
|
||||
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
|
||||
let config = make_config(HashMap::from([(ip, NoiseNode::new_nym_node(dummy_key(1)))]));
|
||||
|
||||
assert!(config.supports_noise(ip));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_noise_true_for_agent_ip() {
|
||||
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
|
||||
let node = NoiseNode::NetworkMonitorAgent {
|
||||
nodes: vec![NetworkMonitorAgentNode {
|
||||
port: 1000,
|
||||
key: dummy_key(1),
|
||||
}],
|
||||
};
|
||||
let config = make_config(HashMap::from([(ip, node)]));
|
||||
|
||||
assert!(config.supports_noise(ip));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_noise_false_for_unknown_ip() {
|
||||
let config = make_config(HashMap::new());
|
||||
|
||||
assert!(!config.supports_noise(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_noise_canonical_ipv6_fallback() {
|
||||
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
|
||||
let config = make_config(HashMap::from([(v4, NoiseNode::new_nym_node(dummy_key(1)))]));
|
||||
|
||||
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
|
||||
assert!(config.supports_noise(v6_mapped));
|
||||
}
|
||||
|
||||
// -- new_with_agents test --
|
||||
|
||||
#[test]
|
||||
fn new_with_agents_builds_correct_view() {
|
||||
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
|
||||
let key_a = dummy_key(1);
|
||||
let key_b = dummy_key(2);
|
||||
|
||||
let agents = HashMap::from([(
|
||||
ip,
|
||||
vec![
|
||||
NetworkMonitorAgentNode {
|
||||
port: 1000,
|
||||
key: key_a,
|
||||
},
|
||||
NetworkMonitorAgentNode {
|
||||
port: 2000,
|
||||
key: key_b,
|
||||
},
|
||||
],
|
||||
)]);
|
||||
|
||||
let config = NoiseConfig::new(
|
||||
Arc::new(x25519::KeyPair::new(&mut deterministic_rng())),
|
||||
NoiseNetworkView::new_with_agents(agents),
|
||||
Duration::from_secs(5),
|
||||
);
|
||||
|
||||
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 1000)), Some(key_a));
|
||||
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 2000)), Some(key_b));
|
||||
assert!(config.supports_noise(ip));
|
||||
}
|
||||
|
||||
// -- swap_view canonicalisation test --
|
||||
|
||||
// Regression: an agent registered via blockchain events flows through `swap_view` (called
|
||||
// from `NetworkMonitorAgentsModule::new_agent` and from the periodic network refresher).
|
||||
// If a non-canonical (IPv4-mapped IPv6) key reaches `swap_view`, lookups via
|
||||
// `supports_noise` (which canonicalises) used to miss, producing the
|
||||
// "can't speak Noise yet, falling back to TCP" warning despite the agent being correctly
|
||||
// authorised in the routing filter.
|
||||
#[tokio::test]
|
||||
async fn swap_view_canonicalises_non_canonical_keys() {
|
||||
let view = NoiseNetworkView::new_empty();
|
||||
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
|
||||
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
|
||||
|
||||
let mut nodes = HashMap::new();
|
||||
// intentionally insert under the IPv4-mapped form — what a buggy caller might do
|
||||
nodes.insert(
|
||||
v6_mapped,
|
||||
NoiseNode::NetworkMonitorAgent {
|
||||
nodes: vec![NetworkMonitorAgentNode {
|
||||
port: 1000,
|
||||
key: dummy_key(1),
|
||||
}],
|
||||
},
|
||||
);
|
||||
|
||||
let permit = view.get_update_permit().await;
|
||||
view.swap_view(permit, nodes);
|
||||
|
||||
let config = NoiseConfig::new(
|
||||
Arc::new(x25519::KeyPair::new(&mut deterministic_rng())),
|
||||
view,
|
||||
Duration::from_secs(5),
|
||||
);
|
||||
|
||||
// lookup via either form must succeed
|
||||
assert!(config.supports_noise(v4));
|
||||
assert!(config.supports_noise(v6_mapped));
|
||||
assert!(config
|
||||
.get_noise_key(SocketAddr::new(v6_mapped, 1000))
|
||||
.is_some());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,6 @@ pub enum Connection<C> {
|
||||
Noise(#[pin] Box<NoiseStream<C>>),
|
||||
}
|
||||
|
||||
impl<C> Connection<C> {
|
||||
pub fn is_noise(&self) -> bool {
|
||||
matches!(self, Connection::Noise(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> AsyncRead for Connection<C>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
|
||||
@@ -66,7 +66,7 @@ pub async fn upgrade_noise_initiator(
|
||||
Error::Prereq(Prerequisite::RemotePublicKey)
|
||||
})?;
|
||||
|
||||
let Some(key) = config.get_noise_key(responder_addr) else {
|
||||
let Some(key) = config.get_noise_key(&responder_addr) else {
|
||||
warn!("{responder_addr} can't speak Noise yet, falling back to TCP");
|
||||
return Ok(Connection::Raw(conn));
|
||||
};
|
||||
@@ -106,7 +106,7 @@ pub async fn upgrade_noise_responder(
|
||||
};
|
||||
|
||||
// if responder doesn't announce noise support, we fallback to tcp
|
||||
if !config.supports_noise(initiator_addr.ip()) {
|
||||
if config.get_noise_support(initiator_addr.ip()).is_none() {
|
||||
warn!("{initiator_addr} can't speak Noise yet, falling back to TCP",);
|
||||
return Ok(Connection::Raw(conn));
|
||||
};
|
||||
|
||||
@@ -110,12 +110,6 @@ pub enum PacketProcessingError {
|
||||
PacketReplay,
|
||||
}
|
||||
|
||||
impl PacketProcessingError {
|
||||
pub fn is_replay(&self) -> bool {
|
||||
matches!(self, PacketProcessingError::PacketReplay)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PartialyUnwrappedPacketWithKeyRotation {
|
||||
pub packet: PartiallyUnwrappedPacket,
|
||||
pub used_key_rotation: u32,
|
||||
|
||||
@@ -29,7 +29,7 @@ pub use sphinx_packet::{
|
||||
packet::builder::DEFAULT_PAYLOAD_SIZE,
|
||||
payload::{
|
||||
PAYLOAD_OVERHEAD_SIZE, Payload,
|
||||
key::{PayloadKey, PayloadKeySeed, derive_payload_key},
|
||||
key::{PayloadKey, PayloadKeySeed},
|
||||
},
|
||||
route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier},
|
||||
surb::{SURB, SURBMaterial},
|
||||
|
||||
@@ -7,9 +7,8 @@ use nyxd_scraper_shared::NyxdScraper;
|
||||
pub use nyxd_scraper_shared::constants;
|
||||
pub use nyxd_scraper_shared::error::ScraperError;
|
||||
pub use nyxd_scraper_shared::{
|
||||
BlockModule, DecodedMessage, MsgModule, NyxdScraperTransaction, ParsedTransactionDetails,
|
||||
ParsedTransactionResponse, PruningOptions, PruningStrategy, StartingBlockOpts, TxModule,
|
||||
parse_msg,
|
||||
BlockModule, MsgModule, NyxdScraperTransaction, ParsedTransactionResponse, PruningOptions,
|
||||
PruningStrategy, StartingBlockOpts, TxModule,
|
||||
};
|
||||
pub use storage::models;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::str::FromStr;
|
||||
// replicate behaviour of `CosmosMessageAddressesParser` from juno
|
||||
pub(crate) fn parse_addresses_from_events(tx: &ParsedTransactionResponse) -> Vec<String> {
|
||||
let mut addresses: Vec<String> = Vec::new();
|
||||
for event in &tx.tx_details.tx_result.events {
|
||||
for event in &tx.tx_result.events {
|
||||
for attribute in &event.attributes {
|
||||
let Ok(value) = attribute.value_str() else {
|
||||
continue;
|
||||
|
||||
@@ -147,7 +147,6 @@ impl PostgresStorageTransaction {
|
||||
for chain_tx in txs {
|
||||
// bdjuno style, base64 encode them
|
||||
let signatures = chain_tx
|
||||
.tx_details
|
||||
.tx
|
||||
.signatures
|
||||
.iter()
|
||||
@@ -155,14 +154,12 @@ impl PostgresStorageTransaction {
|
||||
.collect();
|
||||
|
||||
let messages = chain_tx
|
||||
.decoded_messages
|
||||
.parsed_messages
|
||||
.values()
|
||||
.map(|msg| &msg.decoded_content)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let signer_infos = chain_tx
|
||||
.tx_details
|
||||
.tx
|
||||
.auth_info
|
||||
.signer_infos
|
||||
@@ -170,28 +167,28 @@ impl PostgresStorageTransaction {
|
||||
.map(|info| proto::cosmos::tx::v1beta1::SignerInfo::from(info.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let hash = chain_tx.tx_details.hash.to_string();
|
||||
let height = chain_tx.tx_details.height().into();
|
||||
let index = chain_tx.tx_details.index as i32;
|
||||
let hash = chain_tx.hash.to_string();
|
||||
let height = chain_tx.height.into();
|
||||
let index = chain_tx.index as i32;
|
||||
|
||||
let log = serde_json::to_value(chain_tx.tx_details.tx_result.log.clone())
|
||||
let log = serde_json::to_value(chain_tx.tx_result.log.clone())
|
||||
.inspect_err(|e| error!(hash, height, index, "Failed to parse logs: {e}"))
|
||||
.unwrap_or_default();
|
||||
let events = &chain_tx.tx_details.tx_result.events;
|
||||
let events = &chain_tx.tx_result.events;
|
||||
|
||||
insert_transaction(
|
||||
hash,
|
||||
height,
|
||||
index,
|
||||
chain_tx.tx_details.tx_result.code.is_ok(),
|
||||
chain_tx.tx_result.code.is_ok(),
|
||||
serde_json::Value::Array(messages),
|
||||
chain_tx.tx_details.tx.body.memo.clone(),
|
||||
chain_tx.tx.body.memo.clone(),
|
||||
signatures,
|
||||
serde_json::to_value(signer_infos)?,
|
||||
serde_json::to_value(&chain_tx.tx_details.tx.auth_info.fee)?,
|
||||
chain_tx.tx_details.tx_result.gas_wanted,
|
||||
chain_tx.tx_details.tx_result.gas_used,
|
||||
chain_tx.tx_details.tx_result.log.clone(),
|
||||
serde_json::to_value(&chain_tx.tx.auth_info.fee)?,
|
||||
chain_tx.tx_result.gas_wanted,
|
||||
chain_tx.tx_result.gas_used,
|
||||
chain_tx.tx_result.log.clone(),
|
||||
json!(log),
|
||||
json!(events),
|
||||
self.inner.as_mut(),
|
||||
@@ -210,20 +207,17 @@ impl PostgresStorageTransaction {
|
||||
|
||||
for chain_tx in txs {
|
||||
let involved_addresses = parse_addresses_from_events(chain_tx);
|
||||
for (index, msg) in chain_tx.tx_details.tx.body.messages.iter().enumerate() {
|
||||
let parsed_message = chain_tx
|
||||
.decoded_messages
|
||||
.get(&index)
|
||||
.map(|msg| &msg.decoded_content);
|
||||
for (index, msg) in chain_tx.tx.body.messages.iter().enumerate() {
|
||||
let parsed_message = chain_tx.parsed_messages.get(&index);
|
||||
let value = serde_json::to_value(parsed_message)?;
|
||||
|
||||
insert_message(
|
||||
chain_tx.tx_details.hash.to_string(),
|
||||
chain_tx.hash.to_string(),
|
||||
index as i64,
|
||||
msg.type_url.clone(),
|
||||
value,
|
||||
involved_addresses.clone(),
|
||||
chain_tx.tx_details.height().into(),
|
||||
chain_tx.height.into(),
|
||||
self.inner.as_mut(),
|
||||
)
|
||||
.await?
|
||||
|
||||
@@ -33,9 +33,9 @@ impl TxModule for FancyTxModule {
|
||||
async fn handle_tx(&mut self, tx: &ParsedTransactionResponse) -> Result<(), ScraperError> {
|
||||
println!(
|
||||
"✨ got new tx for height {}: {} ({} msgs)",
|
||||
tx.tx_details.height(),
|
||||
tx.tx_details.hash,
|
||||
tx.tx_details.tx.body.messages.len()
|
||||
tx.block.header.height,
|
||||
tx.hash,
|
||||
tx.parsed_messages.len()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -281,7 +281,7 @@ where
|
||||
&mut self,
|
||||
block: BlockToProcess,
|
||||
) -> Result<(), ScraperError> {
|
||||
debug!("processing block at height {}", block.height);
|
||||
info!("processing block at height {}", block.height);
|
||||
|
||||
let full_info = self
|
||||
.rpc_client
|
||||
@@ -291,13 +291,8 @@ where
|
||||
if let Some(tx_info) = &full_info.transactions {
|
||||
debug!("this block has {} transaction(s)", tx_info.len());
|
||||
for tx in tx_info {
|
||||
let details = &tx.tx_details;
|
||||
debug!(
|
||||
"{} has {} message(s)",
|
||||
details.hash,
|
||||
details.tx.body.messages.len()
|
||||
);
|
||||
for (index, msg) in details.tx.body.messages.iter().enumerate() {
|
||||
debug!("{} has {} message(s)", tx.hash, tx.tx.body.messages.len());
|
||||
for (index, msg) in tx.tx.body.messages.iter().enumerate() {
|
||||
debug!("{index}: {:?}", msg.type_url)
|
||||
}
|
||||
}
|
||||
@@ -320,24 +315,11 @@ where
|
||||
for tx_module in &mut self.tx_modules {
|
||||
tx_module.handle_tx(block_tx).await?;
|
||||
}
|
||||
let tx_details = &block_tx.tx_details;
|
||||
|
||||
// the ones concerned with individual messages
|
||||
for (index, msg) in tx_details.tx.body.messages.iter().enumerate() {
|
||||
let Some(decoded) = block_tx.decoded_messages.get(&index) else {
|
||||
warn!(
|
||||
"height: {} tx: {} tx_index: {}, msg_index: {index}: message failed to get decoded",
|
||||
tx_details.height(),
|
||||
tx_details.hash,
|
||||
tx_details.index,
|
||||
);
|
||||
continue;
|
||||
};
|
||||
for (index, msg) in block_tx.tx.body.messages.iter().enumerate() {
|
||||
for msg_module in &mut self.msg_modules {
|
||||
if msg.type_url == msg_module.type_url() {
|
||||
msg_module
|
||||
.handle_msg(index, msg, decoded, tx_details)
|
||||
.await?
|
||||
msg_module.handle_msg(index, msg, block_tx).await?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,9 @@ use tendermint::{Block, Hash, abci, block, tx};
|
||||
use tendermint_rpc::endpoint::{block as block_endpoint, block_results, validators};
|
||||
use tendermint_rpc::event::{Event, EventData};
|
||||
|
||||
/// Message decoded from the raw transaction and converted into json.
|
||||
/// Note that it might have gone through additional processing as set by the `MessageRegistry`
|
||||
// just get all everything out of tx::Response, but parse raw `tx` bytes
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DecodedMessage {
|
||||
pub type_url: String,
|
||||
pub decoded_content: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ParsedTransactionDetails {
|
||||
pub struct ParsedTransactionResponse {
|
||||
/// The hash of the transaction.
|
||||
///
|
||||
/// Deserialized from a hex-encoded string (there is a discrepancy between
|
||||
@@ -24,6 +17,8 @@ pub struct ParsedTransactionDetails {
|
||||
/// the Tendermint RPC).
|
||||
pub hash: Hash,
|
||||
|
||||
pub height: block::Height,
|
||||
|
||||
pub index: u32,
|
||||
|
||||
pub tx_result: abci::types::ExecTxResult,
|
||||
@@ -32,23 +27,13 @@ pub struct ParsedTransactionDetails {
|
||||
|
||||
pub proof: Option<tx::Proof>,
|
||||
|
||||
pub parsed_messages: BTreeMap<usize, serde_json::Value>,
|
||||
|
||||
pub parsed_message_urls: BTreeMap<usize, String>,
|
||||
|
||||
pub block: Block,
|
||||
}
|
||||
|
||||
impl ParsedTransactionDetails {
|
||||
pub fn height(&self) -> block::Height {
|
||||
self.block.header.height
|
||||
}
|
||||
}
|
||||
|
||||
// just get all everything out of tx::Response, but parse raw `tx` bytes
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ParsedTransactionResponse {
|
||||
pub tx_details: ParsedTransactionDetails,
|
||||
|
||||
pub decoded_messages: BTreeMap<usize, DecodedMessage>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FullBlockInformation {
|
||||
/// Basic block information, including its signers.
|
||||
|
||||
@@ -82,8 +82,10 @@ pub enum ScraperError {
|
||||
source: cosmrs::ErrorReport,
|
||||
},
|
||||
|
||||
#[error("could not parse msg of type {type_url}: {source}")]
|
||||
#[error("could not parse msg in tx {hash} at index {index} into {type_url}: {source}")]
|
||||
MsgParseFailure {
|
||||
hash: Hash,
|
||||
index: usize,
|
||||
type_url: String,
|
||||
#[source]
|
||||
source: cosmrs::ErrorReport,
|
||||
|
||||
@@ -47,7 +47,7 @@ pub fn validator_consensus_address(id: account::Id) -> Result<AccountId, Malform
|
||||
}
|
||||
|
||||
pub fn tx_gas_sum(txs: &[ParsedTransactionResponse]) -> i64 {
|
||||
txs.iter().map(|tx| tx.tx_details.tx_result.gas_used).sum()
|
||||
txs.iter().map(|tx| tx.tx_result.gas_used).sum()
|
||||
}
|
||||
|
||||
pub fn validator_info(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user