Merge branch 'develop' into merge/release/2026.10-waterloo
This commit is contained in:
@@ -25,14 +25,14 @@ jobs:
|
|||||||
echo "file2=$(ls nym-vpn*.deb)" >> $GITHUB_ENV
|
echo "file2=$(ls nym-vpn*.deb)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Upload nym-repo-setup
|
- name: Upload nym-repo-setup
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ env.file1 }}
|
name: ${{ env.file1 }}
|
||||||
path: ppa/packages/nym-repo-setup*.deb
|
path: ppa/packages/nym-repo-setup*.deb
|
||||||
retention-days: 10
|
retention-days: 10
|
||||||
|
|
||||||
- name: Upload nym-vpn
|
- name: Upload nym-vpn
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ env.file2 }}
|
name: ${{ env.file2 }}
|
||||||
path: ppa/packages/nym-vpn*.deb
|
path: ppa/packages/nym-vpn*.deb
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ jobs:
|
|||||||
run: sudo apt-get install -y rsync
|
run: sudo apt-get install -y rsync
|
||||||
- uses: rlespinasse/github-slug-action@v3.x
|
- uses: rlespinasse/github-slug-action@v3.x
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v5.0.0
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 11.1.2
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 24
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
@@ -37,6 +37,9 @@ jobs:
|
|||||||
command: build
|
command: build
|
||||||
args: --workspace --release
|
args: --workspace --release
|
||||||
|
|
||||||
|
- name: Verify doc versions
|
||||||
|
run: ${{ github.workspace }}/documentation/scripts/verify-doc-versions.sh
|
||||||
|
working-directory: ${{ github.workspace }}
|
||||||
- name: Install project dependencies
|
- name: Install project dependencies
|
||||||
run: pnpm i
|
run: pnpm i
|
||||||
- name: Generate llms-full.txt
|
- name: Generate llms-full.txt
|
||||||
|
|||||||
@@ -17,13 +17,16 @@ jobs:
|
|||||||
run: sudo apt-get install rsync
|
run: sudo apt-get install rsync
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
- uses: rlespinasse/github-slug-action@v3.x
|
- uses: rlespinasse/github-slug-action@v3.x
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v5.0.0
|
||||||
|
with:
|
||||||
|
version: 11.1.2
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 24
|
||||||
- name: Setup yarn
|
cache: pnpm
|
||||||
run: npm install -g yarn
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: yarn && yarn build && yarn build:ci:storybook
|
run: pnpm install && pnpm build && pnpm build:ci:storybook
|
||||||
- name: Deploy branch to CI www (storybook)
|
- name: Deploy branch to CI www (storybook)
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
uses: easingthemes/ssh-deploy@main
|
uses: easingthemes/ssh-deploy@main
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: nym-binaries-artifacts
|
name: nym-binaries-artifacts
|
||||||
path: |
|
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/"
|
||||||
@@ -23,7 +23,6 @@ on:
|
|||||||
- 'sdk/ffi/**'
|
- 'sdk/ffi/**'
|
||||||
- 'sdk/rust/**'
|
- 'sdk/rust/**'
|
||||||
- 'service-providers/**'
|
- 'service-providers/**'
|
||||||
- 'nym-browser-extension/storage/**'
|
|
||||||
- 'tools/**'
|
- 'tools/**'
|
||||||
- 'wasm/**'
|
- 'wasm/**'
|
||||||
- 'Cargo.toml'
|
- 'Cargo.toml'
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: ci-crates-preflight
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'Cargo.toml'
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- 'tools/internal/check_publish_preflight.py'
|
||||||
|
- '.github/workflows/ci-crates-preflight.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
preflight:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Preflight publish checks
|
||||||
|
run: python3 tools/internal/check_publish_preflight.py
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: 24
|
||||||
|
|
||||||
- name: Validate version format
|
- name: Validate version format
|
||||||
run: |
|
run: |
|
||||||
@@ -57,7 +57,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Update workspace dependencies
|
- name: Update workspace dependencies
|
||||||
run: |
|
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)
|
- name: Bump versions (local only)
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ jobs:
|
|||||||
- name: Install cargo-workspaces
|
- name: Install cargo-workspaces
|
||||||
run: cargo 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
|
- name: Publish
|
||||||
env:
|
env:
|
||||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ jobs:
|
|||||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@@ -41,7 +42,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: 24
|
||||||
|
|
||||||
- name: Validate version format
|
- name: Validate version format
|
||||||
run: |
|
run: |
|
||||||
@@ -58,7 +59,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Update workspace dependencies
|
- name: Update workspace dependencies
|
||||||
run: |
|
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
|
- name: Bump versions
|
||||||
run: |
|
run: |
|
||||||
@@ -68,9 +71,33 @@ jobs:
|
|||||||
|
|
||||||
- name: Commit and push version bump
|
- name: Commit and push version bump
|
||||||
run: |
|
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 add -A
|
||||||
git commit -m "crates release: bump version to ${{ inputs.version }}"
|
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
|
- name: Show package versions
|
||||||
run: cargo workspaces list --long
|
run: cargo workspaces list --long
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ jobs:
|
|||||||
run: sudo apt-get install -y rsync
|
run: sudo apt-get install -y rsync
|
||||||
- uses: rlespinasse/github-slug-action@v3.x
|
- uses: rlespinasse/github-slug-action@v3.x
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4.2.0
|
uses: pnpm/action-setup@v5.0.0
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 11.1.2
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 24
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
@@ -61,6 +61,9 @@ jobs:
|
|||||||
cd ${{ github.workspace }}/sdk/typescript/packages/sdk && typedoc --skipErrorChecking
|
cd ${{ github.workspace }}/sdk/typescript/packages/sdk && typedoc --skipErrorChecking
|
||||||
cd ${{ github.workspace }}/sdk/typescript/packages/mix-fetch && typedoc --skipErrorChecking
|
cd ${{ github.workspace }}/sdk/typescript/packages/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
|
- name: Install project dependencies
|
||||||
run: pnpm i
|
run: pnpm i
|
||||||
- name: Generate llms-full.txt
|
- name: Generate llms-full.txt
|
||||||
|
|||||||
@@ -20,12 +20,14 @@ jobs:
|
|||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- uses: rlespinasse/github-slug-action@v3.x
|
- uses: rlespinasse/github-slug-action@v3.x
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v5.0.0
|
||||||
|
with:
|
||||||
|
version: 11.1.2
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 24
|
||||||
|
cache: pnpm
|
||||||
- name: Setup yarn
|
|
||||||
run: npm install -g yarn
|
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
@@ -44,16 +46,16 @@ jobs:
|
|||||||
go-version: "1.24.6"
|
go-version: "1.24.6"
|
||||||
|
|
||||||
- name: Install
|
- name: Install
|
||||||
run: yarn
|
run: pnpm i
|
||||||
|
|
||||||
- name: Build packages
|
- name: Build packages
|
||||||
run: yarn build:ci
|
run: pnpm build:ci
|
||||||
|
|
||||||
- name: Install again
|
- name: Install again
|
||||||
run: yarn
|
run: pnpm i
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: yarn lint
|
run: pnpm lint
|
||||||
|
|
||||||
- name: Typecheck with tsc
|
- name: Typecheck with tsc
|
||||||
run: yarn tsc
|
run: pnpm tsc
|
||||||
|
|||||||
@@ -12,30 +12,34 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v5.0.0
|
||||||
|
with:
|
||||||
|
version: 11.1.2
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: nym-wallet/.nvmrc
|
node-version-file: nym-wallet/.nvmrc
|
||||||
cache: yarn
|
cache: pnpm
|
||||||
cache-dependency-path: yarn.lock
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn install --network-timeout 100000
|
run: pnpm install
|
||||||
|
|
||||||
- name: Build TypeScript packages (wallet depends on @nymproject/types, etc.)
|
- name: Build TypeScript packages (wallet depends on @nymproject/types, etc.)
|
||||||
run: yarn build:types
|
run: pnpm build:types
|
||||||
|
|
||||||
- name: Build @nymproject/mui-theme and @nymproject/react (wallet imports subpaths)
|
- name: Build @nymproject/mui-theme and @nymproject/react (wallet imports subpaths)
|
||||||
run: yarn build:packages
|
run: pnpm build:packages
|
||||||
|
|
||||||
- name: Typecheck nym-wallet
|
- name: Typecheck nym-wallet
|
||||||
run: yarn --cwd nym-wallet tsc
|
run: pnpm --filter @nymproject/nym-wallet-app tsc
|
||||||
|
|
||||||
- name: Lint nym-wallet
|
- name: Lint nym-wallet
|
||||||
run: yarn --cwd nym-wallet lint
|
run: pnpm --filter @nymproject/nym-wallet-app lint
|
||||||
|
|
||||||
- name: Yarn audit (workspace lockfile; informational)
|
- name: pnpm audit (workspace lockfile; informational)
|
||||||
run: yarn audit --level critical
|
run: pnpm audit --audit-level critical
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Unit tests (nym-wallet)
|
- name: Unit tests (nym-wallet)
|
||||||
run: yarn --cwd nym-wallet test
|
run: pnpm --filter @nymproject/nym-wallet-app test
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 24
|
||||||
|
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
find . -name Cargo.toml -exec cargo deny --manifest-path {} check \
|
find . -name Cargo.toml -exec cargo deny --manifest-path {} check \
|
||||||
advisories -A advisory-not-detected --hide-inclusion-graph \; &> \
|
advisories -A advisory-not-detected --hide-inclusion-graph \; &> \
|
||||||
>(uniq &> .github/workflows/support-files/notifications/deny.message )
|
>(uniq &> .github/workflows/support-files/notifications/deny.message )
|
||||||
- uses: actions/upload-artifact@v6
|
- uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: report
|
name: report
|
||||||
path: .github/workflows/support-files/notifications/deny.message
|
path: .github/workflows/support-files/notifications/deny.message
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
args: --workspace --release ${{ env.CARGO_FEATURES }}
|
args: --workspace --release ${{ env.CARGO_FEATURES }}
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: my-artifact
|
name: my-artifact
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ jobs:
|
|||||||
run: make contracts
|
run: make contracts
|
||||||
|
|
||||||
- name: Upload Mixnet Contract Artifact
|
- name: Upload Mixnet Contract Artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: mixnet_contract.wasm
|
name: mixnet_contract.wasm
|
||||||
path: contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm
|
path: contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm
|
||||||
retention-days: 5
|
retention-days: 5
|
||||||
|
|
||||||
- name: Upload Vesting Contract Artifact
|
- name: Upload Vesting Contract Artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: vesting_contract.wasm
|
name: vesting_contract.wasm
|
||||||
path: contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm
|
path: contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm
|
||||||
|
|||||||
@@ -23,10 +23,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Node
|
- name: Setup pnpm
|
||||||
uses: actions/setup-node@v4
|
uses: pnpm/action-setup@v5.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 22.13.0
|
version: 11.1.2
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -68,17 +71,17 @@ jobs:
|
|||||||
fileName: '.env'
|
fileName: '.env'
|
||||||
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
|
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
|
||||||
|
|
||||||
- name: Yarn cache clean
|
- name: pnpm cache clean
|
||||||
shell: bash
|
shell: bash
|
||||||
run: cd .. && yarn cache clean
|
run: cd .. && pnpm cache delete
|
||||||
|
|
||||||
- name: Install project dependencies
|
- name: Install project dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: cd .. && yarn --network-timeout 100000
|
run: cd .. && pnpm i
|
||||||
|
|
||||||
- name: Yarn build
|
- name: Build
|
||||||
shell: bash
|
shell: bash
|
||||||
run: cd .. && yarn build
|
run: cd .. && pnpm build
|
||||||
|
|
||||||
- name: Install dependencies and build it
|
- name: Install dependencies and build it
|
||||||
env:
|
env:
|
||||||
@@ -97,7 +100,7 @@ jobs:
|
|||||||
TAURI_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
TAURI_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
TAURI_NOTARIZATION_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
TAURI_NOTARIZATION_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
run: |
|
run: |
|
||||||
yarn build-macx86
|
pnpm build-macx86
|
||||||
|
|
||||||
- name: Create app tarball
|
- name: Create app tarball
|
||||||
run: |
|
run: |
|
||||||
@@ -108,7 +111,7 @@ jobs:
|
|||||||
cd -
|
cd -
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: nym-wallet.app.tar.gz
|
name: nym-wallet.app.tar.gz
|
||||||
path: nym-wallet/target/x86_64-apple-darwin/release/bundle/macos/nym-wallet.app.tar.gz
|
path: nym-wallet/target/x86_64-apple-darwin/release/bundle/macos/nym-wallet.app.tar.gz
|
||||||
|
|||||||
@@ -26,12 +26,17 @@ jobs:
|
|||||||
libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev jq \
|
libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev jq \
|
||||||
libgtk-3-dev squashfs-tools libayatana-appindicator3-dev make libfuse2 unzip librsvg2-dev file \
|
libgtk-3-dev squashfs-tools libayatana-appindicator3-dev make libfuse2 unzip librsvg2-dev file \
|
||||||
libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
|
libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v5.0.0
|
||||||
|
with:
|
||||||
|
version: 11.1.2
|
||||||
|
|
||||||
- name: Node
|
- name: Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22.13.0
|
node-version: 24
|
||||||
cache: 'yarn'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -40,10 +45,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Install project dependencies
|
- name: Install project dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: cd .. && yarn --network-timeout 100000
|
run: cd .. && pnpm i
|
||||||
|
|
||||||
- name: Install app dependencies
|
- name: Install app dependencies
|
||||||
run: yarn
|
run: pnpm
|
||||||
|
|
||||||
- name: Create env file
|
- name: Create env file
|
||||||
uses: timheuer/base64-to-file@v1.2
|
uses: timheuer/base64-to-file@v1.2
|
||||||
@@ -52,7 +57,7 @@ jobs:
|
|||||||
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
|
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
|
||||||
|
|
||||||
- name: Build app
|
- name: Build app
|
||||||
run: yarn build
|
run: pnpm build
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
@@ -132,7 +137,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: nym-wallet-appimage.tar.gz
|
name: nym-wallet-appimage.tar.gz
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
@@ -38,18 +38,15 @@ jobs:
|
|||||||
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
|
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
|
||||||
|
|
||||||
- name: Setup MSBuild.exe
|
- 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
|
- name: Setup pnpm
|
||||||
# only gets yarn from the step below.
|
uses: pnpm/action-setup@v5.0.0
|
||||||
- name: Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
with:
|
||||||
node-version: 22.13.0
|
version: 11.1.2
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
- name: Install Yarn (classic)
|
with:
|
||||||
shell: bash
|
node-version: 24
|
||||||
run: npm install -g yarn@1.22.22
|
|
||||||
|
|
||||||
- name: Strip Authenticode thumbprint (avoid signtool on runner)
|
- name: Strip Authenticode thumbprint (avoid signtool on runner)
|
||||||
working-directory: nym-wallet/src-tauri
|
working-directory: nym-wallet/src-tauri
|
||||||
@@ -118,11 +115,11 @@ jobs:
|
|||||||
' tauri.conf.json
|
' tauri.conf.json
|
||||||
- name: Install project dependencies
|
- name: Install project dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: cd .. && yarn --network-timeout 100000
|
run: cd .. && pnpm i
|
||||||
|
|
||||||
- name: Install app dependencies
|
- name: Install app dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: yarn --network-timeout 100000
|
run: pnpm i
|
||||||
|
|
||||||
- name: Build and sign it
|
- name: Build and sign it
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -136,7 +133,7 @@ jobs:
|
|||||||
SSL_COM_TOTP_SECRET: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_TOTP_SECRET }}
|
SSL_COM_TOTP_SECRET: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_TOTP_SECRET }}
|
||||||
run: |
|
run: |
|
||||||
echo "Starting build process..."
|
echo "Starting build process..."
|
||||||
yarn build
|
pnpm build
|
||||||
|
|
||||||
- name: Check bundle directory
|
- name: Check bundle directory
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -165,7 +162,7 @@ jobs:
|
|||||||
find . -name "*.msi" -type f
|
find . -name "*.msi" -type f
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: nym-wallet.msi
|
name: nym-wallet.msi
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
apk/nyms5-arch64-release.apk
|
apk/nyms5-arch64-release.apk
|
||||||
|
|
||||||
- name: Upload APKs
|
- name: Upload APKs
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: nyms5-apk-arch64
|
name: nyms5-apk-arch64
|
||||||
path: |
|
path: |
|
||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
- name: Download binary artifact
|
- name: Download binary artifact
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: nyms5-apk-arch64
|
name: nyms5-apk-arch64
|
||||||
path: apk
|
path: apk
|
||||||
|
|||||||
@@ -8,15 +8,17 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v5.0.0
|
||||||
|
with:
|
||||||
|
version: 11.1.2
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 24
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
- name: Setup yarn
|
|
||||||
run: npm install -g yarn
|
|
||||||
|
|
||||||
- name: Install rust toolchain
|
- name: Install rust toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
@@ -40,10 +42,10 @@ jobs:
|
|||||||
run: ./wasm/mix-fetch/go-mix-conn/scripts/update-root-certs.sh
|
run: ./wasm/mix-fetch/go-mix-conn/scripts/update-root-certs.sh
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
run: pnpm i
|
||||||
|
|
||||||
- name: Build WASM and Typescript SDK
|
- name: Build WASM and Typescript SDK
|
||||||
run: yarn sdk:build
|
run: pnpm sdk:build
|
||||||
|
|
||||||
- name: Publish to NPM
|
- name: Publish to NPM
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: arc-linux-latest-dind
|
runs-on: arc-linux-latest-dind
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Harbor
|
- name: Login to Harbor
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: harbor.nymte.ch
|
registry: harbor.nymte.ch
|
||||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: arc-linux-latest-dind
|
runs-on: arc-linux-latest-dind
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Harbor
|
- name: Login to Harbor
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: harbor.nymte.ch
|
registry: harbor.nymte.ch
|
||||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: arc-ubuntu-22.04-dind
|
runs-on: arc-ubuntu-22.04-dind
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Harbor
|
- name: Login to Harbor
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: harbor.nymte.ch
|
registry: harbor.nymte.ch
|
||||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
runs-on: arc-linux-latest-dind
|
runs-on: arc-linux-latest-dind
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Harbor
|
- name: Login to Harbor
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: harbor.nymte.ch
|
registry: harbor.nymte.ch
|
||||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: arc-linux-latest-dind
|
runs-on: arc-linux-latest-dind
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Harbor
|
- name: Login to Harbor
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: harbor.nymte.ch
|
registry: harbor.nymte.ch
|
||||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: arc-ubuntu-22.04-dind
|
runs-on: arc-ubuntu-22.04-dind
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Harbor
|
- name: Login to Harbor
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: harbor.nymte.ch
|
registry: harbor.nymte.ch
|
||||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: arc-ubuntu-22.04-dind
|
runs-on: arc-ubuntu-22.04-dind
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Harbor
|
- name: Login to Harbor
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: harbor.nymte.ch
|
registry: harbor.nymte.ch
|
||||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: arc-linux-latest-dind
|
runs-on: arc-linux-latest-dind
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Harbor
|
- name: Login to Harbor
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: harbor.nymte.ch
|
registry: harbor.nymte.ch
|
||||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Harbor
|
- name: Login to Harbor
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: harbor.nymte.ch
|
registry: harbor.nymte.ch
|
||||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: arc-ubuntu-22.04-dind
|
runs-on: arc-ubuntu-22.04-dind
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Harbor
|
- name: Login to Harbor
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: harbor.nymte.ch
|
registry: harbor.nymte.ch
|
||||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 24
|
||||||
- uses: nymtech/nym/.github/actions/nym-hash-releases@develop
|
- uses: nymtech/nym/.github/actions/nym-hash-releases@develop
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
release-tag-or-name-or-id: ${{ inputs.release_tag }}
|
release-tag-or-name-or-id: ${{ inputs.release_tag }}
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v6
|
- uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: Asset Hashes
|
name: Asset Hashes
|
||||||
path: hashes.json
|
path: hashes.json
|
||||||
|
|||||||
@@ -78,3 +78,8 @@ CLAUDE.md
|
|||||||
|
|
||||||
/notes
|
/notes
|
||||||
/target-otel
|
/target-otel
|
||||||
|
test-tutorials/
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
shamefully-hoist=false
|
||||||
|
prefer-workspace-packages=true
|
||||||
|
hoist-pattern[]=*eslint*
|
||||||
|
hoist-pattern[]=*prettier*
|
||||||
|
hoist-pattern[]=*typescript*
|
||||||
|
hoist-pattern[]=*@types*
|
||||||
|
|
||||||
|
auto-install-peers=true
|
||||||
|
strict-peer-dependencies=false
|
||||||
Generated
+1023
-922
File diff suppressed because it is too large
Load Diff
+136
-141
@@ -31,7 +31,6 @@ members = [
|
|||||||
"common/client-libs/mixnet-client",
|
"common/client-libs/mixnet-client",
|
||||||
"common/client-libs/validator-client",
|
"common/client-libs/validator-client",
|
||||||
"common/commands",
|
"common/commands",
|
||||||
"common/nym-common",
|
|
||||||
"common/config",
|
"common/config",
|
||||||
"common/cosmwasm-smart-contracts/coconut-dkg",
|
"common/cosmwasm-smart-contracts/coconut-dkg",
|
||||||
"common/cosmwasm-smart-contracts/contracts-common",
|
"common/cosmwasm-smart-contracts/contracts-common",
|
||||||
@@ -41,6 +40,7 @@ members = [
|
|||||||
"common/cosmwasm-smart-contracts/group-contract",
|
"common/cosmwasm-smart-contracts/group-contract",
|
||||||
"common/cosmwasm-smart-contracts/mixnet-contract",
|
"common/cosmwasm-smart-contracts/mixnet-contract",
|
||||||
"common/cosmwasm-smart-contracts/multisig-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-performance-contract",
|
||||||
"common/cosmwasm-smart-contracts/nym-pool-contract",
|
"common/cosmwasm-smart-contracts/nym-pool-contract",
|
||||||
"common/cosmwasm-smart-contracts/vesting-contract",
|
"common/cosmwasm-smart-contracts/vesting-contract",
|
||||||
@@ -71,11 +71,15 @@ members = [
|
|||||||
"common/node-tester-utils",
|
"common/node-tester-utils",
|
||||||
"common/nonexhaustive-delayqueue",
|
"common/nonexhaustive-delayqueue",
|
||||||
"common/nym-cache",
|
"common/nym-cache",
|
||||||
|
"common/nym-common",
|
||||||
"common/nym-connection-monitor",
|
"common/nym-connection-monitor",
|
||||||
"common/nym-id",
|
"common/nym-id",
|
||||||
"common/nym-kcp",
|
"common/nym-kcp",
|
||||||
"common/nym-lp",
|
|
||||||
"common/nym-kkt",
|
"common/nym-kkt",
|
||||||
|
"common/nym-kkt-ciphersuite",
|
||||||
|
"common/nym-kkt-context",
|
||||||
|
"common/nym-lp",
|
||||||
|
"common/nym-lp-data",
|
||||||
"common/nym-metrics",
|
"common/nym-metrics",
|
||||||
"common/nym_offline_compact_ecash",
|
"common/nym_offline_compact_ecash",
|
||||||
"common/nymnoise",
|
"common/nymnoise",
|
||||||
@@ -91,9 +95,9 @@ members = [
|
|||||||
"common/nymsphinx/params",
|
"common/nymsphinx/params",
|
||||||
"common/nymsphinx/routing",
|
"common/nymsphinx/routing",
|
||||||
"common/nymsphinx/types",
|
"common/nymsphinx/types",
|
||||||
"common/nyxd-scraper-sqlite",
|
|
||||||
"common/nyxd-scraper-psql",
|
"common/nyxd-scraper-psql",
|
||||||
"common/nyxd-scraper-shared",
|
"common/nyxd-scraper-shared",
|
||||||
|
"common/nyxd-scraper-sqlite",
|
||||||
"common/pemstore",
|
"common/pemstore",
|
||||||
"common/registration",
|
"common/registration",
|
||||||
"common/serde-helpers",
|
"common/serde-helpers",
|
||||||
@@ -123,13 +127,14 @@ members = [
|
|||||||
"common/zulip-client",
|
"common/zulip-client",
|
||||||
"documentation/autodoc",
|
"documentation/autodoc",
|
||||||
"gateway",
|
"gateway",
|
||||||
|
"integration-tests",
|
||||||
"nym-api",
|
"nym-api",
|
||||||
"nym-api/nym-api-requests",
|
"nym-api/nym-api-requests",
|
||||||
"nym-authenticator-client",
|
"nym-authenticator-client",
|
||||||
"nym-browser-extension/storage",
|
|
||||||
"nym-credential-proxy/nym-credential-proxy",
|
"nym-credential-proxy/nym-credential-proxy",
|
||||||
"nym-credential-proxy/nym-credential-proxy-requests",
|
"nym-credential-proxy/nym-credential-proxy-requests",
|
||||||
"nym-data-observatory",
|
"nym-data-observatory",
|
||||||
|
"nym-gateway-probe",
|
||||||
"nym-ip-packet-client",
|
"nym-ip-packet-client",
|
||||||
"nym-network-monitor",
|
"nym-network-monitor",
|
||||||
"nym-node",
|
"nym-node",
|
||||||
@@ -141,6 +146,7 @@ members = [
|
|||||||
"nym-outfox",
|
"nym-outfox",
|
||||||
"nym-registration-client",
|
"nym-registration-client",
|
||||||
"nym-signers-monitor",
|
"nym-signers-monitor",
|
||||||
|
"nym-sqlx-pool-guard",
|
||||||
"nym-statistics-api",
|
"nym-statistics-api",
|
||||||
"nym-validator-rewarder",
|
"nym-validator-rewarder",
|
||||||
"nyx-chain-watcher",
|
"nyx-chain-watcher",
|
||||||
@@ -148,19 +154,18 @@ members = [
|
|||||||
"sdk/ffi/go",
|
"sdk/ffi/go",
|
||||||
"sdk/ffi/shared",
|
"sdk/ffi/shared",
|
||||||
"sdk/rust/nym-sdk",
|
"sdk/rust/nym-sdk",
|
||||||
"smolmix/core",
|
|
||||||
"service-providers/common",
|
"service-providers/common",
|
||||||
"service-providers/ip-packet-router",
|
"service-providers/ip-packet-router",
|
||||||
"service-providers/network-requester",
|
"service-providers/network-requester",
|
||||||
"nym-sqlx-pool-guard",
|
"smolmix/core",
|
||||||
"tools/echo-server",
|
"tools/echo-server",
|
||||||
"tools/internal/contract-state-importer/importer-cli",
|
"tools/internal/contract-state-importer/importer-cli",
|
||||||
"tools/internal/contract-state-importer/importer-contract",
|
"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/mixnet-connectivity-check",
|
||||||
# "tools/internal/sdk-version-bump",
|
# "tools/internal/sdk-version-bump",
|
||||||
"tools/internal/ssl-inject",
|
"tools/internal/ssl-inject",
|
||||||
"tools/internal/localnet-orchestrator",
|
|
||||||
"tools/internal/localnet-orchestrator/dkg-bypass-contract",
|
|
||||||
"tools/internal/validator-status-check",
|
"tools/internal/validator-status-check",
|
||||||
"tools/nym-cli",
|
"tools/nym-cli",
|
||||||
"tools/nym-id-cli",
|
"tools/nym-id-cli",
|
||||||
@@ -169,35 +174,30 @@ members = [
|
|||||||
"tools/nymvisor",
|
"tools/nymvisor",
|
||||||
"tools/ts-rs-cli",
|
"tools/ts-rs-cli",
|
||||||
"wasm/client",
|
"wasm/client",
|
||||||
# "wasm/full-nym-wasm", # If we uncomment this again, remember to also uncomment the profile settings below
|
|
||||||
"wasm/mix-fetch",
|
"wasm/mix-fetch",
|
||||||
"wasm/node-tester",
|
|
||||||
"wasm/zknym-lib",
|
"wasm/zknym-lib",
|
||||||
"nym-gateway-probe",
|
|
||||||
"integration-tests",
|
|
||||||
"common/nym-kkt-ciphersuite",
|
|
||||||
"common/nym-kkt-context",
|
|
||||||
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
|
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
|
||||||
"nym-network-monitor-v3/nym-network-monitor-agent", "nym-network-monitor-v3/nym-network-monitor-orchestrator-requests",
|
"nym-network-monitor-v3/nym-network-monitor-agent",
|
||||||
|
"nym-network-monitor-v3/nym-network-monitor-orchestrator-requests",
|
||||||
]
|
]
|
||||||
|
|
||||||
default-members = [
|
default-members = [
|
||||||
"clients/native",
|
"clients/native",
|
||||||
"clients/socks5",
|
"clients/socks5",
|
||||||
"nym-authenticator-client",
|
|
||||||
"nym-api",
|
"nym-api",
|
||||||
|
"nym-authenticator-client",
|
||||||
"nym-credential-proxy/nym-credential-proxy",
|
"nym-credential-proxy/nym-credential-proxy",
|
||||||
"nym-node",
|
"nym-node",
|
||||||
|
"nym-registration-client",
|
||||||
"nym-statistics-api",
|
"nym-statistics-api",
|
||||||
"nym-validator-rewarder",
|
"nym-validator-rewarder",
|
||||||
"nyx-chain-watcher",
|
"nyx-chain-watcher",
|
||||||
"service-providers/ip-packet-router",
|
"service-providers/ip-packet-router",
|
||||||
"service-providers/network-requester",
|
"service-providers/network-requester",
|
||||||
|
"tools/internal/localnet-orchestrator",
|
||||||
"tools/nymvisor",
|
"tools/nymvisor",
|
||||||
"nym-registration-client",
|
|
||||||
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
|
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
|
||||||
"nym-network-monitor-v3/nym-network-monitor-agent",
|
"nym-network-monitor-v3/nym-network-monitor-agent",
|
||||||
"tools/internal/localnet-orchestrator"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
exclude = ["contracts", "nym-wallet", "cpu-cycles"]
|
exclude = ["contracts", "nym-wallet", "cpu-cycles"]
|
||||||
@@ -211,7 +211,7 @@ edition = "2024"
|
|||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
rust-version = "1.87.0"
|
rust-version = "1.87.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
version = "1.20.4"
|
version = "1.21.0"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
addr = "0.15.6"
|
addr = "0.15.6"
|
||||||
@@ -225,16 +225,16 @@ anyhow = "1.0.98"
|
|||||||
arc-swap = "1.7.1"
|
arc-swap = "1.7.1"
|
||||||
argon2 = "0.5.0"
|
argon2 = "0.5.0"
|
||||||
async-trait = "0.1.88"
|
async-trait = "0.1.88"
|
||||||
axum = "0.7.5"
|
axum = "0.8.9"
|
||||||
axum-client-ip = "0.6.1"
|
axum-client-ip = "1.3.1"
|
||||||
axum-extra = "0.9.4"
|
axum-extra = "0.12.6"
|
||||||
axum-test = "16.2.0"
|
axum-test = "20.0.0"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
base85rs = "0.1.3"
|
base85rs = "0.1.3"
|
||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
bip39 = { version = "2.0.0", features = ["zeroize"] }
|
bip39 = { version = "2.0.0", features = ["zeroize"] }
|
||||||
bitvec = "1.0.0"
|
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"
|
bloomfilter = "3.0.1"
|
||||||
bs58 = "0.5.1"
|
bs58 = "0.5.1"
|
||||||
bytecodec = "0.4.15"
|
bytecodec = "0.4.15"
|
||||||
@@ -253,7 +253,7 @@ clap_complete_fig = "4.5"
|
|||||||
colored = "2.2"
|
colored = "2.2"
|
||||||
comfy-table = "7.1.4"
|
comfy-table = "7.1.4"
|
||||||
console = "0.16.0"
|
console = "0.16.0"
|
||||||
console-subscriber = "0.4.1"
|
console-subscriber = "0.5.0"
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
const-str = "0.5.6"
|
const-str = "0.5.6"
|
||||||
const_format = "0.2.34"
|
const_format = "0.2.34"
|
||||||
@@ -330,7 +330,7 @@ pnet_packet = "0.35.0"
|
|||||||
publicsuffix = "2.3.0"
|
publicsuffix = "2.3.0"
|
||||||
proc_pidinfo = "0.1.3"
|
proc_pidinfo = "0.1.3"
|
||||||
quote = "1"
|
quote = "1"
|
||||||
rand = "0.8.5"
|
rand = "0.8.6"
|
||||||
rand09 = { package = "rand", version = "=0.9.2" }
|
rand09 = { package = "rand", version = "=0.9.2" }
|
||||||
rand_chacha = "0.3"
|
rand_chacha = "0.3"
|
||||||
rand_chacha09 = { package = "rand_chacha", version = "=0.9.0" }
|
rand_chacha09 = { package = "rand_chacha", version = "=0.9.0" }
|
||||||
@@ -354,7 +354,6 @@ serde_yaml = "0.9.25"
|
|||||||
serde_plain = "1.0.2"
|
serde_plain = "1.0.2"
|
||||||
sha2 = "0.10.3"
|
sha2 = "0.10.3"
|
||||||
si-scale = "0.2.3"
|
si-scale = "0.2.3"
|
||||||
smolmix = { version = "0.0.1", path = "smolmix/core" }
|
|
||||||
smoltcp = "0.12"
|
smoltcp = "0.12"
|
||||||
snow = "0.9.6"
|
snow = "0.9.6"
|
||||||
sphinx-packet = "=0.6.0"
|
sphinx-packet = "=0.6.0"
|
||||||
@@ -365,7 +364,7 @@ subtle-encoding = "0.5"
|
|||||||
syn = "2"
|
syn = "2"
|
||||||
sysinfo = "0.37.0"
|
sysinfo = "0.37.0"
|
||||||
tap = "1.0.1"
|
tap = "1.0.1"
|
||||||
tar = "0.4.44"
|
tar = "0.4.45"
|
||||||
test-with = { version = "0.15.4", default-features = false }
|
test-with = { version = "0.15.4", default-features = false }
|
||||||
tempfile = "3.20"
|
tempfile = "3.20"
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
@@ -396,7 +395,7 @@ uniffi = "0.29.2"
|
|||||||
uniffi_build = "0.29.0"
|
uniffi_build = "0.29.0"
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
utoipa = "5.2"
|
utoipa = "5.2"
|
||||||
utoipa-swagger-ui = "8.1"
|
utoipa-swagger-ui = "9.0.2"
|
||||||
utoipauto = "0.2"
|
utoipauto = "0.2"
|
||||||
uuid = "1.19.0"
|
uuid = "1.19.0"
|
||||||
vergen = { version = "=8.3.1", default-features = false }
|
vergen = { version = "=8.3.1", default-features = false }
|
||||||
@@ -423,111 +422,115 @@ libcrux-sha3 = "0.0.8"
|
|||||||
libcrux-traits = "0.0.6"
|
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
|
# 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-api-requests = { version = "1.21.0", path = "nym-api/nym-api-requests" }
|
||||||
nym-authenticator-requests = { version = "1.20.4", path = "common/authenticator-requests" }
|
nym-authenticator-requests = { version = "1.21.0", path = "common/authenticator-requests" }
|
||||||
nym-async-file-watcher = { version = "1.20.4", path = "common/async-file-watcher" }
|
nym-async-file-watcher = { version = "1.21.0", path = "common/async-file-watcher" }
|
||||||
nym-authenticator-client = { version = "1.20.4", path = "nym-authenticator-client" }
|
nym-authenticator-client = { version = "1.21.0", path = "nym-authenticator-client" }
|
||||||
nym-bandwidth-controller = { version = "1.20.4", path = "common/bandwidth-controller" }
|
nym-bandwidth-controller = { version = "1.21.0", path = "common/bandwidth-controller" }
|
||||||
nym-bin-common = { version = "1.20.4", path = "common/bin-common" }
|
nym-bin-common = { version = "1.21.0", path = "common/bin-common" }
|
||||||
nym-cache = { version = "1.20.4", path = "common/nym-cache" }
|
nym-cache = { version = "1.21.0", path = "common/nym-cache" }
|
||||||
nym-client-core = { version = "1.20.4", path = "common/client-core", default-features = false }
|
nym-client-core = { version = "1.21.0", 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-config-types = { version = "1.21.0", path = "common/client-core/config-types" }
|
||||||
nym-client-core-gateways-storage = { version = "1.20.4", path = "common/client-core/gateways-storage" }
|
nym-client-core-gateways-storage = { version = "1.21.0", path = "common/client-core/gateways-storage" }
|
||||||
nym-client-core-surb-storage = { version = "1.20.4", path = "common/client-core/surb-storage" }
|
nym-client-core-surb-storage = { version = "1.21.0", path = "common/client-core/surb-storage" }
|
||||||
nym-client-websocket-requests = { version = "1.20.4", path = "clients/native/websocket-requests" }
|
nym-client-websocket-requests = { version = "1.21.0", path = "clients/native/websocket-requests" }
|
||||||
nym-common = { version = "1.20.4", path = "common/nym-common" }
|
nym-common = { version = "1.21.0", path = "common/nym-common" }
|
||||||
nym-compact-ecash = { version = "1.20.4", path = "common/nym_offline_compact_ecash" }
|
nym-compact-ecash = { version = "1.21.0", path = "common/nym_offline_compact_ecash" }
|
||||||
nym-config = { version = "1.20.4", path = "common/config" }
|
nym-config = { version = "1.21.0", path = "common/config" }
|
||||||
nym-contracts-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
nym-contracts-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
||||||
nym-coconut-dkg-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
nym-coconut-dkg-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||||
nym-credential-storage = { version = "1.20.4", path = "common/credential-storage" }
|
nym-credential-storage = { version = "1.21.0", path = "common/credential-storage" }
|
||||||
nym-credential-utils = { version = "1.20.4", path = "common/credential-utils" }
|
nym-credential-utils = { version = "1.21.0", path = "common/credential-utils" }
|
||||||
nym-credential-proxy-lib = { version = "1.20.4", path = "common/credential-proxy" }
|
nym-credential-proxy-lib = { version = "1.21.0", path = "common/credential-proxy" }
|
||||||
nym-credentials = { version = "1.20.4", path = "common/credentials", default-features = false }
|
nym-credentials = { version = "1.21.0", path = "common/credentials", default-features = false }
|
||||||
nym-credentials-interface = { version = "1.20.4", path = "common/credentials-interface" }
|
nym-credentials-interface = { version = "1.21.0", 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-proxy-requests = { version = "1.21.0", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
|
||||||
nym-credential-verification = { version = "1.20.4", path = "common/credential-verification" }
|
nym-credential-verification = { version = "1.21.0", path = "common/credential-verification" }
|
||||||
nym-crypto = { version = "1.20.4", path = "common/crypto", default-features = false }
|
nym-crypto = { version = "1.21.0", path = "common/crypto", default-features = false }
|
||||||
nym-dkg = { version = "1.20.4", path = "common/dkg" }
|
nym-dkg = { version = "1.21.0", path = "common/dkg" }
|
||||||
nym-ecash-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
nym-ecash-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
||||||
nym-ecash-signer-check = { version = "1.20.4", path = "common/ecash-signer-check" }
|
nym-ecash-signer-check = { version = "1.21.0", path = "common/ecash-signer-check" }
|
||||||
nym-ecash-signer-check-types = { version = "1.20.4", path = "common/ecash-signer-check-types" }
|
nym-ecash-signer-check-types = { version = "1.21.0", path = "common/ecash-signer-check-types" }
|
||||||
nym-ecash-time = { version = "1.20.4", path = "common/ecash-time" }
|
nym-ecash-time = { version = "1.21.0", path = "common/ecash-time" }
|
||||||
nym-exit-policy = { version = "1.20.4", path = "common/exit-policy" }
|
nym-exit-policy = { version = "1.21.0", path = "common/exit-policy" }
|
||||||
nym-ffi-shared = { version = "1.20.4", path = "sdk/ffi/shared" }
|
nym-ffi-shared = { version = "1.21.0", path = "sdk/ffi/shared" }
|
||||||
nym-gateway-client = { version = "1.20.4", path = "common/client-libs/gateway-client", default-features = false }
|
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-probe = { version = "1.18.0", path = "nym-gateway-probe" }
|
||||||
nym-gateway-requests = { version = "1.20.4", path = "common/gateway-requests" }
|
nym-gateway-requests = { version = "1.21.0", path = "common/gateway-requests" }
|
||||||
nym-gateway-storage = { version = "1.20.4", path = "common/gateway-storage" }
|
nym-gateway-storage = { version = "1.21.0", path = "common/gateway-storage" }
|
||||||
nym-gateway-stats-storage = { version = "1.20.4", path = "common/gateway-stats-storage" }
|
nym-gateway-stats-storage = { version = "1.21.0", path = "common/gateway-stats-storage" }
|
||||||
nym-group-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/group-contract" }
|
nym-group-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/group-contract" }
|
||||||
nym-http-api-client = { version = "1.20.4", path = "common/http-api-client" }
|
nym-http-api-client = { version = "1.21.0", path = "common/http-api-client" }
|
||||||
nym-http-api-client-macro = { version = "1.20.4", path = "common/http-api-client-macro" }
|
nym-http-api-client-macro = { version = "1.21.0", path = "common/http-api-client-macro" }
|
||||||
nym-http-api-common = { version = "1.20.4", path = "common/http-api-common", default-features = false }
|
nym-http-api-common = { version = "1.21.0", path = "common/http-api-common", default-features = false }
|
||||||
nym-id = { version = "1.20.4", path = "common/nym-id" }
|
nym-id = { version = "1.21.0", path = "common/nym-id" }
|
||||||
nym-ip-packet-client = { version = "1.20.4", path = "nym-ip-packet-client" }
|
nym-ip-packet-client = { version = "1.21.0", path = "nym-ip-packet-client" }
|
||||||
nym-ip-packet-requests = { version = "1.20.4", path = "common/ip-packet-requests" }
|
nym-ip-packet-requests = { version = "1.21.0", path = "common/ip-packet-requests" }
|
||||||
nym-lp = { version = "1.20.4", path = "common/nym-lp" }
|
nym-lp = { version = "1.21.0", path = "common/nym-lp" }
|
||||||
nym-kkt = { version = "0.1.0", path = "common/nym-kkt" }
|
nym-lp-data = { version = "1.21.0", path = "common/nym-lp-data" }
|
||||||
nym-kkt-ciphersuite = { version = "1.20.4", path = "common/nym-kkt-ciphersuite" }
|
nym-kkt = { version = "1.21.0", path = "common/nym-kkt" }
|
||||||
nym-kkt-context = { version = "1.20.4", path = "common/nym-kkt-context" }
|
nym-kkt-ciphersuite = { version = "1.21.0", path = "common/nym-kkt-ciphersuite" }
|
||||||
nym-metrics = { version = "1.20.4", path = "common/nym-metrics" }
|
nym-kkt-context = { version = "1.21.0", path = "common/nym-kkt-context" }
|
||||||
nym-mixnet-client = { version = "1.20.4", path = "common/client-libs/mixnet-client" }
|
nym-metrics = { version = "1.21.0", path = "common/nym-metrics" }
|
||||||
nym-mixnet-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
nym-mixnet-client = { version = "1.21.0", path = "common/client-libs/mixnet-client" }
|
||||||
nym-multisig-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
nym-mixnet-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||||
nym-network-defaults = { version = "1.20.4", path = "common/network-defaults" }
|
nym-multisig-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
||||||
nym-node-tester-utils = { version = "1.20.4", path = "common/node-tester-utils" }
|
nym-network-defaults = { version = "1.21.0", path = "common/network-defaults" }
|
||||||
nym-noise = { version = "1.20.4", path = "common/nymnoise" }
|
nym-node-tester-utils = { version = "1.21.0", path = "common/node-tester-utils" }
|
||||||
nym-noise-keys = { version = "1.20.4", path = "common/nymnoise/keys" }
|
nym-noise = { version = "1.21.0", path = "common/nymnoise" }
|
||||||
nym-nonexhaustive-delayqueue = { version = "1.20.4", path = "common/nonexhaustive-delayqueue" }
|
nym-noise-keys = { version = "1.21.0", path = "common/nymnoise/keys" }
|
||||||
nym-node-requests = { version = "1.20.4", path = "nym-node/nym-node-requests", default-features = false }
|
nym-nonexhaustive-delayqueue = { version = "1.21.0", path = "common/nonexhaustive-delayqueue" }
|
||||||
nym-node-metrics = { version = "1.20.4", path = "nym-node/nym-node-metrics" }
|
nym-node-requests = { version = "1.21.0", path = "nym-node/nym-node-requests", default-features = false }
|
||||||
nym-ordered-buffer = { version = "1.20.4", path = "common/socks5/ordered-buffer" }
|
nym-node-metrics = { version = "1.21.0", path = "nym-node/nym-node-metrics" }
|
||||||
nym-outfox = { version = "1.20.4", path = "nym-outfox" }
|
nym-node-families-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/node-families-contract" }
|
||||||
nym-registration-common = { version = "1.20.4", path = "common/registration" }
|
nym-ordered-buffer = { version = "1.21.0", path = "common/socks5/ordered-buffer" }
|
||||||
nym-pemstore = { version = "1.20.4", path = "common/pemstore" }
|
nym-outfox = { version = "1.21.0", path = "nym-outfox" }
|
||||||
nym-performance-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
nym-registration-common = { version = "1.21.0", path = "common/registration" }
|
||||||
nym-sdk = { version = "1.20.4", path = "sdk/rust/nym-sdk" }
|
nym-pemstore = { version = "1.21.0", path = "common/pemstore" }
|
||||||
nym-serde-helpers = { version = "1.20.4", path = "common/serde-helpers" }
|
nym-performance-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||||
nym-service-providers-common = { version = "1.20.4", path = "service-providers/common" }
|
nym-sdk = { version = "1.21.0", path = "sdk/rust/nym-sdk" }
|
||||||
nym-service-provider-requests-common = { version = "1.20.4", path = "common/service-provider-requests-common" }
|
nym-serde-helpers = { version = "1.21.0", path = "common/serde-helpers" }
|
||||||
nym-socks5-client-core = { version = "1.20.4", path = "common/socks5-client-core" }
|
nym-service-providers-common = { version = "1.21.0", path = "service-providers/common" }
|
||||||
nym-socks5-proxy-helpers = { version = "1.20.4", path = "common/socks5/proxy-helpers" }
|
nym-service-provider-requests-common = { version = "1.21.0", path = "common/service-provider-requests-common" }
|
||||||
nym-socks5-requests = { version = "1.20.4", path = "common/socks5/requests" }
|
nym-socks5-client-core = { version = "1.21.0", path = "common/socks5-client-core" }
|
||||||
nym-sphinx = { version = "1.20.4", path = "common/nymsphinx" }
|
nym-socks5-proxy-helpers = { version = "1.21.0", path = "common/socks5/proxy-helpers" }
|
||||||
nym-sphinx-acknowledgements = { version = "1.20.4", path = "common/nymsphinx/acknowledgements" }
|
nym-socks5-requests = { version = "1.21.0", path = "common/socks5/requests" }
|
||||||
nym-sphinx-addressing = { version = "1.20.4", path = "common/nymsphinx/addressing" }
|
nym-sphinx = { version = "1.21.0", path = "common/nymsphinx" }
|
||||||
nym-sphinx-anonymous-replies = { version = "1.20.4", path = "common/nymsphinx/anonymous-replies" }
|
nym-sphinx-acknowledgements = { version = "1.21.0", path = "common/nymsphinx/acknowledgements" }
|
||||||
nym-sphinx-chunking = { version = "1.20.4", path = "common/nymsphinx/chunking" }
|
nym-sphinx-addressing = { version = "1.21.0", path = "common/nymsphinx/addressing" }
|
||||||
nym-sphinx-cover = { version = "1.20.4", path = "common/nymsphinx/cover" }
|
nym-sphinx-anonymous-replies = { version = "1.21.0", path = "common/nymsphinx/anonymous-replies" }
|
||||||
nym-sphinx-forwarding = { version = "1.20.4", path = "common/nymsphinx/forwarding" }
|
nym-sphinx-chunking = { version = "1.21.0", path = "common/nymsphinx/chunking" }
|
||||||
nym-sphinx-framing = { version = "1.20.4", path = "common/nymsphinx/framing" }
|
nym-sphinx-cover = { version = "1.21.0", path = "common/nymsphinx/cover" }
|
||||||
nym-sphinx-params = { version = "1.20.4", path = "common/nymsphinx/params" }
|
nym-sphinx-forwarding = { version = "1.21.0", path = "common/nymsphinx/forwarding" }
|
||||||
nym-sphinx-routing = { version = "1.20.4", path = "common/nymsphinx/routing" }
|
nym-sphinx-framing = { version = "1.21.0", path = "common/nymsphinx/framing" }
|
||||||
nym-sphinx-types = { version = "1.20.4", path = "common/nymsphinx/types" }
|
nym-sphinx-params = { version = "1.21.0", path = "common/nymsphinx/params" }
|
||||||
nym-statistics-common = { version = "1.20.4", path = "common/statistics" }
|
nym-sphinx-routing = { version = "1.21.0", path = "common/nymsphinx/routing" }
|
||||||
nym-store-cipher = { version = "1.20.4", path = "common/store-cipher" }
|
nym-sphinx-types = { version = "1.21.0", path = "common/nymsphinx/types" }
|
||||||
nym-task = { version = "1.20.4", path = "common/task" }
|
nym-statistics-common = { version = "1.21.0", path = "common/statistics" }
|
||||||
nym-tun = { version = "1.20.4", path = "common/tun" }
|
nym-store-cipher = { version = "1.21.0", path = "common/store-cipher" }
|
||||||
nym-test-utils = { version = "1.20.4", path = "common/test-utils" }
|
nym-task = { version = "1.21.0", path = "common/task" }
|
||||||
nym-ticketbooks-merkle = { version = "1.20.4", path = "common/ticketbooks-merkle" }
|
nym-tun = { version = "1.21.0", path = "common/tun" }
|
||||||
nym-topology = { version = "1.20.4", path = "common/topology" }
|
nym-test-utils = { version = "1.21.0", path = "common/test-utils" }
|
||||||
nym-types = { version = "1.20.4", path = "common/types" }
|
nym-ticketbooks-merkle = { version = "1.21.0", path = "common/ticketbooks-merkle" }
|
||||||
nym-upgrade-mode-check = { version = "1.20.4", path = "common/upgrade-mode-check" }
|
nym-topology = { version = "1.21.0", path = "common/topology" }
|
||||||
nym-validator-client = { version = "1.20.4", path = "common/client-libs/validator-client", default-features = false }
|
nym-types = { version = "1.21.0", path = "common/types" }
|
||||||
nym-vesting-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
nym-upgrade-mode-check = { version = "1.21.0", path = "common/upgrade-mode-check" }
|
||||||
nym-network-monitors-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/network-monitors-contract" }
|
nym-validator-client = { version = "1.21.0", path = "common/client-libs/validator-client", default-features = false }
|
||||||
nym-verloc = { version = "1.20.4", path = "common/verloc" }
|
nym-vesting-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
||||||
nym-wireguard = { version = "1.20.4", path = "common/wireguard" }
|
nym-network-monitors-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/network-monitors-contract" }
|
||||||
nym-wireguard-types = { version = "1.20.4", path = "common/wireguard-types" }
|
nym-verloc = { version = "1.21.0", path = "common/verloc" }
|
||||||
nym-wireguard-private-metadata-shared = { version = "1.20.4", path = "common/wireguard-private-metadata/shared" }
|
nym-wireguard = { version = "1.21.0", path = "common/wireguard" }
|
||||||
nym-wireguard-private-metadata-client = { version = "1.20.4", path = "common/wireguard-private-metadata/client" }
|
nym-wireguard-types = { version = "1.21.0", path = "common/wireguard-types" }
|
||||||
nym-wireguard-private-metadata-server = { version = "1.20.4", path = "common/wireguard-private-metadata/server" }
|
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-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-client-core = { version = "1.21.0", path = "common/wasm/client-core" }
|
||||||
nym-wasm-storage = { version = "1.20.4", path = "common/wasm/storage" }
|
nym-wasm-storage = { version = "1.21.0", path = "common/wasm/storage" }
|
||||||
nym-wasm-utils = { version = "1.20.4", path = "common/wasm/utils", default-features = false }
|
nym-wasm-utils = { version = "1.21.0", path = "common/wasm/utils", default-features = false }
|
||||||
nyxd-scraper-shared = { version = "1.20.4", path = "common/nyxd-scraper-shared" }
|
nyxd-scraper-shared = { version = "1.21.0", path = "common/nyxd-scraper-shared" }
|
||||||
|
|
||||||
|
smolmix = { version = "1.21.0", path = "smolmix/core" }
|
||||||
|
|
||||||
# coconut/DKG related
|
# coconut/DKG related
|
||||||
# unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork
|
# unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork
|
||||||
@@ -594,16 +597,7 @@ opt-level = 3
|
|||||||
# lto = true
|
# lto = true
|
||||||
opt-level = 'z'
|
opt-level = 'z'
|
||||||
|
|
||||||
[profile.release.package.nym-node-tester-wasm]
|
|
||||||
# lto = true
|
|
||||||
opt-level = 'z'
|
|
||||||
|
|
||||||
# Commented out since the crate is also commented out from the inclusion in the
|
|
||||||
# workspace above. We should uncomment this if we re-include it in the
|
|
||||||
# workspace
|
|
||||||
#[profile.release.package.nym-wasm-sdk]
|
|
||||||
## lto = true
|
|
||||||
#opt-level = 'z'
|
|
||||||
|
|
||||||
[profile.release.package.mix-fetch-wasm]
|
[profile.release.package.mix-fetch-wasm]
|
||||||
# lto = true
|
# lto = true
|
||||||
@@ -626,3 +620,4 @@ exit = "deny"
|
|||||||
panic = "deny"
|
panic = "deny"
|
||||||
unimplemented = "deny"
|
unimplemented = "deny"
|
||||||
unreachable = "deny"
|
unreachable = "deny"
|
||||||
|
|
||||||
|
|||||||
@@ -104,23 +104,19 @@ $(eval $(call add_cargo_workspace,wallet,nym-wallet))
|
|||||||
sdk-wasm: sdk-wasm-build sdk-wasm-test sdk-wasm-lint
|
sdk-wasm: sdk-wasm-build sdk-wasm-test sdk-wasm-lint
|
||||||
|
|
||||||
sdk-wasm-build:
|
sdk-wasm-build:
|
||||||
# $(MAKE) -C nym-browser-extension/storage wasm-pack
|
|
||||||
$(MAKE) -C wasm/client
|
$(MAKE) -C wasm/client
|
||||||
$(MAKE) -C wasm/node-tester
|
|
||||||
$(MAKE) -C wasm/mix-fetch
|
$(MAKE) -C wasm/mix-fetch
|
||||||
# $(MAKE) -C wasm/zknym-lib
|
# $(MAKE) -C wasm/zknym-lib
|
||||||
# $(MAKE) -C wasm/full-nym-wasm
|
|
||||||
|
|
||||||
# run this from npm/yarn to ensure tools are in the path, e.g. yarn build:sdk from root of repo
|
# run this from npm/yarn to ensure tools are in the path, e.g. yarn build:sdk from root of repo
|
||||||
sdk-typescript-build:
|
sdk-typescript-build:
|
||||||
npx lerna run --scope @nymproject/sdk build --stream
|
npx lerna run --scope @nymproject/sdk build --stream
|
||||||
npx lerna run --scope @nymproject/mix-fetch build --stream
|
npx lerna run --scope @nymproject/mix-fetch build --stream
|
||||||
npx lerna run --scope @nymproject/node-tester build --stream
|
pnpm --pwd sdk/typescript/codegen/contract-clients build
|
||||||
yarn --cwd sdk/typescript/codegen/contract-clients build
|
|
||||||
|
|
||||||
# NOTE: These targets are part of the main workspace (but not as wasm32-unknown-unknown)
|
# NOTE: These targets are part of the main workspace (but not as wasm32-unknown-unknown)
|
||||||
# WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib
|
|
||||||
WASM_CRATES = nym-client-wasm nym-node-tester-wasm
|
WASM_CRATES = nym-client-wasm
|
||||||
|
|
||||||
sdk-wasm-test:
|
sdk-wasm-test:
|
||||||
#cargo test $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
|
#cargo test $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
|
||||||
@@ -223,7 +219,7 @@ build-nym-cli:
|
|||||||
|
|
||||||
generate-typescript:
|
generate-typescript:
|
||||||
cd tools/ts-rs-cli && cargo run && cd ../..
|
cd tools/ts-rs-cli && cargo run && cd ../..
|
||||||
yarn types:lint:fix
|
pnpm types:lint:fix
|
||||||
|
|
||||||
# Run the integration tests for public nym-api endpoints
|
# Run the integration tests for public nym-api endpoints
|
||||||
run-api-tests:
|
run-api-tests:
|
||||||
|
|||||||
@@ -74,9 +74,9 @@ Nym Node Operators and Validators Terms and Conditions can be found [here](https
|
|||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn install
|
pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn build
|
pnpm build
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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(
|
fn prepare_reconstructed_binary(
|
||||||
reconstructed_messages: Vec<ReconstructedMessage>,
|
reconstructed_messages: Vec<ReconstructedMessage>,
|
||||||
) -> Vec<Result<WsMessage, WsError>> {
|
) -> Vec<Result<WsMessage, WsError>> {
|
||||||
|
#[allow(clippy::result_large_err)] // TODO : remove this once tungstenite is updated
|
||||||
reconstructed_messages
|
reconstructed_messages
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(ServerResponse::Received)
|
.map(ServerResponse::Received)
|
||||||
@@ -484,6 +485,7 @@ fn prepare_reconstructed_binary(
|
|||||||
fn prepare_reconstructed_text(
|
fn prepare_reconstructed_text(
|
||||||
reconstructed_messages: Vec<ReconstructedMessage>,
|
reconstructed_messages: Vec<ReconstructedMessage>,
|
||||||
) -> Vec<Result<WsMessage, WsError>> {
|
) -> Vec<Result<WsMessage, WsError>> {
|
||||||
|
#[allow(clippy::result_large_err)] // TODO : remove this once tungstenite is updated
|
||||||
reconstructed_messages
|
reconstructed_messages
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(ServerResponse::Received)
|
.map(ServerResponse::Received)
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ pub trait BandwidthTicketProvider: Send + Sync {
|
|||||||
) -> Result<PreparedCredential, BandwidthControllerError>;
|
) -> Result<PreparedCredential, BandwidthControllerError>;
|
||||||
|
|
||||||
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError>;
|
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError>;
|
||||||
|
|
||||||
|
async fn close(&self) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||||
@@ -56,6 +58,10 @@ where
|
|||||||
.map_err(|_| BandwidthControllerError::MalformedUpgradeModeToken)?;
|
.map_err(|_| BandwidthControllerError::MalformedUpgradeModeToken)?;
|
||||||
Ok(Some(token))
|
Ok(Some(token))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn close(&self) {
|
||||||
|
self.storage.close().await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
#[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> {
|
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError> {
|
||||||
(**self).get_upgrade_mode_token().await
|
(**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 encryption_keys = init_res.client_keys.encryption_keypair();
|
||||||
let identity_keys = init_res.client_keys.identity_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,
|
// the components are started in very specific order. Unless you know what you are doing,
|
||||||
// do not change that.
|
// do not change that.
|
||||||
let bandwidth_controller = self
|
let bandwidth_controller = self
|
||||||
|
|||||||
@@ -11,11 +11,17 @@ use nym_bandwidth_controller::BandwidthController;
|
|||||||
use nym_client_core_gateways_storage::OnDiskGatewaysDetails;
|
use nym_client_core_gateways_storage::OnDiskGatewaysDetails;
|
||||||
use nym_credential_storage::storage::Storage as CredentialStorage;
|
use nym_credential_storage::storage::Storage as CredentialStorage;
|
||||||
use nym_validator_client::{QueryHttpRpcNyxdClient, nyxd};
|
use nym_validator_client::{QueryHttpRpcNyxdClient, nyxd};
|
||||||
use std::{io, path::Path};
|
use std::{io, path::Path, time::Duration};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use tracing::{error, info, trace};
|
use tracing::{error, info, trace};
|
||||||
use url::Url;
|
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>>(
|
async fn setup_fresh_backend<P: AsRef<Path>>(
|
||||||
db_path: P,
|
db_path: P,
|
||||||
surb_config: &config::ReplySurbs,
|
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);
|
let renamed = db_path.with_extension(new_extension);
|
||||||
|
|
||||||
tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| {
|
// On Windows, sqlx may release its OS file handles asynchronously after
|
||||||
error!(
|
// pool.close() returns, briefly keeping the file locked
|
||||||
"Failed to rename corrupt database file: {} to {}",
|
// (ERROR_SHARING_VIOLATION, os error 32). Retry with a short delay to
|
||||||
db_path.display(),
|
// give the OS time to flush the remaining handles.
|
||||||
renamed.display()
|
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>>(
|
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 pending_acks = Vec::with_capacity(fragments.len());
|
||||||
let mut to_forward: HashMap<_, Vec<_>> = HashMap::new();
|
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 lane = raw.0;
|
||||||
let FragmentWithMaxRetransmissions {
|
let FragmentWithMaxRetransmissions {
|
||||||
fragment,
|
fragment,
|
||||||
@@ -670,7 +670,7 @@ where
|
|||||||
|
|
||||||
Ok(fragments
|
Ok(fragments
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.zip(reply_surbs.into_iter())
|
.zip(reply_surbs)
|
||||||
.map(|(fragment, reply_surb)| {
|
.map(|(fragment, reply_surb)| {
|
||||||
// unwrap here is fine as we know we have a valid topology
|
// unwrap here is fine as we know we have a valid topology
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
|
|||||||
@@ -337,6 +337,8 @@ impl ReplyStorageBackend for Backend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn stop_storage_session(self) -> Result<(), Self::StorageError> {
|
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");
|
debug!("Started PersistentReplyStorage");
|
||||||
if let Err(err) = self.backend.start_storage_session().await {
|
if let Err(err) = self.backend.start_storage_session().await {
|
||||||
error!("failed to start the storage session - {err}");
|
error!("failed to start the storage session - {err}");
|
||||||
|
self.backend.stop_storage_session().await.ok();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,10 +56,11 @@ where
|
|||||||
|
|
||||||
info!("PersistentReplyStorage is flushing all reply-related data to underlying storage");
|
info!("PersistentReplyStorage is flushing all reply-related data to underlying storage");
|
||||||
if let Err(err) = self.backend.flush_surb_storage(&mem_state).await {
|
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}")
|
error!("failed to flush our reply-related data to the persistent storage: {err}");
|
||||||
} else {
|
self.backend.stop_storage_session().await.ok();
|
||||||
info!("Data flush is complete")
|
return;
|
||||||
}
|
}
|
||||||
|
info!("Data flush is complete");
|
||||||
|
|
||||||
if let Err(err) = self.backend.stop_storage_session().await {
|
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")
|
error!("failed to properly stop the storage session - {err}. We might not be able to smoothly restore it")
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ nym-multisig-contract-common = { workspace = true }
|
|||||||
nym-group-contract-common = { workspace = true }
|
nym-group-contract-common = { workspace = true }
|
||||||
nym-performance-contract-common = { workspace = true }
|
nym-performance-contract-common = { workspace = true }
|
||||||
nym-network-monitors-contract-common = { workspace = true }
|
nym-network-monitors-contract-common = { workspace = true }
|
||||||
|
nym-node-families-contract-common = { workspace = true }
|
||||||
nym-serde-helpers = { workspace = true, features = ["hex", "base64"] }
|
nym-serde-helpers = { workspace = true, features = ["hex", "base64"] }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use nym_api_requests::ecash::VerificationKeyResponse;
|
|||||||
use nym_api_requests::models::network_monitor::{
|
use nym_api_requests::models::network_monitor::{
|
||||||
KnownNetworkMonitorResponse, StressTestBatchSubmission,
|
KnownNetworkMonitorResponse, StressTestBatchSubmission,
|
||||||
};
|
};
|
||||||
|
use nym_api_requests::models::node_families::NodeFamily;
|
||||||
use nym_api_requests::models::{
|
use nym_api_requests::models::{
|
||||||
AnnotationResponseV1, ApiHealthResponse, BinaryBuildInformationOwned,
|
AnnotationResponseV1, ApiHealthResponse, BinaryBuildInformationOwned,
|
||||||
ChainBlocksStatusResponse, ChainStatusResponse, KeyRotationInfoResponse,
|
ChainBlocksStatusResponse, ChainStatusResponse, KeyRotationInfoResponse,
|
||||||
@@ -393,6 +394,45 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
Ok(bonds)
|
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]
|
#[deprecated]
|
||||||
#[tracing::instrument(level = "debug", skip_all)]
|
#[tracing::instrument(level = "debug", skip_all)]
|
||||||
async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNodeV1>, NymAPIError> {
|
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 NYM_NODES_ROUTES: &str = "nym-nodes";
|
||||||
|
pub const NODE_FAMILIES_ROUTES: &str = "node-families";
|
||||||
|
|
||||||
pub use nym_nodes::*;
|
pub use nym_nodes::*;
|
||||||
pub mod nym_nodes {
|
pub mod nym_nodes {
|
||||||
|
|||||||
@@ -867,6 +867,10 @@ mod tests {
|
|||||||
MixnetExecuteMsg::TestingResolveAllPendingEvents { .. } => {
|
MixnetExecuteMsg::TestingResolveAllPendingEvents { .. } => {
|
||||||
client.testing_resolve_all_pending_events(None).ignore()
|
client.testing_resolve_all_pending_events(None).ignore()
|
||||||
}
|
}
|
||||||
|
// not expected to be exposed by the client
|
||||||
|
ExecuteMsg::AdminMigrateVestedMixNode { .. }
|
||||||
|
| ExecuteMsg::AdminMigrateVestedDelegation { .. }
|
||||||
|
| ExecuteMsg::AdminBatchMigrateVestedDelegations { .. } => ().ignore(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub mod group_query_client;
|
|||||||
pub mod mixnet_query_client;
|
pub mod mixnet_query_client;
|
||||||
pub mod multisig_query_client;
|
pub mod multisig_query_client;
|
||||||
pub mod network_monitors_query_client;
|
pub mod network_monitors_query_client;
|
||||||
|
pub mod node_families_query_client;
|
||||||
pub mod performance_query_client;
|
pub mod performance_query_client;
|
||||||
pub mod vesting_query_client;
|
pub mod vesting_query_client;
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ pub mod group_signing_client;
|
|||||||
pub mod mixnet_signing_client;
|
pub mod mixnet_signing_client;
|
||||||
pub mod multisig_signing_client;
|
pub mod multisig_signing_client;
|
||||||
pub mod network_monitors_signing_client;
|
pub mod network_monitors_signing_client;
|
||||||
|
pub mod node_families_signing_client;
|
||||||
pub mod performance_signing_client;
|
pub mod performance_signing_client;
|
||||||
pub mod vesting_signing_client;
|
pub mod vesting_signing_client;
|
||||||
|
|
||||||
@@ -36,6 +38,7 @@ pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient};
|
|||||||
pub use network_monitors_query_client::{
|
pub use network_monitors_query_client::{
|
||||||
NetworkMonitorsQueryClient, PagedNetworkMonitorsQueryClient,
|
NetworkMonitorsQueryClient, PagedNetworkMonitorsQueryClient,
|
||||||
};
|
};
|
||||||
|
pub use node_families_query_client::{NodeFamiliesQueryClient, PagedNodeFamiliesQueryClient};
|
||||||
pub use performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient};
|
pub use performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient};
|
||||||
pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient};
|
pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient};
|
||||||
|
|
||||||
@@ -46,6 +49,7 @@ pub use group_signing_client::GroupSigningClient;
|
|||||||
pub use mixnet_signing_client::MixnetSigningClient;
|
pub use mixnet_signing_client::MixnetSigningClient;
|
||||||
pub use multisig_signing_client::MultisigSigningClient;
|
pub use multisig_signing_client::MultisigSigningClient;
|
||||||
pub use network_monitors_signing_client::NetworkMonitorsSigningClient;
|
pub use network_monitors_signing_client::NetworkMonitorsSigningClient;
|
||||||
|
pub use node_families_signing_client::NodeFamiliesSigningClient;
|
||||||
pub use performance_signing_client::PerformanceSigningClient;
|
pub use performance_signing_client::PerformanceSigningClient;
|
||||||
pub use vesting_signing_client::VestingSigningClient;
|
pub use vesting_signing_client::VestingSigningClient;
|
||||||
|
|
||||||
@@ -56,6 +60,7 @@ pub trait NymContractsProvider {
|
|||||||
fn vesting_contract_address(&self) -> Option<&AccountId>;
|
fn vesting_contract_address(&self) -> Option<&AccountId>;
|
||||||
fn performance_contract_address(&self) -> Option<&AccountId>;
|
fn performance_contract_address(&self) -> Option<&AccountId>;
|
||||||
fn network_monitors_contract_address(&self) -> Option<&AccountId>;
|
fn network_monitors_contract_address(&self) -> Option<&AccountId>;
|
||||||
|
fn node_families_contract_address(&self) -> Option<&AccountId>;
|
||||||
|
|
||||||
// coconut-related
|
// coconut-related
|
||||||
fn ecash_contract_address(&self) -> Option<&AccountId>;
|
fn ecash_contract_address(&self) -> Option<&AccountId>;
|
||||||
@@ -70,6 +75,7 @@ pub struct TypedNymContracts {
|
|||||||
pub vesting_contract_address: Option<AccountId>,
|
pub vesting_contract_address: Option<AccountId>,
|
||||||
pub performance_contract_address: Option<AccountId>,
|
pub performance_contract_address: Option<AccountId>,
|
||||||
pub network_monitors_contract_address: Option<AccountId>,
|
pub network_monitors_contract_address: Option<AccountId>,
|
||||||
|
pub node_families_contract_address: Option<AccountId>,
|
||||||
|
|
||||||
pub ecash_contract_address: Option<AccountId>,
|
pub ecash_contract_address: Option<AccountId>,
|
||||||
pub group_contract_address: Option<AccountId>,
|
pub group_contract_address: Option<AccountId>,
|
||||||
@@ -98,6 +104,10 @@ impl TryFrom<NymContracts> for TypedNymContracts {
|
|||||||
.network_monitors_contract_address
|
.network_monitors_contract_address
|
||||||
.map(|addr| addr.parse())
|
.map(|addr| addr.parse())
|
||||||
.transpose()?,
|
.transpose()?,
|
||||||
|
node_families_contract_address: value
|
||||||
|
.node_families_contract_address
|
||||||
|
.map(|addr| addr.parse())
|
||||||
|
.transpose()?,
|
||||||
ecash_contract_address: value
|
ecash_contract_address: value
|
||||||
.ecash_contract_address
|
.ecash_contract_address
|
||||||
.map(|addr| addr.parse())
|
.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()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -304,6 +304,10 @@ impl<C, S> NyxdClient<C, S> {
|
|||||||
self.config.contracts.multisig_contract_address = Some(address);
|
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) {
|
pub fn set_simulated_gas_multiplier(&mut self, multiplier: f32) {
|
||||||
self.config.simulated_gas_multiplier = multiplier;
|
self.config.simulated_gas_multiplier = multiplier;
|
||||||
}
|
}
|
||||||
@@ -332,6 +336,13 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
|
|||||||
.as_ref()
|
.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> {
|
fn ecash_contract_address(&self) -> Option<&AccountId> {
|
||||||
self.config.contracts.ecash_contract_address.as_ref()
|
self.config.contracts.ecash_contract_address.as_ref()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ pub struct Args {
|
|||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pub vesting_contract_address: Option<AccountId>,
|
pub vesting_contract_address: Option<AccountId>,
|
||||||
|
|
||||||
|
#[clap(long)]
|
||||||
|
pub node_families_contract_address: Option<AccountId>,
|
||||||
|
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pub rewarding_denom: Option<String>,
|
pub rewarding_denom: Option<String>,
|
||||||
|
|
||||||
@@ -130,6 +133,14 @@ pub async fn generate(args: Args) {
|
|||||||
.expect("Failed converting vesting contract address to AccountId")
|
.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(|| {
|
let rewarding_denom = args.rewarding_denom.unwrap_or_else(|| {
|
||||||
std::env::var(nym_network_defaults::var_names::MIX_DENOM)
|
std::env::var(nym_network_defaults::var_names::MIX_DENOM)
|
||||||
.expect("Rewarding (mix) denom has to be set")
|
.expect("Rewarding (mix) denom has to be set")
|
||||||
@@ -142,6 +153,7 @@ pub async fn generate(args: Args) {
|
|||||||
let instantiate_msg = InstantiateMsg {
|
let instantiate_msg = InstantiateMsg {
|
||||||
rewarding_validator_address: rewarding_validator_address.to_string(),
|
rewarding_validator_address: rewarding_validator_address.to_string(),
|
||||||
vesting_contract_address: vesting_contract_address.to_string(),
|
vesting_contract_address: vesting_contract_address.to_string(),
|
||||||
|
node_families_contract_address: node_families_contract_address.to_string(),
|
||||||
rewarding_denom,
|
rewarding_denom,
|
||||||
epochs_in_interval: args.epochs_in_interval,
|
epochs_in_interval: args.epochs_in_interval,
|
||||||
epoch_duration: Duration::from_secs(args.epoch_duration),
|
epoch_duration: Duration::from_secs(args.epoch_duration),
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ pub trait ContractOpts {
|
|||||||
|
|
||||||
fn addr_make(&self, input: &str) -> Addr;
|
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) {
|
fn deps_mut_env(&mut self) -> (DepsMut<'_>, Env) {
|
||||||
let env = self.env().clone();
|
let env = self.env().clone();
|
||||||
(self.deps_mut(), env)
|
(self.deps_mut(), env)
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// 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;
|
use cosmwasm_schema::cw_serde;
|
||||||
|
|
||||||
|
/// Public-key + metadata pair surfaced by `GetBlacklistedAccount` /
|
||||||
|
/// `GetBlacklistPaged`. Always empty on a freshly deployed contract.
|
||||||
#[cw_serde]
|
#[cw_serde]
|
||||||
pub struct BlacklistedAccount {
|
pub struct BlacklistedAccount {
|
||||||
pub public_key: String,
|
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]
|
#[cw_serde]
|
||||||
pub struct Blacklisting {
|
pub struct Blacklisting {
|
||||||
pub proposal_id: u64,
|
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]
|
#[cw_serde]
|
||||||
pub struct PagedBlacklistedAccountResponse {
|
pub struct PagedBlacklistedAccountResponse {
|
||||||
pub accounts: Vec<BlacklistedAccount>,
|
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]
|
#[cw_serde]
|
||||||
pub struct BlacklistedAccountResponse {
|
pub struct BlacklistedAccountResponse {
|
||||||
pub account: Option<Blacklisting>,
|
pub account: Option<Blacklisting>,
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
use cosmwasm_schema::cw_serde;
|
use cosmwasm_schema::cw_serde;
|
||||||
use cosmwasm_std::Coin;
|
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]
|
#[cw_serde]
|
||||||
pub struct PoolCounters {
|
pub struct PoolCounters {
|
||||||
/// Represents the total amount of funds deposited into the contract.
|
/// 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_schema::cw_serde;
|
||||||
use cosmwasm_std::{StdError, StdResult};
|
use cosmwasm_std::{StdError, StdResult};
|
||||||
|
|
||||||
|
/// Sequential identifier assigned to every accepted deposit. Starts at 0 and
|
||||||
|
/// is never recycled.
|
||||||
pub type DepositId = u32;
|
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]
|
#[cw_serde]
|
||||||
pub struct Deposit {
|
pub struct Deposit {
|
||||||
pub bs58_encoded_ed25519_pubkey: String,
|
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> {
|
pub fn get_ed25519_pubkey_bytes(raw: &str) -> Result<[u8; 32], EcashContractError> {
|
||||||
let mut ed25519_pubkey_bytes = [0u8; 32];
|
let mut ed25519_pubkey_bytes = [0u8; 32];
|
||||||
bs58::decode(raw)
|
bs58::decode(raw)
|
||||||
@@ -32,10 +39,13 @@ impl Deposit {
|
|||||||
bs58::encode(raw).into_string()
|
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> {
|
pub fn to_bytes(&self) -> Result<[u8; 32], EcashContractError> {
|
||||||
Self::get_ed25519_pubkey_bytes(&self.bs58_encoded_ed25519_pubkey)
|
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> {
|
pub fn try_from_bytes(bytes: &[u8]) -> StdResult<Self> {
|
||||||
if bytes.len() != 32 {
|
if bytes.len() != 32 {
|
||||||
return Err(StdError::generic_err("malformed deposit data"));
|
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]
|
#[cw_serde]
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct LatestDepositResponse {
|
pub struct LatestDepositResponse {
|
||||||
pub deposit: Option<DepositData>,
|
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]
|
#[cw_serde]
|
||||||
pub struct DepositResponse {
|
pub struct DepositResponse {
|
||||||
pub id: DepositId,
|
pub id: DepositId,
|
||||||
@@ -60,6 +74,8 @@ pub struct DepositResponse {
|
|||||||
pub deposit: Option<Deposit>,
|
pub deposit: Option<Deposit>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `(deposit_id, deposit)` pair surfaced by the latest-deposit and paginated
|
||||||
|
/// deposit queries.
|
||||||
#[cw_serde]
|
#[cw_serde]
|
||||||
pub struct DepositData {
|
pub struct DepositData {
|
||||||
pub id: DepositId,
|
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]
|
#[cw_serde]
|
||||||
pub struct PagedDepositsResponse {
|
pub struct PagedDepositsResponse {
|
||||||
pub deposits: Vec<DepositData>,
|
pub deposits: Vec<DepositData>,
|
||||||
|
|||||||
@@ -6,69 +6,108 @@ use cw_controllers::AdminError;
|
|||||||
use cw_utils::PaymentError;
|
use cw_utils::PaymentError;
|
||||||
use thiserror::Error;
|
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)]
|
#[derive(Error, Debug, PartialEq)]
|
||||||
pub enum EcashContractError {
|
pub enum EcashContractError {
|
||||||
|
/// Wrapper for any underlying `cosmwasm_std::StdError` (storage faults,
|
||||||
|
/// address validation, etc.).
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Std(#[from] StdError),
|
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")]
|
#[error("Invalid deposit")]
|
||||||
InvalidDeposit(#[from] PaymentError),
|
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}")]
|
#[error("received wrong amount for deposit. got: {received}. required: {amount}")]
|
||||||
WrongAmount { received: Coin, amount: Coin },
|
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")]
|
#[error("There aren't enough funds in the contract")]
|
||||||
NotEnoughFunds,
|
NotEnoughFunds,
|
||||||
|
|
||||||
|
/// Wrapper for `cw_controllers::AdminError`. Raised by every admin-gated
|
||||||
|
/// and multisig-gated handler when the sender is wrong.
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Admin(#[from] AdminError),
|
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")]
|
#[error("could not find proposal id inside the multisig reply SubMsg")]
|
||||||
MissingProposalId,
|
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")]
|
#[error("the proposal id returned by the multisig contract could not be parsed into an u64")]
|
||||||
MalformedProposalId,
|
MalformedProposalId,
|
||||||
|
|
||||||
|
/// Instantiation given a `group_addr` that failed bech32 validation.
|
||||||
#[error("Group contract invalid address '{addr}'")]
|
#[error("Group contract invalid address '{addr}'")]
|
||||||
InvalidGroup { addr: String },
|
InvalidGroup { addr: String },
|
||||||
|
|
||||||
|
/// **Unreachable** - no current execute path triggers this.
|
||||||
#[error("Unauthorized")]
|
#[error("Unauthorized")]
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
|
|
||||||
|
/// **Unreachable** - preserved for future SemVer comparisons during migration.
|
||||||
#[error("Failed to parse {value} into a valid SemVer version: {error_message}")]
|
#[error("Failed to parse {value} into a valid SemVer version: {error_message}")]
|
||||||
SemVerFailure {
|
SemVerFailure {
|
||||||
value: String,
|
value: String,
|
||||||
error_message: 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")]
|
#[error("received an invalid reply id: {id}. it does not correspond to any sent SubMsg")]
|
||||||
InvalidReplyId { id: u64 },
|
InvalidReplyId { id: u64 },
|
||||||
|
|
||||||
|
/// **Unreachable** - preserved for the (future) typed-deposit-info feature.
|
||||||
#[error("reached the maximum of 255 different deposit types")]
|
#[error("reached the maximum of 255 different deposit types")]
|
||||||
MaximumDepositTypesReached,
|
MaximumDepositTypesReached,
|
||||||
|
|
||||||
|
/// **Unreachable** - preserved for the (future) typed-deposit-info feature.
|
||||||
#[error("compressed deposit info {typ} does not corresponds to any known type")]
|
#[error("compressed deposit info {typ} does not corresponds to any known type")]
|
||||||
UnknownCompressedDepositInfoType { typ: u8 },
|
UnknownCompressedDepositInfoType { typ: u8 },
|
||||||
|
|
||||||
|
/// **Unreachable** - preserved for the (future) typed-deposit-info feature.
|
||||||
#[error("deposit info {typ} does not corresponds to any previously seen type")]
|
#[error("deposit info {typ} does not corresponds to any previously seen type")]
|
||||||
UnknownDepositInfoType { typ: String },
|
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")]
|
#[error("the provided ed25519 identity was malformed")]
|
||||||
MalformedEd25519Identity,
|
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!")]
|
#[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 },
|
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")]
|
#[error("the provided tickets redemption commitment is malformed")]
|
||||||
MalformedRedemptionCommitment,
|
MalformedRedemptionCommitment,
|
||||||
|
|
||||||
|
/// Always thrown by `ProposeToBlacklist` and `AddToBlacklist` until the
|
||||||
|
/// blacklist redesign lands.
|
||||||
#[error("the account blacklisting hasn't been fully implemented yet")]
|
#[error("the account blacklisting hasn't been fully implemented yet")]
|
||||||
UnimplementedBlacklisting,
|
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}')")]
|
#[error("reduced deposit must use the same denom as the default deposit (expected '{expected}', got '{got}')")]
|
||||||
InvalidReducedDepositDenom { expected: String, got: String },
|
InvalidReducedDepositDenom { expected: String, got: String },
|
||||||
|
|
||||||
|
/// `SetReducedDepositPrice` (or migration whitelist seeding) given a
|
||||||
|
/// reduced amount not strictly less than the current default.
|
||||||
#[error(
|
#[error(
|
||||||
"reduced deposit amount ({reduced}) must be strictly less than the default ({default})"
|
"reduced deposit amount ({reduced}) must be strictly less than the default ({default})"
|
||||||
)]
|
)]
|
||||||
@@ -77,9 +116,13 @@ pub enum EcashContractError {
|
|||||||
default: cosmwasm_std::Uint128,
|
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")]
|
#[error("address '{address}' does not have a custom reduced deposit price set")]
|
||||||
NoReducedDepositPrice { address: String },
|
NoReducedDepositPrice { address: String },
|
||||||
|
|
||||||
|
/// `UpdateDefaultDepositValue` or `SetReducedDepositPrice` given an amount
|
||||||
|
/// below `nym_network_defaults::TICKETBOOK_SIZE`.
|
||||||
#[error(
|
#[error(
|
||||||
"deposit amount ({amount}) must be at least the ticket book size ({ticket_book_size})"
|
"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>
|
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// 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";
|
pub const BANDWIDTH_PROPOSAL_ID: &str = "proposal_id";
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// 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";
|
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";
|
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";
|
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";
|
pub const PROPOSAL_ID_ATTRIBUTE_NAME: &str = "proposal_id";
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// 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 blacklist;
|
||||||
pub mod counters;
|
pub mod counters;
|
||||||
pub mod deposit;
|
pub mod deposit;
|
||||||
|
|||||||
@@ -15,100 +15,134 @@ use crate::reduced_deposit::WhitelistedAccountsResponse;
|
|||||||
#[cfg(feature = "schema")]
|
#[cfg(feature = "schema")]
|
||||||
use cosmwasm_schema::QueryResponses;
|
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]
|
#[cw_serde]
|
||||||
pub struct InstantiateMsg {
|
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,
|
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,
|
pub multisig_addr: String,
|
||||||
|
|
||||||
|
/// cw4 group contract referenced by the (stubbed) blacklist proposal flow.
|
||||||
pub group_addr: String,
|
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,
|
pub deposit_amount: Coin,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cw_serde]
|
#[cw_serde]
|
||||||
pub enum ExecuteMsg {
|
pub enum ExecuteMsg {
|
||||||
/// Used by clients to request ticket books from the signers
|
/// Submitted by clients to escrow funds and register a claimed ed25519
|
||||||
DepositTicketBookFunds {
|
/// identity key. Mints a sequential `deposit_id`. The contract does not
|
||||||
identity_key: String,
|
/// 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 {
|
RequestRedemption {
|
||||||
commitment_bs58: String,
|
commitment_bs58: String,
|
||||||
number_of_tickets: u16,
|
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
|
/// **Legacy / dead code.** Only callable by the multisig; bumps the
|
||||||
RedeemTickets {
|
/// unredeemed-tickets counter and emits a `ticket_redemption` event with
|
||||||
n: u16,
|
/// `moved_to_holding_account = "false"`. No known consumer depends on the
|
||||||
gw: String,
|
/// side effects; candidate for removal in a follow-on breaking-schema
|
||||||
},
|
/// change.
|
||||||
|
RedeemTickets { n: u16, gw: String },
|
||||||
|
|
||||||
UpdateAdmin {
|
/// Transfer the contract admin role. Only the current admin may sign.
|
||||||
admin: String,
|
/// 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")]
|
#[serde(alias = "update_deposit_value")]
|
||||||
UpdateDefaultDepositValue {
|
UpdateDefaultDepositValue { new_deposit: Coin },
|
||||||
new_deposit: Coin,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Set (or overwrite) a reduced deposit price for a specific address.
|
/// Set (or overwrite) a reduced deposit price for a specific address.
|
||||||
/// Only callable by the contract admin.
|
/// Only callable by the contract admin.
|
||||||
SetReducedDepositPrice {
|
SetReducedDepositPrice { address: String, deposit: Coin },
|
||||||
address: String,
|
|
||||||
deposit: Coin,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Remove the reduced deposit price for a specific address, reverting them to
|
/// 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.
|
/// the default price. Returns an error if the address has no custom price set.
|
||||||
/// Only callable by the contract admin.
|
/// Only callable by the contract admin.
|
||||||
RemoveReducedDepositPrice {
|
RemoveReducedDepositPrice { address: String },
|
||||||
address: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
// TODO: properly implement
|
/// **Stubbed**: always returns `EcashContractError::UnimplementedBlacklisting`.
|
||||||
ProposeToBlacklist {
|
/// Storage, reply handler, and helper paths exist but are unreachable from
|
||||||
public_key: String,
|
/// the public ExecuteMsg surface. Preserved for the redesign.
|
||||||
},
|
ProposeToBlacklist { public_key: String },
|
||||||
AddToBlacklist {
|
|
||||||
public_key: String,
|
/// **Stubbed**: always returns `EcashContractError::UnimplementedBlacklisting`.
|
||||||
},
|
AddToBlacklist { public_key: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cw_serde]
|
#[cw_serde]
|
||||||
#[cfg_attr(feature = "schema", derive(QueryResponses))]
|
#[cfg_attr(feature = "schema", derive(QueryResponses))]
|
||||||
pub enum QueryMsg {
|
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))]
|
#[cfg_attr(feature = "schema", returns(BlacklistedAccountResponse))]
|
||||||
GetBlacklistedAccount { public_key: String },
|
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))]
|
#[cfg_attr(feature = "schema", returns(PagedBlacklistedAccountResponse))]
|
||||||
GetBlacklistPaged {
|
GetBlacklistPaged {
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
start_after: Option<String>,
|
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))]
|
#[cfg_attr(feature = "schema", returns(Coin))]
|
||||||
#[serde(alias = "get_required_deposit_amount")]
|
#[serde(alias = "get_required_deposit_amount")]
|
||||||
#[serde(alias = "GetRequiredDepositAmount")]
|
#[serde(alias = "GetRequiredDepositAmount")]
|
||||||
GetDefaultDepositAmount {},
|
GetDefaultDepositAmount {},
|
||||||
|
|
||||||
|
/// Per-address reduced deposit price override, if any. `None` for any
|
||||||
|
/// non-whitelisted address.
|
||||||
#[cfg_attr(feature = "schema", returns(Option<Coin>))]
|
#[cfg_attr(feature = "schema", returns(Option<Coin>))]
|
||||||
GetReducedDepositAmount { address: String },
|
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))]
|
#[cfg_attr(feature = "schema", returns(WhitelistedAccountsResponse))]
|
||||||
GetAllWhitelistedAccounts {},
|
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))]
|
#[cfg_attr(feature = "schema", returns(DepositResponse))]
|
||||||
GetDeposit { deposit_id: u32 },
|
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))]
|
#[cfg_attr(feature = "schema", returns(LatestDepositResponse))]
|
||||||
GetLatestDeposit {},
|
GetLatestDeposit {},
|
||||||
|
|
||||||
|
/// Paginated listing of deposits in ascending id order. Defaults: limit 50,
|
||||||
|
/// max 100.
|
||||||
#[cfg_attr(feature = "schema", returns(PagedDepositsResponse))]
|
#[cfg_attr(feature = "schema", returns(PagedDepositsResponse))]
|
||||||
GetDepositsPaged {
|
GetDepositsPaged {
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
start_after: 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))]
|
#[cfg_attr(feature = "schema", returns(DepositsStatistics))]
|
||||||
GetDepositsStatistics {},
|
GetDepositsStatistics {},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// 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
|
// TODO: to be moved to multisig
|
||||||
pub const BATCH_REDEMPTION_PROPOSAL_TITLE: &str = "ecash-redemption";
|
pub const BATCH_REDEMPTION_PROPOSAL_TITLE: &str = "ecash-redemption";
|
||||||
|
|||||||
@@ -4,12 +4,16 @@
|
|||||||
use cosmwasm_schema::cw_serde;
|
use cosmwasm_schema::cw_serde;
|
||||||
use cosmwasm_std::{Addr, Coin};
|
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]
|
#[cw_serde]
|
||||||
pub struct WhitelistedAccount {
|
pub struct WhitelistedAccount {
|
||||||
pub address: Addr,
|
pub address: Addr,
|
||||||
pub deposit: Coin,
|
pub deposit: Coin,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Response shape for `GetAllWhitelistedAccounts`. Unpaginated - the whitelist
|
||||||
|
/// is expected to stay small.
|
||||||
#[cw_serde]
|
#[cw_serde]
|
||||||
pub struct WhitelistedAccountsResponse {
|
pub struct WhitelistedAccountsResponse {
|
||||||
pub whitelisted_accounts: Vec<WhitelistedAccount>,
|
pub whitelisted_accounts: Vec<WhitelistedAccount>,
|
||||||
|
|||||||
@@ -3,12 +3,121 @@
|
|||||||
|
|
||||||
use crate::error::MixnetContractError;
|
use crate::error::MixnetContractError;
|
||||||
use crate::mixnode::PendingMixNodeChanges;
|
use crate::mixnode::PendingMixNodeChanges;
|
||||||
|
use crate::nym_node::NodeOwnershipResponse;
|
||||||
use crate::{
|
use crate::{
|
||||||
EpochEventId, IntervalEventId, MixNodeBond, MixNodeDetails, NodeId, NodeRewarding, NymNodeBond,
|
EpochEventId, EpochId, Interval, IntervalEventId, MixNodeBond, MixNodeDetails, NodeId,
|
||||||
NymNodeDetails, PendingNodeChanges,
|
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 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]
|
#[track_caller]
|
||||||
pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option<Decimal>) {
|
pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option<Decimal>) {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ pub use gateway::{
|
|||||||
Gateway, GatewayBond, GatewayBondResponse, GatewayConfigUpdate, GatewayOwnershipResponse,
|
Gateway, GatewayBond, GatewayBondResponse, GatewayConfigUpdate, GatewayOwnershipResponse,
|
||||||
PagedGatewayResponse,
|
PagedGatewayResponse,
|
||||||
};
|
};
|
||||||
|
pub use helpers::MixnetContractQuerier;
|
||||||
pub use interval::{
|
pub use interval::{
|
||||||
CurrentIntervalResponse, EpochId, EpochState, EpochStatus, Interval, IntervalId,
|
CurrentIntervalResponse, EpochId, EpochState, EpochStatus, Interval, IntervalId,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -190,6 +190,10 @@ impl NodeRewarding {
|
|||||||
truncate_reward(self.operator, denom)
|
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> {
|
pub fn pending_delegator_reward(&self, delegation: &Delegation) -> StdResult<Coin> {
|
||||||
let delegator_reward = self.determine_delegation_reward(delegation)?;
|
let delegator_reward = self.determine_delegation_reward(delegation)?;
|
||||||
Ok(truncate_reward(delegator_reward, &delegation.amount.denom))
|
Ok(truncate_reward(delegator_reward, &delegation.amount.denom))
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ use nym_contracts_common::{ContractBuildInformation, signing::Nonce};
|
|||||||
pub struct InstantiateMsg {
|
pub struct InstantiateMsg {
|
||||||
pub rewarding_validator_address: String,
|
pub rewarding_validator_address: String,
|
||||||
pub vesting_contract_address: String,
|
pub vesting_contract_address: String,
|
||||||
|
pub node_families_contract_address: String,
|
||||||
|
|
||||||
pub rewarding_denom: String,
|
pub rewarding_denom: String,
|
||||||
pub epochs_in_interval: u32,
|
pub epochs_in_interval: u32,
|
||||||
@@ -305,6 +306,22 @@ pub enum ExecuteMsg {
|
|||||||
MigrateVestedDelegation {
|
MigrateVestedDelegation {
|
||||||
mix_id: NodeId,
|
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
|
// testing-only
|
||||||
#[cfg(feature = "contract-testing")]
|
#[cfg(feature = "contract-testing")]
|
||||||
@@ -394,6 +411,15 @@ impl ExecuteMsg {
|
|||||||
}
|
}
|
||||||
ExecuteMsg::MigrateVestedMixNode { .. } => "migrate vested mixnode".into(),
|
ExecuteMsg::MigrateVestedMixNode { .. } => "migrate vested mixnode".into(),
|
||||||
ExecuteMsg::MigrateVestedDelegation { .. } => "migrate vested delegation".to_string(),
|
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::AssignRoles { .. } => "assigning epoch roles".into(),
|
||||||
ExecuteMsg::MigrateMixnode { .. } => "migrating legacy mixnode".into(),
|
ExecuteMsg::MigrateMixnode { .. } => "migrating legacy mixnode".into(),
|
||||||
ExecuteMsg::MigrateGateway { .. } => "migrating legacy gateway".into(),
|
ExecuteMsg::MigrateGateway { .. } => "migrating legacy gateway".into(),
|
||||||
@@ -881,8 +907,15 @@ pub enum QueryMsg {
|
|||||||
GetKeyRotationId {},
|
GetKeyRotationId {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cw_serde]
|
||||||
|
pub struct VestedDelegationMigrationEntry {
|
||||||
|
pub mix_id: NodeId,
|
||||||
|
pub owner: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[cw_serde]
|
#[cw_serde]
|
||||||
pub struct MigrateMsg {
|
pub struct MigrateMsg {
|
||||||
pub unsafe_skip_state_updates: Option<bool>,
|
pub unsafe_skip_state_updates: Option<bool>,
|
||||||
pub vesting_contract_address: Option<String>,
|
pub vesting_contract_address: Option<String>,
|
||||||
|
pub node_families_contract_address: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,6 +212,10 @@ pub struct ContractState {
|
|||||||
/// track-related messages.
|
/// track-related messages.
|
||||||
pub vesting_contract_address: Addr,
|
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).
|
/// The expected denom used for rewarding (and realistically any other operation).
|
||||||
/// Default: `unym`
|
/// Default: `unym`
|
||||||
pub rewarding_denom: String,
|
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>,
|
||||||
|
}
|
||||||
@@ -230,8 +230,8 @@ impl MemoryEcachTicketbookManager {
|
|||||||
expiration_date: t.ticketbook.expiration_date(),
|
expiration_date: t.ticketbook.expiration_date(),
|
||||||
ticketbook_type: t.ticketbook.ticketbook_type().to_string(),
|
ticketbook_type: t.ticketbook.ticketbook_type().to_string(),
|
||||||
epoch_id: t.ticketbook.epoch_id() as u32,
|
epoch_id: t.ticketbook.epoch_id() as u32,
|
||||||
total_tickets: t.ticketbook.spent_tickets() as u32,
|
total_tickets: t.total_tickets,
|
||||||
used_tickets: t.total_tickets,
|
used_tickets: t.ticketbook.spent_tickets() as u32,
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -333,3 +333,339 @@ impl MemoryEcachTicketbookManager {
|
|||||||
guard.emergency_credentials.remove(typ);
|
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 }
|
repository = { workspace = true }
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
documentation.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
|
readme.workspace = true
|
||||||
publish = true
|
publish = true
|
||||||
|
|
||||||
|
|||||||
@@ -1173,7 +1173,16 @@ impl ApiClientCore for Client {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match response {
|
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) => {
|
Err(err) => {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
let is_network_err = err.is_timeout();
|
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"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
const MAX_ERR_SOURCE_ITERATIONS: usize = 4;
|
const MAX_ERR_SOURCE_ITERATIONS: usize = 4;
|
||||||
|
|
||||||
/// This functions attempts to check the error returned by reqwest to see if
|
/// This functions attempts to check the error returned by reqwest to see if rotating host
|
||||||
/// rotating host informtion (for clients with mutliple hosts defined) could be
|
/// information (for clients with multiple hosts defined) could be helpful. This looks for
|
||||||
/// helpful. This looks for situations where the error could plausibly be caused
|
/// situations where the error could plausibly be caused by a network adversary, or where rotating
|
||||||
/// by a network adversary, or where rotating to an equival hostname might help.
|
/// to an equivalent hostname might help.
|
||||||
///
|
///
|
||||||
/// For example --> NetworkUnreachable will not be helped by rotating domains,
|
/// For example --> NetworkUnreachable will not be helped by rotating domains, but ConnectionReset
|
||||||
/// but ConnectionReset might be caused by a network adversary blocking by SNI
|
/// might be caused by a network adversary blocking by SNI which could possibly benefit from
|
||||||
/// which could possibly benefit from rotating domains.
|
/// rotating domains.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
|
pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
|
||||||
if err.is_timeout() {
|
if err.is_timeout() {
|
||||||
@@ -1697,6 +1728,13 @@ where
|
|||||||
decode_raw_response(&headers, full)
|
decode_raw_response(&headers, full)
|
||||||
} else if res.status() == StatusCode::NOT_FOUND {
|
} else if res.status() == StatusCode::NOT_FOUND {
|
||||||
Err(HttpClientError::NotFound { url: Box::new(url) })
|
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 {
|
} else {
|
||||||
let Ok(plaintext) = res.text().await else {
|
let Ok(plaintext) = res.text().await else {
|
||||||
return Err(HttpClientError::RequestFailure {
|
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>
|
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::middleware::client_ip::ClientIpAddr;
|
||||||
use axum::extract::Request;
|
use axum::extract::Request;
|
||||||
use axum::http::HeaderValue;
|
use axum::http::HeaderValue;
|
||||||
use axum::http::header::{HOST, USER_AGENT};
|
use axum::http::header::{HOST, USER_AGENT};
|
||||||
use axum::middleware::Next;
|
use axum::middleware::Next;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum_client_ip::InsecureClientIp;
|
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
@@ -17,24 +17,24 @@ enum LogLevel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn log_request_info(
|
pub async fn log_request_info(
|
||||||
insecure_client_ip: InsecureClientIp,
|
client_ip: ClientIpAddr,
|
||||||
request: Request,
|
request: Request,
|
||||||
next: Next,
|
next: Next,
|
||||||
) -> impl IntoResponse {
|
) -> 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(
|
pub async fn log_request_debug(
|
||||||
insecure_client_ip: InsecureClientIp,
|
client_ip: ClientIpAddr,
|
||||||
request: Request,
|
request: Request,
|
||||||
next: Next,
|
next: Next,
|
||||||
) -> impl IntoResponse {
|
) -> 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
|
/// Simple logger for requests
|
||||||
async fn log_request(
|
async fn log_request(
|
||||||
InsecureClientIp(addr): InsecureClientIp,
|
ClientIpAddr(addr): ClientIpAddr,
|
||||||
request: Request,
|
request: Request,
|
||||||
next: Next,
|
next: Next,
|
||||||
level: LogLevel,
|
level: LogLevel,
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
pub mod bearer_auth;
|
pub mod bearer_auth;
|
||||||
|
pub mod client_ip;
|
||||||
pub mod logging;
|
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)]
|
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)]
|
||||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
|
|||||||
@@ -20,6 +20,22 @@ pub const MAX_NON_STREAM_VERSION: u8 = v8::VERSION;
|
|||||||
/// mixnet sends, matching the node-side enforcement in `ip-packet-router`.
|
/// mixnet sends, matching the node-side enforcement in `ip-packet-router`.
|
||||||
pub const SPHINX_STREAM_VERSION_THRESHOLD: u8 = v9::VERSION;
|
pub const SPHINX_STREAM_VERSION_THRESHOLD: u8 = v9::VERSION;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const _: () = {
|
||||||
|
assert!(SPHINX_STREAM_VERSION_THRESHOLD > MAX_NON_STREAM_VERSION);
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stream_transport_threshold_is_consistent() {
|
||||||
|
assert_eq!(MAX_NON_STREAM_VERSION, 8);
|
||||||
|
assert_eq!(SPHINX_STREAM_VERSION_THRESHOLD, 9);
|
||||||
|
const _: () = assert!(SPHINX_STREAM_VERSION_THRESHOLD > MAX_NON_STREAM_VERSION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// version 3: initial version
|
// version 3: initial version
|
||||||
// version 4: IPv6 support
|
// version 4: IPv6 support
|
||||||
// version 5: Add severity level to info response
|
// version 5: Add severity level to info response
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
documentation.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
|
readme.workspace = true
|
||||||
publish = true
|
publish = true
|
||||||
# Exclude build.rs from published crate - it's only used for dev-time sync
|
# Exclude build.rs from published crate - it's only used for dev-time sync
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "";
|
|||||||
|
|
||||||
pub const NETWORK_MONITORS_CONTRACT_ADDRESS: &str =
|
pub const NETWORK_MONITORS_CONTRACT_ADDRESS: &str =
|
||||||
"n1m3a2ltkjqud8mkmrpqvgllrtv2p4r6js6qwl7p8cqkzrq8jg6e2qwqgl8z";
|
"n1m3a2ltkjqud8mkmrpqvgllrtv2p4r6js6qwl7p8cqkzrq8jg6e2qwqgl8z";
|
||||||
|
// \/ TODO: this has to be updated once the contract is deployed
|
||||||
|
pub const NODE_FAMILIES_CONTRACT_ADDRESS: &str = "";
|
||||||
|
// /\ TODO: this has to be updated once the contract is deployed
|
||||||
|
|
||||||
pub const ECASH_CONTRACT_ADDRESS: &str =
|
pub const ECASH_CONTRACT_ADDRESS: &str =
|
||||||
"n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun";
|
"n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun";
|
||||||
pub const GROUP_CONTRACT_ADDRESS: &str =
|
pub const GROUP_CONTRACT_ADDRESS: &str =
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ pub struct NymContracts {
|
|||||||
pub performance_contract_address: Option<String>,
|
pub performance_contract_address: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub network_monitors_contract_address: Option<String>,
|
pub network_monitors_contract_address: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub node_families_contract_address: Option<String>,
|
||||||
pub ecash_contract_address: Option<String>,
|
pub ecash_contract_address: Option<String>,
|
||||||
pub group_contract_address: Option<String>,
|
pub group_contract_address: Option<String>,
|
||||||
pub multisig_contract_address: Option<String>,
|
pub multisig_contract_address: Option<String>,
|
||||||
@@ -185,6 +187,9 @@ impl NymNetworkDetails {
|
|||||||
))
|
))
|
||||||
.with_mixnet_contract(get_optional_env(var_names::MIXNET_CONTRACT_ADDRESS))
|
.with_mixnet_contract(get_optional_env(var_names::MIXNET_CONTRACT_ADDRESS))
|
||||||
.with_vesting_contract(get_optional_env(var_names::VESTING_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_ecash_contract(get_optional_env(var_names::ECASH_CONTRACT_ADDRESS))
|
||||||
.with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS))
|
.with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS))
|
||||||
.with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
|
.with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
|
||||||
@@ -217,6 +222,9 @@ impl NymNetworkDetails {
|
|||||||
network_monitors_contract_address: parse_optional_str(
|
network_monitors_contract_address: parse_optional_str(
|
||||||
mainnet::NETWORK_MONITORS_CONTRACT_ADDRESS,
|
mainnet::NETWORK_MONITORS_CONTRACT_ADDRESS,
|
||||||
),
|
),
|
||||||
|
node_families_contract_address: parse_optional_str(
|
||||||
|
mainnet::NODE_FAMILIES_CONTRACT_ADDRESS,
|
||||||
|
),
|
||||||
ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS),
|
ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS),
|
||||||
group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS),
|
group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS),
|
||||||
multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS),
|
multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS),
|
||||||
@@ -270,6 +278,8 @@ impl NymNetworkDetails {
|
|||||||
|
|
||||||
set_optional_var(var_names::MIXNET_CONTRACT_ADDRESS, self.contracts.mixnet_contract_address);
|
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::VESTING_CONTRACT_ADDRESS, self.contracts.vesting_contract_address);
|
||||||
|
set_optional_var(var_names::NETWORK_MONITORS_CONTRACT_ADDRESS, self.contracts.network_monitors_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::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::GROUP_CONTRACT_ADDRESS, self.contracts.group_contract_address);
|
||||||
set_optional_var(var_names::MULTISIG_CONTRACT_ADDRESS, self.contracts.multisig_contract_address);
|
set_optional_var(var_names::MULTISIG_CONTRACT_ADDRESS, self.contracts.multisig_contract_address);
|
||||||
@@ -358,6 +368,12 @@ impl NymNetworkDetails {
|
|||||||
self
|
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]
|
#[must_use]
|
||||||
pub fn with_ecash_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
|
pub fn with_ecash_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
|
||||||
self.contracts.ecash_contract_address = contract.map(Into::into);
|
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 ECASH_CONTRACT_ADDRESS: &str = "ECASH_CONTRACT_ADDRESS";
|
||||||
pub const GROUP_CONTRACT_ADDRESS: &str = "GROUP_CONTRACT_ADDRESS";
|
pub const GROUP_CONTRACT_ADDRESS: &str = "GROUP_CONTRACT_ADDRESS";
|
||||||
pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_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 COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS";
|
||||||
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "PERFORMANCE_CONTRACT_ADDRESS";
|
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "PERFORMANCE_CONTRACT_ADDRESS";
|
||||||
pub const NETWORK_MONITORS_CONTRACT_ADDRESS: &str = "NETWORK_MONITORS_CONTRACT_ADDRESS";
|
pub const NETWORK_MONITORS_CONTRACT_ADDRESS: &str = "NETWORK_MONITORS_CONTRACT_ADDRESS";
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ pub enum Error {
|
|||||||
|
|
||||||
#[error("failed to create ipv4 packet")]
|
#[error("failed to create ipv4 packet")]
|
||||||
Ipv4PacketCreationFailure,
|
Ipv4PacketCreationFailure,
|
||||||
|
|
||||||
|
#[error("packet length {length} exceeds the u16 IP header field")]
|
||||||
|
PacketLengthOverflow { length: usize },
|
||||||
}
|
}
|
||||||
|
|
||||||
// Result type based on our error type
|
// Result type based on our error type
|
||||||
|
|||||||
@@ -79,9 +79,14 @@ pub fn wrap_icmp_in_ipv4(
|
|||||||
let mut ipv4_packet =
|
let mut ipv4_packet =
|
||||||
MutableIpv4Packet::owned(ipv4_buffer).ok_or(Error::Ipv4PacketCreationFailure)?;
|
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_version(4);
|
||||||
ipv4_packet.set_header_length(5);
|
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_ttl(64);
|
||||||
ipv4_packet.set_next_level_protocol(pnet_packet::ip::IpNextHeaderProtocols::Icmp);
|
ipv4_packet.set_next_level_protocol(pnet_packet::ip::IpNextHeaderProtocols::Icmp);
|
||||||
ipv4_packet.set_source(source);
|
ipv4_packet.set_source(source);
|
||||||
@@ -101,12 +106,18 @@ pub fn wrap_icmp_in_ipv6(
|
|||||||
source: Ipv6Addr,
|
source: Ipv6Addr,
|
||||||
destination: Ipv6Addr,
|
destination: Ipv6Addr,
|
||||||
) -> Result<Ipv6Packet> {
|
) -> 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 =
|
let mut ipv6_packet =
|
||||||
MutableIpv6Packet::owned(ipv6_buffer).ok_or(Error::Ipv4PacketCreationFailure)?;
|
MutableIpv6Packet::owned(ipv6_buffer).ok_or(Error::Ipv4PacketCreationFailure)?;
|
||||||
|
|
||||||
ipv6_packet.set_version(6);
|
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_next_header(pnet_packet::ip::IpNextHeaderProtocols::Icmpv6);
|
||||||
ipv6_packet.set_hop_limit(64);
|
ipv6_packet.set_hop_limit(64);
|
||||||
ipv6_packet.set_source(source);
|
ipv6_packet.set_source(source);
|
||||||
@@ -164,3 +175,122 @@ pub(crate) fn is_icmp_v6_echo_reply(packet: &Bytes) -> Option<(u16, Ipv6Addr, Ip
|
|||||||
}
|
}
|
||||||
None
|
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 ansi_term::Color::Yellow;
|
||||||
use bytes::{Buf, BytesMut};
|
use bytes::{Buf, BytesMut};
|
||||||
use log::{debug, error, warn};
|
use log::{debug, error, trace, warn};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use crate::MAX_RTO;
|
use crate::MAX_RTO;
|
||||||
@@ -499,21 +499,9 @@ impl KcpSession {
|
|||||||
self.snd_buf.len(),
|
self.snd_buf.len(),
|
||||||
post_retain_sns
|
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 {
|
if removed_count == 0 {
|
||||||
// Use trace level if no segments were removed but buffer wasn't empty
|
trace!(
|
||||||
debug!(
|
|
||||||
"[ConvID: {}, Thread: {:?}] parse_una(una={}): No segments removed from snd_buf (len={}). Remaining sns: {:?}",
|
"[ConvID: {}, Thread: {:?}] parse_una(una={}): No segments removed from snd_buf (len={}). Remaining sns: {:?}",
|
||||||
self.conv,
|
self.conv,
|
||||||
thread::current().id(),
|
thread::current().id(),
|
||||||
@@ -521,6 +509,17 @@ impl KcpSession {
|
|||||||
original_len,
|
original_len,
|
||||||
self.snd_buf.iter().map(|s| s.sn).collect::<Vec<_>>()
|
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.
|
// Update the known acknowledged sequence number.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nym-kkt"
|
name = "nym-kkt"
|
||||||
description = "Key transport protocol for the Nym network"
|
description = "Key transport protocol for the Nym network"
|
||||||
version = "0.1.0"
|
version = "1.21.0"
|
||||||
authors = ["Georgio Nicolas <georgio@nymtech.net>"]
|
authors = ["Georgio Nicolas <georgio@nymtech.net>"]
|
||||||
edition = { workspace = true }
|
edition = { workspace = true }
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
[package]
|
||||||
|
name = "nym-lp-data"
|
||||||
|
description = "Lewes Protocol data structure for the Nym network"
|
||||||
|
version.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
documentation.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
publish = 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;
|
||||||
@@ -110,7 +110,9 @@ impl LpFrame {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn len(&self) -> usize {
|
// is_empty in the sense len == 0 doesn't make sense in that case
|
||||||
|
#[allow(clippy::len_without_is_empty)]
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
LpFrameHeader::SIZE + self.content.len()
|
LpFrameHeader::SIZE + self.content.len()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,6 +167,8 @@ impl SphinxStreamFrameAttributes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(attrs: &LpFrameAttributes) -> Result<Self, MalformedLpPacketError> {
|
pub fn parse(attrs: &LpFrameAttributes) -> Result<Self, MalformedLpPacketError> {
|
||||||
|
// SAFETY : 8 bytes slice into 8 bytes array
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
let stream_id = u64::from_be_bytes(attrs[0..8].try_into().unwrap());
|
let stream_id = u64::from_be_bytes(attrs[0..8].try_into().unwrap());
|
||||||
let msg_type = match attrs[8] {
|
let msg_type = match attrs[8] {
|
||||||
0 => SphinxStreamMsgType::Open,
|
0 => SphinxStreamMsgType::Open,
|
||||||
@@ -175,6 +179,8 @@ impl SphinxStreamFrameAttributes {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// SAFETY : 4 bytes slice into 4 bytes array
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
let sequence_num = u32::from_be_bytes(attrs[9..13].try_into().unwrap());
|
let sequence_num = u32::from_be_bytes(attrs[9..13].try_into().unwrap());
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
stream_id,
|
stream_id,
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::packet::error::MalformedLpPacketError;
|
||||||
use crate::packet::version;
|
use crate::packet::version;
|
||||||
use crate::{packet::error::MalformedLpPacketError, peer_config::LpReceiverIndex};
|
|
||||||
use bytes::{BufMut, BytesMut};
|
use bytes::{BufMut, BytesMut};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
|
pub type LpReceiverIndex = u32;
|
||||||
|
|
||||||
/// Outer header (12 bytes) - always cleartext, used for routing.
|
/// Outer header (12 bytes) - always cleartext, used for routing.
|
||||||
///
|
///
|
||||||
/// This is the first 12 bytes of every LP packet, containing only the fields
|
/// This is the first 12 bytes of every LP packet, containing only the fields
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user