Merge branch 'develop' into max/smolmix-companions-clean

This commit is contained in:
mfahampshire
2026-06-08 22:50:55 +01:00
1111 changed files with 104952 additions and 69163 deletions
+2
View File
@@ -0,0 +1,2 @@
[target.wasm32-unknown-unknown]
rustflags = ["--cfg=getrandom_backend=\"wasm_js\""]
+2 -2
View File
@@ -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
+3 -3
View File
@@ -21,12 +21,12 @@ 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
version: 11.1.2
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
+7 -4
View File
@@ -17,13 +17,16 @@ jobs:
run: sudo apt-get install rsync
continue-on-error: true
- uses: rlespinasse/github-slug-action@v3.x
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
version: 11.1.2
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup yarn
run: npm install -g yarn
node-version: 24
cache: pnpm
- name: Build
run: yarn && yarn build && yarn build:ci:storybook
run: pnpm install && pnpm build && pnpm build:ci:storybook
- name: Deploy branch to CI www (storybook)
continue-on-error: true
uses: easingthemes/ssh-deploy@main
@@ -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: |
-1
View File
@@ -23,7 +23,6 @@ on:
- 'sdk/ffi/**'
- 'sdk/rust/**'
- 'service-providers/**'
- 'nym-browser-extension/storage/**'
- 'tools/**'
- 'wasm/**'
- 'Cargo.toml'
+19
View File
@@ -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
@@ -40,7 +40,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: 24
- name: Validate version format
run: |
@@ -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: |
+5 -1
View File
@@ -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 }}
+30 -3
View File
@@ -19,6 +19,7 @@ jobs:
RUSTUP_PERMIT_COPY_RENAME: 1
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repo
uses: actions/checkout@v6
@@ -41,7 +42,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: 24
- name: Validate version format
run: |
@@ -58,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: |
@@ -68,9 +71,33 @@ jobs:
- 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}"
git checkout -b "$PR_BRANCH"
git add -A
git commit -m "crates release: bump version to ${{ inputs.version }}"
git push
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
+12 -6
View File
@@ -7,7 +7,10 @@ on:
paths:
- "documentation/docs/**"
- "sdk/typescript/packages/sdk/src/**"
- "sdk/typescript/packages/mix-tunnel/src/**"
- "sdk/typescript/packages/mix-fetch/src/**"
- "sdk/typescript/packages/mix-dns/src/**"
- "sdk/typescript/packages/mix-websocket/src/**"
- ".github/workflows/ci-docs.yml"
jobs:
@@ -28,12 +31,12 @@ 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
version: 11.1.2
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
@@ -47,7 +50,7 @@ jobs:
- name: Check if TypeScript SDK source changed
id: check-ts-sdk
run: |
if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -qE '^sdk/typescript/packages/(sdk|mix-fetch)/src/'; then
if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -qE '^sdk/typescript/packages/(sdk|mix-tunnel|mix-fetch|mix-dns|mix-websocket)/src/'; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
@@ -58,8 +61,11 @@ jobs:
if: steps.check-ts-sdk.outputs.changed == 'true'
run: |
npm install -g typedoc@0.25.13 typedoc-plugin-markdown@4.0.3
cd ${{ github.workspace }}/sdk/typescript/packages/sdk && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/mix-fetch && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/sdk && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/mix-tunnel && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/mix-fetch && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/mix-dns && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/mix-websocket && typedoc --skipErrorChecking
- name: Verify doc versions
run: ${{ github.workspace }}/documentation/scripts/verify-doc-versions.sh
+17 -13
View File
@@ -20,12 +20,14 @@ jobs:
- uses: actions/checkout@v6
- uses: rlespinasse/github-slug-action@v3.x
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
version: 11.1.2
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup yarn
run: npm install -g yarn
node-version: 24
cache: pnpm
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
@@ -38,22 +40,24 @@ jobs:
- name: Install wasm-opt
run: cargo install wasm-opt
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.24.6"
# Produce wasm/smolmix/pkg/package.json before any pnpm step. The
# `pnpm dev:on` in `prebuild:ci` adds wasm/smolmix/pkg to the dynamic
# workspace; mix-tunnel's `workspace:*` lookup against @nymproject/
# smolmix-wasm needs the package.json to be present.
- name: Build smolmix wasm
run: make -C wasm/smolmix
- name: Install
run: yarn
run: pnpm i
- name: Build packages
run: yarn build:ci
run: pnpm build:ci
- name: Install again
run: yarn
run: pnpm i
- name: Lint
run: yarn lint
run: pnpm lint
- name: Typecheck with tsc
run: yarn tsc
run: pnpm tsc
+15 -10
View File
@@ -1,6 +1,7 @@
name: ci-nym-wallet-frontend
on:
workflow_dispatch:
pull_request:
paths:
- 'nym-wallet/**'
@@ -12,30 +13,34 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
version: 11.1.2
- uses: actions/setup-node@v4
with:
node-version-file: nym-wallet/.nvmrc
cache: yarn
cache-dependency-path: yarn.lock
cache: pnpm
- name: Install dependencies
run: yarn install --network-timeout 100000
run: pnpm install
- name: Build TypeScript packages (wallet depends on @nymproject/types, etc.)
run: yarn build:types
run: pnpm build:types
- name: Build @nymproject/mui-theme and @nymproject/react (wallet imports subpaths)
run: yarn build:packages
run: pnpm build:packages
- name: Typecheck nym-wallet
run: yarn --cwd nym-wallet tsc
run: pnpm --filter @nymproject/nym-wallet-app tsc
- name: Lint nym-wallet
run: yarn --cwd nym-wallet lint
run: pnpm --filter @nymproject/nym-wallet-app lint
- name: Yarn audit (workspace lockfile; informational)
run: yarn audit --level critical
- name: pnpm audit (workspace lockfile; informational)
run: pnpm audit --audit-level critical
continue-on-error: true
- name: Unit tests (nym-wallet)
run: yarn --cwd nym-wallet test
run: pnpm --filter @nymproject/nym-wallet-app test
+1 -6
View File
@@ -20,7 +20,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
- uses: actions-rs/toolchain@v1
with:
@@ -30,11 +30,6 @@ jobs:
override: true
components: rustfmt, clippy
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.24.6"
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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: |
+2 -2
View File
@@ -27,14 +27,14 @@ 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
+13 -10
View File
@@ -23,10 +23,13 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Node
uses: actions/setup-node@v4
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
node-version: 22.13.0
version: 11.1.2
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
@@ -68,17 +71,17 @@ jobs:
fileName: '.env'
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
- name: Yarn cache clean
- name: pnpm cache clean
shell: bash
run: cd .. && yarn cache clean
run: cd .. && pnpm cache delete
- name: Install project dependencies
shell: bash
run: cd .. && yarn --network-timeout 100000
run: cd .. && pnpm i
- name: Yarn build
- name: Build
shell: bash
run: cd .. && yarn build
run: cd .. && pnpm build
- name: Install dependencies and build it
env:
@@ -97,7 +100,7 @@ jobs:
TAURI_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
TAURI_NOTARIZATION_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
yarn build-macx86
pnpm build-macx86
- name: Create app tarball
run: |
@@ -108,7 +111,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
@@ -26,12 +26,17 @@ jobs:
libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev jq \
libgtk-3-dev squashfs-tools libayatana-appindicator3-dev make libfuse2 unzip librsvg2-dev file \
libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
version: 11.1.2
- name: Node
uses: actions/setup-node@v4
with:
node-version: 22.13.0
cache: 'yarn'
node-version: 24
cache: 'pnpm'
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
@@ -40,10 +45,10 @@ jobs:
- name: Install project dependencies
shell: bash
run: cd .. && yarn --network-timeout 100000
run: cd .. && pnpm i
- name: Install app dependencies
run: yarn
run: pnpm
- name: Create env file
uses: timheuer/base64-to-file@v1.2
@@ -52,7 +57,7 @@ jobs:
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
- name: Build app
run: yarn build
run: pnpm build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
@@ -132,7 +137,7 @@ jobs:
fi
- name: Upload Artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: nym-wallet-appimage.tar.gz
path: |
+11 -14
View File
@@ -38,18 +38,15 @@ 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.
- name: Node
uses: actions/setup-node@v4
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
node-version: 22.13.0
- name: Install Yarn (classic)
shell: bash
run: npm install -g yarn@1.22.22
version: 11.1.2
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Strip Authenticode thumbprint (avoid signtool on runner)
working-directory: nym-wallet/src-tauri
@@ -118,11 +115,11 @@ jobs:
' tauri.conf.json
- name: Install project dependencies
shell: bash
run: cd .. && yarn --network-timeout 100000
run: cd .. && pnpm i
- name: Install app dependencies
shell: bash
run: yarn --network-timeout 100000
run: pnpm i
- name: Build and sign it
shell: bash
@@ -136,7 +133,7 @@ jobs:
SSL_COM_TOTP_SECRET: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_TOTP_SECRET }}
run: |
echo "Starting build process..."
yarn build
pnpm build
- name: Check bundle directory
shell: bash
@@ -165,7 +162,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: |
@@ -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,7 +91,7 @@ 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
+23 -14
View File
@@ -1,6 +1,19 @@
name: publish-sdk-npm
on:
workflow_dispatch:
inputs:
dry_run:
description: "Rehearse the publish (pnpm publish --dry-run, no tarballs uploaded). Untick to publish for real."
type: boolean
default: true
dist_tag:
description: "Tag mode. 'auto' picks per package: new packages and same-major releases -> latest; a breaking major (e.g. mix-fetch v2 over v1) -> next, promote later with `npm dist-tag add`. 'next'/'latest' force that tag on all four."
type: choice
options:
- auto
- next
- latest
default: auto
jobs:
publish:
@@ -8,15 +21,17 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
version: 11.1.2
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
registry-url: "https://registry.npmjs.org"
- name: Setup yarn
run: npm install -g yarn
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
@@ -31,21 +46,15 @@ jobs:
- name: Install wasm-opt
run: cargo install wasm-opt
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.24.6"
- name: Update root CA certificate bundle
run: ./wasm/mix-fetch/go-mix-conn/scripts/update-root-certs.sh
- name: Install dependencies
run: yarn
run: pnpm i
- name: Build WASM and Typescript SDK
run: yarn sdk:build
run: pnpm sdk:build
- name: Publish to NPM
env:
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
DRY_RUN: ${{ inputs.dry_run && '1' || '0' }}
NPM_DIST_TAG: ${{ inputs.dist_tag }}
run: ./sdk/typescript/scripts/publish.sh
+1 -1
View File
@@ -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 }}
+1 -1
View File
@@ -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 }}
+1 -1
View File
@@ -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 }}
+1 -1
View File
@@ -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 }}
+1 -1
View File
@@ -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 }}
+1 -1
View File
@@ -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 }}
+2 -2
View File
@@ -23,14 +23,14 @@ jobs:
uses: actions/checkout@v6
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
- uses: nymtech/nym/.github/actions/nym-hash-releases@develop
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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
+4
View File
@@ -79,3 +79,7 @@ CLAUDE.md
/notes
/target-otel
test-tutorials/
# pnpm
.pnpm-store/
+9
View File
@@ -0,0 +1,9 @@
shamefully-hoist=false
prefer-workspace-packages=true
hoist-pattern[]=*eslint*
hoist-pattern[]=*prettier*
hoist-pattern[]=*typescript*
hoist-pattern[]=*@types*
auto-install-peers=true
strict-peer-dependencies=false
+46
View File
@@ -4,6 +4,52 @@ 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
+1250 -972
View File
File diff suppressed because it is too large Load Diff
+150 -147
View File
@@ -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,9 +40,11 @@ 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",
@@ -70,11 +71,15 @@ 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-lp-data",
"common/nym-metrics",
"common/nym_offline_compact_ecash",
"common/nymnoise",
@@ -90,9 +95,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",
@@ -122,13 +127,14 @@ members = [
"common/zulip-client",
"documentation/autodoc",
"gateway",
"integration-tests",
"nym-api",
"nym-api/nym-api-requests",
"nym-authenticator-client",
"nym-browser-extension/storage",
"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",
@@ -140,6 +146,7 @@ members = [
"nym-outfox",
"nym-registration-client",
"nym-signers-monitor",
"nym-sqlx-pool-guard",
"nym-statistics-api",
"nym-validator-rewarder",
"nyx-chain-watcher",
@@ -147,22 +154,18 @@ members = [
"sdk/ffi/go",
"sdk/ffi/shared",
"sdk/rust/nym-sdk",
"smolmix/core",
"smolmix/dns",
"smolmix/tls",
"smolmix/hyper",
"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",
@@ -171,31 +174,30 @@ members = [
"tools/nymvisor",
"tools/ts-rs-cli",
"wasm/client",
# "wasm/full-nym-wasm", # If we uncomment this again, remember to also uncomment the profile settings below
"wasm/mix-fetch",
"wasm/node-tester",
"wasm/smolmix",
"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",
"tools/internal/localnet-orchestrator"
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
"nym-network-monitor-v3/nym-network-monitor-agent",
]
exclude = ["contracts", "nym-wallet", "cpu-cycles"]
@@ -209,7 +211,7 @@ edition = "2024"
license = "Apache-2.0"
rust-version = "1.87.0"
readme = "README.md"
version = "1.20.4"
version = "1.21.0"
[workspace.dependencies]
addr = "0.15.6"
@@ -223,10 +225,11 @@ anyhow = "1.0.98"
arc-swap = "1.7.1"
argon2 = "0.5.0"
async-trait = "0.1.88"
axum = "0.7.5"
axum-client-ip = "0.6.1"
axum-extra = "0.9.4"
axum-test = "16.2.0"
async-tungstenite = { version = "0.24", default-features = false }
axum = "0.8.9"
axum-client-ip = "1.3.1"
axum-extra = "0.12.6"
axum-test = "20.0.0"
base64 = "0.22.1"
base85rs = "0.1.3"
bincode = "1.3.3"
@@ -251,7 +254,7 @@ clap_complete_fig = "4.5"
colored = "2.2"
comfy-table = "7.1.4"
console = "0.16.0"
console-subscriber = "0.4.1"
console-subscriber = "0.5.0"
console_error_panic_hook = "0.1"
const-str = "0.5.6"
const_format = "0.2.34"
@@ -276,24 +279,27 @@ eyre = "0.6.9"
fastrand = "2.1.1"
flate2 = "1.1.1"
futures = "0.3.31"
futures-rustls = { version = "0.26", default-features = false }
futures-util = "0.3"
generic-array = "0.14.7"
getrandom = "0.2.10"
getrandom03 = { package = "getrandom", version = "=0.3.3" }
getrandom04 = { package = "getrandom", version = "0.4" }
glob = "0.3"
handlebars = "3.5.5"
hex = "0.4.3"
hickory-proto = "0.26.1"
hickory-proto = { version = "0.26.1", default-features = false }
hickory-resolver = "0.26.1"
hkdf = "0.12.3"
hmac = "0.12.1"
http = "1"
http-body-util = "0.1"
httparse = "1.10"
httpcodec = "0.2.3"
human-repr = "1.1.0"
humantime = "2.2.0"
humantime-serde = "1.1.1"
hyper = "1.6.0"
hyper = { version = "1.6.0", default-features = false }
hyper-util = "0.1"
indicatif = "0.18.0"
inquire = "0.6.2"
@@ -328,7 +334,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" }
@@ -339,12 +345,14 @@ regex = "1.10.6"
reqwest = { version = "0.13.1", default-features = false }
rs_merkle = "1.5.0"
rustls = { version = "0.23.37", default-features = false }
rustls-pki-types = "1"
rustls-rustcrypto = "0.0.2-alpha"
schemars = "0.8.22"
semver = "1.0.26"
serde = "1.0.219"
serde_bytes = "0.11.17"
serde_derive = "1.0"
serde_json = "1.0.140"
serde_json = { version = "1.0.140", features = ["float_roundtrip"] }
serde_json_path = "0.7.2"
serde_repr = "0.1"
serde_with = "3.9.0"
@@ -352,6 +360,7 @@ serde_yaml = "0.9.25"
serde_plain = "1.0.2"
sha2 = "0.10.3"
si-scale = "0.2.3"
simple-dns = "0.7"
smoltcp = "0.12"
snow = "0.9.6"
sphinx-packet = "=0.6.0"
@@ -362,7 +371,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"
@@ -375,7 +384,7 @@ tokio-test = "0.4.4"
tokio-tun = "0.11.5"
tokio-rustls = "0.26"
tokio-smoltcp = "0.5"
tokio-tungstenite = { version = "0.20.1" }
tokio-tungstenite = "0.20.1"
tokio-util = "0.7.15"
toml = "0.8.22"
tower = "0.5.2"
@@ -393,7 +402,7 @@ uniffi = "0.29.2"
uniffi_build = "0.29.0"
url = "2.5"
utoipa = "5.2"
utoipa-swagger-ui = "8.1"
utoipa-swagger-ui = "9.0.2"
utoipauto = "0.2"
uuid = "1.19.0"
vergen = { version = "=8.3.1", default-features = false }
@@ -404,6 +413,10 @@ 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"
@@ -416,114 +429,115 @@ 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.20.4", path = "nym-api/nym-api-requests" }
nym-authenticator-requests = { version = "1.20.4", path = "common/authenticator-requests" }
nym-async-file-watcher = { version = "1.20.4", path = "common/async-file-watcher" }
nym-authenticator-client = { version = "1.20.4", path = "nym-authenticator-client" }
nym-bandwidth-controller = { version = "1.20.4", path = "common/bandwidth-controller" }
nym-bin-common = { version = "1.20.4", path = "common/bin-common" }
nym-cache = { version = "1.20.4", path = "common/nym-cache" }
nym-client-core = { version = "1.20.4", path = "common/client-core", default-features = false }
nym-client-core-config-types = { version = "1.20.4", path = "common/client-core/config-types" }
nym-client-core-gateways-storage = { version = "1.20.4", path = "common/client-core/gateways-storage" }
nym-client-core-surb-storage = { version = "1.20.4", path = "common/client-core/surb-storage" }
nym-client-websocket-requests = { version = "1.20.4", path = "clients/native/websocket-requests" }
nym-common = { version = "1.20.4", path = "common/nym-common" }
nym-compact-ecash = { version = "1.20.4", path = "common/nym_offline_compact_ecash" }
nym-config = { version = "1.20.4", path = "common/config" }
nym-contracts-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/contracts-common" }
nym-coconut-dkg-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
nym-credential-storage = { version = "1.20.4", path = "common/credential-storage" }
nym-credential-utils = { version = "1.20.4", path = "common/credential-utils" }
nym-credential-proxy-lib = { version = "1.20.4", path = "common/credential-proxy" }
nym-credentials = { version = "1.20.4", path = "common/credentials", default-features = false }
nym-credentials-interface = { version = "1.20.4", path = "common/credentials-interface" }
nym-credential-proxy-requests = { version = "1.20.4", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
nym-credential-verification = { version = "1.20.4", path = "common/credential-verification" }
nym-crypto = { version = "1.20.4", path = "common/crypto", default-features = false }
nym-dkg = { version = "1.20.4", path = "common/dkg" }
nym-ecash-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/ecash-contract" }
nym-ecash-signer-check = { version = "1.20.4", path = "common/ecash-signer-check" }
nym-ecash-signer-check-types = { version = "1.20.4", path = "common/ecash-signer-check-types" }
nym-ecash-time = { version = "1.20.4", path = "common/ecash-time" }
nym-exit-policy = { version = "1.20.4", path = "common/exit-policy" }
nym-ffi-shared = { version = "1.20.4", path = "sdk/ffi/shared" }
nym-gateway-client = { version = "1.20.4", 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.20.4", path = "common/gateway-requests" }
nym-gateway-storage = { version = "1.20.4", path = "common/gateway-storage" }
nym-gateway-stats-storage = { version = "1.20.4", path = "common/gateway-stats-storage" }
nym-group-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/group-contract" }
nym-http-api-client = { version = "1.20.4", path = "common/http-api-client" }
nym-http-api-client-macro = { version = "1.20.4", path = "common/http-api-client-macro" }
nym-http-api-common = { version = "1.20.4", path = "common/http-api-common", default-features = false }
nym-id = { version = "1.20.4", path = "common/nym-id" }
nym-ip-packet-client = { version = "1.20.4", path = "nym-ip-packet-client" }
nym-ip-packet-requests = { version = "1.20.4", path = "common/ip-packet-requests" }
nym-lp = { version = "1.20.4", path = "common/nym-lp" }
nym-kkt = { version = "0.1.0", path = "common/nym-kkt" }
nym-kkt-ciphersuite = { version = "1.20.4", path = "common/nym-kkt-ciphersuite" }
nym-kkt-context = { version = "1.20.4", path = "common/nym-kkt-context" }
nym-metrics = { version = "1.20.4", path = "common/nym-metrics" }
nym-mixnet-client = { version = "1.20.4", path = "common/client-libs/mixnet-client" }
nym-mixnet-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
nym-multisig-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/multisig-contract" }
nym-network-defaults = { version = "1.20.4", path = "common/network-defaults" }
nym-node-tester-utils = { version = "1.20.4", path = "common/node-tester-utils" }
nym-noise = { version = "1.20.4", path = "common/nymnoise" }
nym-noise-keys = { version = "1.20.4", path = "common/nymnoise/keys" }
nym-nonexhaustive-delayqueue = { version = "1.20.4", path = "common/nonexhaustive-delayqueue" }
nym-node-requests = { version = "1.20.4", path = "nym-node/nym-node-requests", default-features = false }
nym-node-metrics = { version = "1.20.4", path = "nym-node/nym-node-metrics" }
nym-ordered-buffer = { version = "1.20.4", path = "common/socks5/ordered-buffer" }
nym-outfox = { version = "1.20.4", path = "nym-outfox" }
nym-registration-common = { version = "1.20.4", path = "common/registration" }
nym-pemstore = { version = "1.20.4", path = "common/pemstore" }
nym-performance-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
nym-sdk = { version = "1.20.4", path = "sdk/rust/nym-sdk" }
nym-serde-helpers = { version = "1.20.4", path = "common/serde-helpers" }
nym-service-providers-common = { version = "1.20.4", path = "service-providers/common" }
nym-service-provider-requests-common = { version = "1.20.4", path = "common/service-provider-requests-common" }
nym-socks5-client-core = { version = "1.20.4", path = "common/socks5-client-core" }
nym-socks5-proxy-helpers = { version = "1.20.4", path = "common/socks5/proxy-helpers" }
nym-socks5-requests = { version = "1.20.4", path = "common/socks5/requests" }
nym-sphinx = { version = "1.20.4", path = "common/nymsphinx" }
nym-sphinx-acknowledgements = { version = "1.20.4", path = "common/nymsphinx/acknowledgements" }
nym-sphinx-addressing = { version = "1.20.4", path = "common/nymsphinx/addressing" }
nym-sphinx-anonymous-replies = { version = "1.20.4", path = "common/nymsphinx/anonymous-replies" }
nym-sphinx-chunking = { version = "1.20.4", path = "common/nymsphinx/chunking" }
nym-sphinx-cover = { version = "1.20.4", path = "common/nymsphinx/cover" }
nym-sphinx-forwarding = { version = "1.20.4", path = "common/nymsphinx/forwarding" }
nym-sphinx-framing = { version = "1.20.4", path = "common/nymsphinx/framing" }
nym-sphinx-params = { version = "1.20.4", path = "common/nymsphinx/params" }
nym-sphinx-routing = { version = "1.20.4", path = "common/nymsphinx/routing" }
nym-sphinx-types = { version = "1.20.4", path = "common/nymsphinx/types" }
nym-statistics-common = { version = "1.20.4", path = "common/statistics" }
nym-store-cipher = { version = "1.20.4", path = "common/store-cipher" }
nym-task = { version = "1.20.4", path = "common/task" }
nym-tun = { version = "1.20.4", path = "common/tun" }
nym-test-utils = { version = "1.20.4", path = "common/test-utils" }
nym-ticketbooks-merkle = { version = "1.20.4", path = "common/ticketbooks-merkle" }
nym-topology = { version = "1.20.4", path = "common/topology" }
nym-types = { version = "1.20.4", path = "common/types" }
nym-upgrade-mode-check = { version = "1.20.4", path = "common/upgrade-mode-check" }
nym-validator-client = { version = "1.20.4", path = "common/client-libs/validator-client", default-features = false }
nym-vesting-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/vesting-contract" }
nym-verloc = { version = "1.20.4", path = "common/verloc" }
nym-wireguard = { version = "1.20.4", path = "common/wireguard" }
nym-wireguard-types = { version = "1.20.4", path = "common/wireguard-types" }
nym-wireguard-private-metadata-shared = { version = "1.20.4", path = "common/wireguard-private-metadata/shared" }
nym-wireguard-private-metadata-client = { version = "1.20.4", path = "common/wireguard-private-metadata/client" }
nym-wireguard-private-metadata-server = { version = "1.20.4", 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-lp-data = { version = "1.21.0", path = "common/nym-lp-data" }
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-network-monitors-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/network-monitors-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.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" }
smolmix-dns = { version = "1.21.0", path = "smolmix/dns" }
smolmix-hyper = { version = "1.21.0", path = "smolmix/hyper" }
smolmix-tls = { version = "1.21.0", path = "smolmix/tls" }
# coconut/DKG related
# unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork
@@ -590,18 +604,7 @@ opt-level = 3
# lto = true
opt-level = 'z'
[profile.release.package.nym-node-tester-wasm]
# lto = true
opt-level = 'z'
# Commented out since the crate is also commented out from the inclusion in the
# workspace above. We should uncomment this if we re-include it in the
# workspace
#[profile.release.package.nym-wasm-sdk]
## lto = true
#opt-level = 'z'
[profile.release.package.mix-fetch-wasm]
[profile.release.package.smolmix-wasm]
# lto = true
opt-level = 'z'
+11 -11
View File
@@ -104,30 +104,30 @@ $(eval $(call add_cargo_workspace,wallet,nym-wallet))
sdk-wasm: sdk-wasm-build sdk-wasm-test sdk-wasm-lint
sdk-wasm-build:
# $(MAKE) -C nym-browser-extension/storage wasm-pack
$(MAKE) -C wasm/client
$(MAKE) -C wasm/node-tester
$(MAKE) -C wasm/mix-fetch
$(MAKE) -C wasm/smolmix
# $(MAKE) -C wasm/zknym-lib
# $(MAKE) -C wasm/full-nym-wasm
# run this from npm/yarn to ensure tools are in the path, e.g. yarn build:sdk from root of repo
#
# `mix-tunnel` must build before the three feature packages — they import it
# via `workspace:*` and the lerna topological sort will respect that as long
# as we keep them in the same `--scope` invocation.
sdk-typescript-build:
npx lerna run --scope @nymproject/sdk build --stream
npx lerna run --scope @nymproject/mix-fetch build --stream
npx lerna run --scope @nymproject/node-tester build --stream
yarn --cwd sdk/typescript/codegen/contract-clients build
npx lerna run --scope '{@nymproject/mix-tunnel,@nymproject/mix-fetch,@nymproject/mix-dns,@nymproject/mix-websocket}' build --stream
pnpm --pwd sdk/typescript/codegen/contract-clients build
# NOTE: These targets are part of the main workspace (but not as wasm32-unknown-unknown)
# WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib
WASM_CRATES = nym-client-wasm nym-node-tester-wasm
WASM_CRATES = nym-client-wasm
sdk-wasm-test:
#cargo test $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
sdk-wasm-lint:
RUSTFLAGS='--cfg getrandom_backend="wasm_js"' cargo clippy $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
$(MAKE) -C wasm/mix-fetch check-fmt
$(MAKE) -C wasm/smolmix check-fmt
# Add to top-level targets
build: sdk-wasm-build
@@ -223,7 +223,7 @@ build-nym-cli:
generate-typescript:
cd tools/ts-rs-cli && cargo run && cd ../..
yarn types:lint:fix
pnpm types:lint:fix
# Run the integration tests for public nym-api endpoints
run-api-tests:
+2 -2
View File
@@ -74,9 +74,9 @@ Nym Node Operators and Validators Terms and Conditions can be found [here](https
## Getting Started
```bash
yarn install
pnpm install
```
```bash
yarn build
pnpm build
```
+30 -40
View File
@@ -1,21 +1,4 @@
---
ansible_ssh_private_key_file: ~/.ssh/<SSH_KEY>
cli_url: "https://github.com/nymtech/nym/releases/download/nym-binaries-{{ nym_version }}/nym-cli"
tunnel_manager_url: "https://github.com/nymtech/nym/raw/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh"
quic_bridge_deployment_url: "https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/quic_bridge_deployment.sh"
###############################################################################
## GLOBAL VARS
## These values will be used globally unless overwritten per node in inventory/all
###############################################################################
ansible_user: root # used for ssh, like `ssh root@nym-exit.ch-1.mynodes.net`
email: "<EMAIL>" # used in certbot, description.toml and landing page
website: "<WEBSITE>" # it is used in the description.toml
description: "<NODE_PUBLIC_DESCRIPTION>" # or define per node in inventory/all
# operator_name: "<OPERATOR_NAME>" # used in landing page if provided
###############################################################################
## GLOBAL VARS
## These values will be used globally unless overwritten per node in inventory/all
@@ -23,16 +6,41 @@ description: "<NODE_PUBLIC_DESCRIPTION>" # or define per node in inventory/all
## Per node changes in inventory/all will overwrite these global vars
###############################################################################
# moniker: "<MONIKER>" # if not setup here not in inventory/all it get's derived from the hostname
# mode: <MODE> # entry-gateway/exit-gateway/mixnode
# wireguard_enabled: <WIREGUARD_ENABLED> # true/false
hostname: "" # this is a fallback, keep it and setup hostname per node in inventory/all
## MANDATORY - uncomment & define
## --SSH--
#ansible_user: root # used for ssh, like `ssh root@nym-exit.ch-1.mynodes.net`
# ansible_ssh_private_key_file: ~/.ssh/<SSH_KEY>
## --Operator info--
# email: "<EMAIL>" # used in certbot, description.toml and landing page
# website: "<WEBSITE>" # it is used in the description.toml
# description: "<NODE_PUBLIC_DESCRIPTION>" # or define per node in inventory/all
# moniker: "<MONIKER>"
## --Node defaults (can override per node in inventory/all)--
# accept_operator_terms: true # controls --accept-operator-terms-and-conditions, read: https://nym.com/docs/operators/nodes/nym-node/setup#terms--conditions
# mode: exit-gateway # entry-gateway/exit-gateway/mixnode
# wireguard_enabled: true # true/false
hostname: "" # keep this fallback, keep it and setup hostname per node in inventory/all
## OPTIONAL - uncomment & define
# operator_name: "<OPERATOR_NAME>" # used in landing page if provided
# nym_version: "nym-binaries-v2026.7-tola" # to use particular version instead of Latest, provide in such form:
## alternative SSH key var setting, instead of a hardcoded path
## useful if the playbook is shared in a repo by more admins with each having own local key
# ansible_ssh_private_key_file: "{{ lookup('env', '<YOUR_ANSIBLE SSH_KEY_ENV_VAR>') }}"
###############################################################################
## GLOBAL PACKAGES
## GLOBAL PACKAGES & URLs
## These will be installed during deployment
###############################################################################
nym_cli_url: "https://github.com/nymtech/nym/releases/download/{{ nym_version }}/nym-cli"
tunnel_manager_url: "https://github.com/nymtech/nym/raw/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh"
quic_bridge_deployment_url: "https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/quic_bridge_deployment.sh"
packages:
- tmux
@@ -50,24 +58,6 @@ packages:
- ufw
###############################################################################
## OPTIONAL OVERRIDES
## All values below already have defaults in the playbook/roles
## Uncomment only if you want to override them
###############################################################################
###############################################################################
## SYSTEM MAINTENANCE PLAYBOOK KNOBS
###############################################################################
# To use particular version instead of Latest, provide in such form:
# nym_version: "nym-binaries-v2026.7-tola"
## NOTE:
## if you want to pin Nym to a specific version instead of using the
## latest release from GitHub in /tasks/main.yml then
## uncomment the line above and set the tag
###############################################################################
## SYSTEM MAINTENANCE PLAYBOOK KNOBS
###############################################################################
+28 -23
View File
@@ -1,34 +1,39 @@
[nym_nodes]
# READ CONFIGURATION GUIDE:
# https://nym.com//docs/operators/orchestration/ansible#configuration
## READ CONFIGURATION GUIDE:
## https://nym.com/docs/operators/orchestration/ansible#configuration
# VARIABLES INFO
# required vars to set values per node:
# `ansible_host`, `hostname`, `location`
##############
## TEMPLATE ##
##############
## uncomment and exchange the <VARIABLES> with your real values for each node without the <> brackets
# global vars can be set in the group_vars/all.yml, for example:
# `email`, `ansible_user`, `moniker`, `description`, `mode`, `wireguard_enabled`
# othersise they must be set per node!
############
# TEMPLATE #
############
# node1 ansible_host=<YOUR_SERVER_IP> ansible_user=<USER> hostname=<HOSTNAME> location=<LOCATION> email=<EMAIL> mode=<MODE> wireguard_enabled=<true/false> moniker=<MONIKER> description=<DESCRIPTION>
# remove all comments and exchange the <VARIABLES> with your real values for each node
# without <> brackets
####################
## VARIABLES INFO ##
####################
# PRIORITY ORDER
# anything setup globaly can be overwritten in this file per node
# if provided here, it takes priority over the global setting
## --REQUIRED VARS--
## required per node:
## ansible_host, hostname, location
# EXAMPLES
# exit + wireguard gateway:
## --OPTIONAL VARS--
## can be set in the group_vars/all.yml or per node here:
## email, ansible_user, moniker, description, mode, wireguard_enabled
## --PRIORITY ORDER--
## anything setup globaly can be overwritten in this file per node
## if provided here, it takes priority over the global setting
## --EXAMPLES--
## exit + wireguard gateway:
# node2 ansible_host=11.12.13.14 hostname=nym-exit.ch-1.mydomain.net mode=exit-gateway location=CH wireguard_enabled=true
# entry gateway, no wireguard:
## entry gateway, no wireguard:
# node3 ansible_host=12.13.14.15 hostname=nym-entry.ch-2.mydomain.net mode=entry-gateway location=CH wireguard_enabled=false
# NOTE:
# all examples above don't have defined user, email nor description as we use the definition from group_vars/main.yml without an attempt of overwriting it
# all examples above don't have moniker defined as there is a function in /templates/description.toml.j2 deriving it from the hostname
## mixnode (comment out tunnel+quic roles in deploy.yml for these)
# mix-de-1 ansible_host=13.14.15.16 hostname=nym-mix.de-1.example.net location=DE mode=mixnode wireguard_enabled=false
## NOTE:
## all examples above don't have defined user, email nor description as we use global vars from playbooks/group_vars/all.yml
@@ -0,0 +1,42 @@
# Mitigation playbook for CopyFail (CVE-2026-31431) and DirtyFrag (CVE-2026-43284 / CVE-2026-43500)
# This playbook applies interim module blacklists only
# Kernel patches are not yet available (May 2026)
# Once patched kernels ship, use remove_kernel_CVE_mitigations.yml to reverse everything
# This playbook is idempotent - safe to re-run if mitigations were already applied
- name: Mitigate Copy Fail + Dirty Frag
hosts: all
become: true
tasks:
- name: Blacklist algif_aead (Copy Fail)
copy:
dest: /etc/modprobe.d/disable-algif_aead.conf
content: "install algif_aead /bin/false\n"
owner: root
group: root
mode: "0644"
- name: Blacklist esp4, esp6, rxrpc (Dirty Frag)
copy:
dest: /etc/modprobe.d/dirtyfrag.conf
content: |
install esp4 /bin/false
install esp6 /bin/false
install rxrpc /bin/false
owner: root
group: root
mode: "0644"
- name: Unload all affected modules
modprobe:
name: "{{ item }}"
state: absent
loop:
- algif_aead
- esp4
- esp6
- rxrpc
ignore_errors: true
- name: Drop page cache to clear any contamination
shell: echo 3 > /proc/sys/vm/drop_caches
@@ -0,0 +1,111 @@
############################################################################################
############################################################################################
############################################################################################
#### THIS PLAYBOOK IS NOT MEANT TO BE RUN YET, IT IS NOT REFERRED IN ANY DOCUMENTATION! ####
############################################################################################
############################################################################################
############################################################################################
#
# Reversal playbook for mitigate_kernel_CVE.yml (CopyFail CVE-2026-31431 / DirtyFrag CVE-2026-43284 / CVE-2026-43500).
#
# Run this AFTER your distro has shipped the patched kernel.
# This playbook:
# 1. Updates the kernel via apt
# 2. Reboots and waits for reconnect
# 3. Verifies the running kernel is newer than the pre-patch version
# 4. Removes the interim module blacklists
# 5. Re-enables the affected modules live (no second reboot needed)
#
# Debian family only (Debian, Ubuntu). Tested on Debian 11, Debian 12, Ubuntu 20.04, 22.04, 24.04.
#
# For exit-gateway nodes with --wireguard-enabled true:
# After this playbook completes, run the networking restore step on each node via:
# ansible-playbook deploy.yml -t ntm
# See the CVE patch documentation for details.
- name: Remove CVE mitigations and apply patched kernel
hosts: all
become: true
tasks:
- name: Verify OS is Debian family
assert:
that:
- ansible_os_family == "Debian"
fail_msg: "This playbook supports Debian-family distros only (Debian, Ubuntu). For other distros, apply the kernel update and mitigation removal manually."
- name: Update apt cache
apt:
update_cache: true
cache_valid_time: 0
- name: Upgrade kernel packages
apt:
upgrade: full
only_upgrade: false
register: apt_upgrade_result
- name: Record pre-reboot kernel version
command: uname -r
register: kernel_before
changed_when: false
- name: Reboot to load patched kernel
reboot:
msg: "Rebooting to apply patched kernel (CVE-2026-31431 / CVE-2026-43284 / CVE-2026-43500)"
reboot_timeout: 300
pre_reboot_delay: 5
post_reboot_delay: 15
- name: Record post-reboot kernel version
command: uname -r
register: kernel_after
changed_when: false
- name: Show kernel versions before and after reboot
debug:
msg:
- "Kernel before reboot: {{ kernel_before.stdout }}"
- "Kernel after reboot: {{ kernel_after.stdout }}"
- name: Warn if kernel did not change after reboot
debug:
msg: >
WARNING: kernel version did not change after reboot ({{ kernel_after.stdout }}).
The patched kernel may not have been selected by GRUB, or no kernel update was available.
Do NOT remove the interim mitigations until you have confirmed the running kernel is patched.
Check: apt-cache policy linux-image-amd64 # Debian
Check: apt-cache policy linux-image-generic # Ubuntu
when: kernel_before.stdout == kernel_after.stdout
- name: Remove algif_aead blacklist
file:
path: /etc/modprobe.d/disable-algif_aead.conf
state: absent
- name: Remove DirtyFrag blacklist (esp4, esp6, rxrpc)
file:
path: /etc/modprobe.d/dirtyfrag.conf
state: absent
- name: Re-enable affected modules live
modprobe:
name: "{{ item }}"
state: present
loop:
- esp4
- esp6
- rxrpc
- algif_aead
ignore_errors: true
- name: Confirm nym-node service is still running
systemd:
name: nym-node
state: started
register: nym_node_status
failed_when: false
- name: Show nym-node status
debug:
msg: "nym-node service state: {{ nym_node_status.state | default('unknown - service may not exist on this node') }}"
+5 -11
View File
@@ -89,7 +89,6 @@
loop:
- "/etc/nginx/sites-enabled/{{ hostname }}-ssl"
- "/etc/nginx/sites-enabled/nym-wss-config"
when: not le_cert.stat.exists
notify: Restart nginx
- name: Ensure nginx is enabled and running (needed for ACME http-01)
@@ -111,18 +110,13 @@
- name: Obtain/renew certificate
command:
cmd: >-
{% if le_cert.stat.exists %}
certbot certonly --webroot
-w /var/www/{{ hostname }}
certbot certonly --nginx
--non-interactive --agree-tos --keep-until-expiring
-m {{ email }} -d {{ hostname }}
{% else %}
certbot --nginx
--non-interactive --agree-tos --redirect
-m {{ email }} -d {{ hostname }}
{% endif %}
register: certbot_result
failed_when: false
failed_when: false
# re-check cert after certbot attempt
- name: Re-check whether certificate exists after certbot
@@ -170,4 +164,4 @@
changed_when: false
- name: Flush handlers (apply restart after successful tests)
meta: flush_handlers
meta: flush_handlers
+2 -2
View File
@@ -10,7 +10,7 @@ mixnet_bind_address: "0.0.0.0:1789" # maps to --mixnet-bind-address
landing_page_assets_base_dir: "/var/www"
# Flag toggles
# accept_operator_terms: true # controls --accept-operator-terms-and-conditions
accept_operator_terms: false # override in group_vars or inventory
nym_write_flag: true # controls -w
nym_init_only_flag: true # controls --init-only
wss_port: 9001 # controlls --announce-wss-port
@@ -18,7 +18,7 @@ wss_port: 9001 # controlls --announce-wss-port
# Optional: extra flags if you want to append more later
nym_extra_flags: ""
# CLI URL (nym_version can be set elsewhere / via GitHub API)
# CLI URL
nym_cli_url: "https://github.com/nymtech/nym/releases/download/{{ nym_version }}/nym-cli"
# UFW
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-client"
description = "Implementation of the Nym Client"
version = "1.1.76"
version = "1.1.77"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
edition = "2021"
license.workspace = true
+2
View File
@@ -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 -1
View File
@@ -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.76"
version = "1.1.77"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
license.workspace = true
+10
View File
@@ -25,6 +25,8 @@ pub trait BandwidthTicketProvider: Send + Sync {
) -> Result<PreparedCredential, BandwidthControllerError>;
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError>;
async fn close(&self) {}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -56,6 +58,10 @@ where
.map_err(|_| BandwidthControllerError::MalformedUpgradeModeToken)?;
Ok(Some(token))
}
async fn close(&self) {
self.storage.close().await;
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -75,4 +81,8 @@ impl<T: BandwidthTicketProvider + ?Sized + Send> BandwidthTicketProvider for Box
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError> {
(**self).get_upgrade_mode_token().await
}
async fn close(&self) {
(**self).close().await;
}
}
@@ -1023,6 +1023,16 @@ where
let encryption_keys = init_res.client_keys.encryption_keypair();
let identity_keys = init_res.client_keys.identity_keypair();
let credential_store_for_close = credential_store.clone();
let close_credential_token = shutdown_tracker.clone_shutdown_token();
shutdown_tracker.try_spawn_named(
async move {
close_credential_token.cancelled().await;
credential_store_for_close.close().await;
},
"CredentialStorage::close_on_shutdown",
);
// the components are started in very specific order. Unless you know what you are doing,
// do not change that.
let bandwidth_controller = self
@@ -11,11 +11,17 @@ use nym_bandwidth_controller::BandwidthController;
use nym_client_core_gateways_storage::OnDiskGatewaysDetails;
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_validator_client::{QueryHttpRpcNyxdClient, nyxd};
use std::{io, path::Path};
use std::{io, path::Path, time::Duration};
use time::OffsetDateTime;
use tracing::{error, info, trace};
use url::Url;
/// Maximum rename retry attempts when the database file is temporarily locked.
const ARCHIVE_MAX_RETRY_ATTEMPTS: u8 = 15;
/// Delay between archive rename retry attempts.
const ARCHIVE_RETRY_DELAY: Duration = Duration::from_millis(200);
async fn setup_fresh_backend<P: AsRef<Path>>(
db_path: P,
surb_config: &config::ReplySurbs,
@@ -74,13 +80,58 @@ async fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()
};
let renamed = db_path.with_extension(new_extension);
tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| {
error!(
"Failed to rename corrupt database file: {} to {}",
db_path.display(),
renamed.display()
);
})
// On Windows, sqlx may release its OS file handles asynchronously after
// pool.close() returns, briefly keeping the file locked
// (ERROR_SHARING_VIOLATION, os error 32). Retry with a short delay to
// give the OS time to flush the remaining handles.
for attempt in 0..ARCHIVE_MAX_RETRY_ATTEMPTS {
match tokio::fs::rename(db_path, &renamed).await {
Ok(()) => return Ok(()),
Err(e) if is_file_locked_error(&e) && (attempt + 1) < ARCHIVE_MAX_RETRY_ATTEMPTS => {
trace!(
"Database file is temporarily locked, retrying archive \
(attempt {}/{}): {e}",
attempt + 1,
ARCHIVE_MAX_RETRY_ATTEMPTS
);
tokio::time::sleep(ARCHIVE_RETRY_DELAY).await;
}
Err(e) => {
error!(
"Failed to rename corrupt database file: {} to {}",
db_path.display(),
renamed.display()
);
return Err(e);
}
}
}
// Reached only when every attempt was blocked by a file lock.
error!(
"Failed to rename corrupt database file after {} attempts: {} to {}",
ARCHIVE_MAX_RETRY_ATTEMPTS,
db_path.display(),
renamed.display()
);
Err(io::Error::other(
"corrupt database archive blocked by persistent file lock",
))
}
/// Returns `true` when the IO error indicates a temporary file lock held by another handle
/// within the same process. Only meaningful on Windows; always `false` elsewhere.
fn is_file_locked_error(e: &io::Error) -> bool {
#[cfg(windows)]
{
// ERROR_SHARING_VIOLATION = 32, ERROR_LOCK_VIOLATION = 33
matches!(e.raw_os_error(), Some(32) | Some(33))
}
#[cfg(not(windows))]
{
let _ = e;
false
}
}
pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
@@ -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_packet(packet)
.forward_client_packet_without_delay(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)]
@@ -337,6 +337,8 @@ impl ReplyStorageBackend for Backend {
}
async fn stop_storage_session(self) -> Result<(), Self::StorageError> {
self.stop_client_use().await
let result = self.stop_client_use().await;
self.shutdown().await;
result
}
}
+5 -3
View File
@@ -48,6 +48,7 @@ where
debug!("Started PersistentReplyStorage");
if let Err(err) = self.backend.start_storage_session().await {
error!("failed to start the storage session - {err}");
self.backend.stop_storage_session().await.ok();
return;
}
@@ -55,10 +56,11 @@ where
info!("PersistentReplyStorage is flushing all reply-related data to underlying storage");
if let Err(err) = self.backend.flush_surb_storage(&mem_state).await {
error!("failed to flush our reply-related data to the persistent storage: {err}")
} else {
info!("Data flush is complete")
error!("failed to flush our reply-related data to the persistent storage: {err}");
self.backend.stop_storage_session().await.ok();
return;
}
info!("Data flush is complete");
if let Err(err) = self.backend.stop_storage_session().await {
error!("failed to properly stop the storage session - {err}. We might not be able to smoothly restore it")
@@ -34,3 +34,4 @@ 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"] }
+277 -69
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use dashmap::DashMap;
use futures::StreamExt;
use futures::{SinkExt, StreamExt};
use nym_noise::config::NoiseConfig;
use nym_noise::upgrade_noise_initiator;
use nym_sphinx::forwarding::packet::MixPacket;
@@ -14,6 +14,7 @@ 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;
@@ -90,13 +91,17 @@ 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>) -> Self {
fn new(channel: mpsc::Sender<FramedNymPacket>, handle_token: Arc<()>) -> Self {
ConnectionSender {
channel,
current_reconnection_attempt: Arc::new(AtomicU32::new(0)),
handle_token,
}
}
}
@@ -107,6 +112,31 @@ 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 {
@@ -116,6 +146,8 @@ impl ManagedConnection {
message_receiver: mpsc::Receiver<FramedNymPacket>,
connection_timeout: Duration,
current_reconnection: Arc<AtomicU32>,
active_connections: ActiveConnections,
handle_token: Arc<()>,
) -> Self {
ManagedConnection {
address,
@@ -123,72 +155,30 @@ 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);
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;
}
},
// 1. attempt to establish the connection with timeout
let maybe_stream = match tokio::time::timeout(self.connection_timeout, connection_fut).await
{
Ok(stream) => stream,
Err(_) => {
let connect_ms = connect_start.elapsed().as_millis() as u64;
warn!(
debug!(
event = "connection.failed.timeout",
peer = %address,
timeout_ms = self.connection_timeout.as_millis() as u64,
@@ -203,21 +193,133 @@ impl ManagedConnection {
}
};
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}"
);
}
// 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,
exit_reason = "sender_dropped",
"connection manager to {address} finished"
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;
}
}
}
}
}
}
}
@@ -264,13 +366,18 @@ 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);
let new_entry = ConnectionSender::new(sender, Arc::clone(&handle_token));
let current_attempt = Arc::clone(&new_entry.current_reconnection_attempt);
self.active_connections.insert(address, new_entry);
current_attempt
@@ -285,6 +392,7 @@ 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 {
@@ -299,6 +407,8 @@ impl Client {
receiver,
initial_connection_timeout,
current_reconnection_attempt,
active_connections,
handle_token,
)
.run()
.await;
@@ -428,4 +538,102 @@ 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,12 +21,16 @@ impl From<mpsc::UnboundedSender<PacketToForward>> for MixForwardingSender {
}
impl MixForwardingSender {
pub fn forward_packet(&self, packet: impl Into<PacketToForward>) -> Result<(), SendError> {
pub fn forward_packet(&self, packet: PacketToForward) -> Result<(), SendError> {
self.0
.unbounded_send(packet.into())
.unbounded_send(packet)
.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()
@@ -38,35 +42,23 @@ pub type MixForwardingReceiver = mpsc::UnboundedReceiver<PacketToForward>;
pub struct PacketToForward {
pub packet: MixPacket,
pub forward_delay_target: Option<Instant>,
}
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))
}
pub network_monitor_packet: bool,
}
impl PacketToForward {
pub fn new(packet: MixPacket, forward_delay_target: Option<Instant>) -> Self {
pub fn new(
packet: MixPacket,
forward_delay_target: Option<Instant>,
network_monitor_packet: bool,
) -> Self {
PacketToForward {
packet,
forward_delay_target,
network_monitor_packet,
}
}
pub fn new_no_delay(packet: MixPacket) -> Self {
Self::new(packet, None)
pub fn client_packet_without_delay(packet: MixPacket) -> Self {
Self::new(packet, None, false)
}
}
@@ -26,6 +26,8 @@ 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,6 +104,14 @@ 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> {
@@ -114,6 +122,15 @@ 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,11 +15,16 @@ 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::{
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
NymNodeDescriptionV1, NymNodeDescriptionV2, PerformanceHistoryResponse, RewardedSetResponse,
SignerInformationResponse,
AnnotationResponseV1, ApiHealthResponse, BinaryBuildInformationOwned,
ChainBlocksStatusResponse, ChainStatusResponse, KeyRotationInfoResponse,
NodePerformanceResponse, NodeRefreshBody, NymNodeDescriptionV1, NymNodeDescriptionV2,
PerformanceHistoryResponse, RewardedSetResponse, SignerInformationResponse,
StressTestBatchSubmissionResponse,
};
use nym_api_requests::pagination::PaginatedResponse;
use nym_http_api_client::{ApiClient, NO_PARAMS};
@@ -389,6 +394,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],
&params,
)
.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> {
@@ -976,7 +1020,7 @@ pub trait NymApiClientExt: ApiClient {
async fn get_node_annotation(
&self,
node_id: NodeId,
) -> Result<AnnotationResponse, NymAPIError> {
) -> Result<AnnotationResponseV1, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
@@ -1359,6 +1403,53 @@ 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,6 +50,9 @@ 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,6 +13,8 @@ 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;
@@ -22,6 +24,8 @@ 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;
@@ -31,6 +35,10 @@ 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};
@@ -40,6 +48,8 @@ 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;
@@ -49,6 +59,8 @@ 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>;
@@ -62,6 +74,8 @@ 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>,
@@ -86,6 +100,14 @@ impl TryFrom<NymContracts> for TypedNymContracts {
.performance_contract_address
.map(|addr| addr.parse())
.transpose()?,
network_monitors_contract_address: value
.network_monitors_contract_address
.map(|addr| addr.parse())
.transpose()?,
node_families_contract_address: value
.node_families_contract_address
.map(|addr| addr.parse())
.transpose()?,
ecash_contract_address: value
.ecash_contract_address
.map(|addr| addr.parse())
@@ -0,0 +1,107 @@
// 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()
}
};
}
}
@@ -0,0 +1,205 @@
// 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()
}
};
}
}
@@ -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(),
};
}
}
@@ -0,0 +1,281 @@
// 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
}
/// Update the name and/or description of the caller's family. Each
/// argument follows `None = keep` / `Some(_) = replace` semantics; a
/// call with both `None` is a server-side no-op.
async fn update_family(
&self,
updated_name: Option<String>,
updated_description: Option<String>,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_node_families_contract(
fee,
NodeFamiliesExecuteMsg::UpdateFamily {
updated_name,
updated_description,
},
"NodeFamiliesContract::UpdateFamily".to_string(),
vec![],
)
.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::UpdateFamily {
updated_name,
updated_description,
} => client
.update_family(updated_name, updated_description, None)
.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)]
#[derive(Debug, Clone)]
pub(crate) struct SigningClientOptions {
gas_price: GasPrice,
simulated_gas_multiplier: f32,
@@ -80,6 +80,17 @@ 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,6 +24,8 @@ 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;
@@ -40,6 +42,7 @@ pub use crate::nyxd::{
fee::Fee,
};
pub use crate::rpc::TendermintRpcClient;
pub use bip39;
pub use coin::Coin;
pub use cosmrs::{
bank::MsgSend,
@@ -70,14 +73,19 @@ 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;
@@ -262,6 +270,16 @@ 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
}
@@ -286,9 +304,17 @@ impl<C, S> NyxdClient<C, S> {
self.config.contracts.multisig_contract_address = Some(address);
}
pub fn set_node_families_contract_address(&mut self, address: AccountId) {
self.config.contracts.node_families_contract_address = Some(address);
}
pub fn set_simulated_gas_multiplier(&mut self, multiplier: f32) {
self.config.simulated_gas_multiplier = multiplier;
}
pub fn get_nym_contracts(&self) -> TypedNymContracts {
self.config.contracts.clone()
}
}
impl<C, S> NymContractsProvider for NyxdClient<C, S> {
@@ -303,6 +329,19 @@ 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> {
self.config
.contracts
.network_monitors_contract_address
.as_ref()
}
fn node_families_contract_address(&self) -> Option<&AccountId> {
self.config
.contracts
.node_families_contract_address
.as_ref()
}
fn ecash_contract_address(&self) -> Option<&AccountId> {
self.config.contracts.ecash_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)
@@ -1,8 +1,14 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Blacklist types. **The blacklist surface is stubbed today** - the execute
//! handlers always return `UnimplementedBlacklisting` and the storage map is
//! never populated. These types are kept for the planned redesign.
use cosmwasm_schema::cw_serde;
/// Public-key + metadata pair surfaced by `GetBlacklistedAccount` /
/// `GetBlacklistPaged`. Always empty on a freshly deployed contract.
#[cw_serde]
pub struct BlacklistedAccount {
pub public_key: String,
@@ -15,6 +21,8 @@ impl From<(String, Blacklisting)> for BlacklistedAccount {
}
}
/// Per-key blacklist record: the multisig proposal that approved it and the
/// block height at which finalisation landed (None until finalised).
#[cw_serde]
pub struct Blacklisting {
pub proposal_id: u64,
@@ -36,6 +44,8 @@ impl BlacklistedAccount {
}
}
/// Page of blacklist entries returned by `GetBlacklistPaged`. Always empty on
/// a freshly deployed contract.
#[cw_serde]
pub struct PagedBlacklistedAccountResponse {
pub accounts: Vec<BlacklistedAccount>,
@@ -59,6 +69,8 @@ impl PagedBlacklistedAccountResponse {
}
}
/// Response shape for `GetBlacklistedAccount`. `account` is `None` for any
/// key not present in the (currently always-empty) blacklist.
#[cw_serde]
pub struct BlacklistedAccountResponse {
pub account: Option<Blacklisting>,
@@ -4,6 +4,9 @@
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Coin;
/// Pool-level deposit accounting. Updated by every successful
/// `DepositTicketBookFunds` and (for the unredeemed-tickets counter) by every
/// successful legacy `RedeemTickets`.
#[cw_serde]
pub struct PoolCounters {
/// Represents the total amount of funds deposited into the contract.
@@ -5,8 +5,13 @@ use crate::error::EcashContractError;
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{StdError, StdResult};
/// Sequential identifier assigned to every accepted deposit. Starts at 0 and
/// is never recycled.
pub type DepositId = u32;
/// Opaque on-chain record of a deposit: the depositor-claimed bs58-encoded
/// ed25519 identity public key. The contract does not verify control of the
/// corresponding private key.
#[cw_serde]
pub struct Deposit {
pub bs58_encoded_ed25519_pubkey: String,
@@ -19,6 +24,8 @@ impl Deposit {
}
}
/// Decode a bs58-encoded ed25519 public key to its 32-byte raw form.
/// Surfaces `MalformedEd25519Identity` on any bs58 / length failure.
pub fn get_ed25519_pubkey_bytes(raw: &str) -> Result<[u8; 32], EcashContractError> {
let mut ed25519_pubkey_bytes = [0u8; 32];
bs58::decode(raw)
@@ -32,10 +39,13 @@ impl Deposit {
bs58::encode(raw).into_string()
}
/// Decode this deposit's identity key to its 32-byte raw form for storage.
pub fn to_bytes(&self) -> Result<[u8; 32], EcashContractError> {
Self::get_ed25519_pubkey_bytes(&self.bs58_encoded_ed25519_pubkey)
}
/// Reconstruct a `Deposit` from a raw 32-byte ed25519 pubkey as stored
/// under the `"deposit"` namespace.
pub fn try_from_bytes(bytes: &[u8]) -> StdResult<Self> {
if bytes.len() != 32 {
return Err(StdError::generic_err("malformed deposit data"));
@@ -47,12 +57,16 @@ impl Deposit {
}
}
/// Response shape for `GetLatestDeposit`. `deposit` is `None` on a freshly
/// deployed contract.
#[cw_serde]
#[derive(Default)]
pub struct LatestDepositResponse {
pub deposit: Option<DepositData>,
}
/// Response shape for `GetDeposit { deposit_id }`. `deposit` is `None` when
/// the id has not yet been assigned (`id >= total_deposits_made`).
#[cw_serde]
pub struct DepositResponse {
pub id: DepositId,
@@ -60,6 +74,8 @@ pub struct DepositResponse {
pub deposit: Option<Deposit>,
}
/// `(deposit_id, deposit)` pair surfaced by the latest-deposit and paginated
/// deposit queries.
#[cw_serde]
pub struct DepositData {
pub id: DepositId,
@@ -73,6 +89,8 @@ impl From<(DepositId, Deposit)> for DepositData {
}
}
/// Page of deposits returned by `GetDepositsPaged`. `start_next_after` is the
/// id of the last returned entry; pass it as the next call's `start_after`.
#[cw_serde]
pub struct PagedDepositsResponse {
pub deposits: Vec<DepositData>,
@@ -6,69 +6,108 @@ use cw_controllers::AdminError;
use cw_utils::PaymentError;
use thiserror::Error;
/// Errors surfaced by the ecash contract. Each reachable variant is named in at
/// least one scenario of `openspec/specs/ecash-contract/spec.md`.
#[derive(Error, Debug, PartialEq)]
pub enum EcashContractError {
/// Wrapper for any underlying `cosmwasm_std::StdError` (storage faults,
/// address validation, etc.).
#[error(transparent)]
Std(#[from] StdError),
/// Raised by `cw_utils::must_pay` on `DepositTicketBookFunds` when funds
/// are missing, multi-denom, or in the wrong denom. Inner variants
/// `NoFunds`, `MultipleDenoms`, `MissingDenom` are all reachable.
#[error("Invalid deposit")]
InvalidDeposit(#[from] PaymentError),
/// `DepositTicketBookFunds` with the right denom but a non-matching amount.
/// `amount` is the reduced amount (if the sender is whitelisted) or the
/// default amount.
#[error("received wrong amount for deposit. got: {received}. required: {amount}")]
WrongAmount { received: Coin, amount: Coin },
/// **Unreachable** - preserved for forward compatibility (no current
/// execute path triggers this).
#[error("There aren't enough funds in the contract")]
NotEnoughFunds,
/// Wrapper for `cw_controllers::AdminError`. Raised by every admin-gated
/// and multisig-gated handler when the sender is wrong.
#[error(transparent)]
Admin(#[from] AdminError),
/// Redemption-proposal reply could not find a `proposal_id` attribute on
/// the multisig `wasm` event.
#[error("could not find proposal id inside the multisig reply SubMsg")]
MissingProposalId,
// realistically this should NEVER be thrown
/// Redemption-proposal reply found a `proposal_id` attribute that could
/// not be parsed as `u64`. Realistically unreachable.
#[error("the proposal id returned by the multisig contract could not be parsed into an u64")]
MalformedProposalId,
/// Instantiation given a `group_addr` that failed bech32 validation.
#[error("Group contract invalid address '{addr}'")]
InvalidGroup { addr: String },
/// **Unreachable** - no current execute path triggers this.
#[error("Unauthorized")]
Unauthorized,
/// **Unreachable** - preserved for future SemVer comparisons during migration.
#[error("Failed to parse {value} into a valid SemVer version: {error_message}")]
SemVerFailure {
value: String,
error_message: String,
},
/// Reply dispatcher saw an `id` that does not match
/// `BLACKLIST_PROPOSAL_REPLY_ID` or `REDEMPTION_PROPOSAL_REPLY_ID`.
#[error("received an invalid reply id: {id}. it does not correspond to any sent SubMsg")]
InvalidReplyId { id: u64 },
/// **Unreachable** - preserved for the (future) typed-deposit-info feature.
#[error("reached the maximum of 255 different deposit types")]
MaximumDepositTypesReached,
/// **Unreachable** - preserved for the (future) typed-deposit-info feature.
#[error("compressed deposit info {typ} does not corresponds to any known type")]
UnknownCompressedDepositInfoType { typ: u8 },
/// **Unreachable** - preserved for the (future) typed-deposit-info feature.
#[error("deposit info {typ} does not corresponds to any previously seen type")]
UnknownDepositInfoType { typ: String },
/// `DepositTicketBookFunds` with an `identity_key` that fails to bs58-decode
/// to exactly 32 bytes. Raised inside `Deposit::to_bytes` during
/// `save_deposit`.
#[error("the provided ed25519 identity was malformed")]
MalformedEd25519Identity,
/// `nym_network_defaults::TICKETBOOK_SIZE` has diverged from the value
/// snapshotted at instantiation in `Item<Invariants>`. Tripwire for
/// uncoordinated network-defaults bumps.
#[error("the ticket book size has changed since the contract was created! This was not expected! It used to be {at_init} but it's {current} now! Please let the developers know ASAP!")]
TicketBookSizeChanged { at_init: u64, current: u64 },
/// `RequestRedemption` with a `commitment_bs58` that does not decode to a
/// 32-byte sha256 digest.
#[error("the provided tickets redemption commitment is malformed")]
MalformedRedemptionCommitment,
/// Always thrown by `ProposeToBlacklist` and `AddToBlacklist` until the
/// blacklist redesign lands.
#[error("the account blacklisting hasn't been fully implemented yet")]
UnimplementedBlacklisting,
/// `SetReducedDepositPrice` (or migration whitelist seeding) given a coin
/// whose denom does not match `Config::deposit_amount.denom`.
#[error("reduced deposit must use the same denom as the default deposit (expected '{expected}', got '{got}')")]
InvalidReducedDepositDenom { expected: String, got: String },
/// `SetReducedDepositPrice` (or migration whitelist seeding) given a
/// reduced amount not strictly less than the current default.
#[error(
"reduced deposit amount ({reduced}) must be strictly less than the default ({default})"
)]
@@ -77,9 +116,13 @@ pub enum EcashContractError {
default: cosmwasm_std::Uint128,
},
/// `RemoveReducedDepositPrice` invoked for an address with no current
/// reduced-deposit entry.
#[error("address '{address}' does not have a custom reduced deposit price set")]
NoReducedDepositPrice { address: String },
/// `UpdateDefaultDepositValue` or `SetReducedDepositPrice` given an amount
/// below `nym_network_defaults::TICKETBOOK_SIZE`.
#[error(
"deposit amount ({amount}) must be at least the ticket book size ({ticket_book_size})"
)]
@@ -1,4 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
/// Duplicate of `events::PROPOSAL_ID_ATTRIBUTE_NAME`. **Dead code**: not
/// referenced anywhere in the workspace today; preserved here pending a
/// follow-on cleanup. Use `events::PROPOSAL_ID_ATTRIBUTE_NAME` instead.
pub const BANDWIDTH_PROPOSAL_ID: &str = "proposal_id";
@@ -1,9 +1,21 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// event types
//! Event names and attribute keys emitted by the ecash contract. Renaming any
//! of these is a breaking change for indexers and downstream tooling.
/// Event type emitted by every successful `DepositTicketBookFunds`. Carries a
/// single `deposit-id` attribute with the assigned id as a decimal string.
pub const DEPOSITED_FUNDS_EVENT_TYPE: &str = "deposited-funds";
/// Attribute key on the `deposited-funds` event: the newly assigned deposit id.
pub const DEPOSIT_ID: &str = "deposit-id";
/// Name of the cosmwasm-std auto-generated event that carries handler
/// attributes (`updated_deposit`, `action`, `address`, `deposit`,
/// `proposal_id`).
pub const WASM_EVENT_NAME: &str = "wasm";
/// Attribute key carrying the multisig-issued `proposal_id` on the `wasm`
/// event from the redemption-proposal reply handler.
pub const PROPOSAL_ID_ATTRIBUTE_NAME: &str = "proposal_id";
@@ -1,6 +1,12 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Shared types, messages, events, and errors for the ecash contract.
//!
//! Consumed by both the contract crate (`contracts/ecash`) and any off-chain
//! client (gateways, nym-api signers, indexers, validator-client). See
//! `openspec/specs/ecash-contract/spec.md` for the normative interface.
pub mod blacklist;
pub mod counters;
pub mod deposit;
@@ -15,100 +15,134 @@ use crate::reduced_deposit::WhitelistedAccountsResponse;
#[cfg(feature = "schema")]
use cosmwasm_schema::QueryResponses;
/// Instantiation payload. The sender of the instantiate transaction becomes the
/// contract admin; the three addresses are bech32-validated and persisted as
/// immutable cross-contract pointers (see spec requirement "Contract instantiation").
#[cw_serde]
pub struct InstantiateMsg {
/// Cosmos SDK address reserved for the future pool-contract transition.
/// Stored in `Config` but never debited by the current contract.
pub holding_account: String,
/// cw3 multisig contract that gates `RedeemTickets` and (in the redesign)
/// blacklist proposals. Not updatable through any execute path.
pub multisig_addr: String,
/// cw4 group contract referenced by the (stubbed) blacklist proposal flow.
pub group_addr: String,
/// Default per-deposit price. The denom of this coin is the contract's
/// canonical denom for the rest of its lifetime.
pub deposit_amount: Coin,
}
#[cw_serde]
pub enum ExecuteMsg {
/// Used by clients to request ticket books from the signers
DepositTicketBookFunds {
identity_key: String,
},
/// Submitted by clients to escrow funds and register a claimed ed25519
/// identity key. Mints a sequential `deposit_id`. The contract does not
/// verify control of the identity key - that proof is checked off-chain by
/// nym-api signers at blind-sign time.
DepositTicketBookFunds { identity_key: String },
/// Used by gateways to batch redeem tokens from the spent tickets
/// Submitted by gateways to request batch redemption of spent tickets.
/// Dispatches a `Propose` SubMsg to the multisig contract; the actual
/// transfer effect is gated behind multisig approval.
RequestRedemption {
commitment_bs58: String,
number_of_tickets: u16,
},
/// The actual message that gets executed, after multisig votes, that transfers the ticket tokens into gateway's (and the holding) account
RedeemTickets {
n: u16,
gw: String,
},
/// **Legacy / dead code.** Only callable by the multisig; bumps the
/// unredeemed-tickets counter and emits a `ticket_redemption` event with
/// `moved_to_holding_account = "false"`. No known consumer depends on the
/// side effects; candidate for removal in a follow-on breaking-schema
/// change.
RedeemTickets { n: u16, gw: String },
UpdateAdmin {
admin: String,
},
/// Transfer the contract admin role. Only the current admin may sign.
/// Dispatches via the cw_controllers `execute_update_admin` handshake.
UpdateAdmin { admin: String },
/// Overwrite `Config::deposit_amount`. Only callable by the contract admin.
/// Rejects values below `nym_network_defaults::TICKETBOOK_SIZE` and trips
/// `TicketBookSizeChanged` if the snapshotted invariant has diverged from
/// the current crate constant.
#[serde(alias = "update_deposit_value")]
UpdateDefaultDepositValue {
new_deposit: Coin,
},
UpdateDefaultDepositValue { new_deposit: Coin },
/// Set (or overwrite) a reduced deposit price for a specific address.
/// Only callable by the contract admin.
SetReducedDepositPrice {
address: String,
deposit: Coin,
},
SetReducedDepositPrice { address: String, deposit: Coin },
/// Remove the reduced deposit price for a specific address, reverting them to
/// the default price. Returns an error if the address has no custom price set.
/// Only callable by the contract admin.
RemoveReducedDepositPrice {
address: String,
},
RemoveReducedDepositPrice { address: String },
// TODO: properly implement
ProposeToBlacklist {
public_key: String,
},
AddToBlacklist {
public_key: String,
},
/// **Stubbed**: always returns `EcashContractError::UnimplementedBlacklisting`.
/// Storage, reply handler, and helper paths exist but are unreachable from
/// the public ExecuteMsg surface. Preserved for the redesign.
ProposeToBlacklist { public_key: String },
/// **Stubbed**: always returns `EcashContractError::UnimplementedBlacklisting`.
AddToBlacklist { public_key: String },
}
#[cw_serde]
#[cfg_attr(feature = "schema", derive(QueryResponses))]
pub enum QueryMsg {
/// Look up a blacklist entry by its bs58-encoded ed25519 public key. Always
/// returns `None` on a freshly deployed contract because the blacklist
/// execute paths are stubbed.
#[cfg_attr(feature = "schema", returns(BlacklistedAccountResponse))]
GetBlacklistedAccount { public_key: String },
/// Paginated listing of blacklist entries. Always empty today (see stubbed
/// blacklist surface). Defaults: limit 50, max 75.
#[cfg_attr(feature = "schema", returns(PagedBlacklistedAccountResponse))]
GetBlacklistPaged {
limit: Option<u32>,
start_after: Option<String>,
},
/// Default per-deposit price (`Config::deposit_amount`). The
/// `GetRequiredDepositAmount` aliases are kept for backwards compatibility.
#[cfg_attr(feature = "schema", returns(Coin))]
#[serde(alias = "get_required_deposit_amount")]
#[serde(alias = "GetRequiredDepositAmount")]
GetDefaultDepositAmount {},
/// Per-address reduced deposit price override, if any. `None` for any
/// non-whitelisted address.
#[cfg_attr(feature = "schema", returns(Option<Coin>))]
GetReducedDepositAmount { address: String },
/// Enumerate every reduced-deposit whitelist entry in ascending address
/// order. Unpaginated by design (the whitelist is expected to stay small).
#[cfg_attr(feature = "schema", returns(WhitelistedAccountsResponse))]
GetAllWhitelistedAccounts {},
/// Look up a deposit by id. Returns `{ id, deposit: None }` when the id has
/// not yet been assigned.
#[cfg_attr(feature = "schema", returns(DepositResponse))]
GetDeposit { deposit_id: u32 },
/// Most recently assigned deposit (or `{ deposit: None }` on a fresh
/// contract). See `DepositStorage::latest_deposit`.
#[cfg_attr(feature = "schema", returns(LatestDepositResponse))]
GetLatestDeposit {},
/// Paginated listing of deposits in ascending id order. Defaults: limit 50,
/// max 100.
#[cfg_attr(feature = "schema", returns(PagedDepositsResponse))]
GetDepositsPaged {
limit: Option<u32>,
start_after: Option<u32>,
},
/// Aggregate statistics: global totals + per-account custom-price
/// breakdowns. Reassembled in a single read pass from `PoolCounters` and
/// `DepositStatsStorage`.
#[cfg_attr(feature = "schema", returns(DepositsStatistics))]
GetDepositsStatistics {},
}
@@ -1,5 +1,8 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
/// Title used for the cw3 `Propose` message dispatched by `RequestRedemption`.
/// nym-api signers cross-check this exact string when validating that an
/// in-flight proposal originated from the ecash contract.
// TODO: to be moved to multisig
pub const BATCH_REDEMPTION_PROPOSAL_TITLE: &str = "ecash-redemption";
@@ -4,12 +4,16 @@
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Coin};
/// Whitelist entry: an address and the reduced deposit price it may pay.
/// Persisted in the `"reduced_deposits"` storage map.
#[cw_serde]
pub struct WhitelistedAccount {
pub address: Addr,
pub deposit: Coin,
}
/// Response shape for `GetAllWhitelistedAccounts`. Unpaginated - the whitelist
/// is expected to stay small.
#[cw_serde]
pub struct WhitelistedAccountsResponse {
pub whitelisted_accounts: Vec<WhitelistedAccount>,
@@ -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,
@@ -0,0 +1,26 @@
[package]
name = "nym-network-monitors-contract-common"
description = "Common library for the Nym Network Monitors contract"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
readme.workspace = true
version.workspace = true
[dependencies]
thiserror = { workspace = true }
serde = { workspace = true }
schemars = { workspace = true }
cosmwasm-std = { workspace = true }
cosmwasm-schema = { workspace = true }
cw-controllers = { workspace = true }
[features]
schema = []
[lints]
workspace = true
@@ -0,0 +1,8 @@
// 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";
}
@@ -0,0 +1,30 @@
// 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),
}
@@ -0,0 +1,11 @@
// 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::*;
@@ -0,0 +1,78 @@
// 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 {}
@@ -0,0 +1,53 @@
// 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>,
}
@@ -0,0 +1,32 @@
[package]
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
rust-version = "1.85"
readme.workspace = true
publish = true
[dependencies]
thiserror = { workspace = true }
serde = { workspace = true }
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 = []
[lints]
workspace = true
@@ -0,0 +1,114 @@
// 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";
pub const FAMILY_UPDATE_EVENT_NAME: &str = "family_update";
pub const FAMILY_UPDATE_EVENT_FAMILY_ID: &str = "family_id";
pub const FAMILY_UPDATE_EVENT_OWNER_ADDRESS: &str = "owner_address";
/// Attribute carrying the new family name. Only emitted when the
/// `UpdateFamily` message carried `updated_name = Some(_)`.
pub const FAMILY_UPDATE_EVENT_UPDATED_NAME: &str = "updated_name";
/// Attribute carrying the new family description. Only emitted when the
/// `UpdateFamily` message carried `updated_description = Some(_)`.
pub const FAMILY_UPDATE_EVENT_UPDATED_DESCRIPTION: &str = "updated_description";
}
@@ -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,221 @@
// 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 },
/// Update the name and/or description of the family owned by the message
/// sender. Each field is independently optional: `None` leaves the
/// existing value unchanged, `Some(_)` replaces it. Updated values are
/// validated against the same length / normalisation / global-uniqueness
/// rules as [`Self::CreateFamily`].
UpdateFamily {
updated_name: Option<String>,
updated_description: Option<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>,
}

Some files were not shown because too many files have changed in this diff Show More