Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 456574c520 | |||
| acc7d45550 | |||
| f1690f9953 | |||
| 28b22f6b22 | |||
| 6b0a904d10 | |||
| d2833c76c0 | |||
| f2e379f10a | |||
| 8dc53b137c | |||
| 64c68d7b76 | |||
| 71d4b5b3ea | |||
| b3e37ce13c | |||
| bad438b5ad | |||
| bde2b07d0d | |||
| 8795cbe407 | |||
| 26538f5a40 | |||
| 4a266a7348 | |||
| 483bb6f477 | |||
| a68355a75a | |||
| 3c6d88397e | |||
| 1572d8e5c2 | |||
| fd76c5ca4d | |||
| f94589c2d1 | |||
| 1c40499829 | |||
| f8a4d5f1ff | |||
| 42807890af | |||
| 5aa576b596 | |||
| 0215ad9294 | |||
| ad9d66ef3a | |||
| 657aa9f86f | |||
| b45351bb0c | |||
| c7e4255095 | |||
| bafed70f51 | |||
| e0de0d8835 | |||
| 227e6a10e1 | |||
| d3b6a270de | |||
| e12ada0105 | |||
| 71d50d79c2 | |||
| a21a01cf1a | |||
| 362f84b5f6 | |||
| daed9cd15b | |||
| a53ca71bd2 | |||
| a70e68c7bd | |||
| fdebed7c38 | |||
| f576a4ee2d | |||
| a9aafd785e | |||
| 0f7dbb94a8 | |||
| 2d72b1b201 | |||
| 412657f773 | |||
| b501ddd534 | |||
| e9f6d1d47a | |||
| 52b4490e80 | |||
| 7b30c83f9a | |||
| 4aabb4ed56 | |||
| b14c28a462 | |||
| 664782c0c6 | |||
| aeb2f1f0f6 | |||
| 268ba36700 | |||
| c4df05157a | |||
| 09548a9aa9 | |||
| 78b796bf24 | |||
| f5ab7b3eb6 | |||
| 9cf679dadb | |||
| 97a382520c | |||
| f87ce06865 | |||
| 6095215a73 | |||
| 8c6ff79cd1 | |||
| 16678537f7 | |||
| ae877e3867 | |||
| 21479bfb80 | |||
| db8edfe752 | |||
| 73edf28f39 | |||
| d23a42f7f5 | |||
| d0f2c08cd1 | |||
| 5599987d89 | |||
| a93763d73b | |||
| 8e8b6f4467 | |||
| 7feeed41d5 | |||
| e9a20653b8 | |||
| 9438691506 | |||
| 84a4924e77 | |||
| 49277310ba | |||
| 944d2eb7d5 | |||
| bfaf17540e | |||
| 6dbc4efbd9 | |||
| cabbeaf1bf | |||
| a99b8348d7 | |||
| bf85e9eb79 | |||
| 3f1e04ebd4 | |||
| d4c5131bcb | |||
| ef1c1b50d5 | |||
| 23b745d353 | |||
| 3dc94cc85a | |||
| a4c4345257 | |||
| a0fb92cf17 | |||
| 52cc77356e | |||
| a671084f4e | |||
| 3ae986acc8 | |||
| 754994ba01 | |||
| 33b181b26b | |||
| 809559e6dc | |||
| 00cc2f215a |
@@ -25,14 +25,14 @@ jobs:
|
||||
echo "file2=$(ls nym-vpn*.deb)" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload nym-repo-setup
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ env.file1 }}
|
||||
path: ppa/packages/nym-repo-setup*.deb
|
||||
retention-days: 10
|
||||
|
||||
- name: Upload nym-vpn
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ env.file2 }}
|
||||
path: ppa/packages/nym-vpn*.deb
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
run: sudo apt-get install -y rsync
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.2.0
|
||||
uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
version: 9
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -37,6 +37,9 @@ jobs:
|
||||
command: build
|
||||
args: --workspace --release
|
||||
|
||||
- name: Verify doc versions
|
||||
run: ${{ github.workspace }}/documentation/scripts/verify-doc-versions.sh
|
||||
working-directory: ${{ github.workspace }}
|
||||
- name: Install project dependencies
|
||||
run: pnpm i
|
||||
- name: Generate llms-full.txt
|
||||
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
|
||||
- name: Upload Artifact
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nym-binaries-artifacts
|
||||
path: |
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
name: ci-build-upload-network-monitor-agent
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-upload:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [arc-ubuntu-22.04]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Prepare build output directory
|
||||
shell: bash
|
||||
env:
|
||||
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
|
||||
run: |
|
||||
rm -rf ci-builds || true
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get -y install libudev-dev
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
|
||||
|
||||
- name: Build nym-network-monitor-agent
|
||||
shell: bash
|
||||
run: cargo build -p nym-network-monitor-agent --release
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: nym-network-monitor-agent
|
||||
path: target/release/nym-network-monitor-agent
|
||||
retention-days: 30
|
||||
|
||||
- name: Prepare build output
|
||||
shell: bash
|
||||
env:
|
||||
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
|
||||
run: cp target/release/nym-network-monitor-agent "$OUTPUT_DIR"
|
||||
|
||||
- name: Deploy to CI www
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-avzr"
|
||||
SOURCE: "ci-builds/"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
@@ -0,0 +1,19 @@
|
||||
name: ci-crates-preflight
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'Cargo.toml'
|
||||
- '**/Cargo.toml'
|
||||
- 'tools/internal/check_publish_preflight.py'
|
||||
- '.github/workflows/ci-crates-preflight.yml'
|
||||
|
||||
jobs:
|
||||
preflight:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Preflight publish checks
|
||||
run: python3 tools/internal/check_publish_preflight.py
|
||||
@@ -57,7 +57,8 @@ jobs:
|
||||
|
||||
- name: Update workspace dependencies
|
||||
run: |
|
||||
sed -i '/path = /s/version = "${{ steps.current_version.outputs.version }}"/version = "${{ inputs.version }}"/g' Cargo.toml
|
||||
# Match any semver version on lines with `path = `, not just the current workspace version.
|
||||
sed -i '/path = /s/version = "[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"/version = "${{ inputs.version }}"/g' Cargo.toml
|
||||
|
||||
- name: Bump versions (local only)
|
||||
run: |
|
||||
|
||||
@@ -33,7 +33,11 @@ jobs:
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
# `--publish-as-is` skips version bumping since that's done in a separate CI job.
|
||||
- name: Preflight publish checks
|
||||
run: |
|
||||
python3 tools/internal/check_publish_preflight.py
|
||||
|
||||
# --publish-as-is skips version bumping since that's done in a separate CI job.
|
||||
- name: Publish
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
|
||||
@@ -19,6 +19,7 @@ jobs:
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
@@ -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
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
run: sudo apt-get install -y rsync
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.2.0
|
||||
uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
version: 9
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -61,6 +61,9 @@ jobs:
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/sdk && typedoc --skipErrorChecking
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/mix-fetch && typedoc --skipErrorChecking
|
||||
|
||||
- name: Verify doc versions
|
||||
run: ${{ github.workspace }}/documentation/scripts/verify-doc-versions.sh
|
||||
working-directory: ${{ github.workspace }}
|
||||
- name: Install project dependencies
|
||||
run: pnpm i
|
||||
- name: Generate llms-full.txt
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
name: ci-nym-wallet-frontend
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'nym-wallet/**'
|
||||
- '.github/workflows/ci-nym-wallet-frontend.yml'
|
||||
|
||||
jobs:
|
||||
types-lint:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: nym-wallet/.nvmrc
|
||||
cache: yarn
|
||||
cache-dependency-path: yarn.lock
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --network-timeout 100000
|
||||
|
||||
- name: Build TypeScript packages (wallet depends on @nymproject/types, etc.)
|
||||
run: yarn build:types
|
||||
|
||||
- name: Build @nymproject/mui-theme and @nymproject/react (wallet imports subpaths)
|
||||
run: yarn build:packages
|
||||
|
||||
- name: Typecheck nym-wallet
|
||||
run: yarn --cwd nym-wallet tsc
|
||||
|
||||
- name: Lint nym-wallet
|
||||
run: yarn --cwd nym-wallet lint
|
||||
|
||||
- name: Yarn audit (workspace lockfile; informational)
|
||||
run: yarn audit --level critical
|
||||
continue-on-error: true
|
||||
|
||||
- name: Unit tests (nym-wallet)
|
||||
run: yarn --cwd nym-wallet test
|
||||
@@ -41,6 +41,9 @@ jobs:
|
||||
sed -i.bak '1s/^/\[profile.dev\]\ndebug = false\n\n/' Cargo.toml
|
||||
git diff
|
||||
|
||||
- name: Ensure nym-wallet/dist exists for Tauri
|
||||
run: mkdir -p nym-wallet/dist
|
||||
|
||||
- name: Build all binaries
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
@@ -71,3 +74,16 @@ jobs:
|
||||
with:
|
||||
command: clippy
|
||||
args: --manifest-path nym-wallet/Cargo.toml --workspace --all-features --all-targets -- -D warnings
|
||||
|
||||
- name: Install cargo-audit
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: cargo-audit --locked
|
||||
|
||||
- name: Cargo audit (nym-wallet workspace)
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: audit
|
||||
working-directory: nym-wallet
|
||||
continue-on-error: true
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
name: ci-nym-wallet-storybook
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'nym-wallet/**'
|
||||
- '.github/workflows/ci-nym-wallet-storybook.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install rsync
|
||||
run: sudo apt-get install rsync
|
||||
continue-on-error: true
|
||||
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup yarn
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
- name: Build dependencies
|
||||
run: yarn && yarn build
|
||||
|
||||
- name: Build storybook
|
||||
run: yarn storybook:build
|
||||
working-directory: ./nym-wallet
|
||||
|
||||
- name: Deploy branch to CI www (storybook)
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-rltgoDzvO --delete"
|
||||
SOURCE: "nym-wallet/storybook-static/"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/wallet-${{ env.GITHUB_REF_SLUG }}
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
|
||||
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
|
||||
- name: Install rust toolchain
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
find . -name Cargo.toml -exec cargo deny --manifest-path {} check \
|
||||
advisories -A advisory-not-detected --hide-inclusion-graph \; &> \
|
||||
>(uniq &> .github/workflows/support-files/notifications/deny.message )
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: report
|
||||
path: .github/workflows/support-files/notifications/deny.message
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
args: --workspace --release ${{ env.CARGO_FEATURES }}
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: my-artifact
|
||||
path: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
node-version: 22.13.0
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
cd -
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nym-wallet.app.tar.gz
|
||||
path: nym-wallet/target/x86_64-apple-darwin/release/bundle/macos/nym-wallet.app.tar.gz
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
node-version: 22.13.0
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Rust toolchain
|
||||
@@ -72,6 +72,41 @@ jobs:
|
||||
find target/release/bundle -type d -name "*appimage*" -o -name "*AppImage*" || echo "No AppImage directories found"
|
||||
find target/release/bundle -name "*.AppImage" -o -name "*.appimage" || echo "No AppImage files found"
|
||||
fi
|
||||
|
||||
- name: Inspect AppImage (hook + bundled graphics libs)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
APPIMAGE_REL=$(find target/release/bundle -name '*.AppImage' | head -n 1)
|
||||
if [ -z "${APPIMAGE_REL}" ]; then
|
||||
echo "No AppImage under target/release/bundle"
|
||||
exit 1
|
||||
fi
|
||||
APPIMAGE_ABS="${GITHUB_WORKSPACE}/nym-wallet/${APPIMAGE_REL}"
|
||||
chmod +x "${APPIMAGE_ABS}"
|
||||
EXTRACT_DIR=$(mktemp -d)
|
||||
cd "${EXTRACT_DIR}"
|
||||
"${APPIMAGE_ABS}" --appimage-extract
|
||||
# Tauri only stages appimage "files" under /usr/ into the AppDir; paths like /apprun-hooks/ never reach the image.
|
||||
# Wayland + WEBKIT_DISABLE_DMABUF_RENDERER defaults are applied in main() instead (see configure_linux_wayland_defaults).
|
||||
HOOK=$(find squashfs-root -name '99-nym-wayland.sh' 2>/dev/null | head -n 1)
|
||||
if [ -n "${HOOK}" ]; then
|
||||
echo "Found legacy apprun hook at ${HOOK}"
|
||||
else
|
||||
echo "No apprun-hooks/99-nym-wayland.sh (expected): Wayland defaults are set in-process."
|
||||
fi
|
||||
find squashfs-root/usr/lib -maxdepth 6 \
|
||||
\( -name 'libwayland-client.so*' -o -name 'libEGL.so*' -o -name 'libgbm.so*' \) \
|
||||
2>/dev/null | sort > "${GITHUB_WORKSPACE}/nym-wallet/appimage-bundled-graphics-libs.txt"
|
||||
wc -l "${GITHUB_WORKSPACE}/nym-wallet/appimage-bundled-graphics-libs.txt"
|
||||
head -50 "${GITHUB_WORKSPACE}/nym-wallet/appimage-bundled-graphics-libs.txt" || true
|
||||
|
||||
- name: Upload AppImage graphics lib inventory
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: nym-wallet-appimage-lib-inventory
|
||||
path: nym-wallet/appimage-bundled-graphics-libs.txt
|
||||
retention-days: 30
|
||||
|
||||
- name: Create AppImage tarball if needed
|
||||
run: |
|
||||
@@ -97,7 +132,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nym-wallet-appimage.tar.gz
|
||||
path: |
|
||||
|
||||
@@ -26,6 +26,9 @@ jobs:
|
||||
outputs:
|
||||
release_tag: ${{ github.ref_name }}
|
||||
|
||||
env:
|
||||
SIGN_WINDOWS: ${{ github.event_name == 'release' || inputs.sign }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
@@ -35,50 +38,84 @@ 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
|
||||
with:
|
||||
node-version: 21
|
||||
node-version: 22.13.0
|
||||
|
||||
- name: Install Yarn (classic)
|
||||
shell: bash
|
||||
run: npm install -g yarn@1.22.22
|
||||
|
||||
- name: Strip Authenticode thumbprint (avoid signtool on runner)
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ env.SIGN_WINDOWS == 'true' || (github.event_name == 'workflow_dispatch' && !inputs.sign) }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! command -v yq >/dev/null 2>&1; then
|
||||
echo "yq is required on this runner to edit tauri.conf.json"
|
||||
exit 1
|
||||
fi
|
||||
yq eval --inplace '
|
||||
del(.bundle.windows.certificateThumbprint) |
|
||||
del(.bundle.windows.digestAlgorithm) |
|
||||
del(.bundle.windows.timestampUrl)
|
||||
' tauri.conf.json
|
||||
|
||||
- name: Download EV CodeSignTool from ssl.com
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ inputs.sign }}
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
curl -L0 https://www.ssl.com/download/codesigntool-for-linux-and-macos/ -o codesigntool.zip
|
||||
unzip codesigntool.zip
|
||||
- name: Get EV certificate credential id
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ inputs.sign }}
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
id: get_credential_ids
|
||||
shell: bash
|
||||
run: |
|
||||
echo "SSL_COM_CREDENTIAL_ID=$(./CodeSignTool.sh get_credential_ids -username=${{ secrets.SSL_COM_USERNAME }} -password=${{ secrets.SSL_COM_PASSWORD }} | sed -n '1!p' | sed 's/- //')" >> "$GITHUB_OUTPUT"
|
||||
- name: Add custom sign command to tauri.conf.json
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ inputs.sign }}
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
SSL_SIGN_USER: ${{ secrets.SSL_COM_USERNAME }}
|
||||
SSL_SIGN_PASS: ${{ secrets.SSL_COM_PASSWORD }}
|
||||
SSL_SIGN_CRED: ${{ steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
|
||||
SSL_SIGN_TOTP: ${{ secrets.SSL_COM_TOTP_SECRET }}
|
||||
run: |
|
||||
yq eval --inplace '.bundle.windows +=
|
||||
{
|
||||
"signCommand": {
|
||||
"cmd": "C:\Program Files\Git\bin\bash.EXE",
|
||||
"args": [
|
||||
"/c/actions-runner/_work/nym/nym/nym-wallet/src-tauri/CodeSignTool.sh",
|
||||
"sign",
|
||||
"-username ${{ secrets.SSL_COM_USERNAME }}",
|
||||
"-password ${{ secrets.SSL_COM_PASSWORD }}",
|
||||
"-credential_id ${{ steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}",
|
||||
"-totp_secret ${{ secrets.SSL_COM_TOTP_SECRET }}",
|
||||
"-program_name NymWallet",
|
||||
"-input_file_path",
|
||||
"%1",
|
||||
"-override"
|
||||
]
|
||||
set -euo pipefail
|
||||
if ! command -v cygpath >/dev/null 2>&1; then
|
||||
echo "cygpath not found; install Git for Windows or use bash from Git SDK"
|
||||
exit 1
|
||||
fi
|
||||
export SCRIPT_UNIX="$(cygpath -u "$GITHUB_WORKSPACE/nym-wallet/src-tauri/CodeSignTool.sh")"
|
||||
yq eval --inplace '
|
||||
.bundle.windows += {
|
||||
"signCommand": {
|
||||
"cmd": "C:/Program Files/Git/bin/bash.exe",
|
||||
"args": [
|
||||
strenv(SCRIPT_UNIX),
|
||||
"sign",
|
||||
("-username " + strenv(SSL_SIGN_USER)),
|
||||
("-password " + strenv(SSL_SIGN_PASS)),
|
||||
("-credential_id " + strenv(SSL_SIGN_CRED)),
|
||||
("-totp_secret " + strenv(SSL_SIGN_TOTP)),
|
||||
"-program_name NymWallet",
|
||||
"-input_file_path",
|
||||
"%1",
|
||||
"-override"
|
||||
]
|
||||
}
|
||||
}
|
||||
}' tauri.conf.json
|
||||
' tauri.conf.json
|
||||
- name: Install project dependencies
|
||||
shell: bash
|
||||
run: cd .. && yarn --network-timeout 100000
|
||||
@@ -93,10 +130,10 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
SSL_COM_USERNAME: ${{ inputs.sign && secrets.SSL_COM_USERNAME }}
|
||||
SSL_COM_PASSWORD: ${{ inputs.sign && secrets.SSL_COM_PASSWORD }}
|
||||
SSL_COM_CREDENTIAL_ID: ${{ inputs.sign && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
|
||||
SSL_COM_TOTP_SECRET: ${{ inputs.sign && secrets.SSL_COM_TOTP_SECRET }}
|
||||
SSL_COM_USERNAME: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_USERNAME }}
|
||||
SSL_COM_PASSWORD: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_PASSWORD }}
|
||||
SSL_COM_CREDENTIAL_ID: ${{ env.SIGN_WINDOWS == 'true' && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
|
||||
SSL_COM_TOTP_SECRET: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_TOTP_SECRET }}
|
||||
run: |
|
||||
echo "Starting build process..."
|
||||
yarn build
|
||||
@@ -128,7 +165,7 @@ jobs:
|
||||
find . -name "*.msi" -type f
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nym-wallet.msi
|
||||
path: |
|
||||
@@ -167,4 +204,4 @@ jobs:
|
||||
needs: publish-tauri
|
||||
with:
|
||||
release_tag: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
|
||||
secrets: inherit
|
||||
secrets: inherit
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
name: Build and upload Network Monitor Agent container to harbor.nymte.ch
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_image:
|
||||
description: 'Tag image as a release (prefix with golden-)'
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
WORKING_DIRECTORY: "nym-network-monitor-v3/nym-network-monitor-agent"
|
||||
CONTAINER_NAME: "network-monitor-agent"
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
password: ${{ secrets.HARBOR_ROBOT_SECRET }}
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
git config --global user.email "lawrence@nymtech.net"
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from Cargo.toml
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml)
|
||||
echo "result=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set GIT_TAG variable
|
||||
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Initialize RELEASE_TAG
|
||||
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
|
||||
|
||||
- name: Set RELEASE_TAG for release
|
||||
if: github.event.inputs.release_image == 'true'
|
||||
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
|
||||
|
||||
- name: Set IMAGE_NAME_AND_TAGS variable
|
||||
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
|
||||
|
||||
- name: New env vars
|
||||
run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
|
||||
|
||||
- name: Build and push image to Harbor
|
||||
run: |
|
||||
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }}
|
||||
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
name: Build and upload Network Monitor Orchestrator container to harbor.nymte.ch
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_image:
|
||||
description: 'Tag image as a release (prefix with golden-)'
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
WORKING_DIRECTORY: "nym-network-monitor-v3/nym-network-monitor-orchestrator"
|
||||
CONTAINER_NAME: "network-monitor-orchestrator"
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
password: ${{ secrets.HARBOR_ROBOT_SECRET }}
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
git config --global user.email "lawrence@nymtech.net"
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from Cargo.toml
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml)
|
||||
echo "result=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Initialize RELEASE_TAG
|
||||
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
|
||||
|
||||
- name: Set RELEASE_TAG for release
|
||||
if: github.event.inputs.release_image == 'true'
|
||||
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
|
||||
|
||||
- name: Set IMAGE_NAME_AND_TAGS variable
|
||||
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Log image name
|
||||
run: echo "RELEASE_TAG='$RELEASE_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
|
||||
|
||||
- name: Build and push image to Harbor
|
||||
run: |
|
||||
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }}
|
||||
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
with:
|
||||
release-tag-or-name-or-id: ${{ inputs.release_tag }}
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: Asset Hashes
|
||||
path: hashes.json
|
||||
|
||||
@@ -27,6 +27,7 @@ v6-topology.json
|
||||
/explorer/public/downloads/mixmining.json
|
||||
/explorer/public/downloads/topology.json
|
||||
/nym-wallet/dist/*
|
||||
/nym-wallet/appimage-bundled-graphics-libs.txt
|
||||
/clients/validator/examples/nym-driver-example/current-contract.txt
|
||||
validator-api/v4.json
|
||||
validator-api/v6.json
|
||||
@@ -77,3 +78,4 @@ CLAUDE.md
|
||||
|
||||
/notes
|
||||
/target-otel
|
||||
test-tutorials/
|
||||
|
||||
Generated
+1214
-918
File diff suppressed because it is too large
Load Diff
+136
-130
@@ -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,6 +40,7 @@ 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",
|
||||
@@ -70,11 +70,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 +94,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,6 +126,7 @@ members = [
|
||||
"common/zulip-client",
|
||||
"documentation/autodoc",
|
||||
"gateway",
|
||||
"integration-tests",
|
||||
"nym-api",
|
||||
"nym-api/nym-api-requests",
|
||||
"nym-authenticator-client",
|
||||
@@ -129,6 +134,7 @@ members = [
|
||||
"nym-credential-proxy/nym-credential-proxy",
|
||||
"nym-credential-proxy/nym-credential-proxy-requests",
|
||||
"nym-data-observatory",
|
||||
"nym-gateway-probe",
|
||||
"nym-ip-packet-client",
|
||||
"nym-network-monitor",
|
||||
"nym-node",
|
||||
@@ -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,19 +154,18 @@ members = [
|
||||
"sdk/ffi/go",
|
||||
"sdk/ffi/shared",
|
||||
"sdk/rust/nym-sdk",
|
||||
"smolmix/core",
|
||||
"service-providers/common",
|
||||
"service-providers/ip-packet-router",
|
||||
"service-providers/network-requester",
|
||||
"nym-sqlx-pool-guard",
|
||||
"smolmix/core",
|
||||
"tools/echo-server",
|
||||
"tools/internal/contract-state-importer/importer-cli",
|
||||
"tools/internal/contract-state-importer/importer-contract",
|
||||
"tools/internal/localnet-orchestrator",
|
||||
"tools/internal/localnet-orchestrator/dkg-bypass-contract",
|
||||
"tools/internal/mixnet-connectivity-check",
|
||||
# "tools/internal/sdk-version-bump",
|
||||
"tools/internal/ssl-inject",
|
||||
"tools/internal/localnet-orchestrator",
|
||||
"tools/internal/localnet-orchestrator/dkg-bypass-contract",
|
||||
"tools/internal/validator-status-check",
|
||||
"tools/nym-cli",
|
||||
"tools/nym-id-cli",
|
||||
@@ -172,27 +178,23 @@ members = [
|
||||
"wasm/mix-fetch",
|
||||
"wasm/node-tester",
|
||||
"wasm/zknym-lib",
|
||||
"nym-gateway-probe",
|
||||
"integration-tests",
|
||||
"common/nym-kkt-ciphersuite",
|
||||
"common/nym-kkt-context",
|
||||
]
|
||||
|
||||
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"
|
||||
]
|
||||
|
||||
exclude = ["contracts", "nym-wallet", "cpu-cycles"]
|
||||
@@ -206,7 +208,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"
|
||||
@@ -220,16 +222,16 @@ 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"
|
||||
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"
|
||||
bip39 = { version = "2.0.0", features = ["zeroize"] }
|
||||
bitvec = "1.0.0"
|
||||
blake3 = "1.7.0"
|
||||
blake3 = ">=1.7, <1.8.4" # blake3 1.8.4+ requires digest 0.11; workspace is on 0.10
|
||||
bloomfilter = "3.0.1"
|
||||
bs58 = "0.5.1"
|
||||
bytecodec = "0.4.15"
|
||||
@@ -248,7 +250,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"
|
||||
@@ -280,8 +282,8 @@ getrandom03 = { package = "getrandom", version = "=0.3.3" }
|
||||
glob = "0.3"
|
||||
handlebars = "3.5.5"
|
||||
hex = "0.4.3"
|
||||
hickory-proto = "0.25.2"
|
||||
hickory-resolver = "0.25.2"
|
||||
hickory-proto = "0.26.1"
|
||||
hickory-resolver = "0.26.1"
|
||||
hkdf = "0.12.3"
|
||||
hmac = "0.12.1"
|
||||
http = "1"
|
||||
@@ -325,7 +327,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" }
|
||||
@@ -349,7 +351,6 @@ serde_yaml = "0.9.25"
|
||||
serde_plain = "1.0.2"
|
||||
sha2 = "0.10.3"
|
||||
si-scale = "0.2.3"
|
||||
smolmix = { version = "0.0.1", path = "smolmix/core" }
|
||||
smoltcp = "0.12"
|
||||
snow = "0.9.6"
|
||||
sphinx-packet = "=0.6.0"
|
||||
@@ -360,7 +361,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"
|
||||
@@ -391,7 +392,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 }
|
||||
@@ -411,113 +412,117 @@ libcrux-chacha20poly1305 = "0.0.7"
|
||||
libcrux-psq = "0.0.8"
|
||||
libcrux-ml-kem = "0.0.8"
|
||||
libcrux-sha3 = "0.0.8"
|
||||
libcrux-traits = "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-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.20.4", path = "common/wasm/client-core" }
|
||||
nym-wasm-storage = { version = "1.20.4", path = "common/wasm/storage" }
|
||||
nym-wasm-utils = { version = "1.20.4", path = "common/wasm/utils", default-features = false }
|
||||
nyxd-scraper-shared = { version = "1.20.4", path = "common/nyxd-scraper-shared" }
|
||||
nym-wasm-client-core = { version = "1.21.0", path = "common/wasm/client-core" }
|
||||
nym-wasm-storage = { version = "1.21.0", path = "common/wasm/storage" }
|
||||
nym-wasm-utils = { version = "1.21.0", path = "common/wasm/utils", default-features = false }
|
||||
nyxd-scraper-shared = { version = "1.21.0", path = "common/nyxd-scraper-shared" }
|
||||
|
||||
smolmix = { version = "1.21.0", path = "smolmix/core" }
|
||||
|
||||
# coconut/DKG related
|
||||
# unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork
|
||||
@@ -616,3 +621,4 @@ exit = "deny"
|
||||
panic = "deny"
|
||||
unimplemented = "deny"
|
||||
unreachable = "deny"
|
||||
|
||||
|
||||
@@ -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') }}"
|
||||
@@ -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)
|
||||
|
||||
@@ -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>>(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -26,6 +26,7 @@ nym-ecash-contract-common = { workspace = true }
|
||||
nym-multisig-contract-common = { workspace = true }
|
||||
nym-group-contract-common = { workspace = true }
|
||||
nym-performance-contract-common = { workspace = true }
|
||||
nym-node-families-contract-common = { workspace = true }
|
||||
nym-serde-helpers = { workspace = true, features = ["hex", "base64"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -15,6 +15,7 @@ use nym_api_requests::ecash::models::{
|
||||
VerifyEcashTicketBody,
|
||||
};
|
||||
use nym_api_requests::ecash::VerificationKeyResponse;
|
||||
use nym_api_requests::models::node_families::NodeFamily;
|
||||
use nym_api_requests::models::{
|
||||
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
|
||||
ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
|
||||
@@ -389,6 +390,45 @@ pub trait NymApiClientExt: ApiClient {
|
||||
Ok(bonds)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn get_node_families(
|
||||
&self,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
) -> Result<PaginatedResponse<NodeFamily>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
if let Some(page) = page {
|
||||
params.push(("page", page.to_string()))
|
||||
}
|
||||
if let Some(per_page) = per_page {
|
||||
params.push(("per_page", per_page.to_string()))
|
||||
}
|
||||
self.get_json(
|
||||
&[routes::V1_API_VERSION, routes::NODE_FAMILIES_ROUTES],
|
||||
¶ms,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_node_families(&self) -> Result<Vec<NodeFamily>, NymAPIError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut families = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut res = self.get_node_families(Some(page), None).await?;
|
||||
|
||||
families.append(&mut res.data);
|
||||
if families.len() < res.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(families)
|
||||
}
|
||||
|
||||
#[deprecated]
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNodeV1>, NymAPIError> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,7 @@ pub mod ecash_query_client;
|
||||
pub mod group_query_client;
|
||||
pub mod mixnet_query_client;
|
||||
pub mod multisig_query_client;
|
||||
pub mod node_families_query_client;
|
||||
pub mod performance_query_client;
|
||||
pub mod vesting_query_client;
|
||||
|
||||
@@ -22,6 +23,7 @@ pub mod ecash_signing_client;
|
||||
pub mod group_signing_client;
|
||||
pub mod mixnet_signing_client;
|
||||
pub mod multisig_signing_client;
|
||||
pub mod node_families_signing_client;
|
||||
pub mod performance_signing_client;
|
||||
pub mod vesting_signing_client;
|
||||
|
||||
@@ -31,6 +33,7 @@ pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient};
|
||||
pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient};
|
||||
pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient};
|
||||
pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient};
|
||||
pub use node_families_query_client::{NodeFamiliesQueryClient, PagedNodeFamiliesQueryClient};
|
||||
pub use performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient};
|
||||
pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient};
|
||||
|
||||
@@ -40,6 +43,7 @@ pub use ecash_signing_client::EcashSigningClient;
|
||||
pub use group_signing_client::GroupSigningClient;
|
||||
pub use mixnet_signing_client::MixnetSigningClient;
|
||||
pub use multisig_signing_client::MultisigSigningClient;
|
||||
pub use node_families_signing_client::NodeFamiliesSigningClient;
|
||||
pub use performance_signing_client::PerformanceSigningClient;
|
||||
pub use vesting_signing_client::VestingSigningClient;
|
||||
|
||||
@@ -49,6 +53,7 @@ pub trait NymContractsProvider {
|
||||
fn mixnet_contract_address(&self) -> Option<&AccountId>;
|
||||
fn vesting_contract_address(&self) -> Option<&AccountId>;
|
||||
fn performance_contract_address(&self) -> Option<&AccountId>;
|
||||
fn node_families_contract_address(&self) -> Option<&AccountId>;
|
||||
|
||||
// coconut-related
|
||||
fn ecash_contract_address(&self) -> Option<&AccountId>;
|
||||
@@ -62,6 +67,7 @@ pub struct TypedNymContracts {
|
||||
pub mixnet_contract_address: Option<AccountId>,
|
||||
pub vesting_contract_address: Option<AccountId>,
|
||||
pub performance_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 +92,10 @@ impl TryFrom<NymContracts> for TypedNymContracts {
|
||||
.performance_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())
|
||||
|
||||
+441
@@ -0,0 +1,441 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::collect_paged;
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::CosmWasmClient;
|
||||
use async_trait::async_trait;
|
||||
use cosmrs::AccountId;
|
||||
use serde::Deserialize;
|
||||
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
pub use nym_node_families_contract_common::{
|
||||
msg::QueryMsg as NodeFamiliesQueryMsg, AllFamilyMembersPagedResponse,
|
||||
AllPastFamilyInvitationsPagedResponse, FamiliesPagedResponse, FamilyMemberRecord,
|
||||
FamilyMembersPagedResponse, GlobalPastFamilyInvitationCursor, NodeFamily,
|
||||
NodeFamilyByNameResponse, NodeFamilyByOwnerResponse, NodeFamilyId,
|
||||
NodeFamilyMembershipResponse, NodeFamilyResponse, PastFamilyInvitation,
|
||||
PastFamilyInvitationCursor, PastFamilyInvitationForNodeCursor,
|
||||
PastFamilyInvitationsForNodePagedResponse, PastFamilyInvitationsPagedResponse,
|
||||
PastFamilyMember, PastFamilyMemberCursor, PastFamilyMemberForNodeCursor,
|
||||
PastFamilyMembersForNodePagedResponse, PastFamilyMembersPagedResponse,
|
||||
PendingFamilyInvitationDetails, PendingFamilyInvitationResponse,
|
||||
PendingFamilyInvitationsPagedResponse, PendingInvitationsForNodePagedResponse,
|
||||
PendingInvitationsPagedResponse,
|
||||
};
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait NodeFamiliesQueryClient {
|
||||
async fn query_node_families_contract<T>(
|
||||
&self,
|
||||
query: NodeFamiliesQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>;
|
||||
|
||||
async fn get_family_by_id(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<NodeFamilyResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyById { family_id })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_family_by_owner(
|
||||
&self,
|
||||
owner: &AccountId,
|
||||
) -> Result<NodeFamilyByOwnerResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyByOwner {
|
||||
owner: owner.to_string(),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_family_by_name(
|
||||
&self,
|
||||
name: String,
|
||||
) -> Result<NodeFamilyByNameResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyByName { name })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_families_paged(
|
||||
&self,
|
||||
start_after: Option<NodeFamilyId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<FamiliesPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamiliesPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_family_membership(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<NodeFamilyMembershipResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyMembership { node_id })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_family_members_paged(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<FamilyMembersPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyMembersPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_family_members_paged(
|
||||
&self,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<AllFamilyMembersPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetAllFamilyMembersPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_pending_invitation(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
) -> Result<PendingFamilyInvitationResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPendingInvitation {
|
||||
family_id,
|
||||
node_id,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_pending_invitations_for_family_paged(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PendingFamilyInvitationsPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(
|
||||
NodeFamiliesQueryMsg::GetPendingInvitationsForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_pending_invitations_for_node_paged(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
start_after: Option<NodeFamilyId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PendingInvitationsForNodePagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPendingInvitationsForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_pending_invitations_paged(
|
||||
&self,
|
||||
start_after: Option<(NodeFamilyId, NodeId)>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PendingInvitationsPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetAllPendingInvitationsPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_past_invitations_for_family_paged(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<PastFamilyInvitationCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PastFamilyInvitationsPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastInvitationsForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_past_invitations_for_node_paged(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
start_after: Option<PastFamilyInvitationForNodeCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PastFamilyInvitationsForNodePagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastInvitationsForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_past_invitations_paged(
|
||||
&self,
|
||||
start_after: Option<GlobalPastFamilyInvitationCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<AllPastFamilyInvitationsPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetAllPastInvitationsPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_past_members_for_family_paged(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<PastFamilyMemberCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PastFamilyMembersPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastMembersForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_past_members_for_node_paged(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
start_after: Option<PastFamilyMemberForNodeCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PastFamilyMembersForNodePagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastMembersForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// extension trait to the query client to deal with the paged queries
|
||||
// (it didn't feel appropriate to combine it with the existing trait)
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait PagedNodeFamiliesQueryClient: NodeFamiliesQueryClient {
|
||||
async fn get_all_families(&self) -> Result<Vec<NodeFamily>, NyxdError> {
|
||||
collect_paged!(self, get_families_paged, families)
|
||||
}
|
||||
|
||||
async fn get_all_family_members_for_family(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<Vec<FamilyMemberRecord>, NyxdError> {
|
||||
collect_paged!(self, get_family_members_paged, members, family_id)
|
||||
}
|
||||
|
||||
async fn get_all_family_members(&self) -> Result<Vec<FamilyMemberRecord>, NyxdError> {
|
||||
collect_paged!(self, get_all_family_members_paged, members)
|
||||
}
|
||||
|
||||
async fn get_all_pending_invitations_for_family(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<Vec<PendingFamilyInvitationDetails>, NyxdError> {
|
||||
collect_paged!(
|
||||
self,
|
||||
get_pending_invitations_for_family_paged,
|
||||
invitations,
|
||||
family_id
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_all_pending_invitations_for_node(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<Vec<PendingFamilyInvitationDetails>, NyxdError> {
|
||||
collect_paged!(
|
||||
self,
|
||||
get_pending_invitations_for_node_paged,
|
||||
invitations,
|
||||
node_id
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_all_pending_invitations(
|
||||
&self,
|
||||
) -> Result<Vec<PendingFamilyInvitationDetails>, NyxdError> {
|
||||
collect_paged!(self, get_all_pending_invitations_paged, invitations)
|
||||
}
|
||||
|
||||
async fn get_all_past_invitations_for_family(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<Vec<PastFamilyInvitation>, NyxdError> {
|
||||
collect_paged!(
|
||||
self,
|
||||
get_past_invitations_for_family_paged,
|
||||
invitations,
|
||||
family_id
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_all_past_invitations_for_node(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<Vec<PastFamilyInvitation>, NyxdError> {
|
||||
collect_paged!(
|
||||
self,
|
||||
get_past_invitations_for_node_paged,
|
||||
invitations,
|
||||
node_id
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_all_past_invitations(&self) -> Result<Vec<PastFamilyInvitation>, NyxdError> {
|
||||
collect_paged!(self, get_all_past_invitations_paged, invitations)
|
||||
}
|
||||
|
||||
async fn get_all_past_members_for_family(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<Vec<PastFamilyMember>, NyxdError> {
|
||||
collect_paged!(self, get_past_members_for_family_paged, members, family_id)
|
||||
}
|
||||
|
||||
async fn get_all_past_members_for_node(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<Vec<PastFamilyMember>, NyxdError> {
|
||||
collect_paged!(self, get_past_members_for_node_paged, members, node_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> PagedNodeFamiliesQueryClient for T where T: NodeFamiliesQueryClient {}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> NodeFamiliesQueryClient for C
|
||||
where
|
||||
C: CosmWasmClient + NymContractsProvider + Send + Sync,
|
||||
{
|
||||
async fn query_node_families_contract<T>(
|
||||
&self,
|
||||
query: NodeFamiliesQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
let node_families_contract_address = &self
|
||||
.node_families_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("node families contract"))?;
|
||||
self.query_contract_smart(node_families_contract_address, &query)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
use nym_node_families_contract_common::QueryMsg;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_query_variants_are_covered<C: NodeFamiliesQueryClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: NodeFamiliesQueryMsg,
|
||||
) {
|
||||
match msg {
|
||||
NodeFamiliesQueryMsg::GetFamilyById { family_id } => {
|
||||
client.get_family_by_id(family_id).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamilyByOwner { owner } => {
|
||||
client.get_family_by_owner(&owner.parse().unwrap()).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamilyByName { name } => {
|
||||
client.get_family_by_name(name).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamiliesPaged { start_after, limit } => {
|
||||
client.get_families_paged(start_after, limit).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamilyMembership { node_id } => {
|
||||
client.get_family_membership(node_id).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamilyMembersPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_family_members_paged(family_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetAllFamilyMembersPaged { start_after, limit } => client
|
||||
.get_all_family_members_paged(start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPendingInvitation { family_id, node_id } => {
|
||||
client.get_pending_invitation(family_id, node_id).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetPendingInvitationsForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_pending_invitations_for_family_paged(family_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPendingInvitationsForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_pending_invitations_for_node_paged(node_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetAllPendingInvitationsPaged { start_after, limit } => client
|
||||
.get_all_pending_invitations_paged(start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPastInvitationsForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_past_invitations_for_family_paged(family_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPastInvitationsForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_past_invitations_for_node_paged(node_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetAllPastInvitationsPaged { start_after, limit } => client
|
||||
.get_all_past_invitations_paged(start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPastMembersForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_past_members_for_family_paged(family_id, start_after, limit)
|
||||
.ignore(),
|
||||
QueryMsg::GetPastMembersForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_past_members_for_node_paged(node_id, start_after, limit)
|
||||
.ignore(),
|
||||
};
|
||||
}
|
||||
}
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::nyxd::coin::Coin;
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::{Fee, SigningCosmWasmClient};
|
||||
use crate::signing::signer::OfflineSigner;
|
||||
use async_trait::async_trait;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_node_families_contract_common::{
|
||||
Config, ExecuteMsg as NodeFamiliesExecuteMsg, NodeFamilyId,
|
||||
};
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait NodeFamiliesSigningClient {
|
||||
async fn execute_node_families_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: NodeFamiliesExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError>;
|
||||
|
||||
async fn update_node_families_config(
|
||||
&self,
|
||||
config: Config,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::UpdateConfig { config },
|
||||
"NodeFamiliesContract::UpdateConfig".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn create_family(
|
||||
&self,
|
||||
name: String,
|
||||
description: String,
|
||||
fee: Option<Fee>,
|
||||
creation_fee: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::CreateFamily { name, description },
|
||||
"NodeFamiliesContract::CreateFamily".to_string(),
|
||||
creation_fee,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn disband_family(&self, fee: Option<Fee>) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::DisbandFamily {},
|
||||
"NodeFamiliesContract::DisbandFamily".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn invite_to_family(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
validity_secs: Option<u64>,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::InviteToFamily {
|
||||
node_id,
|
||||
validity_secs,
|
||||
},
|
||||
"NodeFamiliesContract::InviteToFamily".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn revoke_family_invitation(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::RevokeFamilyInvitation { node_id },
|
||||
"NodeFamiliesContract::RevokeFamilyInvitation".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn accept_family_invitation(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::AcceptFamilyInvitation { family_id, node_id },
|
||||
"NodeFamiliesContract::AcceptFamilyInvitation".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn reject_family_invitation(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::RejectFamilyInvitation { family_id, node_id },
|
||||
"NodeFamiliesContract::RejectFamilyInvitation".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn leave_family(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::LeaveFamily { node_id },
|
||||
"NodeFamiliesContract::LeaveFamily".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn kick_from_family(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::KickFromFamily { node_id },
|
||||
"NodeFamiliesContract::KickFromFamily".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Cross-contract callback fired by the mixnet contract on node unbonding.
|
||||
/// Exposed for completeness; the families contract rejects this call from
|
||||
/// any sender other than the configured mixnet contract address.
|
||||
async fn on_nym_node_unbond(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::OnNymNodeUnbond { node_id },
|
||||
"NodeFamiliesContract::OnNymNodeUnbond".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> NodeFamiliesSigningClient for C
|
||||
where
|
||||
C: SigningCosmWasmClient + NymContractsProvider + Sync,
|
||||
NyxdError: From<<Self as OfflineSigner>::Error>,
|
||||
{
|
||||
async fn execute_node_families_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: NodeFamiliesExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let node_families_contract_address = &self
|
||||
.node_families_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("node families contract"))?;
|
||||
|
||||
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
|
||||
|
||||
let signer_address = &self.signer_addresses()[0];
|
||||
self.execute(
|
||||
signer_address,
|
||||
node_families_contract_address,
|
||||
&msg,
|
||||
fee,
|
||||
memo,
|
||||
funds,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
use nym_node_families_contract_common::ExecuteMsg;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_execute_variants_are_covered<C: NodeFamiliesSigningClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: NodeFamiliesExecuteMsg,
|
||||
) {
|
||||
match msg {
|
||||
NodeFamiliesExecuteMsg::UpdateConfig { config } => {
|
||||
client.update_node_families_config(config, None).ignore()
|
||||
}
|
||||
NodeFamiliesExecuteMsg::CreateFamily { name, description } => client
|
||||
.create_family(name, description, None, vec![])
|
||||
.ignore(),
|
||||
NodeFamiliesExecuteMsg::DisbandFamily {} => client.disband_family(None).ignore(),
|
||||
NodeFamiliesExecuteMsg::InviteToFamily {
|
||||
node_id,
|
||||
validity_secs,
|
||||
} => client
|
||||
.invite_to_family(node_id, validity_secs, None)
|
||||
.ignore(),
|
||||
NodeFamiliesExecuteMsg::RevokeFamilyInvitation { node_id } => {
|
||||
client.revoke_family_invitation(node_id, None).ignore()
|
||||
}
|
||||
NodeFamiliesExecuteMsg::AcceptFamilyInvitation { family_id, node_id } => client
|
||||
.accept_family_invitation(family_id, node_id, None)
|
||||
.ignore(),
|
||||
NodeFamiliesExecuteMsg::RejectFamilyInvitation { family_id, node_id } => client
|
||||
.reject_family_invitation(family_id, node_id, None)
|
||||
.ignore(),
|
||||
NodeFamiliesExecuteMsg::LeaveFamily { node_id } => {
|
||||
client.leave_family(node_id, None).ignore()
|
||||
}
|
||||
NodeFamiliesExecuteMsg::KickFromFamily { node_id } => {
|
||||
client.kick_from_family(node_id, None).ignore()
|
||||
}
|
||||
ExecuteMsg::OnNymNodeUnbond { node_id } => {
|
||||
client.on_nym_node_unbond(node_id, None).ignore()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -286,6 +286,10 @@ 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;
|
||||
}
|
||||
@@ -304,6 +308,13 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
|
||||
self.config.contracts.performance_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,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,104 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
/// Storage key constants used by the node families contract.
|
||||
///
|
||||
/// They are kept in the common crate so that off-chain tooling (indexers, migration
|
||||
/// scripts) can reference them without depending on the contract crate itself.
|
||||
/// Changing any of these values is a breaking change for already-deployed contracts.
|
||||
pub mod storage_keys {
|
||||
/// `Item<Addr>`: address of the mixnet contract used to validate node existence.
|
||||
pub const MIXNET_CONTRACT_ADDRESS: &str = "mixnet-contract-address";
|
||||
|
||||
/// `Item<Config>`: runtime configuration (fees, length limits) set at instantiation.
|
||||
pub const CONFIG: &str = "config";
|
||||
|
||||
/// `Admin` (cw-controllers): admin allowed to perform privileged operations.
|
||||
pub const CONTRACT_ADMIN: &str = "contract-admin";
|
||||
/// `Item<NodeFamilyId>`: monotonically increasing id counter for new families.
|
||||
pub const NODE_FAMILY_ID_COUNTER: &str = "node-family-id-counter";
|
||||
/// Primary namespace for the current family-members `IndexedMap`,
|
||||
/// keyed by `NodeId` with value [`crate::FamilyMembership`].
|
||||
pub const NODE_FAMILY_MEMBERS: &str = "node-family-members";
|
||||
/// Multi-index over current family members keyed by family id —
|
||||
/// enables paginated listing of all nodes in a given family.
|
||||
pub const NODE_FAMILY_MEMBERS_FAMILY_IDX_NAMESPACE: &str = "node-family-members__family";
|
||||
|
||||
/// Primary namespace for the families `IndexedMap`.
|
||||
pub const FAMILIES_NAMESPACE: &str = "families";
|
||||
/// Secondary unique index keyed by `owner` (one family per owner).
|
||||
pub const FAMILIES_OWNER_IDX_NAMESPACE: &str = "families__owner";
|
||||
/// Secondary unique index keyed by `name` (family names are globally unique).
|
||||
pub const FAMILIES_NAME_IDX_NAMESPACE: &str = "families__name";
|
||||
|
||||
/// Primary namespace for the pending invitations `IndexedMap`.
|
||||
pub const INVITATIONS_NAMESPACE: &str = "invitations";
|
||||
/// Multi-index over pending invitations keyed by family id.
|
||||
pub const INVITATIONS_FAMILY_IDX_NAMESPACE: &str = "invitations__family";
|
||||
/// Multi-index over pending invitations keyed by node id
|
||||
/// (a node can be invited to multiple families simultaneously).
|
||||
pub const INVITATIONS_NODE_IDX_NAMESPACE: &str = "invitations__node";
|
||||
|
||||
/// Primary namespace for the archived (accepted/rejected/revoked) invitations `IndexedMap`.
|
||||
pub const PAST_INVITATIONS_NAMESPACE: &str = "past-invitations";
|
||||
/// Multi-index over past invitations keyed by family id.
|
||||
pub const PAST_INVITATIONS_FAMILY_IDX_NAMESPACE: &str = "past-invitations__family";
|
||||
/// Multi-index over past invitations keyed by node id.
|
||||
pub const PAST_INVITATIONS_NODE_IDX_NAMESPACE: &str = "past-invitations__node";
|
||||
/// `Map<(NodeFamilyId, NodeId), u64>`: per-`(family, node)` counter used to
|
||||
/// disambiguate repeat archive entries (a node can be invited and have the
|
||||
/// invitation reach a terminal state more than once).
|
||||
pub const PAST_INVITATIONS_COUNTER_NAMESPACE: &str = "past-invitations-counter";
|
||||
|
||||
/// Primary namespace for the past-members `IndexedMap`.
|
||||
pub const PAST_FAMILY_MEMBER_NAMESPACE: &str = "past-family-member";
|
||||
/// Multi-index over past members keyed by family id.
|
||||
pub const PAST_FAMILY_MEMBER_FAMILY_IDX_NAMESPACE: &str = "past-family-member__family";
|
||||
/// Multi-index over past members keyed by node id.
|
||||
pub const PAST_FAMILY_MEMBER_NODE_IDX_NAMESPACE: &str = "past-family-member__node";
|
||||
/// `Map<(NodeFamilyId, NodeId), u64>`: per-`(family, node)` counter used to
|
||||
/// disambiguate repeat past-membership entries (a node can join and leave
|
||||
/// the same family more than once).
|
||||
pub const PAST_FAMILY_MEMBER_COUNTER_NAMESPACE: &str = "past-family-member-counter";
|
||||
}
|
||||
|
||||
pub mod events {
|
||||
pub const FAMILY_CREATION_EVENT_NAME: &str = "family_creation";
|
||||
pub const FAMILY_CREATION_EVENT_FAMILY_NAME: &str = "family_name";
|
||||
pub const FAMILY_CREATION_EVENT_OWNER_ADDRESS: &str = "owner_address";
|
||||
pub const FAMILY_CREATION_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_CREATION_EVENT_PAID_FEE: &str = "paid_fee";
|
||||
|
||||
pub const FAMILY_DISBAND_EVENT_NAME: &str = "family_disband";
|
||||
pub const FAMILY_DISBAND_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_DISBAND_EVENT_OWNER_ADDRESS: &str = "owner_address";
|
||||
pub const FAMILY_DISBAND_EVENT_REFUNDED_FEE: &str = "refunded_fee";
|
||||
|
||||
pub const FAMILY_INVITATION_EVENT_NAME: &str = "family_invitation";
|
||||
pub const FAMILY_INVITATION_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_INVITATION_EVENT_NODE_ID: &str = "node_id";
|
||||
pub const FAMILY_INVITATION_EVENT_EXPIRES_AT: &str = "expires_at";
|
||||
|
||||
pub const FAMILY_INVITATION_REVOKED_EVENT_NAME: &str = "family_invitation_revoked";
|
||||
pub const FAMILY_INVITATION_REVOKED_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_INVITATION_REVOKED_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const FAMILY_INVITATION_ACCEPTED_EVENT_NAME: &str = "family_invitation_accepted";
|
||||
pub const FAMILY_INVITATION_ACCEPTED_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_INVITATION_ACCEPTED_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const FAMILY_INVITATION_REJECTED_EVENT_NAME: &str = "family_invitation_rejected";
|
||||
pub const FAMILY_INVITATION_REJECTED_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_INVITATION_REJECTED_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const FAMILY_MEMBER_LEFT_EVENT_NAME: &str = "family_member_left";
|
||||
pub const FAMILY_MEMBER_LEFT_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_MEMBER_LEFT_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const FAMILY_MEMBER_KICKED_EVENT_NAME: &str = "family_member_kicked";
|
||||
pub const FAMILY_MEMBER_KICKED_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_MEMBER_KICKED_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const NODE_UNBOND_CLEANUP_EVENT_NAME: &str = "family_node_unbond_cleanup";
|
||||
pub const NODE_UNBOND_CLEANUP_EVENT_NODE_ID: &str = "node_id";
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::NodeFamilyId;
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use cw_controllers::AdminError;
|
||||
use cw_utils::PaymentError;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors returned from any entry point of the node families contract.
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum NodeFamiliesContractError {
|
||||
/// Returned from `migrate` when the on-chain state cannot be brought forward
|
||||
/// to the current contract version (e.g. unsupported source version, malformed
|
||||
/// stored data).
|
||||
#[error("could not perform contract migration: {comment}")]
|
||||
FailedMigration { comment: String },
|
||||
|
||||
/// The referenced family does not exist (or no longer exists).
|
||||
#[error("family with id {family_id} does not exist")]
|
||||
FamilyNotFound { family_id: NodeFamilyId },
|
||||
|
||||
/// Disbanding was requested on a family that still has members.
|
||||
#[error("family {family_id} cannot be disbanded: it still has {members} member(s)")]
|
||||
FamilyNotEmpty {
|
||||
family_id: NodeFamilyId,
|
||||
members: u64,
|
||||
},
|
||||
|
||||
/// The given node is not currently a member of any family.
|
||||
#[error("node {node_id} is not currently a member of any family")]
|
||||
NodeNotInFamily { node_id: NodeId },
|
||||
|
||||
/// The given node is a member of a different family than the one the
|
||||
/// caller is acting on. Distinct from [`NodeNotInFamily`] (which means the
|
||||
/// node has no membership at all) — surfaces when, e.g., a family owner
|
||||
/// tries to kick a node that belongs to someone else's family.
|
||||
#[error("node {node_id} is not a member of family {family_id}")]
|
||||
NodeNotMemberOfFamily {
|
||||
node_id: NodeId,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// A cross-contract callback (e.g. `OnNymNodeUnbond`) was received from a
|
||||
/// sender that is not the configured mixnet contract address.
|
||||
#[error("address {sender} is not authorised to invoke the mixnet-contract callback")]
|
||||
UnauthorisedMixnetCallback { sender: Addr },
|
||||
|
||||
/// No pending invitation exists for the given `(family, node)` pair.
|
||||
#[error("no pending invitation for node {node_id} from family {family_id}")]
|
||||
InvitationNotFound {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// A pending invitation for the given `(family, node)` pair already exists;
|
||||
/// issuing a new one would silently overwrite it.
|
||||
#[error("a pending invitation for node {node_id} from family {family_id} already exists")]
|
||||
PendingInvitationAlreadyExists {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// The invitation exists but its `expires_at` is at or before the current
|
||||
/// block time, so it can no longer be acted on.
|
||||
#[error(
|
||||
"invitation for node {node_id} from family {family_id} expired at {expires_at} (now: {now})"
|
||||
)]
|
||||
InvitationExpired {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
expires_at: u64,
|
||||
now: u64,
|
||||
},
|
||||
|
||||
/// The funds attached to a paid execution failed `cw_utils` payment
|
||||
/// validation (no funds, wrong/extra denom).
|
||||
#[error("invalid fee provided: {0}")]
|
||||
InvalidDeposit(#[from] PaymentError),
|
||||
|
||||
/// The funds attached to a `CreateFamily` execution don't match the
|
||||
/// configured `create_family_fee`.
|
||||
#[error("expected exactly {expected} as family creation fee; received {received:?}")]
|
||||
InvalidFamilyCreationFee { expected: Coin, received: Vec<Coin> },
|
||||
|
||||
/// The submitted family name normalised to the empty string (i.e. it
|
||||
/// contained no ASCII alphanumeric characters).
|
||||
#[error("family name cannot be empty after normalisation")]
|
||||
EmptyFamilyName,
|
||||
|
||||
/// The submitted family name exceeds the configured length limit.
|
||||
#[error("family name length {length} exceeds the configured limit of {limit}")]
|
||||
FamilyNameTooLong { length: usize, limit: usize },
|
||||
|
||||
/// The submitted family description exceeds the configured length limit.
|
||||
#[error("family description length {length} exceeds the configured limit of {limit}")]
|
||||
FamilyDescriptionTooLong { length: usize, limit: usize },
|
||||
|
||||
/// The transaction sender already owns a family.
|
||||
#[error("address {address} already owns family {family_id}")]
|
||||
SenderAlreadyOwnsAFamily {
|
||||
address: Addr,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// The transaction sender does not currently own any family - emitted by
|
||||
/// owner-gated operations like `disband_family` when the sender has
|
||||
/// nothing to act on.
|
||||
#[error("address {address} does not currently own any family")]
|
||||
SenderDoesntOwnAFamily { address: Addr },
|
||||
|
||||
/// The transaction sender is not the controller of the bonded node
|
||||
/// referenced by the message. Covers all of: sender controls no bonded
|
||||
/// node, sender controls a different node id, and sender's node has
|
||||
/// entered the unbonding state.
|
||||
#[error("address {address} is not the controller of bonded node {node_id}")]
|
||||
SenderDoesntControlNode { address: Addr, node_id: NodeId },
|
||||
|
||||
/// A family with the requested (normalised) name already exists.
|
||||
#[error("a family with name {name:?} already exists (id {family_id})")]
|
||||
FamilyNameAlreadyTaken {
|
||||
name: String,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// A node controlled by the address is currently a member of a family,
|
||||
/// so the address cannot also become a family owner or join another family.
|
||||
#[error("address {address} controls node {node_id} which is currently in family {family_id}")]
|
||||
AlreadyInFamily {
|
||||
address: Addr,
|
||||
node_id: NodeId,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// The node referenced by an invitation does not exist as a bonded node
|
||||
/// in the mixnet contract (or has already unbonded).
|
||||
#[error("node {node_id} is not a bonded node in the mixnet contract")]
|
||||
NodeDoesntExist { node_id: NodeId },
|
||||
|
||||
/// The node referenced by an invitation is already a member of a family,
|
||||
/// so it cannot be invited to another one until it leaves / is removed.
|
||||
#[error("node {node_id} is already a member of family {family_id}")]
|
||||
NodeAlreadyInFamily {
|
||||
node_id: NodeId,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// The sender supplied a `validity_secs` of `0` for an invitation, which
|
||||
/// would create one that is already expired at the moment it is stored.
|
||||
#[error("invitation validity must be strictly positive")]
|
||||
ZeroInvitationValidity,
|
||||
|
||||
/// Wraps errors raised by `cw-controllers::Admin` (e.g. caller is not admin).
|
||||
#[error(transparent)]
|
||||
Admin(#[from] AdminError),
|
||||
|
||||
/// Wraps any underlying `cosmwasm_std::StdError` (storage, serialization, etc.).
|
||||
#[error(transparent)]
|
||||
StdErr(#[from] cosmwasm_std::StdError),
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Common types, messages, errors and storage-key constants shared between the
|
||||
//! node families contract and any off-chain client.
|
||||
//!
|
||||
//! Keeping these in a separate crate allows clients to depend on the contract's
|
||||
//! public surface without pulling in `cw-storage-plus` and other on-chain-only
|
||||
//! dependencies.
|
||||
|
||||
/// Storage-key string constants. See [`constants::storage_keys`].
|
||||
pub mod constants;
|
||||
/// Contract-level error type.
|
||||
pub mod error;
|
||||
/// `InstantiateMsg`, `ExecuteMsg`, `QueryMsg`, `MigrateMsg` definitions.
|
||||
pub mod msg;
|
||||
/// Domain types stored in / returned by the contract.
|
||||
pub mod types;
|
||||
|
||||
pub use error::*;
|
||||
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
pub use types::*;
|
||||
@@ -0,0 +1,211 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::{
|
||||
Config, GlobalPastFamilyInvitationCursor, NodeFamilyId, PastFamilyInvitationCursor,
|
||||
PastFamilyInvitationForNodeCursor, PastFamilyMemberCursor, PastFamilyMemberForNodeCursor,
|
||||
};
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
use crate::{
|
||||
AllFamilyMembersPagedResponse, AllPastFamilyInvitationsPagedResponse, FamiliesPagedResponse,
|
||||
FamilyMembersPagedResponse, NodeFamilyByNameResponse, NodeFamilyByOwnerResponse,
|
||||
NodeFamilyMembershipResponse, NodeFamilyResponse, PastFamilyInvitationsForNodePagedResponse,
|
||||
PastFamilyInvitationsPagedResponse, PastFamilyMembersForNodePagedResponse,
|
||||
PastFamilyMembersPagedResponse, PendingFamilyInvitationResponse,
|
||||
PendingFamilyInvitationsPagedResponse, PendingInvitationsForNodePagedResponse,
|
||||
PendingInvitationsPagedResponse,
|
||||
};
|
||||
|
||||
/// Message used to instantiate the node families contract.
|
||||
#[cw_serde]
|
||||
pub struct InstantiateMsg {
|
||||
pub config: Config,
|
||||
|
||||
pub mixnet_contract_address: String,
|
||||
}
|
||||
|
||||
/// Execute messages accepted by the contract.
|
||||
#[cw_serde]
|
||||
pub enum ExecuteMsg {
|
||||
/// Replace the contract's runtime [`Config`]. Restricted to the contract
|
||||
/// admin.
|
||||
UpdateConfig { config: Config },
|
||||
|
||||
/// Create a new family owned by the message sender. The configured
|
||||
/// `create_family_fee` must be attached as funds.
|
||||
CreateFamily { name: String, description: String },
|
||||
|
||||
/// Disband the family owned by the message sender. The family must have
|
||||
/// no current members; any still-pending invitations are revoked.
|
||||
DisbandFamily {},
|
||||
|
||||
/// Invite a node to the family owned by the message sender. If
|
||||
/// `validity_secs` is omitted the invitation expires
|
||||
/// `default_invitation_validity_secs` seconds (from [`Config`]) after the
|
||||
/// current block time.
|
||||
InviteToFamily {
|
||||
node_id: NodeId,
|
||||
validity_secs: Option<u64>,
|
||||
},
|
||||
|
||||
/// Revoke a still-pending invitation previously issued by the sender's
|
||||
/// family.
|
||||
RevokeFamilyInvitation { node_id: NodeId },
|
||||
|
||||
/// Accept a pending invitation. The sender must control `node_id`.
|
||||
AcceptFamilyInvitation {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// Reject a pending invitation. The sender must control `node_id`.
|
||||
RejectFamilyInvitation {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// Leave the family `node_id` currently belongs to. The sender must
|
||||
/// control `node_id`.
|
||||
LeaveFamily { node_id: NodeId },
|
||||
|
||||
/// Remove `node_id` from the family owned by the message sender.
|
||||
KickFromFamily { node_id: NodeId },
|
||||
|
||||
/// Cross-contract callback fired by the mixnet contract the moment
|
||||
/// node with `node_id` initiates unbonding.
|
||||
/// Removes the node from any family it currently
|
||||
/// belongs to and rejects every pending invitation issued to it.
|
||||
/// Sender must be the configured mixnet contract address.
|
||||
OnNymNodeUnbond { node_id: NodeId },
|
||||
}
|
||||
|
||||
/// Query messages accepted by the contract.
|
||||
#[cw_serde]
|
||||
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
|
||||
pub enum QueryMsg {
|
||||
/// Look up a single family by its id.
|
||||
#[cfg_attr(feature = "schema", returns(NodeFamilyResponse))]
|
||||
GetFamilyById { family_id: NodeFamilyId },
|
||||
|
||||
/// Look up the (at most one) family owned by a given address.
|
||||
#[cfg_attr(feature = "schema", returns(NodeFamilyByOwnerResponse))]
|
||||
GetFamilyByOwner { owner: String },
|
||||
|
||||
/// Look up a single family by its name. The lookup is normalised
|
||||
/// contract-side (lowercased, non-alphanumerics stripped), so equivalent
|
||||
/// inputs resolve to the same family.
|
||||
#[cfg_attr(feature = "schema", returns(NodeFamilyByNameResponse))]
|
||||
GetFamilyByName { name: String },
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(FamiliesPagedResponse))]
|
||||
GetFamiliesPaged {
|
||||
start_after: Option<NodeFamilyId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Look up which family — if any — a node currently belongs to.
|
||||
#[cfg_attr(feature = "schema", returns(NodeFamilyMembershipResponse))]
|
||||
GetFamilyMembership { node_id: NodeId },
|
||||
|
||||
/// Page through every node currently in a given family.
|
||||
#[cfg_attr(feature = "schema", returns(FamilyMembersPagedResponse))]
|
||||
GetFamilyMembersPaged {
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every current family member across all families, in
|
||||
/// ascending [`NodeId`] order. Each entry carries the membership record
|
||||
/// (which in turn names the family the node belongs to).
|
||||
#[cfg_attr(feature = "schema", returns(AllFamilyMembersPagedResponse))]
|
||||
GetAllFamilyMembersPaged {
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Look up the pending invitation for a specific `(family_id, node_id)`
|
||||
/// pair.
|
||||
#[cfg_attr(feature = "schema", returns(PendingFamilyInvitationResponse))]
|
||||
GetPendingInvitation {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// Page through every pending invitation issued by a given family.
|
||||
#[cfg_attr(feature = "schema", returns(PendingFamilyInvitationsPagedResponse))]
|
||||
GetPendingInvitationsForFamilyPaged {
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every pending invitation issued for a given node.
|
||||
#[cfg_attr(feature = "schema", returns(PendingInvitationsForNodePagedResponse))]
|
||||
GetPendingInvitationsForNodePaged {
|
||||
node_id: NodeId,
|
||||
start_after: Option<NodeFamilyId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every pending invitation across all families.
|
||||
#[cfg_attr(feature = "schema", returns(PendingInvitationsPagedResponse))]
|
||||
GetAllPendingInvitationsPaged {
|
||||
start_after: Option<(NodeFamilyId, NodeId)>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived (terminal-state) invitation issued by a
|
||||
/// given family.
|
||||
#[cfg_attr(feature = "schema", returns(PastFamilyInvitationsPagedResponse))]
|
||||
GetPastInvitationsForFamilyPaged {
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<PastFamilyInvitationCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived (terminal-state) invitation issued to a
|
||||
/// given node.
|
||||
#[cfg_attr(feature = "schema", returns(PastFamilyInvitationsForNodePagedResponse))]
|
||||
GetPastInvitationsForNodePaged {
|
||||
node_id: NodeId,
|
||||
start_after: Option<PastFamilyInvitationForNodeCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived (terminal-state) invitation across all
|
||||
/// families.
|
||||
#[cfg_attr(feature = "schema", returns(AllPastFamilyInvitationsPagedResponse))]
|
||||
GetAllPastInvitationsPaged {
|
||||
start_after: Option<GlobalPastFamilyInvitationCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived membership record for a given family
|
||||
/// (nodes that used to belong to it but have since been removed).
|
||||
#[cfg_attr(feature = "schema", returns(PastFamilyMembersPagedResponse))]
|
||||
GetPastMembersForFamilyPaged {
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<PastFamilyMemberCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived membership record for a given node
|
||||
/// (every family the node used to belong to but has since been removed
|
||||
/// from), across all families.
|
||||
#[cfg_attr(feature = "schema", returns(PastFamilyMembersForNodePagedResponse))]
|
||||
GetPastMembersForNodePaged {
|
||||
node_id: NodeId,
|
||||
start_after: Option<PastFamilyMemberForNodeCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Message passed to the contract's `migrate` entry point.
|
||||
#[cw_serde]
|
||||
pub struct MigrateMsg {
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
|
||||
/// Identifier of a node family.
|
||||
///
|
||||
/// Issued sequentially by the contract on family creation; never reused even if the
|
||||
/// family is later disbanded.
|
||||
pub type NodeFamilyId = u32;
|
||||
|
||||
/// Runtime configuration of the node families contract.
|
||||
#[cw_serde]
|
||||
pub struct Config {
|
||||
/// Fee charged on each successful `create_family` execution.
|
||||
pub create_family_fee: Coin,
|
||||
|
||||
/// Maximum allowed length, in characters, of a family name.
|
||||
pub family_name_length_limit: usize,
|
||||
|
||||
/// Maximum allowed length, in characters, of a family description.
|
||||
pub family_description_length_limit: usize,
|
||||
|
||||
/// Default lifetime, in seconds, used by `invite_to_family` when the
|
||||
/// sender doesn't supply an explicit value. Senders may override this
|
||||
/// per-invitation via the optional `validity_secs` argument.
|
||||
pub default_invitation_validity_secs: u64,
|
||||
}
|
||||
|
||||
/// On-chain representation of a node family.
|
||||
#[cw_serde]
|
||||
pub struct NodeFamily {
|
||||
/// The id of the node family
|
||||
pub id: NodeFamilyId,
|
||||
|
||||
/// The name of the node family
|
||||
pub name: String,
|
||||
|
||||
/// Normalised name of the node family used for uniqueness checks
|
||||
pub normalised_name: String,
|
||||
|
||||
/// The optional description of the node family
|
||||
pub description: String,
|
||||
|
||||
/// The owner of the node family
|
||||
pub owner: Addr,
|
||||
|
||||
/// Records the fee paid when the family was created,
|
||||
/// so that the appropriate amount could be returned upon it getting disbanded.
|
||||
pub paid_fee: Coin,
|
||||
|
||||
/// Memoized value of the current number of members in the node family
|
||||
/// Used to detect if the family is empty
|
||||
pub members: u64,
|
||||
|
||||
/// Timestamp of the creation of the node family
|
||||
pub created_at: u64,
|
||||
}
|
||||
|
||||
/// A pending invitation for a node to join a particular family.
|
||||
///
|
||||
/// Invitations are stored until they are accepted, rejected, revoked, or until the
|
||||
/// chain advances past `expires_at` (in which case they remain in storage but are
|
||||
/// treated as inert — there is no background process clearing expired invitations).
|
||||
#[cw_serde]
|
||||
pub struct FamilyInvitation {
|
||||
/// The family that issued the invitation.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The node being invited.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// Block timestamp (unix seconds) after which the invitation is no longer valid.
|
||||
pub expires_at: u64,
|
||||
}
|
||||
|
||||
/// On-chain record of a node's current family membership.
|
||||
///
|
||||
/// A node belongs to at most one family at a time, so this is keyed by
|
||||
/// `NodeId` alone — `family_id` is carried in the value to support reverse
|
||||
/// lookups (all nodes in a given family) via a secondary index.
|
||||
#[cw_serde]
|
||||
pub struct FamilyMembership {
|
||||
/// The family the node is currently a member of.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// Block timestamp (unix seconds) at which the node accepted its
|
||||
/// invitation and joined the family.
|
||||
pub joined_at: u64,
|
||||
}
|
||||
|
||||
/// Historical record of a node that used to be part of a family but has since been
|
||||
/// removed (kicked, left voluntarily, or because the family was disbanded).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyMember {
|
||||
/// The family the node used to belong to.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The node that was removed.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// Block timestamp (unix seconds) at which the membership was terminated.
|
||||
pub removed_at: u64,
|
||||
}
|
||||
|
||||
/// Terminal status for an invitation that has been moved out of the pending set.
|
||||
///
|
||||
/// Note: timed-out invitations are not represented here — they are simply left in
|
||||
/// the pending set (see `FamilyInvitation::expires_at`).
|
||||
#[cw_serde]
|
||||
pub enum FamilyInvitationStatus {
|
||||
/// Still awaiting a response. Recorded with a timestamp for completeness even
|
||||
/// though pending invitations live in a separate map.
|
||||
Pending { at: u64 },
|
||||
/// The invitee accepted and joined the family at the given timestamp.
|
||||
Accepted { at: u64 },
|
||||
/// The invitee explicitly rejected the invitation at the given timestamp.
|
||||
Rejected { at: u64 },
|
||||
/// The family revoked the invitation at the given timestamp before it could
|
||||
/// be accepted or rejected.
|
||||
Revoked { at: u64 },
|
||||
}
|
||||
|
||||
/// Historical record of an invitation that has reached a terminal state
|
||||
/// (`Accepted`, `Rejected`, or `Revoked`). Timed-out invitations are **not**
|
||||
/// archived here — they remain in the pending map until explicitly cleared.
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyInvitation {
|
||||
/// The original invitation as it was issued.
|
||||
pub invitation: FamilyInvitation,
|
||||
|
||||
/// What ultimately happened to it.
|
||||
pub status: FamilyInvitationStatus,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyById`](crate::QueryMsg::GetFamilyById).
|
||||
#[cw_serde]
|
||||
pub struct NodeFamilyResponse {
|
||||
/// The id that was queried, echoed back so paginated callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The matching family, or `None` if no family with `family_id` exists.
|
||||
pub family: Option<NodeFamily>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyByOwner`](crate::QueryMsg::GetFamilyByOwner).
|
||||
#[cw_serde]
|
||||
pub struct NodeFamilyByOwnerResponse {
|
||||
/// The (validated) owner address that was queried, echoed back so callers
|
||||
/// can correlate.
|
||||
pub owner: Addr,
|
||||
|
||||
/// The matching family, or `None` if `owner` does not currently own one.
|
||||
pub family: Option<NodeFamily>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyByName`](crate::QueryMsg::GetFamilyByName).
|
||||
#[cw_serde]
|
||||
pub struct NodeFamilyByNameResponse {
|
||||
/// The name that was queried, echoed back so callers can correlate.
|
||||
pub name: String,
|
||||
|
||||
/// The matching family, or `None` if no family with that name exists.
|
||||
pub family: Option<NodeFamily>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyMembership`](crate::QueryMsg::GetFamilyMembership).
|
||||
#[cw_serde]
|
||||
pub struct NodeFamilyMembershipResponse {
|
||||
/// The node that was queried.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The id of the family the node currently belongs to, or `None` if the
|
||||
/// node is not currently a member of any family.
|
||||
pub family_id: Option<NodeFamilyId>,
|
||||
}
|
||||
|
||||
/// A pending [`FamilyInvitation`] paired with whether it has already timed
|
||||
/// out at the time the query was served.
|
||||
#[cw_serde]
|
||||
pub struct PendingFamilyInvitationDetails {
|
||||
/// The stored invitation as it was issued.
|
||||
pub invitation: FamilyInvitation,
|
||||
|
||||
/// `true` iff `now >= invitation.expires_at` at query time, i.e. the
|
||||
/// invitation is still in the pending map but can no longer be acted on.
|
||||
pub expired: bool,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPendingInvitation`](crate::QueryMsg::GetPendingInvitation).
|
||||
#[cw_serde]
|
||||
pub struct PendingFamilyInvitationResponse {
|
||||
/// The family component of the queried `(family_id, node_id)` key.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The node component of the queried `(family_id, node_id)` key.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The matching pending invitation along with an explicit expiry flag,
|
||||
/// or `None` if no such invitation exists.
|
||||
pub invitation: Option<PendingFamilyInvitationDetails>,
|
||||
}
|
||||
|
||||
/// One entry in a [`FamilyMembersPagedResponse`] page — pairs a node id with
|
||||
/// its [`FamilyMembership`] record (notably its `joined_at` timestamp).
|
||||
#[cw_serde]
|
||||
pub struct FamilyMemberRecord {
|
||||
/// The node currently in the family.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The membership record (carries `family_id` and `joined_at`).
|
||||
pub membership: FamilyMembership,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyMembersPaged`](crate::QueryMsg::GetFamilyMembersPaged).
|
||||
#[cw_serde]
|
||||
pub struct FamilyMembersPagedResponse {
|
||||
/// The family whose members were queried, echoed back so paginated
|
||||
/// callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The members on this page, in ascending [`NodeId`] order.
|
||||
pub members: Vec<FamilyMemberRecord>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (which the caller should treat as end-of-list).
|
||||
pub start_next_after: Option<NodeId>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetAllFamilyMembersPaged`](crate::QueryMsg::GetAllFamilyMembersPaged).
|
||||
#[cw_serde]
|
||||
pub struct AllFamilyMembersPagedResponse {
|
||||
/// The members on this page, in ascending [`NodeId`] order across every
|
||||
/// family.
|
||||
pub members: Vec<FamilyMemberRecord>,
|
||||
|
||||
/// Cursor (last `node_id`) to pass as `start_after` on the next call,
|
||||
/// or `None` if this page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<NodeId>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPendingInvitationsForFamilyPaged`](crate::QueryMsg::GetPendingInvitationsForFamilyPaged).
|
||||
#[cw_serde]
|
||||
pub struct PendingFamilyInvitationsPagedResponse {
|
||||
/// The family whose pending invitations were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The pending invitations on this page, in ascending invitee
|
||||
/// [`NodeId`] order, each stamped with whether it had already timed out
|
||||
/// at the time the query was served.
|
||||
pub invitations: Vec<PendingFamilyInvitationDetails>,
|
||||
|
||||
/// Cursor (last invitee node id) to pass as `start_after` on the next
|
||||
/// call, or `None` if this page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<NodeId>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPendingInvitationsForNodePaged`](crate::QueryMsg::GetPendingInvitationsForNodePaged).
|
||||
#[cw_serde]
|
||||
pub struct PendingInvitationsForNodePagedResponse {
|
||||
/// The node whose pending invitations were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The pending invitations addressed to this node on this page, in
|
||||
/// ascending [`NodeFamilyId`] order, each stamped with whether it had
|
||||
/// already timed out at the time the query was served.
|
||||
pub invitations: Vec<PendingFamilyInvitationDetails>,
|
||||
|
||||
/// Cursor (last issuing family id) to pass as `start_after` on the
|
||||
/// next call, or `None` if this page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<NodeFamilyId>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetAllPendingInvitationsPaged`](crate::QueryMsg::GetAllPendingInvitationsPaged).
|
||||
#[cw_serde]
|
||||
pub struct PendingInvitationsPagedResponse {
|
||||
/// The pending invitations on this page, in ascending
|
||||
/// `(family_id, node_id)` order, each stamped with whether it had
|
||||
/// already timed out at the time the query was served.
|
||||
pub invitations: Vec<PendingFamilyInvitationDetails>,
|
||||
|
||||
/// Cursor (last `(family_id, node_id)` pair) to pass as `start_after`
|
||||
/// on the next call, or `None` if this page is empty (treat as
|
||||
/// end-of-list).
|
||||
pub start_next_after: Option<(NodeFamilyId, NodeId)>,
|
||||
}
|
||||
|
||||
/// Cursor for paginating per-family past-invitation listings: identifies a
|
||||
/// single archive entry within a family by `(node_id, counter)`. The
|
||||
/// `counter` is the per-`(family, node)` archive slot — multiple archived
|
||||
/// invitations can exist for the same `(family, node)` pair (a node may be
|
||||
/// invited and have the invitation reach a terminal state more than once).
|
||||
pub type PastFamilyInvitationCursor = (NodeId, u64);
|
||||
|
||||
/// Cursor for paginating per-node past-invitation listings: identifies a
|
||||
/// single archive entry addressed to a fixed node by `(family_id, counter)`.
|
||||
pub type PastFamilyInvitationForNodeCursor = (NodeFamilyId, u64);
|
||||
|
||||
/// Cursor for paginating global past-invitation listings: identifies a
|
||||
/// single archive entry across all families by `((family_id, node_id), counter)`.
|
||||
pub type GlobalPastFamilyInvitationCursor = ((NodeFamilyId, NodeId), u64);
|
||||
|
||||
/// Response to [`QueryMsg::GetPastInvitationsForFamilyPaged`](crate::QueryMsg::GetPastInvitationsForFamilyPaged).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyInvitationsPagedResponse {
|
||||
/// The family whose archived invitations were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The archived invitations on this page, in ascending
|
||||
/// `(node_id, counter)` order across all terminal statuses.
|
||||
pub invitations: Vec<PastFamilyInvitation>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<PastFamilyInvitationCursor>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPastInvitationsForNodePaged`](crate::QueryMsg::GetPastInvitationsForNodePaged).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyInvitationsForNodePagedResponse {
|
||||
/// The node whose past invitations were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The archived invitations addressed to this node on this page, in
|
||||
/// ascending `(family_id, counter)` order across all terminal statuses.
|
||||
pub invitations: Vec<PastFamilyInvitation>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<PastFamilyInvitationForNodeCursor>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetAllPastInvitationsPaged`](crate::QueryMsg::GetAllPastInvitationsPaged).
|
||||
#[cw_serde]
|
||||
pub struct AllPastFamilyInvitationsPagedResponse {
|
||||
/// The archived invitations on this page, in ascending
|
||||
/// `((family_id, node_id), counter)` order across all terminal statuses.
|
||||
pub invitations: Vec<PastFamilyInvitation>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<GlobalPastFamilyInvitationCursor>,
|
||||
}
|
||||
|
||||
/// Cursor for paginating per-family past-member listings: identifies a single
|
||||
/// archive entry within a family by `(node_id, counter)`. The `counter` is the
|
||||
/// per-`(family, node)` archive slot — multiple archived membership entries
|
||||
/// can exist for the same `(family, node)` pair (a node may join, leave, and
|
||||
/// re-join the same family more than once).
|
||||
pub type PastFamilyMemberCursor = (NodeId, u64);
|
||||
|
||||
/// Cursor for paginating per-node past-member listings: identifies a single
|
||||
/// archive entry for a fixed node by `(family_id, counter)`.
|
||||
pub type PastFamilyMemberForNodeCursor = (NodeFamilyId, u64);
|
||||
|
||||
/// Response to [`QueryMsg::GetPastMembersForFamilyPaged`](crate::QueryMsg::GetPastMembersForFamilyPaged).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyMembersPagedResponse {
|
||||
/// The family whose archived memberships were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The archived membership records on this page, in ascending
|
||||
/// `(node_id, counter)` order.
|
||||
pub members: Vec<PastFamilyMember>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<PastFamilyMemberCursor>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPastMembersForNodePaged`](crate::QueryMsg::GetPastMembersForNodePaged).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyMembersForNodePagedResponse {
|
||||
/// The node whose archived memberships were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The archived membership records for this node on this page, in
|
||||
/// ascending `(family_id, counter)` order.
|
||||
pub members: Vec<PastFamilyMember>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<PastFamilyMemberForNodeCursor>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamiliesPaged`](crate::QueryMsg::GetFamiliesPaged).
|
||||
#[cw_serde]
|
||||
pub struct FamiliesPagedResponse {
|
||||
/// The families on this page, in ascending [`NodeFamilyId`] order.
|
||||
pub families: Vec<NodeFamily>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (which the caller should treat as end-of-list).
|
||||
pub start_next_after: Option<NodeFamilyId>,
|
||||
}
|
||||
@@ -13,8 +13,9 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
use tokio::time::Instant;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{debug, info, instrument, warn};
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub use helpers::{BufferedDeposit, PerformedDeposits, make_deposits_request, split_deposits};
|
||||
@@ -146,9 +147,14 @@ impl DepositsBuffer {
|
||||
|
||||
// if we're here, we know we're below the threshold
|
||||
fn maybe_refill_deposits(&self) {
|
||||
if let Some(mut guard) = self.inner.deposits_refill_task.try_get_new_task_guard() {
|
||||
if let Some((mut guard, completion_guard)) =
|
||||
self.inner.deposits_refill_task.try_get_new_task_guard()
|
||||
{
|
||||
let this = self.clone();
|
||||
*guard = Some(tokio::spawn(async move { this.refill_deposits().await }));
|
||||
*guard = Some(tokio::spawn(async move {
|
||||
let _completion_guard = completion_guard;
|
||||
this.refill_deposits().await
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +185,8 @@ impl DepositsBuffer {
|
||||
requested_on: OffsetDateTime,
|
||||
client_pubkey: PublicKeyUser,
|
||||
) -> Result<BufferedDeposit, CredentialProxyError> {
|
||||
let wait_start = Instant::now();
|
||||
let mut i = 0;
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
if let Some(buffered_deposit) = self.inner.unused_deposits.lock().await.pop() {
|
||||
@@ -195,6 +203,15 @@ impl DepositsBuffer {
|
||||
// make sure there's always a task working in the background in case deposits get used up too quickly
|
||||
self.maybe_refill_deposits()
|
||||
}
|
||||
i += 1;
|
||||
let elapsed = wait_start.elapsed();
|
||||
if elapsed > Duration::from_secs(5) && i % 10 == 0 {
|
||||
warn!("we've been waiting for over 5s to make a deposit - something is wrong!")
|
||||
} else if elapsed > Duration::from_secs(10) && i % 5 == 0 {
|
||||
error!(
|
||||
"we've been waiting for over 10s to make a deposit - something is SERIOUSLY wrong!"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,22 @@
|
||||
|
||||
use crate::error::CredentialProxyError;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Mutex as StdMutex, MutexGuard};
|
||||
use std::sync::{Arc, Mutex as StdMutex, MutexGuard};
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{debug, error};
|
||||
|
||||
pub(super) type RefillTaskResult = Result<(), CredentialProxyError>;
|
||||
|
||||
pub(super) struct InProgressGuard {
|
||||
in_progress: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Drop for InProgressGuard {
|
||||
fn drop(&mut self) {
|
||||
self.in_progress.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct RefillTask {
|
||||
// note that we can only have a single transaction in progress (or it'd mess up with our sequence numbers)
|
||||
@@ -16,7 +26,7 @@ pub(super) struct RefillTask {
|
||||
// we'll have to increase the number of deposits per transaction
|
||||
join_handle: StdMutex<Option<JoinHandle<RefillTaskResult>>>,
|
||||
|
||||
in_progress: AtomicBool,
|
||||
in_progress: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl RefillTask {
|
||||
@@ -28,9 +38,15 @@ impl RefillTask {
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Returns `None` if a refill is already in progress. On success, returns the
|
||||
/// join-handle guard (to store the new `JoinHandle` into) and an [`InProgressGuard`]
|
||||
/// that **must be moved into the spawned task** — it resets the flag when dropped.
|
||||
pub(super) fn try_get_new_task_guard(
|
||||
&self,
|
||||
) -> Option<MutexGuard<'_, Option<JoinHandle<RefillTaskResult>>>> {
|
||||
) -> Option<(
|
||||
MutexGuard<'_, Option<JoinHandle<RefillTaskResult>>>,
|
||||
InProgressGuard,
|
||||
)> {
|
||||
// sanity check for concurrent request
|
||||
if !self.try_set_in_progress() {
|
||||
debug!("another task has already started deposit refill request");
|
||||
@@ -48,7 +64,11 @@ impl RefillTask {
|
||||
}
|
||||
}
|
||||
|
||||
Some(guard)
|
||||
let completion_guard = InProgressGuard {
|
||||
in_progress: Arc::clone(&self.in_progress),
|
||||
};
|
||||
|
||||
Some((guard, completion_guard))
|
||||
}
|
||||
|
||||
pub(super) fn take_task_join_handle(&self) -> Option<JoinHandle<RefillTaskResult>> {
|
||||
@@ -56,3 +76,34 @@ impl RefillTask {
|
||||
self.join_handle.lock().expect("mutex got poisoned").take()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn in_progress_resets_after_guard_drop() {
|
||||
let task = RefillTask::default();
|
||||
|
||||
let (guard, completion_guard) = task.try_get_new_task_guard().unwrap();
|
||||
drop(guard);
|
||||
assert!(task.try_get_new_task_guard().is_none());
|
||||
|
||||
drop(completion_guard);
|
||||
assert!(task.try_get_new_task_guard().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_progress_resets_on_panic() {
|
||||
let task = RefillTask::default();
|
||||
|
||||
let (guard, completion_guard) = task.try_get_new_task_guard().unwrap();
|
||||
drop(guard);
|
||||
|
||||
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
let _g = completion_guard;
|
||||
panic!("simulated refill task panic");
|
||||
}));
|
||||
assert!(task.try_get_new_task_guard().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,8 +230,8 @@ impl MemoryEcachTicketbookManager {
|
||||
expiration_date: t.ticketbook.expiration_date(),
|
||||
ticketbook_type: t.ticketbook.ticketbook_type().to_string(),
|
||||
epoch_id: t.ticketbook.epoch_id() as u32,
|
||||
total_tickets: t.ticketbook.spent_tickets() as u32,
|
||||
used_tickets: t.total_tickets,
|
||||
total_tickets: t.total_tickets,
|
||||
used_tickets: t.ticketbook.spent_tickets() as u32,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -333,3 +333,339 @@ impl MemoryEcachTicketbookManager {
|
||||
guard.emergency_credentials.remove(typ);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use nym_compact_ecash::tests::helpers::generate_expiration_date_signatures;
|
||||
use nym_compact_ecash::{issue, ttp_keygen};
|
||||
use nym_credentials_interface::TicketType;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_ecash_time::EcashTime;
|
||||
use nym_test_utils::helpers::deterministic_rng;
|
||||
|
||||
fn mock_issuance(deposit_id: u32) -> IssuanceTicketBook {
|
||||
let identifier = "foomp";
|
||||
let mut rng = deterministic_rng();
|
||||
let key = ed25519::PrivateKey::new(&mut rng);
|
||||
let typ = TicketType::V1MixnetEntry;
|
||||
IssuanceTicketBook::new(deposit_id, identifier, key, typ)
|
||||
}
|
||||
|
||||
fn mock_ticketbook() -> anyhow::Result<IssuedTicketBook> {
|
||||
let signing_keys = ttp_keygen(1, 1)?.remove(0);
|
||||
let issuance = mock_issuance(42);
|
||||
let expiration_date = issuance.expiration_date();
|
||||
|
||||
let sig_req = issuance.prepare_for_signing();
|
||||
let _exp_date_sigs = generate_expiration_date_signatures(
|
||||
sig_req.expiration_date.ecash_unix_timestamp(),
|
||||
&[signing_keys.secret_key()],
|
||||
&[signing_keys.verification_key()],
|
||||
&signing_keys.verification_key(),
|
||||
&[1],
|
||||
)?;
|
||||
let blind_sig = issue(
|
||||
signing_keys.secret_key(),
|
||||
sig_req.ecash_pub_key,
|
||||
&sig_req.withdrawal_request,
|
||||
expiration_date.ecash_unix_timestamp(),
|
||||
issuance.ticketbook_type().encode(),
|
||||
)?;
|
||||
|
||||
let partial_wallet =
|
||||
issuance.unblind_signature(&signing_keys.verification_key(), &sig_req, blind_sig, 1)?;
|
||||
|
||||
let wallet = issuance.aggregate_signature_shares(
|
||||
&signing_keys.verification_key(),
|
||||
&[partial_wallet],
|
||||
sig_req,
|
||||
)?;
|
||||
|
||||
Ok(issuance.into_issued_ticketbook(wallet, 1))
|
||||
}
|
||||
|
||||
fn mock_verification_key() -> VerificationKeyAuth {
|
||||
ttp_keygen(1, 1).unwrap().remove(0).verification_key()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_ticketbooks_info_empty() {
|
||||
let manager = MemoryEcachTicketbookManager::new();
|
||||
let info = manager.get_ticketbooks_info().await;
|
||||
assert!(info.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_ticketbooks_info_maps_inserted_ticketbook() -> anyhow::Result<()> {
|
||||
let manager = MemoryEcachTicketbookManager::new();
|
||||
let ticketbook = mock_ticketbook()?;
|
||||
let total_tickets = 100;
|
||||
let used_tickets = 25;
|
||||
|
||||
manager
|
||||
.insert_new_ticketbook(&ticketbook, total_tickets, used_tickets)
|
||||
.await;
|
||||
|
||||
let info = manager.get_ticketbooks_info().await;
|
||||
assert_eq!(info.len(), 1);
|
||||
let entry = &info[0];
|
||||
assert_eq!(entry.id, 0);
|
||||
assert_eq!(entry.expiration_date, ticketbook.expiration_date());
|
||||
assert_eq!(
|
||||
entry.ticketbook_type,
|
||||
ticketbook.ticketbook_type().to_string()
|
||||
);
|
||||
assert_eq!(entry.epoch_id, ticketbook.epoch_id() as u32);
|
||||
assert_eq!(entry.total_tickets, total_tickets);
|
||||
assert_eq!(entry.used_tickets, used_tickets);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn contains_ticketbook_reflects_insertion() -> anyhow::Result<()> {
|
||||
let manager = MemoryEcachTicketbookManager::new();
|
||||
let ticketbook = mock_ticketbook()?;
|
||||
|
||||
assert!(!manager.contains_ticketbook(&ticketbook).await);
|
||||
|
||||
manager.insert_new_ticketbook(&ticketbook, 100, 0).await;
|
||||
|
||||
assert!(manager.contains_ticketbook(&ticketbook).await);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn insert_new_ticketbook_assigns_incrementing_ids() -> anyhow::Result<()> {
|
||||
let manager = MemoryEcachTicketbookManager::new();
|
||||
let ticketbook = mock_ticketbook()?;
|
||||
|
||||
manager.insert_new_ticketbook(&ticketbook, 100, 0).await;
|
||||
manager.insert_new_ticketbook(&ticketbook, 100, 0).await;
|
||||
|
||||
let mut ids: Vec<i64> = manager
|
||||
.get_ticketbooks_info()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|i| i.id)
|
||||
.collect();
|
||||
ids.sort();
|
||||
assert_eq!(ids, vec![0, 1]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_next_unspent_ticketbook_updates_spent_and_exhausts() -> anyhow::Result<()> {
|
||||
let manager = MemoryEcachTicketbookManager::new();
|
||||
let ticketbook = mock_ticketbook()?;
|
||||
let typ = ticketbook.ticketbook_type().to_string();
|
||||
|
||||
// total = 3, used = 0 — leaves 3 tickets available
|
||||
manager.insert_new_ticketbook(&ticketbook, 3, 0).await;
|
||||
|
||||
let first = manager
|
||||
.get_next_unspent_ticketbook_and_update(typ.clone(), 2)
|
||||
.await;
|
||||
assert!(first.is_some());
|
||||
let first = first.unwrap();
|
||||
assert_eq!(first.total_tickets, 3);
|
||||
// returned ticketbook reflects state *before* the update
|
||||
assert_eq!(first.ticketbook.spent_tickets(), 0);
|
||||
|
||||
// next withdrawal of 2 should be rejected (only 1 left)
|
||||
let second = manager
|
||||
.get_next_unspent_ticketbook_and_update(typ.clone(), 2)
|
||||
.await;
|
||||
assert!(second.is_none());
|
||||
|
||||
// but a withdrawal of 1 succeeds
|
||||
let third = manager
|
||||
.get_next_unspent_ticketbook_and_update(typ.clone(), 1)
|
||||
.await;
|
||||
assert!(third.is_some());
|
||||
|
||||
// and now nothing left
|
||||
let fourth = manager.get_next_unspent_ticketbook_and_update(typ, 1).await;
|
||||
assert!(fourth.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_next_unspent_ticketbook_filters_by_type() -> anyhow::Result<()> {
|
||||
let manager = MemoryEcachTicketbookManager::new();
|
||||
let ticketbook = mock_ticketbook()?;
|
||||
|
||||
manager.insert_new_ticketbook(&ticketbook, 5, 0).await;
|
||||
|
||||
let mismatched = manager
|
||||
.get_next_unspent_ticketbook_and_update("nonexistent_type".to_string(), 1)
|
||||
.await;
|
||||
assert!(mismatched.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn revert_ticketbook_withdrawal_resets_spent_only_when_expected_matches(
|
||||
) -> anyhow::Result<()> {
|
||||
let manager = MemoryEcachTicketbookManager::new();
|
||||
let ticketbook = mock_ticketbook()?;
|
||||
let typ = ticketbook.ticketbook_type().to_string();
|
||||
|
||||
manager.insert_new_ticketbook(&ticketbook, 10, 0).await;
|
||||
manager
|
||||
.get_next_unspent_ticketbook_and_update(typ.clone(), 4)
|
||||
.await
|
||||
.expect("should withdraw");
|
||||
|
||||
// stale expected_current_total_spent — should be rejected
|
||||
assert!(!manager.revert_ticketbook_withdrawal(0, 4, 99).await);
|
||||
// spent_tickets unchanged
|
||||
let used_after_failed = manager.get_ticketbooks_info().await[0].used_tickets;
|
||||
assert_eq!(used_after_failed, 4);
|
||||
|
||||
// matching expected — should succeed and restore
|
||||
assert!(manager.revert_ticketbook_withdrawal(0, 4, 4).await);
|
||||
let used_after_revert = manager.get_ticketbooks_info().await[0].used_tickets;
|
||||
assert_eq!(used_after_revert, 0);
|
||||
|
||||
// unknown ticketbook_id is rejected
|
||||
assert!(!manager.revert_ticketbook_withdrawal(999, 1, 0).await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pending_ticketbook_round_trip() {
|
||||
let manager = MemoryEcachTicketbookManager::new();
|
||||
let issuance = mock_issuance(7);
|
||||
let deposit_id = issuance.deposit_id() as i64;
|
||||
|
||||
assert!(manager.get_pending_ticketbooks().await.is_empty());
|
||||
|
||||
manager.insert_pending_ticketbook(&issuance).await;
|
||||
|
||||
let pending = manager.get_pending_ticketbooks().await;
|
||||
assert_eq!(pending.len(), 1);
|
||||
assert_eq!(pending[0].pending_id, deposit_id);
|
||||
assert_eq!(
|
||||
pending[0].pending_ticketbook.deposit_id(),
|
||||
issuance.deposit_id()
|
||||
);
|
||||
|
||||
manager.remove_pending_ticketbook(deposit_id).await;
|
||||
assert!(manager.get_pending_ticketbooks().await.is_empty());
|
||||
|
||||
// removing a non-existent id is a no-op
|
||||
manager.remove_pending_ticketbook(999).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn emergency_credential_lifecycle() {
|
||||
let manager = MemoryEcachTicketbookManager::new();
|
||||
|
||||
let cred_a = EmergencyCredentialContent {
|
||||
typ: "type-a".to_string(),
|
||||
content: vec![1, 2, 3],
|
||||
expiration: None,
|
||||
};
|
||||
let cred_b = EmergencyCredentialContent {
|
||||
typ: "type-a".to_string(),
|
||||
content: vec![4, 5, 6],
|
||||
expiration: None,
|
||||
};
|
||||
let cred_c = EmergencyCredentialContent {
|
||||
typ: "type-b".to_string(),
|
||||
content: vec![7, 8, 9],
|
||||
expiration: None,
|
||||
};
|
||||
|
||||
assert!(manager.get_emergency_credential("type-a").await.is_none());
|
||||
|
||||
manager.insert_emergency_credential(&cred_a).await;
|
||||
manager.insert_emergency_credential(&cred_b).await;
|
||||
manager.insert_emergency_credential(&cred_c).await;
|
||||
|
||||
// get returns the first inserted entry for the type
|
||||
let first = manager.get_emergency_credential("type-a").await.unwrap();
|
||||
assert_eq!(first.id, 0);
|
||||
assert_eq!(first.data.content, vec![1, 2, 3]);
|
||||
|
||||
// remove by id drops only that entry; type-a now exposes cred_b
|
||||
manager.remove_emergency_credential(0).await;
|
||||
let after_remove = manager.get_emergency_credential("type-a").await.unwrap();
|
||||
assert_eq!(after_remove.id, 1);
|
||||
assert_eq!(after_remove.data.content, vec![4, 5, 6]);
|
||||
|
||||
// remove by type clears the bucket entirely
|
||||
manager.remove_emergency_credentials_of_type("type-a").await;
|
||||
assert!(manager.get_emergency_credential("type-a").await.is_none());
|
||||
|
||||
// unrelated type is untouched
|
||||
assert!(manager.get_emergency_credential("type-b").await.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn master_verification_key_round_trip() {
|
||||
let manager = MemoryEcachTicketbookManager::new();
|
||||
let key = mock_verification_key();
|
||||
let epoch = EpochVerificationKey {
|
||||
epoch_id: 7,
|
||||
key: key.clone(),
|
||||
};
|
||||
|
||||
assert!(manager.get_master_verification_key(7).await.is_none());
|
||||
|
||||
manager.insert_master_verification_key(&epoch).await;
|
||||
|
||||
assert_eq!(manager.get_master_verification_key(7).await, Some(key));
|
||||
assert!(manager.get_master_verification_key(8).await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn coin_index_signatures_round_trip() {
|
||||
let manager = MemoryEcachTicketbookManager::new();
|
||||
let sigs = AggregatedCoinIndicesSignatures {
|
||||
epoch_id: 3,
|
||||
signatures: vec![],
|
||||
};
|
||||
|
||||
assert!(manager.get_coin_index_signatures(3).await.is_none());
|
||||
|
||||
manager.insert_coin_index_signatures(&sigs).await;
|
||||
|
||||
let retrieved = manager.get_coin_index_signatures(3).await;
|
||||
assert!(retrieved.is_some());
|
||||
assert!(retrieved.unwrap().is_empty());
|
||||
assert!(manager.get_coin_index_signatures(4).await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn expiration_date_signatures_round_trip() {
|
||||
let manager = MemoryEcachTicketbookManager::new();
|
||||
let date = nym_ecash_time::ecash_today().date();
|
||||
let sigs = AggregatedExpirationDateSignatures {
|
||||
epoch_id: 5,
|
||||
expiration_date: date,
|
||||
signatures: vec![],
|
||||
};
|
||||
|
||||
assert!(manager
|
||||
.get_expiration_date_signatures(date, 5)
|
||||
.await
|
||||
.is_none());
|
||||
|
||||
manager.insert_expiration_date_signatures(&sigs).await;
|
||||
|
||||
let retrieved = manager.get_expiration_date_signatures(date, 5).await;
|
||||
assert!(retrieved.is_some());
|
||||
assert!(retrieved.unwrap().is_empty());
|
||||
|
||||
// wrong epoch / wrong date → miss
|
||||
assert!(manager
|
||||
.get_expiration_date_signatures(date, 6)
|
||||
.await
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version.workspace = true
|
||||
# pinned (not inherited from workspace) because this crate is imported by the ecash contract,
|
||||
# and the contracts workspace cannot be built with rustc more recent than 1.86
|
||||
rust-version = "1.86.0"
|
||||
readme.workspace = true
|
||||
publish = true
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
inventory = { workspace = true }
|
||||
fastrand = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt", "macros", "time"] }
|
||||
rustls = { workspace=true }
|
||||
# used for decoding text responses (they were already implicitly included)
|
||||
|
||||
@@ -55,9 +55,8 @@ use std::{
|
||||
|
||||
use hickory_resolver::{
|
||||
TokioResolver,
|
||||
config::{NameServerConfig, NameServerConfigGroup, ResolverConfig, ResolverOpts},
|
||||
lookup_ip::LookupIpIntoIter,
|
||||
name_server::TokioConnectionProvider,
|
||||
config::{CLOUDFLARE, NameServerConfig, QUAD9, ResolverConfig, ResolverOpts},
|
||||
net::{NetError, runtime::TokioRuntimeProvider},
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
|
||||
@@ -113,7 +112,7 @@ pub enum ResolveError {
|
||||
#[error("invalid name: {0}")]
|
||||
InvalidNameError(String),
|
||||
#[error("hickory-dns resolver error: {0}")]
|
||||
ResolveError(#[from] hickory_resolver::ResolveError),
|
||||
ResolveError(#[from] NetError),
|
||||
#[error("high level lookup timed out")]
|
||||
Timeout,
|
||||
#[error("hostname not found in static lookup table")]
|
||||
@@ -123,7 +122,10 @@ pub enum ResolveError {
|
||||
impl ResolveError {
|
||||
/// Returns true if the error is a timeout.
|
||||
pub fn is_timeout(&self) -> bool {
|
||||
matches!(self, ResolveError::Timeout)
|
||||
matches!(
|
||||
self,
|
||||
ResolveError::Timeout | ResolveError::ResolveError(NetError::Timeout)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,18 +169,17 @@ impl Resolve for HickoryDnsResolver {
|
||||
fn resolve(&self, name: Name) -> Resolving {
|
||||
let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let use_shared = self.use_shared;
|
||||
let resolver = if use_system {
|
||||
match self
|
||||
.system_resolver
|
||||
let result = if use_system {
|
||||
self.system_resolver
|
||||
.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(use_shared))
|
||||
{
|
||||
Ok(r) => r.clone(),
|
||||
Err(e) => return Box::pin(return_err(e)),
|
||||
}
|
||||
} else {
|
||||
self.state
|
||||
.get_or_init(|| HickoryDnsResolver::new_resolver(use_shared))
|
||||
.clone()
|
||||
.get_or_try_init(|| HickoryDnsResolver::new_resolver(use_shared))
|
||||
};
|
||||
|
||||
let resolver = match result {
|
||||
Ok(r) => r.clone(),
|
||||
Err(err) => return Box::pin(return_err(err)),
|
||||
};
|
||||
|
||||
let maybe_static = self.static_base.clone();
|
||||
@@ -227,9 +228,11 @@ async fn resolve(
|
||||
let primary_err = match resolve_fut.await {
|
||||
Err(_) => ResolveError::Timeout,
|
||||
Ok(Ok(lookup)) => {
|
||||
let addrs: Addrs = Box::new(SocketAddrs {
|
||||
iter: lookup.into_iter(),
|
||||
});
|
||||
// Shuffle so that successive connection attempts cycle through all
|
||||
// returned IPs rather than always hitting the same first address.
|
||||
let mut ips = Vec::from_iter(lookup.iter());
|
||||
fastrand::shuffle(&mut ips);
|
||||
let addrs: Addrs = Box::new(ips.into_iter().map(|ip| SocketAddr::new(ip, 0)));
|
||||
return Ok(addrs);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
@@ -256,18 +259,6 @@ async fn resolve(
|
||||
Err(primary_err)
|
||||
}
|
||||
|
||||
struct SocketAddrs {
|
||||
iter: LookupIpIntoIter,
|
||||
}
|
||||
|
||||
impl Iterator for SocketAddrs {
|
||||
type Item = SocketAddr;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.iter.next().map(|ip_addr| SocketAddr::new(ip_addr, 0))
|
||||
}
|
||||
}
|
||||
|
||||
impl HickoryDnsResolver {
|
||||
/// Returns an instance of the shared resolver.
|
||||
pub fn shared() -> Self {
|
||||
@@ -288,7 +279,7 @@ impl HickoryDnsResolver {
|
||||
.clone()
|
||||
} else {
|
||||
self.state
|
||||
.get_or_init(|| HickoryDnsResolver::new_resolver(self.use_shared))
|
||||
.get_or_try_init(|| HickoryDnsResolver::new_resolver(self.use_shared))?
|
||||
.clone()
|
||||
};
|
||||
|
||||
@@ -311,11 +302,11 @@ impl HickoryDnsResolver {
|
||||
}
|
||||
}
|
||||
|
||||
fn new_resolver(use_shared: bool) -> TokioResolver {
|
||||
fn new_resolver(use_shared: bool) -> Result<TokioResolver, ResolveError> {
|
||||
// using a closure here is slightly gross, but this makes sure that if the
|
||||
// lazy-init returns an error it can be handled by the client
|
||||
if use_shared {
|
||||
SHARED_RESOLVER.state.get_or_init(new_resolver).clone()
|
||||
SHARED_RESOLVER.state.get_or_try_init(new_resolver).cloned()
|
||||
} else {
|
||||
new_resolver()
|
||||
}
|
||||
@@ -367,7 +358,7 @@ impl HickoryDnsResolver {
|
||||
/// Clear entries from the static table that would return entries during the pre-resolve stage.
|
||||
/// This means that all lookups will attempt to use the network resolver again before the static
|
||||
/// table is consulted.
|
||||
///
|
||||
///
|
||||
/// Entries elevated to pre-resolve from fallback (added from default or using
|
||||
/// [`set_fallback`]`) will have their cache timeout cleared. Entries added directly to
|
||||
/// pre-resolve (using [`Self::set_static_preresolve`]) will be removed.
|
||||
@@ -438,20 +429,7 @@ impl HickoryDnsResolver {
|
||||
|
||||
/// Get the list of currently available nameserver configs.
|
||||
pub fn all_configured_name_servers(&self) -> Vec<NameServerConfig> {
|
||||
default_nameserver_group().to_vec()
|
||||
}
|
||||
|
||||
/// Get the list of currently used nameserver configs.
|
||||
pub fn active_name_servers(&self) -> Vec<NameServerConfig> {
|
||||
if !self.use_shared {
|
||||
return self
|
||||
.state
|
||||
.get()
|
||||
.map(|r| r.config().name_servers().to_vec())
|
||||
.unwrap_or(self.all_configured_name_servers());
|
||||
}
|
||||
|
||||
SHARED_RESOLVER.active_name_servers()
|
||||
default_nameserver_group()
|
||||
}
|
||||
|
||||
/// Do a trial resolution using each nameserver individually to test which are working and which
|
||||
@@ -477,65 +455,60 @@ impl HickoryDnsResolver {
|
||||
///
|
||||
/// Caches successfully resolved addresses for 30 minutes to prevent continual use of remote lookup.
|
||||
/// This resolver is intended to be used for OUR API endpoints that do not rapidly rotate IPs.
|
||||
fn new_resolver() -> TokioResolver {
|
||||
fn new_resolver() -> Result<TokioResolver, ResolveError> {
|
||||
let name_servers = default_nameserver_group_ipv4_only();
|
||||
|
||||
configure_and_build_resolver(name_servers)
|
||||
}
|
||||
|
||||
fn configure_and_build_resolver<G>(name_servers: G) -> TokioResolver
|
||||
where
|
||||
G: Into<NameServerConfigGroup>,
|
||||
{
|
||||
fn configure_and_build_resolver(
|
||||
name_servers: Vec<NameServerConfig>,
|
||||
) -> Result<TokioResolver, ResolveError> {
|
||||
let options = HickoryDnsResolver::default_options();
|
||||
let name_servers: NameServerConfigGroup = name_servers.into();
|
||||
info!("building new configured resolver");
|
||||
debug!("configuring resolver with {options:?}, {name_servers:?}");
|
||||
|
||||
let config = ResolverConfig::from_parts(None, Vec::new(), name_servers);
|
||||
let mut resolver_builder =
|
||||
TokioResolver::builder_with_config(config, TokioConnectionProvider::default());
|
||||
TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
|
||||
|
||||
resolver_builder = resolver_builder.with_options(options);
|
||||
|
||||
resolver_builder.build()
|
||||
Ok(resolver_builder.build()?)
|
||||
}
|
||||
|
||||
fn filter_ipv4(nameservers: impl AsRef<[NameServerConfig]>) -> Vec<NameServerConfig> {
|
||||
fn filter_ipv4(nameservers: impl IntoIterator<Item = NameServerConfig>) -> Vec<NameServerConfig> {
|
||||
nameservers
|
||||
.as_ref()
|
||||
.iter()
|
||||
.filter(|ns| ns.socket_addr.is_ipv4())
|
||||
.cloned()
|
||||
.into_iter()
|
||||
.filter(|ns| ns.ip.is_ipv4())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn filter_ipv6(nameservers: impl AsRef<[NameServerConfig]>) -> Vec<NameServerConfig> {
|
||||
fn filter_ipv6(nameservers: impl IntoIterator<Item = NameServerConfig>) -> Vec<NameServerConfig> {
|
||||
nameservers
|
||||
.as_ref()
|
||||
.iter()
|
||||
.filter(|ns| ns.socket_addr.is_ipv6())
|
||||
.cloned()
|
||||
.into_iter()
|
||||
.filter(|ns| ns.ip.is_ipv6())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn default_nameserver_group() -> NameServerConfigGroup {
|
||||
let mut name_servers = NameServerConfigGroup::quad9_tls();
|
||||
name_servers.merge(NameServerConfigGroup::quad9_https());
|
||||
name_servers.merge(NameServerConfigGroup::cloudflare_tls());
|
||||
name_servers.merge(NameServerConfigGroup::cloudflare_https());
|
||||
name_servers
|
||||
fn default_nameserver_group() -> Vec<NameServerConfig> {
|
||||
QUAD9
|
||||
.tls()
|
||||
.chain(QUAD9.https())
|
||||
.chain(CLOUDFLARE.tls())
|
||||
.chain(CLOUDFLARE.https())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn default_nameserver_group_ipv4_only() -> NameServerConfigGroup {
|
||||
filter_ipv4(&default_nameserver_group() as &[NameServerConfig]).into()
|
||||
fn default_nameserver_group_ipv4_only() -> Vec<NameServerConfig> {
|
||||
filter_ipv4(default_nameserver_group())
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn default_nameserver_group_ipv6_only() -> NameServerConfigGroup {
|
||||
filter_ipv6(&default_nameserver_group() as &[NameServerConfig]).into()
|
||||
fn default_nameserver_group_ipv6_only() -> Vec<NameServerConfig> {
|
||||
filter_ipv6(default_nameserver_group())
|
||||
}
|
||||
|
||||
/// Create a new resolver with the default configuration, which reads from the system DNS config
|
||||
@@ -550,7 +523,7 @@ fn new_resolver_system() -> Result<TokioResolver, ResolveError> {
|
||||
|
||||
resolver_builder = resolver_builder.with_options(options);
|
||||
|
||||
Ok(resolver_builder.build())
|
||||
Ok(resolver_builder.build()?)
|
||||
}
|
||||
|
||||
fn new_default_static_fallback() -> StaticResolver {
|
||||
@@ -577,7 +550,7 @@ async fn trial_nameservers_inner(
|
||||
async fn trial_lookup(name_server: NameServerConfig, query: &str) -> Result<(), ResolveError> {
|
||||
debug!("running ns trial {name_server:?} query={query}");
|
||||
|
||||
let resolver = configure_and_build_resolver(vec![name_server]);
|
||||
let resolver = configure_and_build_resolver(vec![name_server])?;
|
||||
|
||||
match tokio::time::timeout(DEFAULT_OVERALL_LOOKUP_TIMEOUT, resolver.ipv4_lookup(query)).await {
|
||||
Ok(Ok(_)) => Ok(()),
|
||||
@@ -590,8 +563,10 @@ async fn trial_lookup(name_server: NameServerConfig, query: &str) -> Result<(),
|
||||
mod test {
|
||||
use super::*;
|
||||
use itertools::Itertools;
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
};
|
||||
|
||||
/// IP addresses guaranteed to fail attempts to resolve
|
||||
///
|
||||
@@ -670,26 +645,16 @@ mod test {
|
||||
let mut ns_ips = GUARANTEED_BROKEN_IPS_1.to_vec();
|
||||
ns_ips.push(good_cf_ip);
|
||||
|
||||
let broken_ns_https = NameServerConfigGroup::from_ips_https(
|
||||
&ns_ips,
|
||||
443,
|
||||
"cloudflare-dns.com".to_string(),
|
||||
true,
|
||||
);
|
||||
let domain = Arc::<str>::from("cloudflare-dns.com");
|
||||
let path = Arc::<str>::from("/dns-query");
|
||||
let broken_ns_https = GUARANTEED_BROKEN_IPS_1
|
||||
.iter()
|
||||
.chain([&good_cf_ip])
|
||||
.map(|ip| NameServerConfig::https(*ip, domain.clone(), Some(path.clone())))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let inner = configure_and_build_resolver(broken_ns_https);
|
||||
|
||||
// create a new resolver that won't mess with the shared resolver used by other tests
|
||||
let resolver = HickoryDnsResolver {
|
||||
use_shared: false,
|
||||
state: Arc::new(OnceCell::with_value(inner)),
|
||||
static_base: Some(Default::default()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let name_servers = resolver.state.get().unwrap().config().name_servers();
|
||||
for (ns, result) in trial_nameservers_inner(name_servers).await {
|
||||
if ns.socket_addr.ip() == good_cf_ip {
|
||||
for (ns, result) in trial_nameservers_inner(&broken_ns_https).await {
|
||||
if ns.ip == good_cf_ip {
|
||||
assert!(result.is_ok())
|
||||
} else {
|
||||
assert!(result.is_err())
|
||||
@@ -705,21 +670,20 @@ mod test {
|
||||
fn build_broken_resolver() -> Result<TokioResolver, ResolveError> {
|
||||
info!("building new faulty resolver");
|
||||
|
||||
let mut broken_ns_group = NameServerConfigGroup::from_ips_tls(
|
||||
GUARANTEED_BROKEN_IPS_1,
|
||||
853,
|
||||
"cloudflare-dns.com".to_string(),
|
||||
true,
|
||||
);
|
||||
let broken_ns_https = NameServerConfigGroup::from_ips_https(
|
||||
GUARANTEED_BROKEN_IPS_1,
|
||||
443,
|
||||
"cloudflare-dns.com".to_string(),
|
||||
true,
|
||||
);
|
||||
broken_ns_group.merge(broken_ns_https);
|
||||
let domain = Arc::<str>::from("cloudflare-dns.com");
|
||||
let path = Arc::<str>::from("/dns-query");
|
||||
let broken_ns_group = GUARANTEED_BROKEN_IPS_1
|
||||
.iter()
|
||||
.map(|ip| NameServerConfig::tls(*ip, domain.clone()))
|
||||
.chain(
|
||||
GUARANTEED_BROKEN_IPS_1
|
||||
.iter()
|
||||
.map(|ip| NameServerConfig::https(*ip, domain.clone(), Some(path.clone())))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(configure_and_build_resolver(broken_ns_group))
|
||||
configure_and_build_resolver(broken_ns_group)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -740,7 +704,7 @@ mod test {
|
||||
build_broken_resolver()?;
|
||||
let domain = "ifconfig.me";
|
||||
let result = resolver.resolve_str(domain).await;
|
||||
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
|
||||
assert!(result.is_err_and(|e| e.is_timeout()));
|
||||
|
||||
let duration = time_start.elapsed();
|
||||
assert!(duration < resolver.overall_dns_timeout + Duration::from_secs(1));
|
||||
@@ -774,25 +738,11 @@ mod test {
|
||||
// unsuccessful lookup - primary times out, and not in static table
|
||||
let domain = "non-existent.nymtech.net";
|
||||
let result = resolver.resolve_str(domain).await;
|
||||
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
|
||||
assert!(result.is_err_and(|e| e.is_timeout()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_resolver_uses_ipv4_only_nameservers() {
|
||||
let resolver = HickoryDnsResolver::thread_resolver();
|
||||
resolver
|
||||
.active_name_servers()
|
||||
.iter()
|
||||
.all(|cfg| cfg.socket_addr.is_ipv4());
|
||||
|
||||
SHARED_RESOLVER
|
||||
.active_name_servers()
|
||||
.iter()
|
||||
.all(|cfg| cfg.socket_addr.is_ipv4());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
|
||||
// This test impacts the state of the shared resolver and as such is disabled to avoid
|
||||
|
||||
@@ -324,12 +324,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
// NOTE THIS TEST IS DISABLED BECAUSE IT INTERACTS WITH LIVE NETWORK AND EXPECTS
|
||||
// SPECIFIC BEHAVIOR. Keeping in case logic changes and needs tested locally.
|
||||
#[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
|
||||
async fn fallback_on_failure() {
|
||||
let url1 = Url::new(
|
||||
"https://fake-domain.nymtech.net",
|
||||
Some(vec![
|
||||
"https://fake-front-1.nymtech.net",
|
||||
"https://fake-front-2.nymtech.net",
|
||||
"https://wrong.host.badssl.com/",
|
||||
"https://untrusted-root.badssl.com/",
|
||||
]),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -339,8 +342,12 @@ mod tests {
|
||||
)
|
||||
.unwrap(); // fastly
|
||||
|
||||
// use non-shared client and no-hickory so that we aren't reliant on a shared http client
|
||||
// that could be impacted by independent interleaved tests.
|
||||
let client = ClientBuilder::new_with_urls(vec![url1, url2])
|
||||
.expect("bad url")
|
||||
.non_shared()
|
||||
.no_hickory_dns()
|
||||
.with_fronting(Some(FrontPolicy::Always))
|
||||
.build()
|
||||
.expect("failed to build client");
|
||||
@@ -352,7 +359,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
client.current_url().front_str(),
|
||||
Some("fake-front-1.nymtech.net"),
|
||||
Some("wrong.host.badssl.com"),
|
||||
);
|
||||
|
||||
let result = client
|
||||
@@ -365,14 +372,14 @@ mod tests {
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
// Check that the host configuration updated the front on error.
|
||||
// Check that the host configuration updated the front on error (tls error wring host)
|
||||
assert_eq!(
|
||||
client.current_url().as_str(),
|
||||
"https://fake-domain.nymtech.net/",
|
||||
);
|
||||
assert_eq!(
|
||||
client.current_url().front_str(),
|
||||
Some("fake-front-2.nymtech.net"),
|
||||
Some("untrusted-root.badssl.com"),
|
||||
);
|
||||
|
||||
let result = client
|
||||
@@ -385,7 +392,8 @@ mod tests {
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
// Check that the host configuration updated the domain and front on error.
|
||||
// Check that the host configuration updated the domain and front on error (tls error
|
||||
// untrusted root)
|
||||
assert_eq!(
|
||||
client.current_url().as_str(),
|
||||
"https://validator.global.ssl.fastly.net/",
|
||||
|
||||
@@ -141,9 +141,7 @@
|
||||
|
||||
use http::header::USER_AGENT;
|
||||
pub use inventory;
|
||||
pub use reqwest;
|
||||
pub use reqwest::ClientBuilder as ReqwestClientBuilder;
|
||||
pub use reqwest::StatusCode;
|
||||
pub use reqwest::{self, ClientBuilder as ReqwestClientBuilder, StatusCode};
|
||||
use std::error::Error;
|
||||
|
||||
pub mod registry;
|
||||
@@ -152,19 +150,21 @@ use crate::path::RequestPath;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use cfg_if::cfg_if;
|
||||
use http::HeaderMap;
|
||||
use http::header::{ACCEPT, CONTENT_TYPE};
|
||||
use http::{
|
||||
HeaderMap,
|
||||
header::{ACCEPT, CONTENT_TYPE},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use mime::Mime;
|
||||
use reqwest::header::HeaderValue;
|
||||
use reqwest::{RequestBuilder, Response};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
use reqwest::{RequestBuilder, Response, header::HeaderValue};
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::io::ErrorKind;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
fmt::Display,
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, instrument, warn};
|
||||
|
||||
@@ -1173,7 +1173,16 @@ impl ApiClientCore for Client {
|
||||
};
|
||||
|
||||
match response {
|
||||
Ok(resp) => return Ok(resp),
|
||||
Ok(resp) => {
|
||||
// Check if the response includes a rate limit error from the vercel API
|
||||
if is_http_rate_limit_err(&resp) {
|
||||
warn!("encountered vercel rate limit error for {}", url.as_str());
|
||||
// if we have multiple urls, update to the next
|
||||
self.maybe_rotate_hosts(Some(url.clone()));
|
||||
}
|
||||
|
||||
return Ok(resp);
|
||||
}
|
||||
Err(err) => {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let is_network_err = err.is_timeout();
|
||||
@@ -1226,17 +1235,39 @@ impl ApiClientCore for Client {
|
||||
}
|
||||
}
|
||||
|
||||
const VERCEL_CHALLENGE_HEADER: &str = "x-vercel-mitigated";
|
||||
const VERCEL_CHALLENGE_VALUE: &[u8] = b"challenge";
|
||||
|
||||
/// Check for Rate Limit challenge response from the vercel API
|
||||
pub(crate) fn is_http_rate_limit_err(resp: &Response) -> bool {
|
||||
let status = resp.status() == StatusCode::FORBIDDEN;
|
||||
let header = resp
|
||||
.headers()
|
||||
.get(VERCEL_CHALLENGE_HEADER)
|
||||
.is_some_and(|v| v.as_bytes() == VERCEL_CHALLENGE_VALUE);
|
||||
let content_type = resp
|
||||
.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.parse::<Mime>().ok())
|
||||
.is_some_and(|mime_type| {
|
||||
mime_type.type_() == mime::TEXT && mime_type.subtype() == mime::HTML
|
||||
});
|
||||
|
||||
status && header && content_type
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const MAX_ERR_SOURCE_ITERATIONS: usize = 4;
|
||||
|
||||
/// This functions attempts to check the error returned by reqwest to see if
|
||||
/// rotating host informtion (for clients with mutliple hosts defined) could be
|
||||
/// helpful. This looks for situations where the error could plausibly be caused
|
||||
/// by a network adversary, or where rotating to an equival hostname might help.
|
||||
/// This functions attempts to check the error returned by reqwest to see if rotating host
|
||||
/// information (for clients with multiple hosts defined) could be helpful. This looks for
|
||||
/// situations where the error could plausibly be caused by a network adversary, or where rotating
|
||||
/// to an equivalent hostname might help.
|
||||
///
|
||||
/// For example --> NetworkUnreachable will not be helped by rotating domains,
|
||||
/// but ConnectionReset might be caused by a network adversary blocking by SNI
|
||||
/// which could possibly benefit from rotating domains.
|
||||
/// For example --> NetworkUnreachable will not be helped by rotating domains, but ConnectionReset
|
||||
/// might be caused by a network adversary blocking by SNI which could possibly benefit from
|
||||
/// rotating domains.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
|
||||
if err.is_timeout() {
|
||||
@@ -1274,7 +1305,7 @@ pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
|
||||
} else if let Some(_tls_err) = e.downcast_ref::<rustls::Error>() {
|
||||
// try downcast to TLS error
|
||||
return true;
|
||||
} else if let Some(resolve_err) = e.downcast_ref::<hickory_resolver::ResolveError>() {
|
||||
} else if let Some(resolve_err) = e.downcast_ref::<hickory_resolver::net::NetError>() {
|
||||
// try downcast to DNS error
|
||||
return resolve_err.is_nx_domain();
|
||||
} else {
|
||||
@@ -1697,6 +1728,13 @@ where
|
||||
decode_raw_response(&headers, full)
|
||||
} else if res.status() == StatusCode::NOT_FOUND {
|
||||
Err(HttpClientError::NotFound { url: Box::new(url) })
|
||||
} else if is_http_rate_limit_err(&res) {
|
||||
Err(HttpClientError::EndpointFailure {
|
||||
url: Box::new(url),
|
||||
status,
|
||||
headers: Box::new(headers),
|
||||
error: String::from("received vercel rate limit challenge response"),
|
||||
})
|
||||
} else {
|
||||
let Ok(plaintext) = res.text().await else {
|
||||
return Err(HttpClientError::RequestFailure {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use axum::extract::{ConnectInfo, FromRequestParts};
|
||||
use axum::http::request::Parts;
|
||||
use axum_client_ip::RightmostXForwardedFor;
|
||||
use std::convert::Infallible;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use tracing::warn;
|
||||
|
||||
/// Best-effort client IP extractor.
|
||||
///
|
||||
/// Prefers the rightmost entry of `X-Forwarded-For` (set by a trusted reverse
|
||||
/// proxy); falls back to the TCP peer address when the header is absent, and to
|
||||
/// the unspecified address when neither is available (tests).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ClientIpAddr(pub IpAddr);
|
||||
|
||||
impl<S> FromRequestParts<S> for ClientIpAddr
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = Infallible;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
if let Ok(RightmostXForwardedFor(ip)) =
|
||||
RightmostXForwardedFor::from_request_parts(parts, state).await
|
||||
{
|
||||
return Ok(ClientIpAddr(ip));
|
||||
}
|
||||
if let Ok(ConnectInfo(addr)) =
|
||||
ConnectInfo::<SocketAddr>::from_request_parts(parts, state).await
|
||||
{
|
||||
return Ok(ClientIpAddr(addr.ip()));
|
||||
}
|
||||
warn!("ClientIpAddr: no X-Forwarded-For or ConnectInfo found; using 0.0.0.0 fallback");
|
||||
Ok(ClientIpAddr(IpAddr::V4(Ipv4Addr::UNSPECIFIED)))
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::middleware::client_ip::ClientIpAddr;
|
||||
use axum::extract::Request;
|
||||
use axum::http::HeaderValue;
|
||||
use axum::http::header::{HOST, USER_AGENT};
|
||||
use axum::middleware::Next;
|
||||
use axum::response::IntoResponse;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use colored::Colorize;
|
||||
use std::time::Instant;
|
||||
use tracing::{debug, info};
|
||||
@@ -17,24 +17,24 @@ enum LogLevel {
|
||||
}
|
||||
|
||||
pub async fn log_request_info(
|
||||
insecure_client_ip: InsecureClientIp,
|
||||
client_ip: ClientIpAddr,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> impl IntoResponse {
|
||||
log_request(insecure_client_ip, request, next, LogLevel::Info).await
|
||||
log_request(client_ip, request, next, LogLevel::Info).await
|
||||
}
|
||||
|
||||
pub async fn log_request_debug(
|
||||
insecure_client_ip: InsecureClientIp,
|
||||
client_ip: ClientIpAddr,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> impl IntoResponse {
|
||||
log_request(insecure_client_ip, request, next, LogLevel::Debug).await
|
||||
log_request(client_ip, request, next, LogLevel::Debug).await
|
||||
}
|
||||
|
||||
/// Simple logger for requests
|
||||
async fn log_request(
|
||||
InsecureClientIp(addr): InsecureClientIp,
|
||||
ClientIpAddr(addr): ClientIpAddr,
|
||||
request: Request,
|
||||
next: Next,
|
||||
level: LogLevel,
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod bearer_auth;
|
||||
pub mod client_ip;
|
||||
pub mod logging;
|
||||
|
||||
@@ -129,6 +129,41 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum OutputV2 {
|
||||
#[default]
|
||||
Json,
|
||||
Yaml,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::IntoParams, utoipa::ToSchema))]
|
||||
#[serde(default)]
|
||||
pub struct OutputParamsV2 {
|
||||
pub output: Option<OutputV2>,
|
||||
}
|
||||
|
||||
impl OutputParamsV2 {
|
||||
pub fn get_output(&self) -> OutputV2 {
|
||||
self.output.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn to_response<T: Serialize>(self, data: T) -> FormattedResponse<T> {
|
||||
self.get_output().to_response(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputV2 {
|
||||
pub fn to_response<T: Serialize>(self, data: T) -> FormattedResponse<T> {
|
||||
match self {
|
||||
OutputV2::Json => FormattedResponse::Json(Json::from(data)),
|
||||
OutputV2::Yaml => FormattedResponse::Yaml(Yaml::from(data)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
|
||||
@@ -24,11 +24,15 @@ pub const SPHINX_STREAM_VERSION_THRESHOLD: u8 = v9::VERSION;
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const _: () = {
|
||||
assert!(SPHINX_STREAM_VERSION_THRESHOLD > MAX_NON_STREAM_VERSION);
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn stream_transport_threshold_is_consistent() {
|
||||
assert_eq!(MAX_NON_STREAM_VERSION, 8);
|
||||
assert_eq!(SPHINX_STREAM_VERSION_THRESHOLD, 9);
|
||||
assert!(SPHINX_STREAM_VERSION_THRESHOLD > MAX_NON_STREAM_VERSION);
|
||||
const _: () = assert!(SPHINX_STREAM_VERSION_THRESHOLD > MAX_NON_STREAM_VERSION);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version.workspace = true
|
||||
# pinned (not inherited from workspace) because this crate is imported by the ecash contract,
|
||||
# and the contracts workspace cannot be built with rustc more recent than 1.86
|
||||
rust-version = "1.86.0"
|
||||
readme.workspace = true
|
||||
publish = true
|
||||
# Exclude build.rs from published crate - it's only used for dev-time sync
|
||||
|
||||
@@ -22,6 +22,10 @@ pub const VESTING_CONTRACT_ADDRESS: &str =
|
||||
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "";
|
||||
// /\ TODO: this has to be updated once the contract is deployed
|
||||
|
||||
// \/ TODO: this has to be updated once the contract is deployed
|
||||
pub const NODE_FAMILIES_CONTRACT_ADDRESS: &str = "";
|
||||
// /\ TODO: this has to be updated once the contract is deployed
|
||||
|
||||
pub const ECASH_CONTRACT_ADDRESS: &str =
|
||||
"n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun";
|
||||
pub const GROUP_CONTRACT_ADDRESS: &str =
|
||||
@@ -43,10 +47,6 @@ pub const NYM_APIS: &[ApiUrlConst] = &[
|
||||
url: NYM_API,
|
||||
front_hosts: None,
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nym-frontdoor.vercel.app/api/",
|
||||
front_hosts: Some(&["vercel.app", "vercel.com"]),
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
|
||||
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
|
||||
@@ -68,7 +68,7 @@ pub const UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY: &str =
|
||||
pub const NYM_VPN_APIS: &[ApiUrlConst] = &[
|
||||
ApiUrlConst {
|
||||
url: NYM_VPN_API,
|
||||
front_hosts: Some(&["vercel.app", "vercel.com"]),
|
||||
front_hosts: None,
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nymvpn-frontdoor.global.ssl.fastly.net/api/",
|
||||
|
||||
@@ -39,6 +39,8 @@ pub struct NymContracts {
|
||||
pub vesting_contract_address: Option<String>,
|
||||
#[serde(default)]
|
||||
pub performance_contract_address: Option<String>,
|
||||
#[serde(default)]
|
||||
pub node_families_contract_address: Option<String>,
|
||||
pub ecash_contract_address: Option<String>,
|
||||
pub group_contract_address: Option<String>,
|
||||
pub multisig_contract_address: Option<String>,
|
||||
@@ -174,6 +176,9 @@ impl NymNetworkDetails {
|
||||
))
|
||||
.with_mixnet_contract(get_optional_env(var_names::MIXNET_CONTRACT_ADDRESS))
|
||||
.with_vesting_contract(get_optional_env(var_names::VESTING_CONTRACT_ADDRESS))
|
||||
.with_node_families_contract(get_optional_env(
|
||||
var_names::NODE_FAMILIES_CONTRACT_ADDRESS,
|
||||
))
|
||||
.with_ecash_contract(get_optional_env(var_names::ECASH_CONTRACT_ADDRESS))
|
||||
.with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS))
|
||||
.with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
|
||||
@@ -199,6 +204,9 @@ impl NymNetworkDetails {
|
||||
performance_contract_address: parse_optional_str(
|
||||
mainnet::PERFORMANCE_CONTRACT_ADDRESS,
|
||||
),
|
||||
node_families_contract_address: parse_optional_str(
|
||||
mainnet::NODE_FAMILIES_CONTRACT_ADDRESS,
|
||||
),
|
||||
ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS),
|
||||
group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS),
|
||||
multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS),
|
||||
@@ -252,6 +260,7 @@ impl NymNetworkDetails {
|
||||
|
||||
set_optional_var(var_names::MIXNET_CONTRACT_ADDRESS, self.contracts.mixnet_contract_address);
|
||||
set_optional_var(var_names::VESTING_CONTRACT_ADDRESS, self.contracts.vesting_contract_address);
|
||||
set_optional_var(var_names::NODE_FAMILIES_CONTRACT_ADDRESS, self.contracts.node_families_contract_address);
|
||||
set_optional_var(var_names::ECASH_CONTRACT_ADDRESS, self.contracts.ecash_contract_address);
|
||||
set_optional_var(var_names::GROUP_CONTRACT_ADDRESS, self.contracts.group_contract_address);
|
||||
set_optional_var(var_names::MULTISIG_CONTRACT_ADDRESS, self.contracts.multisig_contract_address);
|
||||
@@ -340,6 +349,12 @@ impl NymNetworkDetails {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_node_families_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
|
||||
self.contracts.node_families_contract_address = contract.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_ecash_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
|
||||
self.contracts.ecash_contract_address = contract.map(Into::into);
|
||||
|
||||
@@ -17,6 +17,7 @@ pub const VESTING_CONTRACT_ADDRESS: &str = "VESTING_CONTRACT_ADDRESS";
|
||||
pub const ECASH_CONTRACT_ADDRESS: &str = "ECASH_CONTRACT_ADDRESS";
|
||||
pub const GROUP_CONTRACT_ADDRESS: &str = "GROUP_CONTRACT_ADDRESS";
|
||||
pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_CONTRACT_ADDRESS";
|
||||
pub const NODE_FAMILIES_CONTRACT_ADDRESS: &str = "NODE_FAMILIES_CONTRACT_ADDRESS";
|
||||
pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS";
|
||||
pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS";
|
||||
pub const NYXD: &str = "NYXD";
|
||||
|
||||
@@ -25,6 +25,9 @@ pub enum Error {
|
||||
|
||||
#[error("failed to create ipv4 packet")]
|
||||
Ipv4PacketCreationFailure,
|
||||
|
||||
#[error("packet length {length} exceeds the u16 IP header field")]
|
||||
PacketLengthOverflow { length: usize },
|
||||
}
|
||||
|
||||
// Result type based on our error type
|
||||
|
||||
@@ -79,9 +79,14 @@ pub fn wrap_icmp_in_ipv4(
|
||||
let mut ipv4_packet =
|
||||
MutableIpv4Packet::owned(ipv4_buffer).ok_or(Error::Ipv4PacketCreationFailure)?;
|
||||
|
||||
let total_length_u16 =
|
||||
u16::try_from(total_length).map_err(|_| Error::PacketLengthOverflow {
|
||||
length: total_length,
|
||||
})?;
|
||||
|
||||
ipv4_packet.set_version(4);
|
||||
ipv4_packet.set_header_length(5);
|
||||
ipv4_packet.set_total_length(total_length as u16);
|
||||
ipv4_packet.set_total_length(total_length_u16);
|
||||
ipv4_packet.set_ttl(64);
|
||||
ipv4_packet.set_next_level_protocol(pnet_packet::ip::IpNextHeaderProtocols::Icmp);
|
||||
ipv4_packet.set_source(source);
|
||||
@@ -101,12 +106,18 @@ pub fn wrap_icmp_in_ipv6(
|
||||
source: Ipv6Addr,
|
||||
destination: Ipv6Addr,
|
||||
) -> Result<Ipv6Packet> {
|
||||
let ipv6_buffer = vec![0u8; 40 + icmp_echo_request.packet().len()];
|
||||
let payload_length = icmp_echo_request.packet().len();
|
||||
let payload_length_u16 =
|
||||
u16::try_from(payload_length).map_err(|_| Error::PacketLengthOverflow {
|
||||
length: payload_length,
|
||||
})?;
|
||||
|
||||
let ipv6_buffer = vec![0u8; 40 + payload_length];
|
||||
let mut ipv6_packet =
|
||||
MutableIpv6Packet::owned(ipv6_buffer).ok_or(Error::Ipv4PacketCreationFailure)?;
|
||||
|
||||
ipv6_packet.set_version(6);
|
||||
ipv6_packet.set_payload_length(icmp_echo_request.packet().len() as u16);
|
||||
ipv6_packet.set_payload_length(payload_length_u16);
|
||||
ipv6_packet.set_next_header(pnet_packet::ip::IpNextHeaderProtocols::Icmpv6);
|
||||
ipv6_packet.set_hop_limit(64);
|
||||
ipv6_packet.set_source(source);
|
||||
@@ -164,3 +175,122 @@ pub(crate) fn is_icmp_v6_echo_reply(packet: &Bytes) -> Option<(u16, Ipv6Addr, Ip
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pnet_packet::icmp::IcmpTypes;
|
||||
use pnet_packet::icmpv6::Icmpv6Types;
|
||||
use pnet_packet::ip::IpNextHeaderProtocols;
|
||||
|
||||
const V4_SRC: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1);
|
||||
const V4_DST: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2);
|
||||
const V6_SRC: Ipv6Addr = Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1);
|
||||
const V6_DST: Ipv6Addr = Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 2);
|
||||
|
||||
#[test]
|
||||
fn icmpv4_echo_request_sets_fields_and_valid_checksum() {
|
||||
let echo = create_icmpv4_echo_request(42, 7).unwrap();
|
||||
assert_eq!(echo.get_sequence_number(), 42);
|
||||
assert_eq!(echo.get_identifier(), 7);
|
||||
assert_eq!(echo.get_icmp_type(), IcmpTypes::EchoRequest);
|
||||
|
||||
// pnet's `checksum` skips the checksum word, so recomputing on the produced
|
||||
// packet must equal the stored value.
|
||||
let icmp = IcmpPacket::new(echo.packet()).unwrap();
|
||||
assert_eq!(echo.get_checksum(), pnet_packet::icmp::checksum(&icmp));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn icmpv6_echo_request_sets_fields_and_valid_checksum() {
|
||||
let echo = create_icmpv6_echo_request(99, 12, &V6_SRC, &V6_DST).unwrap();
|
||||
assert_eq!(echo.get_sequence_number(), 99);
|
||||
assert_eq!(echo.get_identifier(), 12);
|
||||
assert_eq!(echo.get_icmpv6_type(), Icmpv6Types::EchoRequest);
|
||||
|
||||
let icmpv6 = icmpv6::Icmpv6Packet::new(echo.packet()).unwrap();
|
||||
assert_eq!(
|
||||
echo.get_checksum(),
|
||||
pnet_packet::icmpv6::checksum(&icmpv6, &V6_SRC, &V6_DST)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrap_icmp_in_ipv4_sets_headers_and_payload() {
|
||||
let echo = create_icmpv4_echo_request(1, 2).unwrap();
|
||||
let echo_bytes = echo.packet().to_vec();
|
||||
let packet = wrap_icmp_in_ipv4(echo, V4_SRC, V4_DST).unwrap();
|
||||
|
||||
assert_eq!(packet.get_version(), 4);
|
||||
assert_eq!(packet.get_header_length(), 5);
|
||||
assert_eq!(packet.get_total_length() as usize, 20 + echo_bytes.len());
|
||||
assert_eq!(packet.get_ttl(), 64);
|
||||
assert_eq!(
|
||||
packet.get_next_level_protocol(),
|
||||
IpNextHeaderProtocols::Icmp
|
||||
);
|
||||
assert_eq!(packet.get_source(), V4_SRC);
|
||||
assert_eq!(packet.get_destination(), V4_DST);
|
||||
assert_eq!(packet.payload(), echo_bytes.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrap_icmp_in_ipv6_sets_headers_and_payload() {
|
||||
let echo = create_icmpv6_echo_request(1, 2, &V6_SRC, &V6_DST).unwrap();
|
||||
let echo_bytes = echo.packet().to_vec();
|
||||
let packet = wrap_icmp_in_ipv6(echo, V6_SRC, V6_DST).unwrap();
|
||||
|
||||
assert_eq!(packet.get_version(), 6);
|
||||
assert_eq!(packet.get_payload_length() as usize, echo_bytes.len());
|
||||
assert_eq!(packet.get_next_header(), IpNextHeaderProtocols::Icmpv6);
|
||||
assert_eq!(packet.get_hop_limit(), 64);
|
||||
assert_eq!(packet.get_source(), V6_SRC);
|
||||
assert_eq!(packet.get_destination(), V6_DST);
|
||||
assert_eq!(packet.payload(), echo_bytes.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_ipv4_checksum_is_zero_on_correctly_checksummed_packet() {
|
||||
let echo = create_icmpv4_echo_request(1, 2).unwrap();
|
||||
let packet = wrap_icmp_in_ipv4(echo, V4_SRC, V4_DST).unwrap();
|
||||
// RFC 1071: summing every 16-bit word of a header that already contains its
|
||||
// own checksum yields all-ones; the one's complement is therefore zero.
|
||||
assert_eq!(compute_ipv4_checksum(&packet), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_icmp_echo_reply_extracts_identifier_and_addresses() {
|
||||
// pnet's EchoReply/EchoRequest share the same byte layout (only the ICMP
|
||||
// type field differs) and `is_icmp_echo_reply` does not check the type,
|
||||
// so a wrapped echo *request* exercises the same parsing path.
|
||||
let identifier = 1234;
|
||||
let echo = create_icmpv4_echo_request(7, identifier).unwrap();
|
||||
let packet = wrap_icmp_in_ipv4(echo, V4_SRC, V4_DST).unwrap();
|
||||
let bytes = Bytes::copy_from_slice(packet.packet());
|
||||
|
||||
assert_eq!(
|
||||
is_icmp_echo_reply(&bytes),
|
||||
Some((identifier, V4_SRC, V4_DST))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_icmp_v6_echo_reply_extracts_identifier_and_addresses() {
|
||||
let identifier = 5678;
|
||||
let echo = create_icmpv6_echo_request(7, identifier, &V6_SRC, &V6_DST).unwrap();
|
||||
let packet = wrap_icmp_in_ipv6(echo, V6_SRC, V6_DST).unwrap();
|
||||
let bytes = Bytes::copy_from_slice(packet.packet());
|
||||
|
||||
assert_eq!(
|
||||
is_icmp_v6_echo_reply(&bytes),
|
||||
Some((identifier, V6_SRC, V6_DST))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_icmp_echo_reply_returns_none_for_undersized_bytes() {
|
||||
let bytes = Bytes::from_static(&[0u8; 4]);
|
||||
assert!(is_icmp_echo_reply(&bytes).is_none());
|
||||
assert!(is_icmp_v6_echo_reply(&bytes).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
|
||||
use ansi_term::Color::Yellow;
|
||||
use bytes::{Buf, BytesMut};
|
||||
use log::{debug, error, warn};
|
||||
use log::{debug, error, trace, warn};
|
||||
use std::thread;
|
||||
|
||||
use crate::MAX_RTO;
|
||||
@@ -499,21 +499,9 @@ impl KcpSession {
|
||||
self.snd_buf.len(),
|
||||
post_retain_sns
|
||||
);
|
||||
// Corrected format string arguments for the removed count log
|
||||
debug!(
|
||||
"[ConvID: {}, Thread: {:?}] parse_una(una={}): Removed {} segment(s) from snd_buf ({} -> {}). Remaining sns: {:?}",
|
||||
self.conv,
|
||||
thread::current().id(),
|
||||
una,
|
||||
removed_count,
|
||||
original_len,
|
||||
self.snd_buf.len(),
|
||||
post_retain_sns
|
||||
);
|
||||
|
||||
if removed_count > 0 {
|
||||
// Use trace level if no segments were removed but buffer wasn't empty
|
||||
debug!(
|
||||
if removed_count == 0 {
|
||||
trace!(
|
||||
"[ConvID: {}, Thread: {:?}] parse_una(una={}): No segments removed from snd_buf (len={}). Remaining sns: {:?}",
|
||||
self.conv,
|
||||
thread::current().id(),
|
||||
@@ -521,6 +509,17 @@ impl KcpSession {
|
||||
original_len,
|
||||
self.snd_buf.iter().map(|s| s.sn).collect::<Vec<_>>()
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
"[ConvID: {}, Thread: {:?}] parse_una(una={}): Removed {} segment(s) from snd_buf ({} -> {}). Remaining sns: {:?}",
|
||||
self.conv,
|
||||
thread::current().id(),
|
||||
una,
|
||||
removed_count,
|
||||
original_len,
|
||||
self.snd_buf.len(),
|
||||
post_retain_sns
|
||||
);
|
||||
}
|
||||
|
||||
// Update the known acknowledged sequence number.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-kkt"
|
||||
description = "Key transport protocol for the Nym network"
|
||||
version = "0.1.0"
|
||||
version = "1.21.0"
|
||||
authors = ["Georgio Nicolas <georgio@nymtech.net>"]
|
||||
edition = { workspace = true }
|
||||
license.workspace = true
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "nym-lp-data"
|
||||
description = "Lewes Protocol data structure for the Nym network"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bytes.workspace = true
|
||||
num_enum.workspace = true
|
||||
tracing.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
nym-common.workspace = true
|
||||
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod packet;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user