Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 23f33bb982 | |||
| ea4d657fdf | |||
| fe144da4b2 | |||
| 00aedbb144 | |||
| 6a33354f84 | |||
| 19d8381950 | |||
| 27266e5e68 | |||
| 371a0b2ba8 | |||
| bde2b07d0d | |||
| 26538f5a40 | |||
| 483bb6f477 | |||
| a68355a75a | |||
| 1572d8e5c2 | |||
| fd76c5ca4d | |||
| f94589c2d1 | |||
| 1c40499829 | |||
| f8a4d5f1ff | |||
| 42807890af | |||
| 5aa576b596 | |||
| 0215ad9294 | |||
| 227e6a10e1 | |||
| d3b6a270de | |||
| e12ada0105 | |||
| 71d50d79c2 | |||
| a21a01cf1a | |||
| 362f84b5f6 | |||
| daed9cd15b | |||
| a53ca71bd2 | |||
| a70e68c7bd | |||
| fdebed7c38 | |||
| f576a4ee2d | |||
| a9aafd785e | |||
| 0f7dbb94a8 | |||
| 2d72b1b201 | |||
| 412657f773 |
@@ -25,14 +25,14 @@ jobs:
|
||||
echo "file2=$(ls nym-vpn*.deb)" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload nym-repo-setup
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ env.file1 }}
|
||||
path: ppa/packages/nym-repo-setup*.deb
|
||||
retention-days: 10
|
||||
|
||||
- name: Upload nym-vpn
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ env.file2 }}
|
||||
path: ppa/packages/nym-vpn*.deb
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
run: sudo apt-get install -y rsync
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.2.0
|
||||
uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
version: 9
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -37,6 +37,9 @@ jobs:
|
||||
command: build
|
||||
args: --workspace --release
|
||||
|
||||
- name: Verify doc versions
|
||||
run: ${{ github.workspace }}/documentation/scripts/verify-doc-versions.sh
|
||||
working-directory: ${{ github.workspace }}
|
||||
- name: Install project dependencies
|
||||
run: pnpm i
|
||||
- name: Generate llms-full.txt
|
||||
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
|
||||
- name: Upload Artifact
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nym-binaries-artifacts
|
||||
path: |
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
name: ci-build-upload-network-monitor-agent
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-upload:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [arc-ubuntu-22.04]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Prepare build output directory
|
||||
shell: bash
|
||||
env:
|
||||
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
|
||||
run: |
|
||||
rm -rf ci-builds || true
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get -y install libudev-dev
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
|
||||
|
||||
- name: Build nym-network-monitor-agent
|
||||
shell: bash
|
||||
run: cargo build -p nym-network-monitor-agent --release
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: nym-network-monitor-agent
|
||||
path: target/release/nym-network-monitor-agent
|
||||
retention-days: 30
|
||||
|
||||
- name: Prepare build output
|
||||
shell: bash
|
||||
env:
|
||||
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
|
||||
run: cp target/release/nym-network-monitor-agent "$OUTPUT_DIR"
|
||||
|
||||
- name: Deploy to CI www
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-avzr"
|
||||
SOURCE: "ci-builds/"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
@@ -0,0 +1,19 @@
|
||||
name: ci-crates-preflight
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'Cargo.toml'
|
||||
- '**/Cargo.toml'
|
||||
- 'tools/internal/check_publish_preflight.py'
|
||||
- '.github/workflows/ci-crates-preflight.yml'
|
||||
|
||||
jobs:
|
||||
preflight:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Preflight publish checks
|
||||
run: python3 tools/internal/check_publish_preflight.py
|
||||
@@ -57,7 +57,8 @@ jobs:
|
||||
|
||||
- name: Update workspace dependencies
|
||||
run: |
|
||||
sed -i '/path = /s/version = "${{ steps.current_version.outputs.version }}"/version = "${{ inputs.version }}"/g' Cargo.toml
|
||||
# Match any semver version on lines with `path = `, not just the current workspace version.
|
||||
sed -i '/path = /s/version = "[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"/version = "${{ inputs.version }}"/g' Cargo.toml
|
||||
|
||||
- name: Bump versions (local only)
|
||||
run: |
|
||||
|
||||
@@ -33,7 +33,11 @@ jobs:
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
# `--publish-as-is` skips version bumping since that's done in a separate CI job.
|
||||
- name: Preflight publish checks
|
||||
run: |
|
||||
python3 tools/internal/check_publish_preflight.py
|
||||
|
||||
# --publish-as-is skips version bumping since that's done in a separate CI job.
|
||||
- name: Publish
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
|
||||
@@ -19,6 +19,7 @@ jobs:
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
@@ -58,7 +59,9 @@ jobs:
|
||||
|
||||
- name: Update workspace dependencies
|
||||
run: |
|
||||
sed -i '/path = /s/version = "${{ steps.current_version.outputs.version }}"/version = "${{ inputs.version }}"/g' Cargo.toml
|
||||
# Match any semver version on lines with `path = `, not just the current workspace version.
|
||||
# This catches entries whose version has drifted (e.g. nym-sqlx-pool-guard at 1.2.0).
|
||||
sed -i '/path = /s/version = "[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"/version = "${{ inputs.version }}"/g' Cargo.toml
|
||||
|
||||
- name: Bump versions
|
||||
run: |
|
||||
@@ -68,9 +71,33 @@ jobs:
|
||||
|
||||
- name: Commit and push version bump
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE_BRANCH="${GITHUB_REF_NAME}"
|
||||
PR_BRANCH="ci/crates-version-bump-${{ inputs.version }}-${GITHUB_RUN_ID}"
|
||||
|
||||
git checkout -b "$PR_BRANCH"
|
||||
git add -A
|
||||
git commit -m "crates release: bump version to ${{ inputs.version }}"
|
||||
git push
|
||||
git push -u origin "$PR_BRANCH"
|
||||
|
||||
cat > /tmp/crates-version-bump-pr-body.md <<'EOF'
|
||||
This PR was created by CI because direct pushes to the release branch are blocked by branch protection rules.
|
||||
|
||||
## Summary
|
||||
- Bump workspace crate versions to the requested release version.
|
||||
- Update workspace dependency versions accordingly.
|
||||
|
||||
## Notes
|
||||
- Merge this PR to proceed with crates.io publishing.
|
||||
EOF
|
||||
|
||||
gh pr create \
|
||||
--base "$BASE_BRANCH" \
|
||||
--head "$PR_BRANCH" \
|
||||
--title "crates release: bump version to ${{ inputs.version }}" \
|
||||
--body-file /tmp/crates-version-bump-pr-body.md
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Show package versions
|
||||
run: cargo workspaces list --long
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
run: sudo apt-get install -y rsync
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.2.0
|
||||
uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
version: 9
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -61,6 +61,9 @@ jobs:
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/sdk && typedoc --skipErrorChecking
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/mix-fetch && typedoc --skipErrorChecking
|
||||
|
||||
- name: Verify doc versions
|
||||
run: ${{ github.workspace }}/documentation/scripts/verify-doc-versions.sh
|
||||
working-directory: ${{ github.workspace }}
|
||||
- name: Install project dependencies
|
||||
run: pnpm i
|
||||
- name: Generate llms-full.txt
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
find . -name Cargo.toml -exec cargo deny --manifest-path {} check \
|
||||
advisories -A advisory-not-detected --hide-inclusion-graph \; &> \
|
||||
>(uniq &> .github/workflows/support-files/notifications/deny.message )
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: report
|
||||
path: .github/workflows/support-files/notifications/deny.message
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
args: --workspace --release ${{ env.CARGO_FEATURES }}
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: my-artifact
|
||||
path: |
|
||||
|
||||
@@ -27,14 +27,14 @@ jobs:
|
||||
run: make contracts
|
||||
|
||||
- name: Upload Mixnet Contract Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: mixnet_contract.wasm
|
||||
path: contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm
|
||||
retention-days: 5
|
||||
|
||||
- name: Upload Vesting Contract Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: vesting_contract.wasm
|
||||
path: contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm
|
||||
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
cd -
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nym-wallet.app.tar.gz
|
||||
path: nym-wallet/target/x86_64-apple-darwin/release/bundle/macos/nym-wallet.app.tar.gz
|
||||
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nym-wallet-appimage.tar.gz
|
||||
path: |
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
|
||||
|
||||
- name: Setup MSBuild.exe
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
uses: microsoft/setup-msbuild@v3
|
||||
|
||||
# No cache:yarn here: setup-node needs yarn on PATH to populate the cache, but this runner
|
||||
# only gets yarn from the step below.
|
||||
@@ -165,7 +165,7 @@ jobs:
|
||||
find . -name "*.msi" -type f
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nym-wallet.msi
|
||||
path: |
|
||||
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
apk/nyms5-arch64-release.apk
|
||||
|
||||
- name: Upload APKs
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nyms5-apk-arch64
|
||||
path: |
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: Download binary artifact
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: nyms5-apk-arch64
|
||||
path: apk
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
with:
|
||||
release-tag-or-name-or-id: ${{ inputs.release_tag }}
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: Asset Hashes
|
||||
path: hashes.json
|
||||
|
||||
@@ -78,3 +78,4 @@ CLAUDE.md
|
||||
|
||||
/notes
|
||||
/target-otel
|
||||
test-tutorials/
|
||||
|
||||
Generated
+141
-123
File diff suppressed because it is too large
Load Diff
+125
-121
@@ -31,7 +31,6 @@ members = [
|
||||
"common/client-libs/mixnet-client",
|
||||
"common/client-libs/validator-client",
|
||||
"common/commands",
|
||||
"common/nym-common",
|
||||
"common/config",
|
||||
"common/cosmwasm-smart-contracts/coconut-dkg",
|
||||
"common/cosmwasm-smart-contracts/contracts-common",
|
||||
@@ -41,6 +40,7 @@ members = [
|
||||
"common/cosmwasm-smart-contracts/group-contract",
|
||||
"common/cosmwasm-smart-contracts/mixnet-contract",
|
||||
"common/cosmwasm-smart-contracts/multisig-contract",
|
||||
"common/cosmwasm-smart-contracts/node-families-contract",
|
||||
"common/cosmwasm-smart-contracts/nym-performance-contract",
|
||||
"common/cosmwasm-smart-contracts/nym-pool-contract",
|
||||
"common/cosmwasm-smart-contracts/vesting-contract",
|
||||
@@ -70,11 +70,14 @@ members = [
|
||||
"common/node-tester-utils",
|
||||
"common/nonexhaustive-delayqueue",
|
||||
"common/nym-cache",
|
||||
"common/nym-common",
|
||||
"common/nym-connection-monitor",
|
||||
"common/nym-id",
|
||||
"common/nym-kcp",
|
||||
"common/nym-lp",
|
||||
"common/nym-kkt",
|
||||
"common/nym-kkt-ciphersuite",
|
||||
"common/nym-kkt-context",
|
||||
"common/nym-lp",
|
||||
"common/nym-metrics",
|
||||
"common/nym_offline_compact_ecash",
|
||||
"common/nymnoise",
|
||||
@@ -90,9 +93,9 @@ members = [
|
||||
"common/nymsphinx/params",
|
||||
"common/nymsphinx/routing",
|
||||
"common/nymsphinx/types",
|
||||
"common/nyxd-scraper-sqlite",
|
||||
"common/nyxd-scraper-psql",
|
||||
"common/nyxd-scraper-shared",
|
||||
"common/nyxd-scraper-sqlite",
|
||||
"common/pemstore",
|
||||
"common/registration",
|
||||
"common/serde-helpers",
|
||||
@@ -122,6 +125,7 @@ members = [
|
||||
"common/zulip-client",
|
||||
"documentation/autodoc",
|
||||
"gateway",
|
||||
"integration-tests",
|
||||
"nym-api",
|
||||
"nym-api/nym-api-requests",
|
||||
"nym-authenticator-client",
|
||||
@@ -129,6 +133,7 @@ members = [
|
||||
"nym-credential-proxy/nym-credential-proxy",
|
||||
"nym-credential-proxy/nym-credential-proxy-requests",
|
||||
"nym-data-observatory",
|
||||
"nym-gateway-probe",
|
||||
"nym-ip-packet-client",
|
||||
"nym-network-monitor",
|
||||
"nym-node",
|
||||
@@ -140,6 +145,7 @@ members = [
|
||||
"nym-outfox",
|
||||
"nym-registration-client",
|
||||
"nym-signers-monitor",
|
||||
"nym-sqlx-pool-guard",
|
||||
"nym-statistics-api",
|
||||
"nym-validator-rewarder",
|
||||
"nyx-chain-watcher",
|
||||
@@ -147,19 +153,18 @@ members = [
|
||||
"sdk/ffi/go",
|
||||
"sdk/ffi/shared",
|
||||
"sdk/rust/nym-sdk",
|
||||
"smolmix/core",
|
||||
"service-providers/common",
|
||||
"service-providers/ip-packet-router",
|
||||
"service-providers/network-requester",
|
||||
"nym-sqlx-pool-guard",
|
||||
"smolmix/core",
|
||||
"tools/echo-server",
|
||||
"tools/internal/contract-state-importer/importer-cli",
|
||||
"tools/internal/contract-state-importer/importer-contract",
|
||||
"tools/internal/localnet-orchestrator",
|
||||
"tools/internal/localnet-orchestrator/dkg-bypass-contract",
|
||||
"tools/internal/mixnet-connectivity-check",
|
||||
# "tools/internal/sdk-version-bump",
|
||||
"tools/internal/ssl-inject",
|
||||
"tools/internal/localnet-orchestrator",
|
||||
"tools/internal/localnet-orchestrator/dkg-bypass-contract",
|
||||
"tools/internal/validator-status-check",
|
||||
"tools/nym-cli",
|
||||
"tools/nym-id-cli",
|
||||
@@ -172,27 +177,23 @@ members = [
|
||||
"wasm/mix-fetch",
|
||||
"wasm/node-tester",
|
||||
"wasm/zknym-lib",
|
||||
"nym-gateway-probe",
|
||||
"integration-tests",
|
||||
"common/nym-kkt-ciphersuite",
|
||||
"common/nym-kkt-context",
|
||||
]
|
||||
|
||||
default-members = [
|
||||
"clients/native",
|
||||
"clients/socks5",
|
||||
"nym-authenticator-client",
|
||||
"nym-api",
|
||||
"nym-authenticator-client",
|
||||
"nym-credential-proxy/nym-credential-proxy",
|
||||
"nym-node",
|
||||
"nym-registration-client",
|
||||
"nym-statistics-api",
|
||||
"nym-validator-rewarder",
|
||||
"nyx-chain-watcher",
|
||||
"service-providers/ip-packet-router",
|
||||
"service-providers/network-requester",
|
||||
"tools/internal/localnet-orchestrator",
|
||||
"tools/nymvisor",
|
||||
"nym-registration-client",
|
||||
"tools/internal/localnet-orchestrator"
|
||||
]
|
||||
|
||||
exclude = ["contracts", "nym-wallet", "cpu-cycles"]
|
||||
@@ -206,7 +207,7 @@ edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
rust-version = "1.87.0"
|
||||
readme = "README.md"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
addr = "0.15.6"
|
||||
@@ -229,7 +230,7 @@ base85rs = "0.1.3"
|
||||
bincode = "1.3.3"
|
||||
bip39 = { version = "2.0.0", features = ["zeroize"] }
|
||||
bitvec = "1.0.0"
|
||||
blake3 = "1.7.0"
|
||||
blake3 = ">=1.7, <1.8.4" # blake3 1.8.4+ requires digest 0.11; workspace is on 0.10
|
||||
bloomfilter = "3.0.1"
|
||||
bs58 = "0.5.1"
|
||||
bytecodec = "0.4.15"
|
||||
@@ -325,7 +326,7 @@ pnet_packet = "0.35.0"
|
||||
publicsuffix = "2.3.0"
|
||||
proc_pidinfo = "0.1.3"
|
||||
quote = "1"
|
||||
rand = "0.8.5"
|
||||
rand = "0.8.6"
|
||||
rand09 = { package = "rand", version = "=0.9.2" }
|
||||
rand_chacha = "0.3"
|
||||
rand_chacha09 = { package = "rand_chacha", version = "=0.9.0" }
|
||||
@@ -349,7 +350,6 @@ serde_yaml = "0.9.25"
|
||||
serde_plain = "1.0.2"
|
||||
sha2 = "0.10.3"
|
||||
si-scale = "0.2.3"
|
||||
smolmix = { version = "0.0.1", path = "smolmix/core" }
|
||||
smoltcp = "0.12"
|
||||
snow = "0.9.6"
|
||||
sphinx-packet = "=0.6.0"
|
||||
@@ -360,7 +360,7 @@ subtle-encoding = "0.5"
|
||||
syn = "2"
|
||||
sysinfo = "0.37.0"
|
||||
tap = "1.0.1"
|
||||
tar = "0.4.44"
|
||||
tar = "0.4.45"
|
||||
test-with = { version = "0.15.4", default-features = false }
|
||||
tempfile = "3.20"
|
||||
thiserror = "2.0"
|
||||
@@ -414,110 +414,113 @@ libcrux-sha3 = "0.0.8"
|
||||
libcrux-traits = "0.0.6"
|
||||
|
||||
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
|
||||
nym-api-requests = { version = "1.20.4", path = "nym-api/nym-api-requests" }
|
||||
nym-authenticator-requests = { version = "1.20.4", path = "common/authenticator-requests" }
|
||||
nym-async-file-watcher = { version = "1.20.4", path = "common/async-file-watcher" }
|
||||
nym-authenticator-client = { version = "1.20.4", path = "nym-authenticator-client" }
|
||||
nym-bandwidth-controller = { version = "1.20.4", path = "common/bandwidth-controller" }
|
||||
nym-bin-common = { version = "1.20.4", path = "common/bin-common" }
|
||||
nym-cache = { version = "1.20.4", path = "common/nym-cache" }
|
||||
nym-client-core = { version = "1.20.4", path = "common/client-core", default-features = false }
|
||||
nym-client-core-config-types = { version = "1.20.4", path = "common/client-core/config-types" }
|
||||
nym-client-core-gateways-storage = { version = "1.20.4", path = "common/client-core/gateways-storage" }
|
||||
nym-client-core-surb-storage = { version = "1.20.4", path = "common/client-core/surb-storage" }
|
||||
nym-client-websocket-requests = { version = "1.20.4", path = "clients/native/websocket-requests" }
|
||||
nym-common = { version = "1.20.4", path = "common/nym-common" }
|
||||
nym-compact-ecash = { version = "1.20.4", path = "common/nym_offline_compact_ecash" }
|
||||
nym-config = { version = "1.20.4", path = "common/config" }
|
||||
nym-contracts-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-coconut-dkg-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-credential-storage = { version = "1.20.4", path = "common/credential-storage" }
|
||||
nym-credential-utils = { version = "1.20.4", path = "common/credential-utils" }
|
||||
nym-credential-proxy-lib = { version = "1.20.4", path = "common/credential-proxy" }
|
||||
nym-credentials = { version = "1.20.4", path = "common/credentials", default-features = false }
|
||||
nym-credentials-interface = { version = "1.20.4", path = "common/credentials-interface" }
|
||||
nym-credential-proxy-requests = { version = "1.20.4", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
|
||||
nym-credential-verification = { version = "1.20.4", path = "common/credential-verification" }
|
||||
nym-crypto = { version = "1.20.4", path = "common/crypto", default-features = false }
|
||||
nym-dkg = { version = "1.20.4", path = "common/dkg" }
|
||||
nym-ecash-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-ecash-signer-check = { version = "1.20.4", path = "common/ecash-signer-check" }
|
||||
nym-ecash-signer-check-types = { version = "1.20.4", path = "common/ecash-signer-check-types" }
|
||||
nym-ecash-time = { version = "1.20.4", path = "common/ecash-time" }
|
||||
nym-exit-policy = { version = "1.20.4", path = "common/exit-policy" }
|
||||
nym-ffi-shared = { version = "1.20.4", path = "sdk/ffi/shared" }
|
||||
nym-gateway-client = { version = "1.20.4", path = "common/client-libs/gateway-client", default-features = false }
|
||||
nym-api-requests = { version = "1.21.0", path = "nym-api/nym-api-requests" }
|
||||
nym-authenticator-requests = { version = "1.21.0", path = "common/authenticator-requests" }
|
||||
nym-async-file-watcher = { version = "1.21.0", path = "common/async-file-watcher" }
|
||||
nym-authenticator-client = { version = "1.21.0", path = "nym-authenticator-client" }
|
||||
nym-bandwidth-controller = { version = "1.21.0", path = "common/bandwidth-controller" }
|
||||
nym-bin-common = { version = "1.21.0", path = "common/bin-common" }
|
||||
nym-cache = { version = "1.21.0", path = "common/nym-cache" }
|
||||
nym-client-core = { version = "1.21.0", path = "common/client-core", default-features = false }
|
||||
nym-client-core-config-types = { version = "1.21.0", path = "common/client-core/config-types" }
|
||||
nym-client-core-gateways-storage = { version = "1.21.0", path = "common/client-core/gateways-storage" }
|
||||
nym-client-core-surb-storage = { version = "1.21.0", path = "common/client-core/surb-storage" }
|
||||
nym-client-websocket-requests = { version = "1.21.0", path = "clients/native/websocket-requests" }
|
||||
nym-common = { version = "1.21.0", path = "common/nym-common" }
|
||||
nym-compact-ecash = { version = "1.21.0", path = "common/nym_offline_compact_ecash" }
|
||||
nym-config = { version = "1.21.0", path = "common/config" }
|
||||
nym-contracts-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-coconut-dkg-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-credential-storage = { version = "1.21.0", path = "common/credential-storage" }
|
||||
nym-credential-utils = { version = "1.21.0", path = "common/credential-utils" }
|
||||
nym-credential-proxy-lib = { version = "1.21.0", path = "common/credential-proxy" }
|
||||
nym-credentials = { version = "1.21.0", path = "common/credentials", default-features = false }
|
||||
nym-credentials-interface = { version = "1.21.0", path = "common/credentials-interface" }
|
||||
nym-credential-proxy-requests = { version = "1.21.0", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
|
||||
nym-credential-verification = { version = "1.21.0", path = "common/credential-verification" }
|
||||
nym-crypto = { version = "1.21.0", path = "common/crypto", default-features = false }
|
||||
nym-dkg = { version = "1.21.0", path = "common/dkg" }
|
||||
nym-ecash-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-ecash-signer-check = { version = "1.21.0", path = "common/ecash-signer-check" }
|
||||
nym-ecash-signer-check-types = { version = "1.21.0", path = "common/ecash-signer-check-types" }
|
||||
nym-ecash-time = { version = "1.21.0", path = "common/ecash-time" }
|
||||
nym-exit-policy = { version = "1.21.0", path = "common/exit-policy" }
|
||||
nym-ffi-shared = { version = "1.21.0", path = "sdk/ffi/shared" }
|
||||
nym-gateway-client = { version = "1.21.0", path = "common/client-libs/gateway-client", default-features = false }
|
||||
nym-gateway-probe = { version = "1.18.0", path = "nym-gateway-probe" }
|
||||
nym-gateway-requests = { version = "1.20.4", path = "common/gateway-requests" }
|
||||
nym-gateway-storage = { version = "1.20.4", path = "common/gateway-storage" }
|
||||
nym-gateway-stats-storage = { version = "1.20.4", path = "common/gateway-stats-storage" }
|
||||
nym-group-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-http-api-client = { version = "1.20.4", path = "common/http-api-client" }
|
||||
nym-http-api-client-macro = { version = "1.20.4", path = "common/http-api-client-macro" }
|
||||
nym-http-api-common = { version = "1.20.4", path = "common/http-api-common", default-features = false }
|
||||
nym-id = { version = "1.20.4", path = "common/nym-id" }
|
||||
nym-ip-packet-client = { version = "1.20.4", path = "nym-ip-packet-client" }
|
||||
nym-ip-packet-requests = { version = "1.20.4", path = "common/ip-packet-requests" }
|
||||
nym-lp = { version = "1.20.4", path = "common/nym-lp" }
|
||||
nym-kkt = { version = "0.1.0", path = "common/nym-kkt" }
|
||||
nym-kkt-ciphersuite = { version = "1.20.4", path = "common/nym-kkt-ciphersuite" }
|
||||
nym-kkt-context = { version = "1.20.4", path = "common/nym-kkt-context" }
|
||||
nym-metrics = { version = "1.20.4", path = "common/nym-metrics" }
|
||||
nym-mixnet-client = { version = "1.20.4", path = "common/client-libs/mixnet-client" }
|
||||
nym-mixnet-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-multisig-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-network-defaults = { version = "1.20.4", path = "common/network-defaults" }
|
||||
nym-node-tester-utils = { version = "1.20.4", path = "common/node-tester-utils" }
|
||||
nym-noise = { version = "1.20.4", path = "common/nymnoise" }
|
||||
nym-noise-keys = { version = "1.20.4", path = "common/nymnoise/keys" }
|
||||
nym-nonexhaustive-delayqueue = { version = "1.20.4", path = "common/nonexhaustive-delayqueue" }
|
||||
nym-node-requests = { version = "1.20.4", path = "nym-node/nym-node-requests", default-features = false }
|
||||
nym-node-metrics = { version = "1.20.4", path = "nym-node/nym-node-metrics" }
|
||||
nym-ordered-buffer = { version = "1.20.4", path = "common/socks5/ordered-buffer" }
|
||||
nym-outfox = { version = "1.20.4", path = "nym-outfox" }
|
||||
nym-registration-common = { version = "1.20.4", path = "common/registration" }
|
||||
nym-pemstore = { version = "1.20.4", path = "common/pemstore" }
|
||||
nym-performance-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-sdk = { version = "1.20.4", path = "sdk/rust/nym-sdk" }
|
||||
nym-serde-helpers = { version = "1.20.4", path = "common/serde-helpers" }
|
||||
nym-service-providers-common = { version = "1.20.4", path = "service-providers/common" }
|
||||
nym-service-provider-requests-common = { version = "1.20.4", path = "common/service-provider-requests-common" }
|
||||
nym-socks5-client-core = { version = "1.20.4", path = "common/socks5-client-core" }
|
||||
nym-socks5-proxy-helpers = { version = "1.20.4", path = "common/socks5/proxy-helpers" }
|
||||
nym-socks5-requests = { version = "1.20.4", path = "common/socks5/requests" }
|
||||
nym-sphinx = { version = "1.20.4", path = "common/nymsphinx" }
|
||||
nym-sphinx-acknowledgements = { version = "1.20.4", path = "common/nymsphinx/acknowledgements" }
|
||||
nym-sphinx-addressing = { version = "1.20.4", path = "common/nymsphinx/addressing" }
|
||||
nym-sphinx-anonymous-replies = { version = "1.20.4", path = "common/nymsphinx/anonymous-replies" }
|
||||
nym-sphinx-chunking = { version = "1.20.4", path = "common/nymsphinx/chunking" }
|
||||
nym-sphinx-cover = { version = "1.20.4", path = "common/nymsphinx/cover" }
|
||||
nym-sphinx-forwarding = { version = "1.20.4", path = "common/nymsphinx/forwarding" }
|
||||
nym-sphinx-framing = { version = "1.20.4", path = "common/nymsphinx/framing" }
|
||||
nym-sphinx-params = { version = "1.20.4", path = "common/nymsphinx/params" }
|
||||
nym-sphinx-routing = { version = "1.20.4", path = "common/nymsphinx/routing" }
|
||||
nym-sphinx-types = { version = "1.20.4", path = "common/nymsphinx/types" }
|
||||
nym-statistics-common = { version = "1.20.4", path = "common/statistics" }
|
||||
nym-store-cipher = { version = "1.20.4", path = "common/store-cipher" }
|
||||
nym-task = { version = "1.20.4", path = "common/task" }
|
||||
nym-tun = { version = "1.20.4", path = "common/tun" }
|
||||
nym-test-utils = { version = "1.20.4", path = "common/test-utils" }
|
||||
nym-ticketbooks-merkle = { version = "1.20.4", path = "common/ticketbooks-merkle" }
|
||||
nym-topology = { version = "1.20.4", path = "common/topology" }
|
||||
nym-types = { version = "1.20.4", path = "common/types" }
|
||||
nym-upgrade-mode-check = { version = "1.20.4", path = "common/upgrade-mode-check" }
|
||||
nym-validator-client = { version = "1.20.4", path = "common/client-libs/validator-client", default-features = false }
|
||||
nym-vesting-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
nym-verloc = { version = "1.20.4", path = "common/verloc" }
|
||||
nym-wireguard = { version = "1.20.4", path = "common/wireguard" }
|
||||
nym-wireguard-types = { version = "1.20.4", path = "common/wireguard-types" }
|
||||
nym-wireguard-private-metadata-shared = { version = "1.20.4", path = "common/wireguard-private-metadata/shared" }
|
||||
nym-wireguard-private-metadata-client = { version = "1.20.4", path = "common/wireguard-private-metadata/client" }
|
||||
nym-wireguard-private-metadata-server = { version = "1.20.4", path = "common/wireguard-private-metadata/server" }
|
||||
nym-gateway-requests = { version = "1.21.0", path = "common/gateway-requests" }
|
||||
nym-gateway-storage = { version = "1.21.0", path = "common/gateway-storage" }
|
||||
nym-gateway-stats-storage = { version = "1.21.0", path = "common/gateway-stats-storage" }
|
||||
nym-group-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-http-api-client = { version = "1.21.0", path = "common/http-api-client" }
|
||||
nym-http-api-client-macro = { version = "1.21.0", path = "common/http-api-client-macro" }
|
||||
nym-http-api-common = { version = "1.21.0", path = "common/http-api-common", default-features = false }
|
||||
nym-id = { version = "1.21.0", path = "common/nym-id" }
|
||||
nym-ip-packet-client = { version = "1.21.0", path = "nym-ip-packet-client" }
|
||||
nym-ip-packet-requests = { version = "1.21.0", path = "common/ip-packet-requests" }
|
||||
nym-lp = { version = "1.21.0", path = "common/nym-lp" }
|
||||
nym-kkt = { version = "1.21.0", path = "common/nym-kkt" }
|
||||
nym-kkt-ciphersuite = { version = "1.21.0", path = "common/nym-kkt-ciphersuite" }
|
||||
nym-kkt-context = { version = "1.21.0", path = "common/nym-kkt-context" }
|
||||
nym-metrics = { version = "1.21.0", path = "common/nym-metrics" }
|
||||
nym-mixnet-client = { version = "1.21.0", path = "common/client-libs/mixnet-client" }
|
||||
nym-mixnet-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-multisig-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-network-defaults = { version = "1.21.0", path = "common/network-defaults" }
|
||||
nym-node-tester-utils = { version = "1.21.0", path = "common/node-tester-utils" }
|
||||
nym-noise = { version = "1.21.0", path = "common/nymnoise" }
|
||||
nym-noise-keys = { version = "1.21.0", path = "common/nymnoise/keys" }
|
||||
nym-nonexhaustive-delayqueue = { version = "1.21.0", path = "common/nonexhaustive-delayqueue" }
|
||||
nym-node-requests = { version = "1.21.0", path = "nym-node/nym-node-requests", default-features = false }
|
||||
nym-node-metrics = { version = "1.21.0", path = "nym-node/nym-node-metrics" }
|
||||
nym-node-families-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/node-families-contract" }
|
||||
nym-ordered-buffer = { version = "1.21.0", path = "common/socks5/ordered-buffer" }
|
||||
nym-outfox = { version = "1.21.0", path = "nym-outfox" }
|
||||
nym-registration-common = { version = "1.21.0", path = "common/registration" }
|
||||
nym-pemstore = { version = "1.21.0", path = "common/pemstore" }
|
||||
nym-performance-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-sdk = { version = "1.21.0", path = "sdk/rust/nym-sdk" }
|
||||
nym-serde-helpers = { version = "1.21.0", path = "common/serde-helpers" }
|
||||
nym-service-providers-common = { version = "1.21.0", path = "service-providers/common" }
|
||||
nym-service-provider-requests-common = { version = "1.21.0", path = "common/service-provider-requests-common" }
|
||||
nym-socks5-client-core = { version = "1.21.0", path = "common/socks5-client-core" }
|
||||
nym-socks5-proxy-helpers = { version = "1.21.0", path = "common/socks5/proxy-helpers" }
|
||||
nym-socks5-requests = { version = "1.21.0", path = "common/socks5/requests" }
|
||||
nym-sphinx = { version = "1.21.0", path = "common/nymsphinx" }
|
||||
nym-sphinx-acknowledgements = { version = "1.21.0", path = "common/nymsphinx/acknowledgements" }
|
||||
nym-sphinx-addressing = { version = "1.21.0", path = "common/nymsphinx/addressing" }
|
||||
nym-sphinx-anonymous-replies = { version = "1.21.0", path = "common/nymsphinx/anonymous-replies" }
|
||||
nym-sphinx-chunking = { version = "1.21.0", path = "common/nymsphinx/chunking" }
|
||||
nym-sphinx-cover = { version = "1.21.0", path = "common/nymsphinx/cover" }
|
||||
nym-sphinx-forwarding = { version = "1.21.0", path = "common/nymsphinx/forwarding" }
|
||||
nym-sphinx-framing = { version = "1.21.0", path = "common/nymsphinx/framing" }
|
||||
nym-sphinx-params = { version = "1.21.0", path = "common/nymsphinx/params" }
|
||||
nym-sphinx-routing = { version = "1.21.0", path = "common/nymsphinx/routing" }
|
||||
nym-sphinx-types = { version = "1.21.0", path = "common/nymsphinx/types" }
|
||||
nym-statistics-common = { version = "1.21.0", path = "common/statistics" }
|
||||
nym-store-cipher = { version = "1.21.0", path = "common/store-cipher" }
|
||||
nym-task = { version = "1.21.0", path = "common/task" }
|
||||
nym-tun = { version = "1.21.0", path = "common/tun" }
|
||||
nym-test-utils = { version = "1.21.0", path = "common/test-utils" }
|
||||
nym-ticketbooks-merkle = { version = "1.21.0", path = "common/ticketbooks-merkle" }
|
||||
nym-topology = { version = "1.21.0", path = "common/topology" }
|
||||
nym-types = { version = "1.21.0", path = "common/types" }
|
||||
nym-upgrade-mode-check = { version = "1.21.0", path = "common/upgrade-mode-check" }
|
||||
nym-validator-client = { version = "1.21.0", path = "common/client-libs/validator-client", default-features = false }
|
||||
nym-vesting-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
nym-verloc = { version = "1.21.0", path = "common/verloc" }
|
||||
nym-wireguard = { version = "1.21.0", path = "common/wireguard" }
|
||||
nym-wireguard-types = { version = "1.21.0", path = "common/wireguard-types" }
|
||||
nym-wireguard-private-metadata-shared = { version = "1.21.0", path = "common/wireguard-private-metadata/shared" }
|
||||
nym-wireguard-private-metadata-client = { version = "1.21.0", path = "common/wireguard-private-metadata/client" }
|
||||
nym-wireguard-private-metadata-server = { version = "1.21.0", path = "common/wireguard-private-metadata/server" }
|
||||
nym-sqlx-pool-guard = { version = "1.2.0", path = "nym-sqlx-pool-guard" }
|
||||
nym-wasm-client-core = { version = "1.20.4", path = "common/wasm/client-core" }
|
||||
nym-wasm-storage = { version = "1.20.4", path = "common/wasm/storage" }
|
||||
nym-wasm-utils = { version = "1.20.4", path = "common/wasm/utils", default-features = false }
|
||||
nyxd-scraper-shared = { version = "1.20.4", path = "common/nyxd-scraper-shared" }
|
||||
nym-wasm-client-core = { version = "1.21.0", path = "common/wasm/client-core" }
|
||||
nym-wasm-storage = { version = "1.21.0", path = "common/wasm/storage" }
|
||||
nym-wasm-utils = { version = "1.21.0", path = "common/wasm/utils", default-features = false }
|
||||
nyxd-scraper-shared = { version = "1.21.0", path = "common/nyxd-scraper-shared" }
|
||||
|
||||
smolmix = { version = "1.21.0", path = "smolmix/core" }
|
||||
|
||||
# coconut/DKG related
|
||||
# unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork
|
||||
@@ -616,3 +619,4 @@ exit = "deny"
|
||||
panic = "deny"
|
||||
unimplemented = "deny"
|
||||
unreachable = "deny"
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Mitigation playbook for CopyFail (CVE-2026-31431) and DirtyFrag (CVE-2026-43284 / CVE-2026-43500)
|
||||
# This playbook applies interim module blacklists only
|
||||
# Kernel patches are not yet available (May 2026)
|
||||
# Once patched kernels ship, use remove_kernel_CVE_mitigations.yml to reverse everything
|
||||
# This playbook is idempotent - safe to re-run if mitigations were already applied
|
||||
|
||||
- name: Mitigate Copy Fail + Dirty Frag
|
||||
hosts: all
|
||||
become: true
|
||||
tasks:
|
||||
- name: Blacklist algif_aead (Copy Fail)
|
||||
copy:
|
||||
dest: /etc/modprobe.d/disable-algif_aead.conf
|
||||
content: "install algif_aead /bin/false\n"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: Blacklist esp4, esp6, rxrpc (Dirty Frag)
|
||||
copy:
|
||||
dest: /etc/modprobe.d/dirtyfrag.conf
|
||||
content: |
|
||||
install esp4 /bin/false
|
||||
install esp6 /bin/false
|
||||
install rxrpc /bin/false
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: Unload all affected modules
|
||||
modprobe:
|
||||
name: "{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- algif_aead
|
||||
- esp4
|
||||
- esp6
|
||||
- rxrpc
|
||||
ignore_errors: true
|
||||
|
||||
- name: Drop page cache to clear any contamination
|
||||
shell: echo 3 > /proc/sys/vm/drop_caches
|
||||
@@ -0,0 +1,111 @@
|
||||
############################################################################################
|
||||
############################################################################################
|
||||
############################################################################################
|
||||
#### THIS PLAYBOOK IS NOT MEANT TO BE RUN YET, IT IS NOT REFERRED IN ANY DOCUMENTATION! ####
|
||||
############################################################################################
|
||||
############################################################################################
|
||||
############################################################################################
|
||||
#
|
||||
# Reversal playbook for mitigate_kernel_CVE.yml (CopyFail CVE-2026-31431 / DirtyFrag CVE-2026-43284 / CVE-2026-43500).
|
||||
#
|
||||
# Run this AFTER your distro has shipped the patched kernel.
|
||||
# This playbook:
|
||||
# 1. Updates the kernel via apt
|
||||
# 2. Reboots and waits for reconnect
|
||||
# 3. Verifies the running kernel is newer than the pre-patch version
|
||||
# 4. Removes the interim module blacklists
|
||||
# 5. Re-enables the affected modules live (no second reboot needed)
|
||||
#
|
||||
# Debian family only (Debian, Ubuntu). Tested on Debian 11, Debian 12, Ubuntu 20.04, 22.04, 24.04.
|
||||
#
|
||||
# For exit-gateway nodes with --wireguard-enabled true:
|
||||
# After this playbook completes, run the networking restore step on each node via:
|
||||
# ansible-playbook deploy.yml -t ntm
|
||||
# See the CVE patch documentation for details.
|
||||
|
||||
- name: Remove CVE mitigations and apply patched kernel
|
||||
hosts: all
|
||||
become: true
|
||||
|
||||
tasks:
|
||||
- name: Verify OS is Debian family
|
||||
assert:
|
||||
that:
|
||||
- ansible_os_family == "Debian"
|
||||
fail_msg: "This playbook supports Debian-family distros only (Debian, Ubuntu). For other distros, apply the kernel update and mitigation removal manually."
|
||||
|
||||
- name: Update apt cache
|
||||
apt:
|
||||
update_cache: true
|
||||
cache_valid_time: 0
|
||||
|
||||
- name: Upgrade kernel packages
|
||||
apt:
|
||||
upgrade: full
|
||||
only_upgrade: false
|
||||
register: apt_upgrade_result
|
||||
|
||||
- name: Record pre-reboot kernel version
|
||||
command: uname -r
|
||||
register: kernel_before
|
||||
changed_when: false
|
||||
|
||||
- name: Reboot to load patched kernel
|
||||
reboot:
|
||||
msg: "Rebooting to apply patched kernel (CVE-2026-31431 / CVE-2026-43284 / CVE-2026-43500)"
|
||||
reboot_timeout: 300
|
||||
pre_reboot_delay: 5
|
||||
post_reboot_delay: 15
|
||||
|
||||
- name: Record post-reboot kernel version
|
||||
command: uname -r
|
||||
register: kernel_after
|
||||
changed_when: false
|
||||
|
||||
- name: Show kernel versions before and after reboot
|
||||
debug:
|
||||
msg:
|
||||
- "Kernel before reboot: {{ kernel_before.stdout }}"
|
||||
- "Kernel after reboot: {{ kernel_after.stdout }}"
|
||||
|
||||
- name: Warn if kernel did not change after reboot
|
||||
debug:
|
||||
msg: >
|
||||
WARNING: kernel version did not change after reboot ({{ kernel_after.stdout }}).
|
||||
The patched kernel may not have been selected by GRUB, or no kernel update was available.
|
||||
Do NOT remove the interim mitigations until you have confirmed the running kernel is patched.
|
||||
Check: apt-cache policy linux-image-amd64 # Debian
|
||||
Check: apt-cache policy linux-image-generic # Ubuntu
|
||||
when: kernel_before.stdout == kernel_after.stdout
|
||||
|
||||
- name: Remove algif_aead blacklist
|
||||
file:
|
||||
path: /etc/modprobe.d/disable-algif_aead.conf
|
||||
state: absent
|
||||
|
||||
- name: Remove DirtyFrag blacklist (esp4, esp6, rxrpc)
|
||||
file:
|
||||
path: /etc/modprobe.d/dirtyfrag.conf
|
||||
state: absent
|
||||
|
||||
- name: Re-enable affected modules live
|
||||
modprobe:
|
||||
name: "{{ item }}"
|
||||
state: present
|
||||
loop:
|
||||
- esp4
|
||||
- esp6
|
||||
- rxrpc
|
||||
- algif_aead
|
||||
ignore_errors: true
|
||||
|
||||
- name: Confirm nym-node service is still running
|
||||
systemd:
|
||||
name: nym-node
|
||||
state: started
|
||||
register: nym_node_status
|
||||
failed_when: false
|
||||
|
||||
- name: Show nym-node status
|
||||
debug:
|
||||
msg: "nym-node service state: {{ nym_node_status.state | default('unknown - service may not exist on this node') }}"
|
||||
@@ -472,6 +472,7 @@ impl Handler {
|
||||
fn prepare_reconstructed_binary(
|
||||
reconstructed_messages: Vec<ReconstructedMessage>,
|
||||
) -> Vec<Result<WsMessage, WsError>> {
|
||||
#[allow(clippy::result_large_err)] // TODO : remove this once tungstenite is updated
|
||||
reconstructed_messages
|
||||
.into_iter()
|
||||
.map(ServerResponse::Received)
|
||||
@@ -484,6 +485,7 @@ fn prepare_reconstructed_binary(
|
||||
fn prepare_reconstructed_text(
|
||||
reconstructed_messages: Vec<ReconstructedMessage>,
|
||||
) -> Vec<Result<WsMessage, WsError>> {
|
||||
#[allow(clippy::result_large_err)] // TODO : remove this once tungstenite is updated
|
||||
reconstructed_messages
|
||||
.into_iter()
|
||||
.map(ServerResponse::Received)
|
||||
|
||||
@@ -439,7 +439,7 @@ where
|
||||
let mut pending_acks = Vec::with_capacity(fragments.len());
|
||||
let mut to_forward: HashMap<_, Vec<_>> = HashMap::new();
|
||||
|
||||
for (raw, prepared) in fragments.into_iter().zip(prepared_fragments.into_iter()) {
|
||||
for (raw, prepared) in fragments.into_iter().zip(prepared_fragments) {
|
||||
let lane = raw.0;
|
||||
let FragmentWithMaxRetransmissions {
|
||||
fragment,
|
||||
@@ -670,7 +670,7 @@ where
|
||||
|
||||
Ok(fragments
|
||||
.into_iter()
|
||||
.zip(reply_surbs.into_iter())
|
||||
.zip(reply_surbs)
|
||||
.map(|(fragment, reply_surb)| {
|
||||
// unwrap here is fine as we know we have a valid topology
|
||||
#[allow(clippy::unwrap_used)]
|
||||
|
||||
@@ -26,6 +26,7 @@ nym-ecash-contract-common = { workspace = true }
|
||||
nym-multisig-contract-common = { workspace = true }
|
||||
nym-group-contract-common = { workspace = true }
|
||||
nym-performance-contract-common = { workspace = true }
|
||||
nym-node-families-contract-common = { workspace = true }
|
||||
nym-serde-helpers = { workspace = true, features = ["hex", "base64"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -15,6 +15,7 @@ use nym_api_requests::ecash::models::{
|
||||
VerifyEcashTicketBody,
|
||||
};
|
||||
use nym_api_requests::ecash::VerificationKeyResponse;
|
||||
use nym_api_requests::models::node_families::NodeFamily;
|
||||
use nym_api_requests::models::{
|
||||
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
|
||||
ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
|
||||
@@ -389,6 +390,45 @@ pub trait NymApiClientExt: ApiClient {
|
||||
Ok(bonds)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn get_node_families(
|
||||
&self,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
) -> Result<PaginatedResponse<NodeFamily>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
if let Some(page) = page {
|
||||
params.push(("page", page.to_string()))
|
||||
}
|
||||
if let Some(per_page) = per_page {
|
||||
params.push(("per_page", per_page.to_string()))
|
||||
}
|
||||
self.get_json(
|
||||
&[routes::V1_API_VERSION, routes::NODE_FAMILIES_ROUTES],
|
||||
¶ms,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_node_families(&self) -> Result<Vec<NodeFamily>, NymAPIError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut families = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut res = self.get_node_families(Some(page), None).await?;
|
||||
|
||||
families.append(&mut res.data);
|
||||
if families.len() < res.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(families)
|
||||
}
|
||||
|
||||
#[deprecated]
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNodeV1>, NymAPIError> {
|
||||
|
||||
@@ -38,6 +38,7 @@ pub mod ecash {
|
||||
}
|
||||
|
||||
pub const NYM_NODES_ROUTES: &str = "nym-nodes";
|
||||
pub const NODE_FAMILIES_ROUTES: &str = "node-families";
|
||||
|
||||
pub use nym_nodes::*;
|
||||
pub mod nym_nodes {
|
||||
|
||||
@@ -867,6 +867,10 @@ mod tests {
|
||||
MixnetExecuteMsg::TestingResolveAllPendingEvents { .. } => {
|
||||
client.testing_resolve_all_pending_events(None).ignore()
|
||||
}
|
||||
// not expected to be exposed by the client
|
||||
ExecuteMsg::AdminMigrateVestedMixNode { .. }
|
||||
| ExecuteMsg::AdminMigrateVestedDelegation { .. }
|
||||
| ExecuteMsg::AdminBatchMigrateVestedDelegations { .. } => ().ignore(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ pub mod ecash_query_client;
|
||||
pub mod group_query_client;
|
||||
pub mod mixnet_query_client;
|
||||
pub mod multisig_query_client;
|
||||
pub mod node_families_query_client;
|
||||
pub mod performance_query_client;
|
||||
pub mod vesting_query_client;
|
||||
|
||||
@@ -22,6 +23,7 @@ pub mod ecash_signing_client;
|
||||
pub mod group_signing_client;
|
||||
pub mod mixnet_signing_client;
|
||||
pub mod multisig_signing_client;
|
||||
pub mod node_families_signing_client;
|
||||
pub mod performance_signing_client;
|
||||
pub mod vesting_signing_client;
|
||||
|
||||
@@ -31,6 +33,7 @@ pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient};
|
||||
pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient};
|
||||
pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient};
|
||||
pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient};
|
||||
pub use node_families_query_client::{NodeFamiliesQueryClient, PagedNodeFamiliesQueryClient};
|
||||
pub use performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient};
|
||||
pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient};
|
||||
|
||||
@@ -40,6 +43,7 @@ pub use ecash_signing_client::EcashSigningClient;
|
||||
pub use group_signing_client::GroupSigningClient;
|
||||
pub use mixnet_signing_client::MixnetSigningClient;
|
||||
pub use multisig_signing_client::MultisigSigningClient;
|
||||
pub use node_families_signing_client::NodeFamiliesSigningClient;
|
||||
pub use performance_signing_client::PerformanceSigningClient;
|
||||
pub use vesting_signing_client::VestingSigningClient;
|
||||
|
||||
@@ -49,6 +53,7 @@ pub trait NymContractsProvider {
|
||||
fn mixnet_contract_address(&self) -> Option<&AccountId>;
|
||||
fn vesting_contract_address(&self) -> Option<&AccountId>;
|
||||
fn performance_contract_address(&self) -> Option<&AccountId>;
|
||||
fn node_families_contract_address(&self) -> Option<&AccountId>;
|
||||
|
||||
// coconut-related
|
||||
fn ecash_contract_address(&self) -> Option<&AccountId>;
|
||||
@@ -62,6 +67,7 @@ pub struct TypedNymContracts {
|
||||
pub mixnet_contract_address: Option<AccountId>,
|
||||
pub vesting_contract_address: Option<AccountId>,
|
||||
pub performance_contract_address: Option<AccountId>,
|
||||
pub node_families_contract_address: Option<AccountId>,
|
||||
|
||||
pub ecash_contract_address: Option<AccountId>,
|
||||
pub group_contract_address: Option<AccountId>,
|
||||
@@ -86,6 +92,10 @@ impl TryFrom<NymContracts> for TypedNymContracts {
|
||||
.performance_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
.transpose()?,
|
||||
node_families_contract_address: value
|
||||
.node_families_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
.transpose()?,
|
||||
ecash_contract_address: value
|
||||
.ecash_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
|
||||
+441
@@ -0,0 +1,441 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::collect_paged;
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::CosmWasmClient;
|
||||
use async_trait::async_trait;
|
||||
use cosmrs::AccountId;
|
||||
use serde::Deserialize;
|
||||
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
pub use nym_node_families_contract_common::{
|
||||
msg::QueryMsg as NodeFamiliesQueryMsg, AllFamilyMembersPagedResponse,
|
||||
AllPastFamilyInvitationsPagedResponse, FamiliesPagedResponse, FamilyMemberRecord,
|
||||
FamilyMembersPagedResponse, GlobalPastFamilyInvitationCursor, NodeFamily,
|
||||
NodeFamilyByNameResponse, NodeFamilyByOwnerResponse, NodeFamilyId,
|
||||
NodeFamilyMembershipResponse, NodeFamilyResponse, PastFamilyInvitation,
|
||||
PastFamilyInvitationCursor, PastFamilyInvitationForNodeCursor,
|
||||
PastFamilyInvitationsForNodePagedResponse, PastFamilyInvitationsPagedResponse,
|
||||
PastFamilyMember, PastFamilyMemberCursor, PastFamilyMemberForNodeCursor,
|
||||
PastFamilyMembersForNodePagedResponse, PastFamilyMembersPagedResponse,
|
||||
PendingFamilyInvitationDetails, PendingFamilyInvitationResponse,
|
||||
PendingFamilyInvitationsPagedResponse, PendingInvitationsForNodePagedResponse,
|
||||
PendingInvitationsPagedResponse,
|
||||
};
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait NodeFamiliesQueryClient {
|
||||
async fn query_node_families_contract<T>(
|
||||
&self,
|
||||
query: NodeFamiliesQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>;
|
||||
|
||||
async fn get_family_by_id(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<NodeFamilyResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyById { family_id })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_family_by_owner(
|
||||
&self,
|
||||
owner: &AccountId,
|
||||
) -> Result<NodeFamilyByOwnerResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyByOwner {
|
||||
owner: owner.to_string(),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_family_by_name(
|
||||
&self,
|
||||
name: String,
|
||||
) -> Result<NodeFamilyByNameResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyByName { name })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_families_paged(
|
||||
&self,
|
||||
start_after: Option<NodeFamilyId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<FamiliesPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamiliesPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_family_membership(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<NodeFamilyMembershipResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyMembership { node_id })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_family_members_paged(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<FamilyMembersPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyMembersPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_family_members_paged(
|
||||
&self,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<AllFamilyMembersPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetAllFamilyMembersPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_pending_invitation(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
) -> Result<PendingFamilyInvitationResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPendingInvitation {
|
||||
family_id,
|
||||
node_id,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_pending_invitations_for_family_paged(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PendingFamilyInvitationsPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(
|
||||
NodeFamiliesQueryMsg::GetPendingInvitationsForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_pending_invitations_for_node_paged(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
start_after: Option<NodeFamilyId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PendingInvitationsForNodePagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPendingInvitationsForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_pending_invitations_paged(
|
||||
&self,
|
||||
start_after: Option<(NodeFamilyId, NodeId)>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PendingInvitationsPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetAllPendingInvitationsPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_past_invitations_for_family_paged(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<PastFamilyInvitationCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PastFamilyInvitationsPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastInvitationsForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_past_invitations_for_node_paged(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
start_after: Option<PastFamilyInvitationForNodeCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PastFamilyInvitationsForNodePagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastInvitationsForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_past_invitations_paged(
|
||||
&self,
|
||||
start_after: Option<GlobalPastFamilyInvitationCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<AllPastFamilyInvitationsPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetAllPastInvitationsPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_past_members_for_family_paged(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<PastFamilyMemberCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PastFamilyMembersPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastMembersForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_past_members_for_node_paged(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
start_after: Option<PastFamilyMemberForNodeCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PastFamilyMembersForNodePagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastMembersForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// extension trait to the query client to deal with the paged queries
|
||||
// (it didn't feel appropriate to combine it with the existing trait)
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait PagedNodeFamiliesQueryClient: NodeFamiliesQueryClient {
|
||||
async fn get_all_families(&self) -> Result<Vec<NodeFamily>, NyxdError> {
|
||||
collect_paged!(self, get_families_paged, families)
|
||||
}
|
||||
|
||||
async fn get_all_family_members_for_family(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<Vec<FamilyMemberRecord>, NyxdError> {
|
||||
collect_paged!(self, get_family_members_paged, members, family_id)
|
||||
}
|
||||
|
||||
async fn get_all_family_members(&self) -> Result<Vec<FamilyMemberRecord>, NyxdError> {
|
||||
collect_paged!(self, get_all_family_members_paged, members)
|
||||
}
|
||||
|
||||
async fn get_all_pending_invitations_for_family(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<Vec<PendingFamilyInvitationDetails>, NyxdError> {
|
||||
collect_paged!(
|
||||
self,
|
||||
get_pending_invitations_for_family_paged,
|
||||
invitations,
|
||||
family_id
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_all_pending_invitations_for_node(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<Vec<PendingFamilyInvitationDetails>, NyxdError> {
|
||||
collect_paged!(
|
||||
self,
|
||||
get_pending_invitations_for_node_paged,
|
||||
invitations,
|
||||
node_id
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_all_pending_invitations(
|
||||
&self,
|
||||
) -> Result<Vec<PendingFamilyInvitationDetails>, NyxdError> {
|
||||
collect_paged!(self, get_all_pending_invitations_paged, invitations)
|
||||
}
|
||||
|
||||
async fn get_all_past_invitations_for_family(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<Vec<PastFamilyInvitation>, NyxdError> {
|
||||
collect_paged!(
|
||||
self,
|
||||
get_past_invitations_for_family_paged,
|
||||
invitations,
|
||||
family_id
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_all_past_invitations_for_node(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<Vec<PastFamilyInvitation>, NyxdError> {
|
||||
collect_paged!(
|
||||
self,
|
||||
get_past_invitations_for_node_paged,
|
||||
invitations,
|
||||
node_id
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_all_past_invitations(&self) -> Result<Vec<PastFamilyInvitation>, NyxdError> {
|
||||
collect_paged!(self, get_all_past_invitations_paged, invitations)
|
||||
}
|
||||
|
||||
async fn get_all_past_members_for_family(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<Vec<PastFamilyMember>, NyxdError> {
|
||||
collect_paged!(self, get_past_members_for_family_paged, members, family_id)
|
||||
}
|
||||
|
||||
async fn get_all_past_members_for_node(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<Vec<PastFamilyMember>, NyxdError> {
|
||||
collect_paged!(self, get_past_members_for_node_paged, members, node_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> PagedNodeFamiliesQueryClient for T where T: NodeFamiliesQueryClient {}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> NodeFamiliesQueryClient for C
|
||||
where
|
||||
C: CosmWasmClient + NymContractsProvider + Send + Sync,
|
||||
{
|
||||
async fn query_node_families_contract<T>(
|
||||
&self,
|
||||
query: NodeFamiliesQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
let node_families_contract_address = &self
|
||||
.node_families_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("node families contract"))?;
|
||||
self.query_contract_smart(node_families_contract_address, &query)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
use nym_node_families_contract_common::QueryMsg;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_query_variants_are_covered<C: NodeFamiliesQueryClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: NodeFamiliesQueryMsg,
|
||||
) {
|
||||
match msg {
|
||||
NodeFamiliesQueryMsg::GetFamilyById { family_id } => {
|
||||
client.get_family_by_id(family_id).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamilyByOwner { owner } => {
|
||||
client.get_family_by_owner(&owner.parse().unwrap()).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamilyByName { name } => {
|
||||
client.get_family_by_name(name).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamiliesPaged { start_after, limit } => {
|
||||
client.get_families_paged(start_after, limit).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamilyMembership { node_id } => {
|
||||
client.get_family_membership(node_id).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamilyMembersPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_family_members_paged(family_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetAllFamilyMembersPaged { start_after, limit } => client
|
||||
.get_all_family_members_paged(start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPendingInvitation { family_id, node_id } => {
|
||||
client.get_pending_invitation(family_id, node_id).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetPendingInvitationsForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_pending_invitations_for_family_paged(family_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPendingInvitationsForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_pending_invitations_for_node_paged(node_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetAllPendingInvitationsPaged { start_after, limit } => client
|
||||
.get_all_pending_invitations_paged(start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPastInvitationsForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_past_invitations_for_family_paged(family_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPastInvitationsForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_past_invitations_for_node_paged(node_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetAllPastInvitationsPaged { start_after, limit } => client
|
||||
.get_all_past_invitations_paged(start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPastMembersForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_past_members_for_family_paged(family_id, start_after, limit)
|
||||
.ignore(),
|
||||
QueryMsg::GetPastMembersForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_past_members_for_node_paged(node_id, start_after, limit)
|
||||
.ignore(),
|
||||
};
|
||||
}
|
||||
}
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::nyxd::coin::Coin;
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::{Fee, SigningCosmWasmClient};
|
||||
use crate::signing::signer::OfflineSigner;
|
||||
use async_trait::async_trait;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_node_families_contract_common::{
|
||||
Config, ExecuteMsg as NodeFamiliesExecuteMsg, NodeFamilyId,
|
||||
};
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait NodeFamiliesSigningClient {
|
||||
async fn execute_node_families_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: NodeFamiliesExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError>;
|
||||
|
||||
async fn update_node_families_config(
|
||||
&self,
|
||||
config: Config,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::UpdateConfig { config },
|
||||
"NodeFamiliesContract::UpdateConfig".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn create_family(
|
||||
&self,
|
||||
name: String,
|
||||
description: String,
|
||||
fee: Option<Fee>,
|
||||
creation_fee: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::CreateFamily { name, description },
|
||||
"NodeFamiliesContract::CreateFamily".to_string(),
|
||||
creation_fee,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn disband_family(&self, fee: Option<Fee>) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::DisbandFamily {},
|
||||
"NodeFamiliesContract::DisbandFamily".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn invite_to_family(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
validity_secs: Option<u64>,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::InviteToFamily {
|
||||
node_id,
|
||||
validity_secs,
|
||||
},
|
||||
"NodeFamiliesContract::InviteToFamily".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn revoke_family_invitation(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::RevokeFamilyInvitation { node_id },
|
||||
"NodeFamiliesContract::RevokeFamilyInvitation".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn accept_family_invitation(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::AcceptFamilyInvitation { family_id, node_id },
|
||||
"NodeFamiliesContract::AcceptFamilyInvitation".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn reject_family_invitation(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::RejectFamilyInvitation { family_id, node_id },
|
||||
"NodeFamiliesContract::RejectFamilyInvitation".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn leave_family(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::LeaveFamily { node_id },
|
||||
"NodeFamiliesContract::LeaveFamily".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn kick_from_family(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::KickFromFamily { node_id },
|
||||
"NodeFamiliesContract::KickFromFamily".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Cross-contract callback fired by the mixnet contract on node unbonding.
|
||||
/// Exposed for completeness; the families contract rejects this call from
|
||||
/// any sender other than the configured mixnet contract address.
|
||||
async fn on_nym_node_unbond(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::OnNymNodeUnbond { node_id },
|
||||
"NodeFamiliesContract::OnNymNodeUnbond".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> NodeFamiliesSigningClient for C
|
||||
where
|
||||
C: SigningCosmWasmClient + NymContractsProvider + Sync,
|
||||
NyxdError: From<<Self as OfflineSigner>::Error>,
|
||||
{
|
||||
async fn execute_node_families_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: NodeFamiliesExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let node_families_contract_address = &self
|
||||
.node_families_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("node families contract"))?;
|
||||
|
||||
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
|
||||
|
||||
let signer_address = &self.signer_addresses()[0];
|
||||
self.execute(
|
||||
signer_address,
|
||||
node_families_contract_address,
|
||||
&msg,
|
||||
fee,
|
||||
memo,
|
||||
funds,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
use nym_node_families_contract_common::ExecuteMsg;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_execute_variants_are_covered<C: NodeFamiliesSigningClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: NodeFamiliesExecuteMsg,
|
||||
) {
|
||||
match msg {
|
||||
NodeFamiliesExecuteMsg::UpdateConfig { config } => {
|
||||
client.update_node_families_config(config, None).ignore()
|
||||
}
|
||||
NodeFamiliesExecuteMsg::CreateFamily { name, description } => client
|
||||
.create_family(name, description, None, vec![])
|
||||
.ignore(),
|
||||
NodeFamiliesExecuteMsg::DisbandFamily {} => client.disband_family(None).ignore(),
|
||||
NodeFamiliesExecuteMsg::InviteToFamily {
|
||||
node_id,
|
||||
validity_secs,
|
||||
} => client
|
||||
.invite_to_family(node_id, validity_secs, None)
|
||||
.ignore(),
|
||||
NodeFamiliesExecuteMsg::RevokeFamilyInvitation { node_id } => {
|
||||
client.revoke_family_invitation(node_id, None).ignore()
|
||||
}
|
||||
NodeFamiliesExecuteMsg::AcceptFamilyInvitation { family_id, node_id } => client
|
||||
.accept_family_invitation(family_id, node_id, None)
|
||||
.ignore(),
|
||||
NodeFamiliesExecuteMsg::RejectFamilyInvitation { family_id, node_id } => client
|
||||
.reject_family_invitation(family_id, node_id, None)
|
||||
.ignore(),
|
||||
NodeFamiliesExecuteMsg::LeaveFamily { node_id } => {
|
||||
client.leave_family(node_id, None).ignore()
|
||||
}
|
||||
NodeFamiliesExecuteMsg::KickFromFamily { node_id } => {
|
||||
client.kick_from_family(node_id, None).ignore()
|
||||
}
|
||||
ExecuteMsg::OnNymNodeUnbond { node_id } => {
|
||||
client.on_nym_node_unbond(node_id, None).ignore()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -286,6 +286,10 @@ impl<C, S> NyxdClient<C, S> {
|
||||
self.config.contracts.multisig_contract_address = Some(address);
|
||||
}
|
||||
|
||||
pub fn set_node_families_contract_address(&mut self, address: AccountId) {
|
||||
self.config.contracts.node_families_contract_address = Some(address);
|
||||
}
|
||||
|
||||
pub fn set_simulated_gas_multiplier(&mut self, multiplier: f32) {
|
||||
self.config.simulated_gas_multiplier = multiplier;
|
||||
}
|
||||
@@ -304,6 +308,13 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
|
||||
self.config.contracts.performance_contract_address.as_ref()
|
||||
}
|
||||
|
||||
fn node_families_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config
|
||||
.contracts
|
||||
.node_families_contract_address
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
fn ecash_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config.contracts.ecash_contract_address.as_ref()
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ pub struct Args {
|
||||
#[clap(long)]
|
||||
pub vesting_contract_address: Option<AccountId>,
|
||||
|
||||
#[clap(long)]
|
||||
pub node_families_contract_address: Option<AccountId>,
|
||||
|
||||
#[clap(long)]
|
||||
pub rewarding_denom: Option<String>,
|
||||
|
||||
@@ -130,6 +133,14 @@ pub async fn generate(args: Args) {
|
||||
.expect("Failed converting vesting contract address to AccountId")
|
||||
});
|
||||
|
||||
let node_families_contract_address = args.node_families_contract_address.unwrap_or_else(|| {
|
||||
let address =
|
||||
std::env::var(nym_network_defaults::var_names::NODE_FAMILIES_CONTRACT_ADDRESS)
|
||||
.expect("node families contract address has to be set");
|
||||
AccountId::from_str(address.as_str())
|
||||
.expect("Failed converting node families contract address to AccountId")
|
||||
});
|
||||
|
||||
let rewarding_denom = args.rewarding_denom.unwrap_or_else(|| {
|
||||
std::env::var(nym_network_defaults::var_names::MIX_DENOM)
|
||||
.expect("Rewarding (mix) denom has to be set")
|
||||
@@ -142,6 +153,7 @@ pub async fn generate(args: Args) {
|
||||
let instantiate_msg = InstantiateMsg {
|
||||
rewarding_validator_address: rewarding_validator_address.to_string(),
|
||||
vesting_contract_address: vesting_contract_address.to_string(),
|
||||
node_families_contract_address: node_families_contract_address.to_string(),
|
||||
rewarding_denom,
|
||||
epochs_in_interval: args.epochs_in_interval,
|
||||
epoch_duration: Duration::from_secs(args.epoch_duration),
|
||||
|
||||
@@ -26,6 +26,14 @@ pub trait ContractOpts {
|
||||
|
||||
fn addr_make(&self, input: &str) -> Addr;
|
||||
|
||||
fn make_sender_with_funds(&self, input: &str, funds: &[Coin]) -> MessageInfo {
|
||||
message_info(&self.addr_make(input), funds)
|
||||
}
|
||||
|
||||
fn make_sender(&self, input: &str) -> MessageInfo {
|
||||
self.make_sender_with_funds(input, &[])
|
||||
}
|
||||
|
||||
fn deps_mut_env(&mut self) -> (DepsMut<'_>, Env) {
|
||||
let env = self.env().clone();
|
||||
(self.deps_mut(), env)
|
||||
|
||||
@@ -3,12 +3,121 @@
|
||||
|
||||
use crate::error::MixnetContractError;
|
||||
use crate::mixnode::PendingMixNodeChanges;
|
||||
use crate::nym_node::NodeOwnershipResponse;
|
||||
use crate::{
|
||||
EpochEventId, IntervalEventId, MixNodeBond, MixNodeDetails, NodeId, NodeRewarding, NymNodeBond,
|
||||
NymNodeDetails, PendingNodeChanges,
|
||||
EpochEventId, EpochId, Interval, IntervalEventId, MixNodeBond, MixNodeDetails, NodeId,
|
||||
NodeRewarding, NymNodeBond, NymNodeDetails, PendingNodeChanges, QueryMsg,
|
||||
};
|
||||
use cosmwasm_std::{Coin, Decimal, StdError, StdResult, Uint128};
|
||||
use cosmwasm_std::{
|
||||
Addr, Binary, Coin, CustomQuery, Decimal, QuerierWrapper, StdError, StdResult, Uint128,
|
||||
from_json,
|
||||
};
|
||||
use cw_storage_plus::{Key, Namespace, Path, PrimaryKey};
|
||||
use nym_contracts_common::IdentityKeyRef;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::ops::Deref;
|
||||
|
||||
pub trait MixnetContractQuerier {
|
||||
#[allow(dead_code)]
|
||||
fn query_mixnet_contract<T: DeserializeOwned>(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
msg: &QueryMsg,
|
||||
) -> StdResult<T>;
|
||||
|
||||
fn query_mixnet_contract_storage(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl Into<Binary>,
|
||||
) -> StdResult<Option<Vec<u8>>>;
|
||||
|
||||
fn query_mixnet_contract_storage_value<T: DeserializeOwned>(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl Into<Binary>,
|
||||
) -> StdResult<Option<T>> {
|
||||
match self.query_mixnet_contract_storage(address, key)? {
|
||||
None => Ok(None),
|
||||
Some(value) => Ok(Some(from_json(&value)?)),
|
||||
}
|
||||
}
|
||||
|
||||
fn query_current_mixnet_interval(&self, address: impl Into<String>) -> StdResult<Interval> {
|
||||
self.query_mixnet_contract_storage_value(address, b"ci")?
|
||||
.ok_or(StdError::not_found(
|
||||
"unable to retrieve interval information from the mixnet contract storage",
|
||||
))
|
||||
}
|
||||
|
||||
fn query_current_absolute_mixnet_epoch_id(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
) -> StdResult<EpochId> {
|
||||
self.query_current_mixnet_interval(address)
|
||||
.map(|interval| interval.current_epoch_absolute_id())
|
||||
}
|
||||
|
||||
fn check_node_existence(&self, address: impl Into<String>, node_id: NodeId) -> StdResult<bool> {
|
||||
let mixnet_contract_address = address.into();
|
||||
|
||||
if let Some(nym_node) = self.query_nymnode_bond(mixnet_contract_address.clone(), node_id)? {
|
||||
return Ok(!nym_node.is_unbonding);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn query_nymnode_bond(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
node_id: NodeId,
|
||||
) -> StdResult<Option<NymNodeBond>> {
|
||||
// construct proper map key
|
||||
let pk_namespace = "nn";
|
||||
let path: Path<NymNodeBond> = Path::new(
|
||||
Namespace::from_static_str(pk_namespace).as_slice(),
|
||||
&node_id.key().iter().map(Key::as_ref).collect::<Vec<_>>(),
|
||||
);
|
||||
let storage_key = path.deref();
|
||||
|
||||
self.query_mixnet_contract_storage_value(address, storage_key)
|
||||
}
|
||||
|
||||
fn query_nymnode_ownership(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
owner: &Addr,
|
||||
) -> StdResult<Option<NymNodeBond>> {
|
||||
let resp: NodeOwnershipResponse = self.query_mixnet_contract(
|
||||
address,
|
||||
&QueryMsg::GetOwnedNymNode {
|
||||
address: owner.to_string(),
|
||||
},
|
||||
)?;
|
||||
Ok(resp.details.map(|d| d.bond_information))
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> MixnetContractQuerier for QuerierWrapper<'_, C>
|
||||
where
|
||||
C: CustomQuery,
|
||||
{
|
||||
fn query_mixnet_contract<T: DeserializeOwned>(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
msg: &QueryMsg,
|
||||
) -> StdResult<T> {
|
||||
self.query_wasm_smart(address, msg)
|
||||
}
|
||||
|
||||
fn query_mixnet_contract_storage(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl Into<Binary>,
|
||||
) -> StdResult<Option<Vec<u8>>> {
|
||||
self.query_wasm_raw(address, key)
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option<Decimal>) {
|
||||
|
||||
@@ -30,6 +30,7 @@ pub use gateway::{
|
||||
Gateway, GatewayBond, GatewayBondResponse, GatewayConfigUpdate, GatewayOwnershipResponse,
|
||||
PagedGatewayResponse,
|
||||
};
|
||||
pub use helpers::MixnetContractQuerier;
|
||||
pub use interval::{
|
||||
CurrentIntervalResponse, EpochId, EpochState, EpochStatus, Interval, IntervalId,
|
||||
};
|
||||
|
||||
@@ -190,6 +190,10 @@ impl NodeRewarding {
|
||||
truncate_reward(self.operator, denom)
|
||||
}
|
||||
|
||||
pub fn delegations_with_reward(&self, denom: impl Into<String>) -> Coin {
|
||||
truncate_reward(self.delegates, denom)
|
||||
}
|
||||
|
||||
pub fn pending_delegator_reward(&self, delegation: &Delegation) -> StdResult<Coin> {
|
||||
let delegator_reward = self.determine_delegation_reward(delegation)?;
|
||||
Ok(truncate_reward(delegator_reward, &delegation.amount.denom))
|
||||
|
||||
@@ -63,6 +63,7 @@ use nym_contracts_common::{ContractBuildInformation, signing::Nonce};
|
||||
pub struct InstantiateMsg {
|
||||
pub rewarding_validator_address: String,
|
||||
pub vesting_contract_address: String,
|
||||
pub node_families_contract_address: String,
|
||||
|
||||
pub rewarding_denom: String,
|
||||
pub epochs_in_interval: u32,
|
||||
@@ -305,6 +306,22 @@ pub enum ExecuteMsg {
|
||||
MigrateVestedDelegation {
|
||||
mix_id: NodeId,
|
||||
},
|
||||
/// Admin-only: forcibly migrate the vested mixnode owned by `owner`.
|
||||
/// Used to drain the last vested entries so the mixnet contract can drop its dependency on the vesting contract.
|
||||
AdminMigrateVestedMixNode {
|
||||
owner: String,
|
||||
},
|
||||
/// Admin-only: forcibly migrate the vested delegation `(mix_id, owner)`.
|
||||
/// Used to drain the last vested entries so the mixnet contract can drop its dependency on the vesting contract.
|
||||
AdminMigrateVestedDelegation {
|
||||
mix_id: NodeId,
|
||||
owner: String,
|
||||
},
|
||||
/// Admin-only: batch variant of [`ExecuteMsg::AdminMigrateVestedDelegation`].
|
||||
/// Reverts the entire batch on the first error, so callers should treat it as all-or-nothing.
|
||||
AdminBatchMigrateVestedDelegations {
|
||||
entries: Vec<VestedDelegationMigrationEntry>,
|
||||
},
|
||||
|
||||
// testing-only
|
||||
#[cfg(feature = "contract-testing")]
|
||||
@@ -394,6 +411,15 @@ impl ExecuteMsg {
|
||||
}
|
||||
ExecuteMsg::MigrateVestedMixNode { .. } => "migrate vested mixnode".into(),
|
||||
ExecuteMsg::MigrateVestedDelegation { .. } => "migrate vested delegation".to_string(),
|
||||
ExecuteMsg::AdminMigrateVestedMixNode { owner } => {
|
||||
format!("admin migrating vested mixnode of {owner}")
|
||||
}
|
||||
ExecuteMsg::AdminMigrateVestedDelegation { mix_id, owner } => {
|
||||
format!("admin migrating vested delegation of {owner} on mixnode {mix_id}")
|
||||
}
|
||||
ExecuteMsg::AdminBatchMigrateVestedDelegations { entries } => {
|
||||
format!("admin batch migrating {} vested delegations", entries.len())
|
||||
}
|
||||
ExecuteMsg::AssignRoles { .. } => "assigning epoch roles".into(),
|
||||
ExecuteMsg::MigrateMixnode { .. } => "migrating legacy mixnode".into(),
|
||||
ExecuteMsg::MigrateGateway { .. } => "migrating legacy gateway".into(),
|
||||
@@ -881,8 +907,15 @@ pub enum QueryMsg {
|
||||
GetKeyRotationId {},
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct VestedDelegationMigrationEntry {
|
||||
pub mix_id: NodeId,
|
||||
pub owner: String,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct MigrateMsg {
|
||||
pub unsafe_skip_state_updates: Option<bool>,
|
||||
pub vesting_contract_address: Option<String>,
|
||||
pub node_families_contract_address: String,
|
||||
}
|
||||
|
||||
@@ -212,6 +212,10 @@ pub struct ContractState {
|
||||
/// track-related messages.
|
||||
pub vesting_contract_address: Addr,
|
||||
|
||||
/// Address of the node families contract. It is called whenever nym-node unbonds
|
||||
/// so that it could be removed from any family it belongs to.
|
||||
pub node_families_contract_address: Addr,
|
||||
|
||||
/// The expected denom used for rewarding (and realistically any other operation).
|
||||
/// Default: `unym`
|
||||
pub rewarding_denom: String,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "nym-node-families-contract-common"
|
||||
description = "Common crate for Nym's node families contract"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version = "1.85"
|
||||
readme.workspace = true
|
||||
publish = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
|
||||
cosmwasm-std = { workspace = true }
|
||||
cosmwasm-schema = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
cw-utils = { workspace = true }
|
||||
|
||||
nym-contracts-common = { workspace = true }
|
||||
nym-mixnet-contract-common = { workspace = true }
|
||||
|
||||
[features]
|
||||
schema = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
/// Storage key constants used by the node families contract.
|
||||
///
|
||||
/// They are kept in the common crate so that off-chain tooling (indexers, migration
|
||||
/// scripts) can reference them without depending on the contract crate itself.
|
||||
/// Changing any of these values is a breaking change for already-deployed contracts.
|
||||
pub mod storage_keys {
|
||||
/// `Item<Addr>`: address of the mixnet contract used to validate node existence.
|
||||
pub const MIXNET_CONTRACT_ADDRESS: &str = "mixnet-contract-address";
|
||||
|
||||
/// `Item<Config>`: runtime configuration (fees, length limits) set at instantiation.
|
||||
pub const CONFIG: &str = "config";
|
||||
|
||||
/// `Admin` (cw-controllers): admin allowed to perform privileged operations.
|
||||
pub const CONTRACT_ADMIN: &str = "contract-admin";
|
||||
/// `Item<NodeFamilyId>`: monotonically increasing id counter for new families.
|
||||
pub const NODE_FAMILY_ID_COUNTER: &str = "node-family-id-counter";
|
||||
/// Primary namespace for the current family-members `IndexedMap`,
|
||||
/// keyed by `NodeId` with value [`crate::FamilyMembership`].
|
||||
pub const NODE_FAMILY_MEMBERS: &str = "node-family-members";
|
||||
/// Multi-index over current family members keyed by family id —
|
||||
/// enables paginated listing of all nodes in a given family.
|
||||
pub const NODE_FAMILY_MEMBERS_FAMILY_IDX_NAMESPACE: &str = "node-family-members__family";
|
||||
|
||||
/// Primary namespace for the families `IndexedMap`.
|
||||
pub const FAMILIES_NAMESPACE: &str = "families";
|
||||
/// Secondary unique index keyed by `owner` (one family per owner).
|
||||
pub const FAMILIES_OWNER_IDX_NAMESPACE: &str = "families__owner";
|
||||
/// Secondary unique index keyed by `name` (family names are globally unique).
|
||||
pub const FAMILIES_NAME_IDX_NAMESPACE: &str = "families__name";
|
||||
|
||||
/// Primary namespace for the pending invitations `IndexedMap`.
|
||||
pub const INVITATIONS_NAMESPACE: &str = "invitations";
|
||||
/// Multi-index over pending invitations keyed by family id.
|
||||
pub const INVITATIONS_FAMILY_IDX_NAMESPACE: &str = "invitations__family";
|
||||
/// Multi-index over pending invitations keyed by node id
|
||||
/// (a node can be invited to multiple families simultaneously).
|
||||
pub const INVITATIONS_NODE_IDX_NAMESPACE: &str = "invitations__node";
|
||||
|
||||
/// Primary namespace for the archived (accepted/rejected/revoked) invitations `IndexedMap`.
|
||||
pub const PAST_INVITATIONS_NAMESPACE: &str = "past-invitations";
|
||||
/// Multi-index over past invitations keyed by family id.
|
||||
pub const PAST_INVITATIONS_FAMILY_IDX_NAMESPACE: &str = "past-invitations__family";
|
||||
/// Multi-index over past invitations keyed by node id.
|
||||
pub const PAST_INVITATIONS_NODE_IDX_NAMESPACE: &str = "past-invitations__node";
|
||||
/// `Map<(NodeFamilyId, NodeId), u64>`: per-`(family, node)` counter used to
|
||||
/// disambiguate repeat archive entries (a node can be invited and have the
|
||||
/// invitation reach a terminal state more than once).
|
||||
pub const PAST_INVITATIONS_COUNTER_NAMESPACE: &str = "past-invitations-counter";
|
||||
|
||||
/// Primary namespace for the past-members `IndexedMap`.
|
||||
pub const PAST_FAMILY_MEMBER_NAMESPACE: &str = "past-family-member";
|
||||
/// Multi-index over past members keyed by family id.
|
||||
pub const PAST_FAMILY_MEMBER_FAMILY_IDX_NAMESPACE: &str = "past-family-member__family";
|
||||
/// Multi-index over past members keyed by node id.
|
||||
pub const PAST_FAMILY_MEMBER_NODE_IDX_NAMESPACE: &str = "past-family-member__node";
|
||||
/// `Map<(NodeFamilyId, NodeId), u64>`: per-`(family, node)` counter used to
|
||||
/// disambiguate repeat past-membership entries (a node can join and leave
|
||||
/// the same family more than once).
|
||||
pub const PAST_FAMILY_MEMBER_COUNTER_NAMESPACE: &str = "past-family-member-counter";
|
||||
}
|
||||
|
||||
pub mod events {
|
||||
pub const FAMILY_CREATION_EVENT_NAME: &str = "family_creation";
|
||||
pub const FAMILY_CREATION_EVENT_FAMILY_NAME: &str = "family_name";
|
||||
pub const FAMILY_CREATION_EVENT_OWNER_ADDRESS: &str = "owner_address";
|
||||
pub const FAMILY_CREATION_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_CREATION_EVENT_PAID_FEE: &str = "paid_fee";
|
||||
|
||||
pub const FAMILY_DISBAND_EVENT_NAME: &str = "family_disband";
|
||||
pub const FAMILY_DISBAND_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_DISBAND_EVENT_OWNER_ADDRESS: &str = "owner_address";
|
||||
pub const FAMILY_DISBAND_EVENT_REFUNDED_FEE: &str = "refunded_fee";
|
||||
|
||||
pub const FAMILY_INVITATION_EVENT_NAME: &str = "family_invitation";
|
||||
pub const FAMILY_INVITATION_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_INVITATION_EVENT_NODE_ID: &str = "node_id";
|
||||
pub const FAMILY_INVITATION_EVENT_EXPIRES_AT: &str = "expires_at";
|
||||
|
||||
pub const FAMILY_INVITATION_REVOKED_EVENT_NAME: &str = "family_invitation_revoked";
|
||||
pub const FAMILY_INVITATION_REVOKED_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_INVITATION_REVOKED_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const FAMILY_INVITATION_ACCEPTED_EVENT_NAME: &str = "family_invitation_accepted";
|
||||
pub const FAMILY_INVITATION_ACCEPTED_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_INVITATION_ACCEPTED_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const FAMILY_INVITATION_REJECTED_EVENT_NAME: &str = "family_invitation_rejected";
|
||||
pub const FAMILY_INVITATION_REJECTED_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_INVITATION_REJECTED_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const FAMILY_MEMBER_LEFT_EVENT_NAME: &str = "family_member_left";
|
||||
pub const FAMILY_MEMBER_LEFT_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_MEMBER_LEFT_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const FAMILY_MEMBER_KICKED_EVENT_NAME: &str = "family_member_kicked";
|
||||
pub const FAMILY_MEMBER_KICKED_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_MEMBER_KICKED_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const NODE_UNBOND_CLEANUP_EVENT_NAME: &str = "family_node_unbond_cleanup";
|
||||
pub const NODE_UNBOND_CLEANUP_EVENT_NODE_ID: &str = "node_id";
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::NodeFamilyId;
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use cw_controllers::AdminError;
|
||||
use cw_utils::PaymentError;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors returned from any entry point of the node families contract.
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum NodeFamiliesContractError {
|
||||
/// Returned from `migrate` when the on-chain state cannot be brought forward
|
||||
/// to the current contract version (e.g. unsupported source version, malformed
|
||||
/// stored data).
|
||||
#[error("could not perform contract migration: {comment}")]
|
||||
FailedMigration { comment: String },
|
||||
|
||||
/// The referenced family does not exist (or no longer exists).
|
||||
#[error("family with id {family_id} does not exist")]
|
||||
FamilyNotFound { family_id: NodeFamilyId },
|
||||
|
||||
/// Disbanding was requested on a family that still has members.
|
||||
#[error("family {family_id} cannot be disbanded: it still has {members} member(s)")]
|
||||
FamilyNotEmpty {
|
||||
family_id: NodeFamilyId,
|
||||
members: u64,
|
||||
},
|
||||
|
||||
/// The given node is not currently a member of any family.
|
||||
#[error("node {node_id} is not currently a member of any family")]
|
||||
NodeNotInFamily { node_id: NodeId },
|
||||
|
||||
/// The given node is a member of a different family than the one the
|
||||
/// caller is acting on. Distinct from [`NodeNotInFamily`] (which means the
|
||||
/// node has no membership at all) — surfaces when, e.g., a family owner
|
||||
/// tries to kick a node that belongs to someone else's family.
|
||||
#[error("node {node_id} is not a member of family {family_id}")]
|
||||
NodeNotMemberOfFamily {
|
||||
node_id: NodeId,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// A cross-contract callback (e.g. `OnNymNodeUnbond`) was received from a
|
||||
/// sender that is not the configured mixnet contract address.
|
||||
#[error("address {sender} is not authorised to invoke the mixnet-contract callback")]
|
||||
UnauthorisedMixnetCallback { sender: Addr },
|
||||
|
||||
/// No pending invitation exists for the given `(family, node)` pair.
|
||||
#[error("no pending invitation for node {node_id} from family {family_id}")]
|
||||
InvitationNotFound {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// A pending invitation for the given `(family, node)` pair already exists;
|
||||
/// issuing a new one would silently overwrite it.
|
||||
#[error("a pending invitation for node {node_id} from family {family_id} already exists")]
|
||||
PendingInvitationAlreadyExists {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// The invitation exists but its `expires_at` is at or before the current
|
||||
/// block time, so it can no longer be acted on.
|
||||
#[error(
|
||||
"invitation for node {node_id} from family {family_id} expired at {expires_at} (now: {now})"
|
||||
)]
|
||||
InvitationExpired {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
expires_at: u64,
|
||||
now: u64,
|
||||
},
|
||||
|
||||
/// The funds attached to a paid execution failed `cw_utils` payment
|
||||
/// validation (no funds, wrong/extra denom).
|
||||
#[error("invalid fee provided: {0}")]
|
||||
InvalidDeposit(#[from] PaymentError),
|
||||
|
||||
/// The funds attached to a `CreateFamily` execution don't match the
|
||||
/// configured `create_family_fee`.
|
||||
#[error("expected exactly {expected} as family creation fee; received {received:?}")]
|
||||
InvalidFamilyCreationFee { expected: Coin, received: Vec<Coin> },
|
||||
|
||||
/// The submitted family name normalised to the empty string (i.e. it
|
||||
/// contained no ASCII alphanumeric characters).
|
||||
#[error("family name cannot be empty after normalisation")]
|
||||
EmptyFamilyName,
|
||||
|
||||
/// The submitted family name exceeds the configured length limit.
|
||||
#[error("family name length {length} exceeds the configured limit of {limit}")]
|
||||
FamilyNameTooLong { length: usize, limit: usize },
|
||||
|
||||
/// The submitted family description exceeds the configured length limit.
|
||||
#[error("family description length {length} exceeds the configured limit of {limit}")]
|
||||
FamilyDescriptionTooLong { length: usize, limit: usize },
|
||||
|
||||
/// The transaction sender already owns a family.
|
||||
#[error("address {address} already owns family {family_id}")]
|
||||
SenderAlreadyOwnsAFamily {
|
||||
address: Addr,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// The transaction sender does not currently own any family - emitted by
|
||||
/// owner-gated operations like `disband_family` when the sender has
|
||||
/// nothing to act on.
|
||||
#[error("address {address} does not currently own any family")]
|
||||
SenderDoesntOwnAFamily { address: Addr },
|
||||
|
||||
/// The transaction sender is not the controller of the bonded node
|
||||
/// referenced by the message. Covers all of: sender controls no bonded
|
||||
/// node, sender controls a different node id, and sender's node has
|
||||
/// entered the unbonding state.
|
||||
#[error("address {address} is not the controller of bonded node {node_id}")]
|
||||
SenderDoesntControlNode { address: Addr, node_id: NodeId },
|
||||
|
||||
/// A family with the requested (normalised) name already exists.
|
||||
#[error("a family with name {name:?} already exists (id {family_id})")]
|
||||
FamilyNameAlreadyTaken {
|
||||
name: String,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// A node controlled by the address is currently a member of a family,
|
||||
/// so the address cannot also become a family owner or join another family.
|
||||
#[error("address {address} controls node {node_id} which is currently in family {family_id}")]
|
||||
AlreadyInFamily {
|
||||
address: Addr,
|
||||
node_id: NodeId,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// The node referenced by an invitation does not exist as a bonded node
|
||||
/// in the mixnet contract (or has already unbonded).
|
||||
#[error("node {node_id} is not a bonded node in the mixnet contract")]
|
||||
NodeDoesntExist { node_id: NodeId },
|
||||
|
||||
/// The node referenced by an invitation is already a member of a family,
|
||||
/// so it cannot be invited to another one until it leaves / is removed.
|
||||
#[error("node {node_id} is already a member of family {family_id}")]
|
||||
NodeAlreadyInFamily {
|
||||
node_id: NodeId,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// The sender supplied a `validity_secs` of `0` for an invitation, which
|
||||
/// would create one that is already expired at the moment it is stored.
|
||||
#[error("invitation validity must be strictly positive")]
|
||||
ZeroInvitationValidity,
|
||||
|
||||
/// Wraps errors raised by `cw-controllers::Admin` (e.g. caller is not admin).
|
||||
#[error(transparent)]
|
||||
Admin(#[from] AdminError),
|
||||
|
||||
/// Wraps any underlying `cosmwasm_std::StdError` (storage, serialization, etc.).
|
||||
#[error(transparent)]
|
||||
StdErr(#[from] cosmwasm_std::StdError),
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Common types, messages, errors and storage-key constants shared between the
|
||||
//! node families contract and any off-chain client.
|
||||
//!
|
||||
//! Keeping these in a separate crate allows clients to depend on the contract's
|
||||
//! public surface without pulling in `cw-storage-plus` and other on-chain-only
|
||||
//! dependencies.
|
||||
|
||||
/// Storage-key string constants. See [`constants::storage_keys`].
|
||||
pub mod constants;
|
||||
/// Contract-level error type.
|
||||
pub mod error;
|
||||
/// `InstantiateMsg`, `ExecuteMsg`, `QueryMsg`, `MigrateMsg` definitions.
|
||||
pub mod msg;
|
||||
/// Domain types stored in / returned by the contract.
|
||||
pub mod types;
|
||||
|
||||
pub use error::*;
|
||||
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
pub use types::*;
|
||||
@@ -0,0 +1,211 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::{
|
||||
Config, GlobalPastFamilyInvitationCursor, NodeFamilyId, PastFamilyInvitationCursor,
|
||||
PastFamilyInvitationForNodeCursor, PastFamilyMemberCursor, PastFamilyMemberForNodeCursor,
|
||||
};
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
use crate::{
|
||||
AllFamilyMembersPagedResponse, AllPastFamilyInvitationsPagedResponse, FamiliesPagedResponse,
|
||||
FamilyMembersPagedResponse, NodeFamilyByNameResponse, NodeFamilyByOwnerResponse,
|
||||
NodeFamilyMembershipResponse, NodeFamilyResponse, PastFamilyInvitationsForNodePagedResponse,
|
||||
PastFamilyInvitationsPagedResponse, PastFamilyMembersForNodePagedResponse,
|
||||
PastFamilyMembersPagedResponse, PendingFamilyInvitationResponse,
|
||||
PendingFamilyInvitationsPagedResponse, PendingInvitationsForNodePagedResponse,
|
||||
PendingInvitationsPagedResponse,
|
||||
};
|
||||
|
||||
/// Message used to instantiate the node families contract.
|
||||
#[cw_serde]
|
||||
pub struct InstantiateMsg {
|
||||
pub config: Config,
|
||||
|
||||
pub mixnet_contract_address: String,
|
||||
}
|
||||
|
||||
/// Execute messages accepted by the contract.
|
||||
#[cw_serde]
|
||||
pub enum ExecuteMsg {
|
||||
/// Replace the contract's runtime [`Config`]. Restricted to the contract
|
||||
/// admin.
|
||||
UpdateConfig { config: Config },
|
||||
|
||||
/// Create a new family owned by the message sender. The configured
|
||||
/// `create_family_fee` must be attached as funds.
|
||||
CreateFamily { name: String, description: String },
|
||||
|
||||
/// Disband the family owned by the message sender. The family must have
|
||||
/// no current members; any still-pending invitations are revoked.
|
||||
DisbandFamily {},
|
||||
|
||||
/// Invite a node to the family owned by the message sender. If
|
||||
/// `validity_secs` is omitted the invitation expires
|
||||
/// `default_invitation_validity_secs` seconds (from [`Config`]) after the
|
||||
/// current block time.
|
||||
InviteToFamily {
|
||||
node_id: NodeId,
|
||||
validity_secs: Option<u64>,
|
||||
},
|
||||
|
||||
/// Revoke a still-pending invitation previously issued by the sender's
|
||||
/// family.
|
||||
RevokeFamilyInvitation { node_id: NodeId },
|
||||
|
||||
/// Accept a pending invitation. The sender must control `node_id`.
|
||||
AcceptFamilyInvitation {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// Reject a pending invitation. The sender must control `node_id`.
|
||||
RejectFamilyInvitation {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// Leave the family `node_id` currently belongs to. The sender must
|
||||
/// control `node_id`.
|
||||
LeaveFamily { node_id: NodeId },
|
||||
|
||||
/// Remove `node_id` from the family owned by the message sender.
|
||||
KickFromFamily { node_id: NodeId },
|
||||
|
||||
/// Cross-contract callback fired by the mixnet contract the moment
|
||||
/// node with `node_id` initiates unbonding.
|
||||
/// Removes the node from any family it currently
|
||||
/// belongs to and rejects every pending invitation issued to it.
|
||||
/// Sender must be the configured mixnet contract address.
|
||||
OnNymNodeUnbond { node_id: NodeId },
|
||||
}
|
||||
|
||||
/// Query messages accepted by the contract.
|
||||
#[cw_serde]
|
||||
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
|
||||
pub enum QueryMsg {
|
||||
/// Look up a single family by its id.
|
||||
#[cfg_attr(feature = "schema", returns(NodeFamilyResponse))]
|
||||
GetFamilyById { family_id: NodeFamilyId },
|
||||
|
||||
/// Look up the (at most one) family owned by a given address.
|
||||
#[cfg_attr(feature = "schema", returns(NodeFamilyByOwnerResponse))]
|
||||
GetFamilyByOwner { owner: String },
|
||||
|
||||
/// Look up a single family by its name. The lookup is normalised
|
||||
/// contract-side (lowercased, non-alphanumerics stripped), so equivalent
|
||||
/// inputs resolve to the same family.
|
||||
#[cfg_attr(feature = "schema", returns(NodeFamilyByNameResponse))]
|
||||
GetFamilyByName { name: String },
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(FamiliesPagedResponse))]
|
||||
GetFamiliesPaged {
|
||||
start_after: Option<NodeFamilyId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Look up which family — if any — a node currently belongs to.
|
||||
#[cfg_attr(feature = "schema", returns(NodeFamilyMembershipResponse))]
|
||||
GetFamilyMembership { node_id: NodeId },
|
||||
|
||||
/// Page through every node currently in a given family.
|
||||
#[cfg_attr(feature = "schema", returns(FamilyMembersPagedResponse))]
|
||||
GetFamilyMembersPaged {
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every current family member across all families, in
|
||||
/// ascending [`NodeId`] order. Each entry carries the membership record
|
||||
/// (which in turn names the family the node belongs to).
|
||||
#[cfg_attr(feature = "schema", returns(AllFamilyMembersPagedResponse))]
|
||||
GetAllFamilyMembersPaged {
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Look up the pending invitation for a specific `(family_id, node_id)`
|
||||
/// pair.
|
||||
#[cfg_attr(feature = "schema", returns(PendingFamilyInvitationResponse))]
|
||||
GetPendingInvitation {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// Page through every pending invitation issued by a given family.
|
||||
#[cfg_attr(feature = "schema", returns(PendingFamilyInvitationsPagedResponse))]
|
||||
GetPendingInvitationsForFamilyPaged {
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every pending invitation issued for a given node.
|
||||
#[cfg_attr(feature = "schema", returns(PendingInvitationsForNodePagedResponse))]
|
||||
GetPendingInvitationsForNodePaged {
|
||||
node_id: NodeId,
|
||||
start_after: Option<NodeFamilyId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every pending invitation across all families.
|
||||
#[cfg_attr(feature = "schema", returns(PendingInvitationsPagedResponse))]
|
||||
GetAllPendingInvitationsPaged {
|
||||
start_after: Option<(NodeFamilyId, NodeId)>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived (terminal-state) invitation issued by a
|
||||
/// given family.
|
||||
#[cfg_attr(feature = "schema", returns(PastFamilyInvitationsPagedResponse))]
|
||||
GetPastInvitationsForFamilyPaged {
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<PastFamilyInvitationCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived (terminal-state) invitation issued to a
|
||||
/// given node.
|
||||
#[cfg_attr(feature = "schema", returns(PastFamilyInvitationsForNodePagedResponse))]
|
||||
GetPastInvitationsForNodePaged {
|
||||
node_id: NodeId,
|
||||
start_after: Option<PastFamilyInvitationForNodeCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived (terminal-state) invitation across all
|
||||
/// families.
|
||||
#[cfg_attr(feature = "schema", returns(AllPastFamilyInvitationsPagedResponse))]
|
||||
GetAllPastInvitationsPaged {
|
||||
start_after: Option<GlobalPastFamilyInvitationCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived membership record for a given family
|
||||
/// (nodes that used to belong to it but have since been removed).
|
||||
#[cfg_attr(feature = "schema", returns(PastFamilyMembersPagedResponse))]
|
||||
GetPastMembersForFamilyPaged {
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<PastFamilyMemberCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived membership record for a given node
|
||||
/// (every family the node used to belong to but has since been removed
|
||||
/// from), across all families.
|
||||
#[cfg_attr(feature = "schema", returns(PastFamilyMembersForNodePagedResponse))]
|
||||
GetPastMembersForNodePaged {
|
||||
node_id: NodeId,
|
||||
start_after: Option<PastFamilyMemberForNodeCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Message passed to the contract's `migrate` entry point.
|
||||
#[cw_serde]
|
||||
pub struct MigrateMsg {
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
|
||||
/// Identifier of a node family.
|
||||
///
|
||||
/// Issued sequentially by the contract on family creation; never reused even if the
|
||||
/// family is later disbanded.
|
||||
pub type NodeFamilyId = u32;
|
||||
|
||||
/// Runtime configuration of the node families contract.
|
||||
#[cw_serde]
|
||||
pub struct Config {
|
||||
/// Fee charged on each successful `create_family` execution.
|
||||
pub create_family_fee: Coin,
|
||||
|
||||
/// Maximum allowed length, in characters, of a family name.
|
||||
pub family_name_length_limit: usize,
|
||||
|
||||
/// Maximum allowed length, in characters, of a family description.
|
||||
pub family_description_length_limit: usize,
|
||||
|
||||
/// Default lifetime, in seconds, used by `invite_to_family` when the
|
||||
/// sender doesn't supply an explicit value. Senders may override this
|
||||
/// per-invitation via the optional `validity_secs` argument.
|
||||
pub default_invitation_validity_secs: u64,
|
||||
}
|
||||
|
||||
/// On-chain representation of a node family.
|
||||
#[cw_serde]
|
||||
pub struct NodeFamily {
|
||||
/// The id of the node family
|
||||
pub id: NodeFamilyId,
|
||||
|
||||
/// The name of the node family
|
||||
pub name: String,
|
||||
|
||||
/// Normalised name of the node family used for uniqueness checks
|
||||
pub normalised_name: String,
|
||||
|
||||
/// The optional description of the node family
|
||||
pub description: String,
|
||||
|
||||
/// The owner of the node family
|
||||
pub owner: Addr,
|
||||
|
||||
/// Records the fee paid when the family was created,
|
||||
/// so that the appropriate amount could be returned upon it getting disbanded.
|
||||
pub paid_fee: Coin,
|
||||
|
||||
/// Memoized value of the current number of members in the node family
|
||||
/// Used to detect if the family is empty
|
||||
pub members: u64,
|
||||
|
||||
/// Timestamp of the creation of the node family
|
||||
pub created_at: u64,
|
||||
}
|
||||
|
||||
/// A pending invitation for a node to join a particular family.
|
||||
///
|
||||
/// Invitations are stored until they are accepted, rejected, revoked, or until the
|
||||
/// chain advances past `expires_at` (in which case they remain in storage but are
|
||||
/// treated as inert — there is no background process clearing expired invitations).
|
||||
#[cw_serde]
|
||||
pub struct FamilyInvitation {
|
||||
/// The family that issued the invitation.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The node being invited.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// Block timestamp (unix seconds) after which the invitation is no longer valid.
|
||||
pub expires_at: u64,
|
||||
}
|
||||
|
||||
/// On-chain record of a node's current family membership.
|
||||
///
|
||||
/// A node belongs to at most one family at a time, so this is keyed by
|
||||
/// `NodeId` alone — `family_id` is carried in the value to support reverse
|
||||
/// lookups (all nodes in a given family) via a secondary index.
|
||||
#[cw_serde]
|
||||
pub struct FamilyMembership {
|
||||
/// The family the node is currently a member of.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// Block timestamp (unix seconds) at which the node accepted its
|
||||
/// invitation and joined the family.
|
||||
pub joined_at: u64,
|
||||
}
|
||||
|
||||
/// Historical record of a node that used to be part of a family but has since been
|
||||
/// removed (kicked, left voluntarily, or because the family was disbanded).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyMember {
|
||||
/// The family the node used to belong to.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The node that was removed.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// Block timestamp (unix seconds) at which the membership was terminated.
|
||||
pub removed_at: u64,
|
||||
}
|
||||
|
||||
/// Terminal status for an invitation that has been moved out of the pending set.
|
||||
///
|
||||
/// Note: timed-out invitations are not represented here — they are simply left in
|
||||
/// the pending set (see `FamilyInvitation::expires_at`).
|
||||
#[cw_serde]
|
||||
pub enum FamilyInvitationStatus {
|
||||
/// Still awaiting a response. Recorded with a timestamp for completeness even
|
||||
/// though pending invitations live in a separate map.
|
||||
Pending { at: u64 },
|
||||
/// The invitee accepted and joined the family at the given timestamp.
|
||||
Accepted { at: u64 },
|
||||
/// The invitee explicitly rejected the invitation at the given timestamp.
|
||||
Rejected { at: u64 },
|
||||
/// The family revoked the invitation at the given timestamp before it could
|
||||
/// be accepted or rejected.
|
||||
Revoked { at: u64 },
|
||||
}
|
||||
|
||||
/// Historical record of an invitation that has reached a terminal state
|
||||
/// (`Accepted`, `Rejected`, or `Revoked`). Timed-out invitations are **not**
|
||||
/// archived here — they remain in the pending map until explicitly cleared.
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyInvitation {
|
||||
/// The original invitation as it was issued.
|
||||
pub invitation: FamilyInvitation,
|
||||
|
||||
/// What ultimately happened to it.
|
||||
pub status: FamilyInvitationStatus,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyById`](crate::QueryMsg::GetFamilyById).
|
||||
#[cw_serde]
|
||||
pub struct NodeFamilyResponse {
|
||||
/// The id that was queried, echoed back so paginated callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The matching family, or `None` if no family with `family_id` exists.
|
||||
pub family: Option<NodeFamily>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyByOwner`](crate::QueryMsg::GetFamilyByOwner).
|
||||
#[cw_serde]
|
||||
pub struct NodeFamilyByOwnerResponse {
|
||||
/// The (validated) owner address that was queried, echoed back so callers
|
||||
/// can correlate.
|
||||
pub owner: Addr,
|
||||
|
||||
/// The matching family, or `None` if `owner` does not currently own one.
|
||||
pub family: Option<NodeFamily>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyByName`](crate::QueryMsg::GetFamilyByName).
|
||||
#[cw_serde]
|
||||
pub struct NodeFamilyByNameResponse {
|
||||
/// The name that was queried, echoed back so callers can correlate.
|
||||
pub name: String,
|
||||
|
||||
/// The matching family, or `None` if no family with that name exists.
|
||||
pub family: Option<NodeFamily>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyMembership`](crate::QueryMsg::GetFamilyMembership).
|
||||
#[cw_serde]
|
||||
pub struct NodeFamilyMembershipResponse {
|
||||
/// The node that was queried.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The id of the family the node currently belongs to, or `None` if the
|
||||
/// node is not currently a member of any family.
|
||||
pub family_id: Option<NodeFamilyId>,
|
||||
}
|
||||
|
||||
/// A pending [`FamilyInvitation`] paired with whether it has already timed
|
||||
/// out at the time the query was served.
|
||||
#[cw_serde]
|
||||
pub struct PendingFamilyInvitationDetails {
|
||||
/// The stored invitation as it was issued.
|
||||
pub invitation: FamilyInvitation,
|
||||
|
||||
/// `true` iff `now >= invitation.expires_at` at query time, i.e. the
|
||||
/// invitation is still in the pending map but can no longer be acted on.
|
||||
pub expired: bool,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPendingInvitation`](crate::QueryMsg::GetPendingInvitation).
|
||||
#[cw_serde]
|
||||
pub struct PendingFamilyInvitationResponse {
|
||||
/// The family component of the queried `(family_id, node_id)` key.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The node component of the queried `(family_id, node_id)` key.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The matching pending invitation along with an explicit expiry flag,
|
||||
/// or `None` if no such invitation exists.
|
||||
pub invitation: Option<PendingFamilyInvitationDetails>,
|
||||
}
|
||||
|
||||
/// One entry in a [`FamilyMembersPagedResponse`] page — pairs a node id with
|
||||
/// its [`FamilyMembership`] record (notably its `joined_at` timestamp).
|
||||
#[cw_serde]
|
||||
pub struct FamilyMemberRecord {
|
||||
/// The node currently in the family.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The membership record (carries `family_id` and `joined_at`).
|
||||
pub membership: FamilyMembership,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyMembersPaged`](crate::QueryMsg::GetFamilyMembersPaged).
|
||||
#[cw_serde]
|
||||
pub struct FamilyMembersPagedResponse {
|
||||
/// The family whose members were queried, echoed back so paginated
|
||||
/// callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The members on this page, in ascending [`NodeId`] order.
|
||||
pub members: Vec<FamilyMemberRecord>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (which the caller should treat as end-of-list).
|
||||
pub start_next_after: Option<NodeId>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetAllFamilyMembersPaged`](crate::QueryMsg::GetAllFamilyMembersPaged).
|
||||
#[cw_serde]
|
||||
pub struct AllFamilyMembersPagedResponse {
|
||||
/// The members on this page, in ascending [`NodeId`] order across every
|
||||
/// family.
|
||||
pub members: Vec<FamilyMemberRecord>,
|
||||
|
||||
/// Cursor (last `node_id`) to pass as `start_after` on the next call,
|
||||
/// or `None` if this page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<NodeId>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPendingInvitationsForFamilyPaged`](crate::QueryMsg::GetPendingInvitationsForFamilyPaged).
|
||||
#[cw_serde]
|
||||
pub struct PendingFamilyInvitationsPagedResponse {
|
||||
/// The family whose pending invitations were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The pending invitations on this page, in ascending invitee
|
||||
/// [`NodeId`] order, each stamped with whether it had already timed out
|
||||
/// at the time the query was served.
|
||||
pub invitations: Vec<PendingFamilyInvitationDetails>,
|
||||
|
||||
/// Cursor (last invitee node id) to pass as `start_after` on the next
|
||||
/// call, or `None` if this page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<NodeId>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPendingInvitationsForNodePaged`](crate::QueryMsg::GetPendingInvitationsForNodePaged).
|
||||
#[cw_serde]
|
||||
pub struct PendingInvitationsForNodePagedResponse {
|
||||
/// The node whose pending invitations were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The pending invitations addressed to this node on this page, in
|
||||
/// ascending [`NodeFamilyId`] order, each stamped with whether it had
|
||||
/// already timed out at the time the query was served.
|
||||
pub invitations: Vec<PendingFamilyInvitationDetails>,
|
||||
|
||||
/// Cursor (last issuing family id) to pass as `start_after` on the
|
||||
/// next call, or `None` if this page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<NodeFamilyId>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetAllPendingInvitationsPaged`](crate::QueryMsg::GetAllPendingInvitationsPaged).
|
||||
#[cw_serde]
|
||||
pub struct PendingInvitationsPagedResponse {
|
||||
/// The pending invitations on this page, in ascending
|
||||
/// `(family_id, node_id)` order, each stamped with whether it had
|
||||
/// already timed out at the time the query was served.
|
||||
pub invitations: Vec<PendingFamilyInvitationDetails>,
|
||||
|
||||
/// Cursor (last `(family_id, node_id)` pair) to pass as `start_after`
|
||||
/// on the next call, or `None` if this page is empty (treat as
|
||||
/// end-of-list).
|
||||
pub start_next_after: Option<(NodeFamilyId, NodeId)>,
|
||||
}
|
||||
|
||||
/// Cursor for paginating per-family past-invitation listings: identifies a
|
||||
/// single archive entry within a family by `(node_id, counter)`. The
|
||||
/// `counter` is the per-`(family, node)` archive slot — multiple archived
|
||||
/// invitations can exist for the same `(family, node)` pair (a node may be
|
||||
/// invited and have the invitation reach a terminal state more than once).
|
||||
pub type PastFamilyInvitationCursor = (NodeId, u64);
|
||||
|
||||
/// Cursor for paginating per-node past-invitation listings: identifies a
|
||||
/// single archive entry addressed to a fixed node by `(family_id, counter)`.
|
||||
pub type PastFamilyInvitationForNodeCursor = (NodeFamilyId, u64);
|
||||
|
||||
/// Cursor for paginating global past-invitation listings: identifies a
|
||||
/// single archive entry across all families by `((family_id, node_id), counter)`.
|
||||
pub type GlobalPastFamilyInvitationCursor = ((NodeFamilyId, NodeId), u64);
|
||||
|
||||
/// Response to [`QueryMsg::GetPastInvitationsForFamilyPaged`](crate::QueryMsg::GetPastInvitationsForFamilyPaged).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyInvitationsPagedResponse {
|
||||
/// The family whose archived invitations were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The archived invitations on this page, in ascending
|
||||
/// `(node_id, counter)` order across all terminal statuses.
|
||||
pub invitations: Vec<PastFamilyInvitation>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<PastFamilyInvitationCursor>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPastInvitationsForNodePaged`](crate::QueryMsg::GetPastInvitationsForNodePaged).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyInvitationsForNodePagedResponse {
|
||||
/// The node whose past invitations were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The archived invitations addressed to this node on this page, in
|
||||
/// ascending `(family_id, counter)` order across all terminal statuses.
|
||||
pub invitations: Vec<PastFamilyInvitation>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<PastFamilyInvitationForNodeCursor>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetAllPastInvitationsPaged`](crate::QueryMsg::GetAllPastInvitationsPaged).
|
||||
#[cw_serde]
|
||||
pub struct AllPastFamilyInvitationsPagedResponse {
|
||||
/// The archived invitations on this page, in ascending
|
||||
/// `((family_id, node_id), counter)` order across all terminal statuses.
|
||||
pub invitations: Vec<PastFamilyInvitation>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<GlobalPastFamilyInvitationCursor>,
|
||||
}
|
||||
|
||||
/// Cursor for paginating per-family past-member listings: identifies a single
|
||||
/// archive entry within a family by `(node_id, counter)`. The `counter` is the
|
||||
/// per-`(family, node)` archive slot — multiple archived membership entries
|
||||
/// can exist for the same `(family, node)` pair (a node may join, leave, and
|
||||
/// re-join the same family more than once).
|
||||
pub type PastFamilyMemberCursor = (NodeId, u64);
|
||||
|
||||
/// Cursor for paginating per-node past-member listings: identifies a single
|
||||
/// archive entry for a fixed node by `(family_id, counter)`.
|
||||
pub type PastFamilyMemberForNodeCursor = (NodeFamilyId, u64);
|
||||
|
||||
/// Response to [`QueryMsg::GetPastMembersForFamilyPaged`](crate::QueryMsg::GetPastMembersForFamilyPaged).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyMembersPagedResponse {
|
||||
/// The family whose archived memberships were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The archived membership records on this page, in ascending
|
||||
/// `(node_id, counter)` order.
|
||||
pub members: Vec<PastFamilyMember>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<PastFamilyMemberCursor>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPastMembersForNodePaged`](crate::QueryMsg::GetPastMembersForNodePaged).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyMembersForNodePagedResponse {
|
||||
/// The node whose archived memberships were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The archived membership records for this node on this page, in
|
||||
/// ascending `(family_id, counter)` order.
|
||||
pub members: Vec<PastFamilyMember>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<PastFamilyMemberForNodeCursor>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamiliesPaged`](crate::QueryMsg::GetFamiliesPaged).
|
||||
#[cw_serde]
|
||||
pub struct FamiliesPagedResponse {
|
||||
/// The families on this page, in ascending [`NodeFamilyId`] order.
|
||||
pub families: Vec<NodeFamily>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (which the caller should treat as end-of-list).
|
||||
pub start_next_after: Option<NodeFamilyId>,
|
||||
}
|
||||
@@ -8,7 +8,9 @@ license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version.workspace = true
|
||||
# pinned (not inherited from workspace) because this crate is imported by the ecash contract,
|
||||
# and the contracts workspace cannot be built with rustc more recent than 1.86
|
||||
rust-version = "1.86.0"
|
||||
readme.workspace = true
|
||||
publish = true
|
||||
|
||||
|
||||
@@ -1173,7 +1173,16 @@ impl ApiClientCore for Client {
|
||||
};
|
||||
|
||||
match response {
|
||||
Ok(resp) => return Ok(resp),
|
||||
Ok(resp) => {
|
||||
// Check if the response includes a rate limit error from the vercel API
|
||||
if is_http_rate_limit_err(&resp) {
|
||||
warn!("encountered vercel rate limit error for {}", url.as_str());
|
||||
// if we have multiple urls, update to the next
|
||||
self.maybe_rotate_hosts(Some(url.clone()));
|
||||
}
|
||||
|
||||
return Ok(resp);
|
||||
}
|
||||
Err(err) => {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let is_network_err = err.is_timeout();
|
||||
@@ -1226,17 +1235,39 @@ impl ApiClientCore for Client {
|
||||
}
|
||||
}
|
||||
|
||||
const VERCEL_CHALLENGE_HEADER: &str = "x-vercel-mitigated";
|
||||
const VERCEL_CHALLENGE_VALUE: &[u8] = b"challenge";
|
||||
|
||||
/// Check for Rate Limit challenge response from the vercel API
|
||||
pub(crate) fn is_http_rate_limit_err(resp: &Response) -> bool {
|
||||
let status = resp.status() == StatusCode::FORBIDDEN;
|
||||
let header = resp
|
||||
.headers()
|
||||
.get(VERCEL_CHALLENGE_HEADER)
|
||||
.is_some_and(|v| v.as_bytes() == VERCEL_CHALLENGE_VALUE);
|
||||
let content_type = resp
|
||||
.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.parse::<Mime>().ok())
|
||||
.is_some_and(|mime_type| {
|
||||
mime_type.type_() == mime::TEXT && mime_type.subtype() == mime::HTML
|
||||
});
|
||||
|
||||
status && header && content_type
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const MAX_ERR_SOURCE_ITERATIONS: usize = 4;
|
||||
|
||||
/// This functions attempts to check the error returned by reqwest to see if
|
||||
/// rotating host informtion (for clients with mutliple hosts defined) could be
|
||||
/// helpful. This looks for situations where the error could plausibly be caused
|
||||
/// by a network adversary, or where rotating to an equival hostname might help.
|
||||
/// This functions attempts to check the error returned by reqwest to see if rotating host
|
||||
/// information (for clients with multiple hosts defined) could be helpful. This looks for
|
||||
/// situations where the error could plausibly be caused by a network adversary, or where rotating
|
||||
/// to an equivalent hostname might help.
|
||||
///
|
||||
/// For example --> NetworkUnreachable will not be helped by rotating domains,
|
||||
/// but ConnectionReset might be caused by a network adversary blocking by SNI
|
||||
/// which could possibly benefit from rotating domains.
|
||||
/// For example --> NetworkUnreachable will not be helped by rotating domains, but ConnectionReset
|
||||
/// might be caused by a network adversary blocking by SNI which could possibly benefit from
|
||||
/// rotating domains.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
|
||||
if err.is_timeout() {
|
||||
@@ -1697,6 +1728,13 @@ where
|
||||
decode_raw_response(&headers, full)
|
||||
} else if res.status() == StatusCode::NOT_FOUND {
|
||||
Err(HttpClientError::NotFound { url: Box::new(url) })
|
||||
} else if is_http_rate_limit_err(&res) {
|
||||
Err(HttpClientError::EndpointFailure {
|
||||
url: Box::new(url),
|
||||
status,
|
||||
headers: Box::new(headers),
|
||||
error: String::from("received vercel rate limit challenge response"),
|
||||
})
|
||||
} else {
|
||||
let Ok(plaintext) = res.text().await else {
|
||||
return Err(HttpClientError::RequestFailure {
|
||||
|
||||
@@ -129,6 +129,41 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum OutputV2 {
|
||||
#[default]
|
||||
Json,
|
||||
Yaml,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::IntoParams, utoipa::ToSchema))]
|
||||
#[serde(default)]
|
||||
pub struct OutputParamsV2 {
|
||||
pub output: Option<OutputV2>,
|
||||
}
|
||||
|
||||
impl OutputParamsV2 {
|
||||
pub fn get_output(&self) -> OutputV2 {
|
||||
self.output.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn to_response<T: Serialize>(self, data: T) -> FormattedResponse<T> {
|
||||
self.get_output().to_response(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputV2 {
|
||||
pub fn to_response<T: Serialize>(self, data: T) -> FormattedResponse<T> {
|
||||
match self {
|
||||
OutputV2::Json => FormattedResponse::Json(Json::from(data)),
|
||||
OutputV2::Yaml => FormattedResponse::Yaml(Yaml::from(data)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
|
||||
@@ -32,6 +32,7 @@ mod tests {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version.workspace = true
|
||||
# pinned (not inherited from workspace) because this crate is imported by the ecash contract,
|
||||
# and the contracts workspace cannot be built with rustc more recent than 1.86
|
||||
rust-version = "1.86.0"
|
||||
readme.workspace = true
|
||||
publish = true
|
||||
# Exclude build.rs from published crate - it's only used for dev-time sync
|
||||
|
||||
@@ -22,6 +22,10 @@ pub const VESTING_CONTRACT_ADDRESS: &str =
|
||||
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "";
|
||||
// /\ TODO: this has to be updated once the contract is deployed
|
||||
|
||||
// \/ TODO: this has to be updated once the contract is deployed
|
||||
pub const NODE_FAMILIES_CONTRACT_ADDRESS: &str = "";
|
||||
// /\ TODO: this has to be updated once the contract is deployed
|
||||
|
||||
pub const ECASH_CONTRACT_ADDRESS: &str =
|
||||
"n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun";
|
||||
pub const GROUP_CONTRACT_ADDRESS: &str =
|
||||
@@ -43,10 +47,6 @@ pub const NYM_APIS: &[ApiUrlConst] = &[
|
||||
url: NYM_API,
|
||||
front_hosts: None,
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nym-frontdoor.vercel.app/api/",
|
||||
front_hosts: Some(&["vercel.app", "vercel.com"]),
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
|
||||
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
|
||||
@@ -68,7 +68,7 @@ pub const UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY: &str =
|
||||
pub const NYM_VPN_APIS: &[ApiUrlConst] = &[
|
||||
ApiUrlConst {
|
||||
url: NYM_VPN_API,
|
||||
front_hosts: Some(&["vercel.app", "vercel.com"]),
|
||||
front_hosts: None,
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nymvpn-frontdoor.global.ssl.fastly.net/api/",
|
||||
|
||||
@@ -39,6 +39,8 @@ pub struct NymContracts {
|
||||
pub vesting_contract_address: Option<String>,
|
||||
#[serde(default)]
|
||||
pub performance_contract_address: Option<String>,
|
||||
#[serde(default)]
|
||||
pub node_families_contract_address: Option<String>,
|
||||
pub ecash_contract_address: Option<String>,
|
||||
pub group_contract_address: Option<String>,
|
||||
pub multisig_contract_address: Option<String>,
|
||||
@@ -174,6 +176,9 @@ impl NymNetworkDetails {
|
||||
))
|
||||
.with_mixnet_contract(get_optional_env(var_names::MIXNET_CONTRACT_ADDRESS))
|
||||
.with_vesting_contract(get_optional_env(var_names::VESTING_CONTRACT_ADDRESS))
|
||||
.with_node_families_contract(get_optional_env(
|
||||
var_names::NODE_FAMILIES_CONTRACT_ADDRESS,
|
||||
))
|
||||
.with_ecash_contract(get_optional_env(var_names::ECASH_CONTRACT_ADDRESS))
|
||||
.with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS))
|
||||
.with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
|
||||
@@ -199,6 +204,9 @@ impl NymNetworkDetails {
|
||||
performance_contract_address: parse_optional_str(
|
||||
mainnet::PERFORMANCE_CONTRACT_ADDRESS,
|
||||
),
|
||||
node_families_contract_address: parse_optional_str(
|
||||
mainnet::NODE_FAMILIES_CONTRACT_ADDRESS,
|
||||
),
|
||||
ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS),
|
||||
group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS),
|
||||
multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS),
|
||||
@@ -252,6 +260,7 @@ impl NymNetworkDetails {
|
||||
|
||||
set_optional_var(var_names::MIXNET_CONTRACT_ADDRESS, self.contracts.mixnet_contract_address);
|
||||
set_optional_var(var_names::VESTING_CONTRACT_ADDRESS, self.contracts.vesting_contract_address);
|
||||
set_optional_var(var_names::NODE_FAMILIES_CONTRACT_ADDRESS, self.contracts.node_families_contract_address);
|
||||
set_optional_var(var_names::ECASH_CONTRACT_ADDRESS, self.contracts.ecash_contract_address);
|
||||
set_optional_var(var_names::GROUP_CONTRACT_ADDRESS, self.contracts.group_contract_address);
|
||||
set_optional_var(var_names::MULTISIG_CONTRACT_ADDRESS, self.contracts.multisig_contract_address);
|
||||
@@ -340,6 +349,12 @@ impl NymNetworkDetails {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_node_families_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
|
||||
self.contracts.node_families_contract_address = contract.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_ecash_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
|
||||
self.contracts.ecash_contract_address = contract.map(Into::into);
|
||||
|
||||
@@ -17,6 +17,7 @@ pub const VESTING_CONTRACT_ADDRESS: &str = "VESTING_CONTRACT_ADDRESS";
|
||||
pub const ECASH_CONTRACT_ADDRESS: &str = "ECASH_CONTRACT_ADDRESS";
|
||||
pub const GROUP_CONTRACT_ADDRESS: &str = "GROUP_CONTRACT_ADDRESS";
|
||||
pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_CONTRACT_ADDRESS";
|
||||
pub const NODE_FAMILIES_CONTRACT_ADDRESS: &str = "NODE_FAMILIES_CONTRACT_ADDRESS";
|
||||
pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS";
|
||||
pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS";
|
||||
pub const NYXD: &str = "NYXD";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-kkt"
|
||||
description = "Key transport protocol for the Nym network"
|
||||
version = "0.1.0"
|
||||
version = "1.21.0"
|
||||
authors = ["Georgio Nicolas <georgio@nymtech.net>"]
|
||||
edition = { workspace = true }
|
||||
license.workspace = true
|
||||
|
||||
@@ -31,9 +31,7 @@ where
|
||||
// instead we could maybe use the `from_bytes` variant and adding some suffix
|
||||
// when computing the digest until we produce a valid scalar.
|
||||
let mut bytes = [0u8; 64];
|
||||
let pad_size = 64usize
|
||||
.checked_sub(D::OutputSize::to_usize())
|
||||
.unwrap_or_default();
|
||||
let pad_size = 64usize.saturating_sub(D::OutputSize::to_usize());
|
||||
|
||||
bytes[pad_size..].copy_from_slice(&digest);
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ impl SecretKeyAuth {
|
||||
let ys_len = self.ys.len();
|
||||
let mut bytes = Vec::with_capacity(8 + (ys_len + 1) * 32);
|
||||
bytes.extend_from_slice(&self.x.to_bytes());
|
||||
bytes.extend_from_slice(&ys_len.to_le_bytes());
|
||||
bytes.extend_from_slice(&(ys_len as u64).to_le_bytes());
|
||||
for y in self.ys.iter() {
|
||||
bytes.extend_from_slice(&y.to_bytes())
|
||||
}
|
||||
@@ -337,7 +337,7 @@ impl VerificationKeyAuth {
|
||||
|
||||
bytes.extend_from_slice(&self.alpha.to_affine().to_compressed());
|
||||
|
||||
bytes.extend_from_slice(&beta_g1_len.to_le_bytes());
|
||||
bytes.extend_from_slice(&(beta_g1_len as u64).to_le_bytes());
|
||||
|
||||
for beta_g1 in self.beta_g1.iter() {
|
||||
bytes.extend_from_slice(&beta_g1.to_affine().to_compressed())
|
||||
|
||||
@@ -8,7 +8,9 @@ license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version.workspace = true
|
||||
# pinned (not inherited from workspace) because this crate is imported by the ecash contract,
|
||||
# and the contracts workspace cannot be built with rustc more recent than 1.86
|
||||
rust-version = "1.86.0"
|
||||
readme.workspace = true
|
||||
publish = true
|
||||
|
||||
|
||||
@@ -109,8 +109,8 @@ impl PacketSender {
|
||||
|
||||
let mut results = Vec::with_capacity(self.packets_per_node);
|
||||
|
||||
let mut seq = self.random_sequence_number();
|
||||
for _ in 0..self.packets_per_node {
|
||||
let start_seq = self.random_sequence_number();
|
||||
for seq in start_seq..start_seq + self.packets_per_node as u64 {
|
||||
let packet = EchoPacket::new(seq, &self.identity);
|
||||
let start = Instant::now();
|
||||
// TODO: should we get the start time after or before actually sending the data?
|
||||
@@ -210,7 +210,6 @@ impl PacketSender {
|
||||
let time_taken = Instant::now().duration_since(start);
|
||||
results.push(time_taken);
|
||||
|
||||
seq += 1;
|
||||
sleep(self.delay_between_packets).await;
|
||||
}
|
||||
|
||||
|
||||
Generated
+78
-261
@@ -2,17 +2,6 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.11"
|
||||
@@ -87,7 +76,7 @@ dependencies = [
|
||||
"ark-serialize",
|
||||
"ark-std",
|
||||
"derivative",
|
||||
"digest 0.10.7",
|
||||
"digest",
|
||||
"itertools 0.10.5",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
@@ -141,7 +130,7 @@ checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5"
|
||||
dependencies = [
|
||||
"ark-serialize-derive",
|
||||
"ark-std",
|
||||
"digest 0.10.7",
|
||||
"digest",
|
||||
"num-bigint",
|
||||
]
|
||||
|
||||
@@ -167,12 +156,6 @@ dependencies = [
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.2.0"
|
||||
@@ -215,25 +198,13 @@ version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d"
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330"
|
||||
dependencies = [
|
||||
"byte-tools",
|
||||
"crypto-mac",
|
||||
"digest 0.8.1",
|
||||
"opaque-debug",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -251,12 +222,6 @@ dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byte-tools"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
@@ -321,26 +286,6 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chacha"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddf3c081b5fba1e5615640aae998e0fbd10c24cbd897ee39ed754a77601a4862"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"keystream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
@@ -380,7 +325,7 @@ dependencies = [
|
||||
"ark-serialize",
|
||||
"cosmwasm-core",
|
||||
"curve25519-dalek",
|
||||
"digest 0.10.7",
|
||||
"digest",
|
||||
"ecdsa",
|
||||
"ed25519-zebra",
|
||||
"k256",
|
||||
@@ -491,9 +436,9 @@ version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
"rand_core",
|
||||
"subtle 2.4.1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -503,29 +448,10 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-mac"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5"
|
||||
dependencies = [
|
||||
"generic-array 0.12.4",
|
||||
"subtle 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctr"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
@@ -535,10 +461,10 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"curve25519-dalek-derive",
|
||||
"digest 0.10.7",
|
||||
"digest",
|
||||
"fiat-crypto",
|
||||
"rustc_version",
|
||||
"subtle 2.4.1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -709,7 +635,7 @@ dependencies = [
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-group-contract-common",
|
||||
"nym-multisig-contract-common 1.20.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"nym-multisig-contract-common 1.21.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -796,15 +722,6 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
|
||||
dependencies = [
|
||||
"generic-array 0.12.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -814,7 +731,7 @@ dependencies = [
|
||||
"block-buffer",
|
||||
"const-oid",
|
||||
"crypto-common",
|
||||
"subtle 2.4.1",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -825,7 +742,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
||||
|
||||
[[package]]
|
||||
name = "easy-addr"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"quote",
|
||||
@@ -839,7 +756,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
|
||||
dependencies = [
|
||||
"der",
|
||||
"digest 0.10.7",
|
||||
"digest",
|
||||
"elliptic-curve",
|
||||
"rfc6979",
|
||||
"signature",
|
||||
@@ -866,7 +783,7 @@ dependencies = [
|
||||
"rand_core",
|
||||
"serde",
|
||||
"sha2",
|
||||
"subtle 2.4.1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -899,13 +816,13 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"crypto-bigint",
|
||||
"digest 0.10.7",
|
||||
"digest",
|
||||
"ff",
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
"group",
|
||||
"rand_core",
|
||||
"sec1",
|
||||
"subtle 2.4.1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -922,7 +839,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
"subtle 2.4.1",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -931,15 +848,6 @@ version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c007b1ae3abe1cb6f85a16305acd418b7ca6343b953633fee2b76d8f108b830f"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -970,7 +878,7 @@ checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
||||
dependencies = [
|
||||
"ff",
|
||||
"rand_core",
|
||||
"subtle 2.4.1",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1004,22 +912,13 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1048,15 +947,6 @@ dependencies = [
|
||||
"hashbrown 0.15.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
@@ -1102,12 +992,6 @@ dependencies = [
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keystream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28"
|
||||
|
||||
[[package]]
|
||||
name = "konst"
|
||||
version = "0.3.16"
|
||||
@@ -1141,24 +1025,6 @@ version = "0.2.180"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
||||
|
||||
[[package]]
|
||||
name = "lioness"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"blake2",
|
||||
"chacha",
|
||||
"keystream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.2"
|
||||
@@ -1166,18 +1032,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
|
||||
|
||||
[[package]]
|
||||
name = "mixnet-vesting-integration-tests"
|
||||
name = "node-families"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-multi-test",
|
||||
"cw-controllers",
|
||||
"cw-storage-plus",
|
||||
"cw-utils",
|
||||
"cw2",
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-crypto",
|
||||
"nym-mixnet-contract",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-vesting-contract",
|
||||
"nym-vesting-contract-common",
|
||||
"rand_chacha",
|
||||
"nym-node-families-contract-common",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1212,7 +1083,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1248,9 +1118,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-coconut-dkg-common"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a95dc43ef8954a4f79846e3224434cf389d4a9c14b77f526dae3cfd2221c6141"
|
||||
checksum = "fcdfaf17c8b2a73bd6d14a3b9432118d66cc2335a371f185791099d32c25feb0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1258,14 +1128,12 @@ dependencies = [
|
||||
"cw2",
|
||||
"cw4",
|
||||
"nym-contracts-common",
|
||||
"nym-multisig-contract-common 1.20.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"nym-multisig-contract-common 1.21.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-contracts-common"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47bb3e8427c193cd500c802274b11879086863c3643525b6ece3e9ab1c77bddc"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
@@ -1279,9 +1147,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-contracts-common-testing"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3628aac6715e844f3ab20e3b8ae8c4684f144ccb78e205f002c1c3ae375e956"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cosmwasm-std",
|
||||
@@ -1295,16 +1161,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-crypto"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b710addc28c9950dd961e7dd3837ef3b479492d2b21b5f2437eb7d2899403027"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bs58",
|
||||
"curve25519-dalek",
|
||||
"ed25519-dalek",
|
||||
"nym-pemstore",
|
||||
"nym-sphinx-types",
|
||||
"rand",
|
||||
"sha2",
|
||||
"subtle-encoding",
|
||||
@@ -1332,7 +1195,7 @@ dependencies = [
|
||||
"nym-contracts-common-testing",
|
||||
"nym-crypto",
|
||||
"nym-ecash-contract-common",
|
||||
"nym-multisig-contract-common 1.20.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"nym-multisig-contract-common 1.21.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"nym-network-defaults",
|
||||
"schemars",
|
||||
"semver",
|
||||
@@ -1343,7 +1206,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-ecash-contract-common"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
@@ -1351,15 +1214,15 @@ dependencies = [
|
||||
"cw-controllers",
|
||||
"cw-utils",
|
||||
"cw2",
|
||||
"nym-multisig-contract-common 1.20.4",
|
||||
"nym-multisig-contract-common 1.21.0",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-group-contract-common"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb13102740426a4a2b683f54bbd6614fe9ecd745f5117bcf197c49c300b15edf"
|
||||
checksum = "f6f9773b3adc4b979c4e0f86d8932a78d10149df36e136f770653ad5405ce41a"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cw-controllers",
|
||||
@@ -1385,6 +1248,7 @@ dependencies = [
|
||||
"nym-crypto",
|
||||
"nym-mixnet-contract",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-node-families-contract-common",
|
||||
"nym-vesting-contract-common",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
@@ -1394,9 +1258,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-mixnet-contract-common"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41c21bceb3bb8ee2789851b3f381fc035485af825bf7290b7c99a5af4e8f6ba1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
@@ -1416,7 +1278,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-multisig-contract-common"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1431,9 +1293,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-multisig-contract-common"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4a20b931ee849f6179ce2b387accd058720017f644ffbc8c2422f3e9ac3ff54"
|
||||
checksum = "7681c7b43201d45a4958eab012a93e285ce47f7bba405e1ba1808edd195f3347"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1448,19 +1310,30 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-defaults"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9834193b4641acdf9f360aea684a6bd841cad287930bc0d7c3241a133756464"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"cargo_metadata 0.19.2",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-families-contract-common"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"cw-utils",
|
||||
"nym-contracts-common",
|
||||
"nym-mixnet-contract-common",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-pemstore"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03077f9ebeb40caf8aa8e6f7bf8728449f73733e7a246986e492fa34ad3e70ab"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"pem",
|
||||
"tracing",
|
||||
@@ -1477,20 +1350,22 @@ dependencies = [
|
||||
"cw-controllers",
|
||||
"cw-storage-plus",
|
||||
"cw2",
|
||||
"node-families",
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-crypto",
|
||||
"nym-mixnet-contract",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-node-families-contract-common",
|
||||
"nym-performance-contract-common",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-performance-contract-common"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42129a72f4b0dc0304a48b0ca1769b27694d913687ace5692d4c6924ca9f2a13"
|
||||
checksum = "c6e47f2a04d8b0e1c492cdd7dcfc98face5efc2a4cb999c03887a5b77e4cf055"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1518,9 +1393,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-pool-contract-common"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70239cc26beda3ad19289188c50d554522af29646d7d3f855bda6fc8ed332fe7"
|
||||
checksum = "9965dce8aaee8e9943b342695ff3fda45c8a4f1daab943ed752a8a0b47aa04c7"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1530,16 +1405,6 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-sphinx-types"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ba662d39fd6da9e13166fa1162ff41c2cfaed78a77c70df72fbda6fef5eb4f5"
|
||||
dependencies = [
|
||||
"sphinx-packet",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-vesting-contract"
|
||||
version = "1.4.1"
|
||||
@@ -1563,9 +1428,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-vesting-contract-common"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "676c2793efbf9ccdf86bb788c903f778a2d5993a5174729303f9511a297f4ca8"
|
||||
checksum = "af0179bfbb30551414277ca7bed631d48f3321df7b18e3032f23f2f111a577d7"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1582,12 +1447,6 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
|
||||
|
||||
[[package]]
|
||||
name = "p256"
|
||||
version = "0.13.2"
|
||||
@@ -1730,9 +1589,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
@@ -1758,16 +1617,6 @@ dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_distr"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.10.0"
|
||||
@@ -1824,7 +1673,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
"subtle 2.4.1",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1902,8 +1751,8 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"der",
|
||||
"generic-array 0.14.7",
|
||||
"subtle 2.4.1",
|
||||
"generic-array",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -1996,7 +1845,7 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest 0.10.7",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2005,36 +1854,10 @@ version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
"digest",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinx-packet"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c26f0c20d909fdda1c5d0ece3973127ca421984d55b000215df365e93722fc6e"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arrayref",
|
||||
"blake2",
|
||||
"bs58",
|
||||
"byteorder",
|
||||
"chacha",
|
||||
"ctr",
|
||||
"curve25519-dalek",
|
||||
"digest 0.10.7",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"lioness",
|
||||
"rand",
|
||||
"rand_distr",
|
||||
"sha2",
|
||||
"subtle 2.4.1",
|
||||
"x25519-dalek",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
@@ -2051,12 +1874,6 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.4.1"
|
||||
|
||||
+30
-20
@@ -4,12 +4,12 @@ members = [
|
||||
"coconut-dkg",
|
||||
"ecash",
|
||||
"mixnet",
|
||||
"mixnet-vesting-integration-tests",
|
||||
"nym-pool",
|
||||
"multisig/cw3-flex-multisig",
|
||||
"multisig/cw4-group",
|
||||
"vesting",
|
||||
"node-families",
|
||||
"nym-pool",
|
||||
"performance",
|
||||
"vesting",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
@@ -19,6 +19,8 @@ homepage = "https://nymtech.net"
|
||||
documentation = "https://nymtech.net"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
rust-version = "1.86.0"
|
||||
readme = "../README.md"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
@@ -48,7 +50,7 @@ cw3-fixed-multisig = "=2.0.0"
|
||||
cw4 = "=2.0.0"
|
||||
cw20 = "=2.0.0"
|
||||
cw20-base = "2.0.0"
|
||||
rand = "0.8.5"
|
||||
rand = "0.8.6"
|
||||
rand_chacha = "0.3.1"
|
||||
semver = "1.0.21"
|
||||
serde = "1.0.196"
|
||||
@@ -57,31 +59,33 @@ schemars = "0.8.16"
|
||||
|
||||
thiserror = "2.0.11"
|
||||
|
||||
easy-addr = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/easy_addr" }
|
||||
easy-addr = { version = "1.21.0", path = "../common/cosmwasm-smart-contracts/easy_addr" }
|
||||
# For local development with modifications, add a [patch.crates-io] section (see bottom of file)
|
||||
nym-coconut-dkg-common = "1.20.4"
|
||||
nym-contracts-common = "1.20.4"
|
||||
nym-contracts-common-testing = "1.20.4"
|
||||
nym-crypto = { version = "1.20.4", default-features = false }
|
||||
nym-ecash-contract-common = "1.20.4"
|
||||
nym-group-contract-common = "1.20.4"
|
||||
nym-mixnet-contract-common = "1.20.4"
|
||||
nym-multisig-contract-common = "1.20.4"
|
||||
nym-network-defaults = { version = "1.20.4", default-features = false }
|
||||
nym-performance-contract-common = "1.20.4"
|
||||
nym-pool-contract-common = "1.20.4"
|
||||
nym-vesting-contract-common = "1.20.4"
|
||||
nym-coconut-dkg-common = "1.21.0"
|
||||
nym-contracts-common = "1.21.0"
|
||||
nym-contracts-common-testing = "1.21.0"
|
||||
nym-crypto = { version = "1.21.0", default-features = false }
|
||||
nym-ecash-contract-common = "1.21.0"
|
||||
nym-group-contract-common = "1.21.0"
|
||||
nym-mixnet-contract-common = "1.21.0"
|
||||
nym-multisig-contract-common = "1.21.0"
|
||||
nym-network-defaults = { version = "1.21.0", default-features = false }
|
||||
nym-performance-contract-common = "1.21.0"
|
||||
nym-pool-contract-common = "1.21.0"
|
||||
nym-vesting-contract-common = "1.21.0"
|
||||
|
||||
# Aliases for crates that some contracts import under different names
|
||||
contracts-common = { version = "1.20.4", package = "nym-contracts-common" }
|
||||
mixnet-contract-common = { version = "1.20.4", package = "nym-mixnet-contract-common" }
|
||||
vesting-contract-common = { version = "1.20.4", package = "nym-vesting-contract-common" }
|
||||
contracts-common = { version = "1.21.0", package = "nym-contracts-common" }
|
||||
mixnet-contract-common = { version = "1.21.0", package = "nym-mixnet-contract-common" }
|
||||
vesting-contract-common = { version = "1.21.0", package = "nym-vesting-contract-common" }
|
||||
nym-node-families-contract-common = { version = "1.21.0", package = "nym-node-families-contract-common" }
|
||||
|
||||
# Internal contract workspace members (for cross-contract testing)
|
||||
cw3-flex-multisig = { version = "2.0.0", path = "multisig/cw3-flex-multisig" }
|
||||
cw4-group = { version = "2.0.0", path = "multisig/cw4-group" }
|
||||
nym-mixnet-contract = { version = "1.5.1", path = "mixnet" }
|
||||
nym-vesting-contract = { version = "1.4.1", path = "vesting" }
|
||||
node-families = { version = "0.1.0", path = "node-families" }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
unwrap_used = "deny"
|
||||
@@ -98,4 +102,10 @@ unreachable = "deny"
|
||||
# nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
|
||||
[patch.crates-io]
|
||||
nym-network-defaults = { path = "../common/network-defaults" }
|
||||
nym-crypto = { path = "../common/crypto" }
|
||||
nym-contracts-common-testing = { path = "../common/cosmwasm-smart-contracts/contracts-common-testing" }
|
||||
nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-ecash-contract-common = { path = "../common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-node-families-contract-common = { path = "../common/cosmwasm-smart-contracts/node-families-contract" }
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
[package]
|
||||
name = "mixnet-vesting-integration-tests"
|
||||
version = "0.1.0"
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
||||
# cosmwasm dependencies
|
||||
cosmwasm-std = { workspace = true }
|
||||
cw-multi-test = { workspace = true }
|
||||
|
||||
# contracts dependencies
|
||||
nym-mixnet-contract-common = { workspace = true }
|
||||
nym-vesting-contract-common = { workspace = true }
|
||||
nym-contracts-common = { workspace = true }
|
||||
|
||||
nym-mixnet-contract = { workspace = true }
|
||||
nym-vesting-contract = { workspace = true }
|
||||
|
||||
# other local dependencies
|
||||
nym-crypto = { workspace = true, features = ["asymmetric", "rand"] }
|
||||
|
||||
# external dependencies
|
||||
rand_chacha = "0.3"
|
||||
|
||||
[[test]]
|
||||
name = "mixnet-vesting-test"
|
||||
path = "src/tests.rs"
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::support::setup::{MIX_DENOM, REWARDING_VALIDATOR};
|
||||
use cosmwasm_std::Decimal;
|
||||
use nym_contracts_common::Percent;
|
||||
use nym_mixnet_contract_common::reward_params::RewardedSetParams;
|
||||
use nym_mixnet_contract_common::InitialRewardingParams;
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn default_mixnet_init_msg() -> nym_mixnet_contract_common::InstantiateMsg {
|
||||
nym_mixnet_contract_common::InstantiateMsg {
|
||||
rewarding_validator_address: REWARDING_VALIDATOR.to_string(),
|
||||
vesting_contract_address: "placeholder".to_string(),
|
||||
rewarding_denom: MIX_DENOM.to_string(),
|
||||
epochs_in_interval: 720,
|
||||
epoch_duration: Duration::from_secs(60 * 60),
|
||||
initial_rewarding_params: InitialRewardingParams {
|
||||
initial_reward_pool: Decimal::from_atomics(250_000_000_000_000u128, 0).unwrap(),
|
||||
initial_staking_supply: Decimal::from_atomics(223_000_000_000_000u128, 0).unwrap(),
|
||||
staking_supply_scale_factor: Percent::hundred(),
|
||||
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
|
||||
active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(),
|
||||
interval_pool_emission: Percent::from_percentage_value(2).unwrap(),
|
||||
rewarded_set_params: RewardedSetParams {
|
||||
entry_gateways: 70,
|
||||
exit_gateways: 50,
|
||||
mixnodes: 120,
|
||||
standby: 0,
|
||||
},
|
||||
},
|
||||
current_nym_node_version: "1.1.10".to_string(),
|
||||
version_score_weights: Default::default(),
|
||||
version_score_params: Default::default(),
|
||||
profit_margin: Default::default(),
|
||||
interval_operating_cost: Default::default(),
|
||||
key_validity_in_epochs: None,
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::support::setup::{MIXNET_OWNER, MIX_DENOM, REWARDING_VALIDATOR, VESTING_OWNER};
|
||||
use cosmwasm_std::{coin, coins, Addr, Coin, Empty};
|
||||
use cw_multi_test::{Contract, ContractWrapper};
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn mixnet_owner() -> Addr {
|
||||
Addr::unchecked(MIXNET_OWNER)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn vesting_owner() -> Addr {
|
||||
Addr::unchecked(VESTING_OWNER)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn rewarding_validator() -> Addr {
|
||||
Addr::unchecked(REWARDING_VALIDATOR)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn mix_coins(amount: u128) -> Vec<Coin> {
|
||||
coins(amount, MIX_DENOM)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn mix_coin(amount: u128) -> Coin {
|
||||
coin(amount, MIX_DENOM)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn test_rng() -> ChaCha20Rng {
|
||||
let dummy_seed = [42u8; 32];
|
||||
ChaCha20Rng::from_seed(dummy_seed)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn mixnet_contract_wrapper() -> Box<dyn Contract<Empty>> {
|
||||
Box::new(
|
||||
ContractWrapper::new(
|
||||
mixnet_contract::contract::execute,
|
||||
mixnet_contract::contract::instantiate,
|
||||
mixnet_contract::contract::query,
|
||||
)
|
||||
.with_migrate(mixnet_contract::contract::migrate),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn vesting_contract_wrapper() -> Box<dyn Contract<Empty>> {
|
||||
Box::new(
|
||||
ContractWrapper::new(
|
||||
vesting_contract::contract::execute,
|
||||
vesting_contract::contract::instantiate,
|
||||
vesting_contract::contract::query,
|
||||
)
|
||||
.with_migrate(vesting_contract::contract::migrate),
|
||||
)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod fixtures;
|
||||
pub mod helpers;
|
||||
pub mod setup;
|
||||
@@ -1,480 +0,0 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::support::fixtures;
|
||||
use crate::support::helpers::{
|
||||
mixnet_contract_wrapper, rewarding_validator, test_rng, vesting_contract_wrapper,
|
||||
};
|
||||
use cosmwasm_std::{coins, Addr, Coin, Decimal, Timestamp};
|
||||
use cw_multi_test::{App, AppBuilder, Executor};
|
||||
use nym_contracts_common::signing::{ContractMessageContent, MessageSignature, Nonce};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_mixnet_contract_common::nym_node::{EpochAssignmentResponse, Role, RolesMetadataResponse};
|
||||
use nym_mixnet_contract_common::reward_params::{NodeRewardingParameters, Performance};
|
||||
use nym_mixnet_contract_common::{
|
||||
CurrentIntervalResponse, MixnodeBondingPayload, NodeCostParams, RewardedSet, RewardingParams,
|
||||
RoleAssignment, SignableMixNodeBondingMsg,
|
||||
};
|
||||
use nym_mixnet_contract_common::{
|
||||
ExecuteMsg as MixnetExecuteMsg, MixNode, QueryMsg as MixnetQueryMsg,
|
||||
};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// our global accounts that should always get some coins at the start
|
||||
pub const MIXNET_OWNER: &str = "mixnet-owner";
|
||||
pub const VESTING_OWNER: &str = "vesting-owner";
|
||||
pub const REWARDING_VALIDATOR: &str = "rewarding-validator";
|
||||
pub const MIX_DENOM: &str = "unym";
|
||||
|
||||
#[allow(unused)]
|
||||
pub struct ContractInstantiationResult {
|
||||
mixnet_contract_address: Addr,
|
||||
vesting_contract_address: Addr,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub struct TestSetupBuilder {
|
||||
mixnet_init_msg: nym_mixnet_contract_common::InstantiateMsg,
|
||||
initial_balances: HashMap<Addr, Vec<Coin>>,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl TestSetupBuilder {
|
||||
pub fn new() -> Self {
|
||||
TestSetupBuilder {
|
||||
mixnet_init_msg: fixtures::default_mixnet_init_msg(),
|
||||
initial_balances: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_mixnet_init_msg(
|
||||
mut self,
|
||||
mixnet_init_msg: nym_mixnet_contract_common::InstantiateMsg,
|
||||
) -> Self {
|
||||
self.mixnet_init_msg = mixnet_init_msg;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_initial_balances(mut self, initial_balances: HashMap<Addr, Vec<Coin>>) -> Self {
|
||||
self.initial_balances = initial_balances;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_initial_balance(mut self, addr: impl Into<String>, balance: Vec<Coin>) -> Self {
|
||||
self.initial_balances.insert(Addr::unchecked(addr), balance);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> TestSetup {
|
||||
TestSetup::new(self.initial_balances, self.mixnet_init_msg)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub struct TestSetup {
|
||||
pub app: App,
|
||||
pub rng: ChaCha20Rng,
|
||||
|
||||
pub mixnet_contract: Addr,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl TestSetup {
|
||||
pub fn new_simple() -> Self {
|
||||
TestSetup::new(Default::default(), fixtures::default_mixnet_init_msg())
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
initial_balances: HashMap<Addr, Vec<Coin>>,
|
||||
custom_mixnet_init: nym_mixnet_contract_common::InstantiateMsg,
|
||||
) -> Self {
|
||||
let (app, contracts) = instantiate_contracts(initial_balances, Some(custom_mixnet_init));
|
||||
TestSetup {
|
||||
app,
|
||||
rng: test_rng(),
|
||||
mixnet_contract: contracts.mixnet_contract_address,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mixnet_contract(&self) -> Addr {
|
||||
self.mixnet_contract.clone()
|
||||
}
|
||||
|
||||
pub fn skip_to_current_epoch_end(&mut self) {
|
||||
let current_interval: CurrentIntervalResponse = self
|
||||
.app
|
||||
.wrap()
|
||||
.query_wasm_smart(
|
||||
self.mixnet_contract(),
|
||||
&MixnetQueryMsg::GetCurrentIntervalDetails {},
|
||||
)
|
||||
.unwrap();
|
||||
let epoch_end = current_interval.interval.current_epoch_end_unix_timestamp();
|
||||
|
||||
self.app.update_block(|current_block| {
|
||||
// skip few blocks just in case
|
||||
current_block.height += 10;
|
||||
current_block.time = Timestamp::from_seconds(epoch_end as u64)
|
||||
})
|
||||
}
|
||||
|
||||
fn get_rewarded_set(&self) -> RewardedSet {
|
||||
let metadata: RolesMetadataResponse = self
|
||||
.app
|
||||
.wrap()
|
||||
.query_wasm_smart(
|
||||
self.mixnet_contract(),
|
||||
&MixnetQueryMsg::GetRewardedSetMetadata {},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let entry: EpochAssignmentResponse = self
|
||||
.app
|
||||
.wrap()
|
||||
.query_wasm_smart(
|
||||
self.mixnet_contract(),
|
||||
&MixnetQueryMsg::GetRoleAssignment {
|
||||
role: Role::EntryGateway,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(entry.epoch_id, metadata.metadata.epoch_id);
|
||||
|
||||
let exit: EpochAssignmentResponse = self
|
||||
.app
|
||||
.wrap()
|
||||
.query_wasm_smart(
|
||||
self.mixnet_contract(),
|
||||
&MixnetQueryMsg::GetRoleAssignment {
|
||||
role: Role::ExitGateway,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(exit.epoch_id, metadata.metadata.epoch_id);
|
||||
|
||||
let layer1: EpochAssignmentResponse = self
|
||||
.app
|
||||
.wrap()
|
||||
.query_wasm_smart(
|
||||
self.mixnet_contract(),
|
||||
&MixnetQueryMsg::GetRoleAssignment { role: Role::Layer1 },
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(layer1.epoch_id, metadata.metadata.epoch_id);
|
||||
|
||||
let layer2: EpochAssignmentResponse = self
|
||||
.app
|
||||
.wrap()
|
||||
.query_wasm_smart(
|
||||
self.mixnet_contract(),
|
||||
&MixnetQueryMsg::GetRoleAssignment { role: Role::Layer2 },
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(layer2.epoch_id, metadata.metadata.epoch_id);
|
||||
|
||||
let layer3: EpochAssignmentResponse = self
|
||||
.app
|
||||
.wrap()
|
||||
.query_wasm_smart(
|
||||
self.mixnet_contract(),
|
||||
&MixnetQueryMsg::GetRoleAssignment { role: Role::Layer3 },
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(layer3.epoch_id, metadata.metadata.epoch_id);
|
||||
|
||||
let standby: EpochAssignmentResponse = self
|
||||
.app
|
||||
.wrap()
|
||||
.query_wasm_smart(
|
||||
self.mixnet_contract(),
|
||||
&MixnetQueryMsg::GetRoleAssignment {
|
||||
role: Role::Standby,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(standby.epoch_id, metadata.metadata.epoch_id);
|
||||
|
||||
RewardedSet {
|
||||
entry_gateways: entry.nodes,
|
||||
exit_gateways: exit.nodes,
|
||||
layer1: layer1.nodes,
|
||||
layer2: layer2.nodes,
|
||||
layer3: layer3.nodes,
|
||||
standby: standby.nodes,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn full_mixnet_epoch_operations(&mut self) {
|
||||
let rewarded_set = self.get_rewarded_set();
|
||||
|
||||
let current_params: RewardingParams = self
|
||||
.app
|
||||
.wrap()
|
||||
.query_wasm_smart(
|
||||
self.mixnet_contract(),
|
||||
&MixnetQueryMsg::GetRewardingParams {},
|
||||
)
|
||||
.unwrap();
|
||||
// TODO: handle paging
|
||||
|
||||
// begin epoch transition
|
||||
self.app
|
||||
.execute_contract(
|
||||
rewarding_validator(),
|
||||
self.mixnet_contract(),
|
||||
&MixnetExecuteMsg::BeginEpochTransition {},
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let work =
|
||||
Decimal::one() / Decimal::from_ratio(rewarded_set.rewarded_set_size() as u64, 1u64);
|
||||
let params = NodeRewardingParameters::new(Performance::hundred(), work);
|
||||
|
||||
let mut nodes = rewarded_set
|
||||
.layer1
|
||||
.iter()
|
||||
.chain(rewarded_set.layer2.iter())
|
||||
.chain(rewarded_set.layer3.iter())
|
||||
.chain(rewarded_set.entry_gateways.iter())
|
||||
.chain(rewarded_set.exit_gateways.iter())
|
||||
.chain(rewarded_set.standby.iter())
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
nodes.sort();
|
||||
|
||||
// reward
|
||||
for (node_id) in nodes {
|
||||
self.app
|
||||
.execute_contract(
|
||||
rewarding_validator(),
|
||||
self.mixnet_contract(),
|
||||
&MixnetExecuteMsg::RewardNode { node_id, params },
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// events
|
||||
self.app
|
||||
.execute_contract(
|
||||
rewarding_validator(),
|
||||
self.mixnet_contract(),
|
||||
&MixnetExecuteMsg::ReconcileEpochEvents { limit: None },
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// don't bother changing the active set, use the same node for update and advance
|
||||
|
||||
self.app
|
||||
.execute_contract(
|
||||
rewarding_validator(),
|
||||
self.mixnet_contract(),
|
||||
&MixnetExecuteMsg::AssignRoles {
|
||||
assignment: RoleAssignment {
|
||||
role: Role::EntryGateway,
|
||||
nodes: rewarded_set.entry_gateways,
|
||||
},
|
||||
},
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.app
|
||||
.execute_contract(
|
||||
rewarding_validator(),
|
||||
self.mixnet_contract(),
|
||||
&MixnetExecuteMsg::AssignRoles {
|
||||
assignment: RoleAssignment {
|
||||
role: Role::ExitGateway,
|
||||
nodes: rewarded_set.exit_gateways,
|
||||
},
|
||||
},
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.app
|
||||
.execute_contract(
|
||||
rewarding_validator(),
|
||||
self.mixnet_contract(),
|
||||
&MixnetExecuteMsg::AssignRoles {
|
||||
assignment: RoleAssignment {
|
||||
role: Role::Layer1,
|
||||
nodes: rewarded_set.layer1,
|
||||
},
|
||||
},
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.app
|
||||
.execute_contract(
|
||||
rewarding_validator(),
|
||||
self.mixnet_contract(),
|
||||
&MixnetExecuteMsg::AssignRoles {
|
||||
assignment: RoleAssignment {
|
||||
role: Role::Layer2,
|
||||
nodes: rewarded_set.layer2,
|
||||
},
|
||||
},
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.app
|
||||
.execute_contract(
|
||||
rewarding_validator(),
|
||||
self.mixnet_contract(),
|
||||
&MixnetExecuteMsg::AssignRoles {
|
||||
assignment: RoleAssignment {
|
||||
role: Role::Layer3,
|
||||
nodes: rewarded_set.layer3,
|
||||
},
|
||||
},
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.app
|
||||
.execute_contract(
|
||||
rewarding_validator(),
|
||||
self.mixnet_contract(),
|
||||
&MixnetExecuteMsg::AssignRoles {
|
||||
assignment: RoleAssignment {
|
||||
role: Role::Standby,
|
||||
nodes: rewarded_set.standby,
|
||||
},
|
||||
},
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn advance_mixnet_epoch(&mut self) {
|
||||
self.skip_to_current_epoch_end();
|
||||
self.full_mixnet_epoch_operations();
|
||||
}
|
||||
|
||||
pub fn valid_mixnode_with_sig(
|
||||
&mut self,
|
||||
owner: &str,
|
||||
cost_params: NodeCostParams,
|
||||
stake: Coin,
|
||||
) -> (MixNode, MessageSignature) {
|
||||
let signing_nonce: Nonce = self
|
||||
.app
|
||||
.wrap()
|
||||
.query_wasm_smart(
|
||||
self.mixnet_contract(),
|
||||
&MixnetQueryMsg::GetSigningNonce {
|
||||
address: owner.to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let keypair = ed25519::KeyPair::new(&mut self.rng);
|
||||
let identity_key = keypair.public_key().to_base58_string();
|
||||
let legit_sphinx_keys = nym_crypto::asymmetric::x25519::KeyPair::new(&mut self.rng);
|
||||
|
||||
let mixnode = MixNode {
|
||||
identity_key,
|
||||
sphinx_key: legit_sphinx_keys.public_key().to_base58_string(),
|
||||
host: "mix.node.org".to_string(),
|
||||
mix_port: 1789,
|
||||
verloc_port: 1790,
|
||||
http_api_port: 8000,
|
||||
version: "1.1.14".to_string(),
|
||||
};
|
||||
|
||||
let payload = MixnodeBondingPayload::new(mixnode.clone(), cost_params);
|
||||
let content = ContractMessageContent::new(Addr::unchecked(owner), vec![stake], payload);
|
||||
let sign_payload = SignableMixNodeBondingMsg::new(signing_nonce, content);
|
||||
let plaintext = sign_payload.to_plaintext().unwrap();
|
||||
let signature = keypair.private_key().sign(plaintext);
|
||||
let msg_signature = MessageSignature::from(signature.to_bytes().as_ref());
|
||||
|
||||
(mixnode, msg_signature)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn instantiate_contracts(
|
||||
mut initial_funds: HashMap<Addr, Vec<Coin>>,
|
||||
custom_mixnet_init: Option<nym_mixnet_contract_common::InstantiateMsg>,
|
||||
) -> (App, ContractInstantiationResult) {
|
||||
// add our global addresses to the map
|
||||
initial_funds.insert(
|
||||
Addr::unchecked(MIXNET_OWNER),
|
||||
coins(100_000_000_000, MIX_DENOM),
|
||||
);
|
||||
|
||||
initial_funds.insert(
|
||||
Addr::unchecked(VESTING_OWNER),
|
||||
coins(100_000_000_000, MIX_DENOM),
|
||||
);
|
||||
|
||||
initial_funds.insert(
|
||||
Addr::unchecked(REWARDING_VALIDATOR),
|
||||
coins(1_000_000_000_000, MIX_DENOM),
|
||||
);
|
||||
|
||||
let mut app = AppBuilder::new().build(|router, _api, storage| {
|
||||
for (addr, funds) in initial_funds {
|
||||
router
|
||||
.bank
|
||||
.init_balance(storage, &addr, funds.clone())
|
||||
.unwrap()
|
||||
}
|
||||
});
|
||||
|
||||
let mixnet_code_id = app.store_code(mixnet_contract_wrapper());
|
||||
let vesting_code_id = app.store_code(vesting_contract_wrapper());
|
||||
|
||||
let mixnet_contract_address = app
|
||||
.instantiate_contract(
|
||||
mixnet_code_id,
|
||||
Addr::unchecked(MIXNET_OWNER),
|
||||
&custom_mixnet_init.unwrap_or(fixtures::default_mixnet_init_msg()),
|
||||
&[],
|
||||
"mixnet-contract",
|
||||
Some(MIXNET_OWNER.to_string()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let vesting_contract_address = app
|
||||
.instantiate_contract(
|
||||
vesting_code_id,
|
||||
Addr::unchecked(VESTING_OWNER),
|
||||
&nym_vesting_contract_common::InitMsg {
|
||||
mixnet_contract_address: mixnet_contract_address.to_string(),
|
||||
mix_denom: MIX_DENOM.to_string(),
|
||||
},
|
||||
&[],
|
||||
"vesting-contract",
|
||||
Some(VESTING_OWNER.to_string()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// now fix up vesting contract address...
|
||||
app.migrate_contract(
|
||||
Addr::unchecked(MIXNET_OWNER),
|
||||
mixnet_contract_address.clone(),
|
||||
&nym_mixnet_contract_common::MigrateMsg {
|
||||
vesting_contract_address: Some(vesting_contract_address.to_string()),
|
||||
unsafe_skip_state_updates: Some(true),
|
||||
},
|
||||
mixnet_code_id,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
(
|
||||
app,
|
||||
ContractInstantiationResult {
|
||||
mixnet_contract_address,
|
||||
vesting_contract_address,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
mod support;
|
||||
@@ -28,6 +28,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
[dependencies]
|
||||
mixnet-contract-common = { workspace = true }
|
||||
vesting-contract-common = { workspace = true }
|
||||
nym-node-families-contract-common = { workspace = true }
|
||||
nym-contracts-common = { workspace = true }
|
||||
nym-contracts-common-testing = { workspace = true, optional = true }
|
||||
|
||||
@@ -41,6 +42,8 @@ bs58 = { workspace = true }
|
||||
serde = { workspace = true, default-features = false, features = ["derive"] }
|
||||
semver = { workspace = true }
|
||||
|
||||
nym-crypto = { workspace = true, optional = true }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow.workspace = true
|
||||
@@ -56,7 +59,7 @@ nym-contracts-common-testing = { workspace = true }
|
||||
[features]
|
||||
default = []
|
||||
contract-testing = ["mixnet-contract-common/contract-testing"]
|
||||
testable-mixnet-contract = ["nym-contracts-common-testing"]
|
||||
testable-mixnet-contract = ["nym-contracts-common-testing", "nym-crypto", "nym-crypto/asymmetric", "nym-crypto/rand"]
|
||||
schema-gen = ["mixnet-contract-common/schema", "cosmwasm-schema"]
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"epoch_duration",
|
||||
"epochs_in_interval",
|
||||
"initial_rewarding_params",
|
||||
"node_families_contract_address",
|
||||
"rewarding_denom",
|
||||
"rewarding_validator_address",
|
||||
"vesting_contract_address"
|
||||
@@ -50,6 +51,9 @@
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_families_contract_address": {
|
||||
"type": "string"
|
||||
},
|
||||
"profit_margin": {
|
||||
"default": {
|
||||
"maximum": "1",
|
||||
@@ -1267,6 +1271,81 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "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.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"admin_migrate_vested_mix_node"
|
||||
],
|
||||
"properties": {
|
||||
"admin_migrate_vested_mix_node": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "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.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"admin_migrate_vested_delegation"
|
||||
],
|
||||
"properties": {
|
||||
"admin_migrate_vested_delegation": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"mix_id",
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"mix_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Admin-only: batch variant of [`ExecuteMsg::AdminMigrateVestedDelegation`]. Reverts the entire batch on the first error, so callers should treat it as all-or-nothing.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"admin_batch_migrate_vested_delegations"
|
||||
],
|
||||
"properties": {
|
||||
"admin_batch_migrate_vested_delegations": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"entries"
|
||||
],
|
||||
"properties": {
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/VestedDelegationMigrationEntry"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
@@ -1958,6 +2037,24 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"VestedDelegationMigrationEntry": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"mix_id",
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"mix_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3509,7 +3606,13 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "MigrateMsg",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_families_contract_address"
|
||||
],
|
||||
"properties": {
|
||||
"node_families_contract_address": {
|
||||
"type": "string"
|
||||
},
|
||||
"unsafe_skip_state_updates": {
|
||||
"type": [
|
||||
"boolean",
|
||||
@@ -10774,12 +10877,21 @@
|
||||
"description": "The current state of the mixnet contract.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_families_contract_address",
|
||||
"params",
|
||||
"rewarding_denom",
|
||||
"rewarding_validator_address",
|
||||
"vesting_contract_address"
|
||||
],
|
||||
"properties": {
|
||||
"node_families_contract_address": {
|
||||
"description": "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.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
]
|
||||
},
|
||||
"owner": {
|
||||
"description": "Address of the contract owner.",
|
||||
"default": null,
|
||||
|
||||
@@ -976,6 +976,81 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "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.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"admin_migrate_vested_mix_node"
|
||||
],
|
||||
"properties": {
|
||||
"admin_migrate_vested_mix_node": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "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.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"admin_migrate_vested_delegation"
|
||||
],
|
||||
"properties": {
|
||||
"admin_migrate_vested_delegation": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"mix_id",
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"mix_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Admin-only: batch variant of [`ExecuteMsg::AdminMigrateVestedDelegation`]. Reverts the entire batch on the first error, so callers should treat it as all-or-nothing.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"admin_batch_migrate_vested_delegations"
|
||||
],
|
||||
"properties": {
|
||||
"admin_batch_migrate_vested_delegations": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"entries"
|
||||
],
|
||||
"properties": {
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/VestedDelegationMigrationEntry"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
@@ -1667,6 +1742,24 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"VestedDelegationMigrationEntry": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"mix_id",
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"mix_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"epoch_duration",
|
||||
"epochs_in_interval",
|
||||
"initial_rewarding_params",
|
||||
"node_families_contract_address",
|
||||
"rewarding_denom",
|
||||
"rewarding_validator_address",
|
||||
"vesting_contract_address"
|
||||
@@ -46,6 +47,9 @@
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_families_contract_address": {
|
||||
"type": "string"
|
||||
},
|
||||
"profit_margin": {
|
||||
"default": {
|
||||
"maximum": "1",
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "MigrateMsg",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_families_contract_address"
|
||||
],
|
||||
"properties": {
|
||||
"node_families_contract_address": {
|
||||
"type": "string"
|
||||
},
|
||||
"unsafe_skip_state_updates": {
|
||||
"type": [
|
||||
"boolean",
|
||||
|
||||
@@ -4,12 +4,21 @@
|
||||
"description": "The current state of the mixnet contract.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_families_contract_address",
|
||||
"params",
|
||||
"rewarding_denom",
|
||||
"rewarding_validator_address",
|
||||
"vesting_contract_address"
|
||||
],
|
||||
"properties": {
|
||||
"node_families_contract_address": {
|
||||
"description": "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.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
]
|
||||
},
|
||||
"owner": {
|
||||
"description": "Address of the contract owner.",
|
||||
"default": null,
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::constants::INITIAL_PLEDGE_AMOUNT;
|
||||
use crate::interval::storage as interval_storage;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use crate::nodes::storage as nymnodes_storage;
|
||||
use crate::queued_migrations::introduce_key_rotation_id;
|
||||
use crate::queued_migrations::introduce_node_families_contract;
|
||||
use crate::rewards::storage::RewardingStorage;
|
||||
use cosmwasm_std::{
|
||||
entry_point, to_json_binary, Addr, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse,
|
||||
@@ -28,6 +28,7 @@ fn default_initial_state(
|
||||
owner: Addr,
|
||||
rewarding_validator_address: Addr,
|
||||
vesting_contract_address: Addr,
|
||||
node_families_contract_address: Addr,
|
||||
) -> ContractState {
|
||||
// we have to temporarily preserve this functionalities until it can be removed
|
||||
#[allow(deprecated)]
|
||||
@@ -35,6 +36,7 @@ fn default_initial_state(
|
||||
owner: Some(owner),
|
||||
rewarding_validator_address,
|
||||
vesting_contract_address,
|
||||
node_families_contract_address,
|
||||
rewarding_denom: msg.rewarding_denom.clone(),
|
||||
params: ContractStateParams {
|
||||
delegations_params: DelegationsParams {
|
||||
@@ -90,11 +92,15 @@ pub fn instantiate(
|
||||
|
||||
let rewarding_validator_address = deps.api.addr_validate(&msg.rewarding_validator_address)?;
|
||||
let vesting_contract_address = deps.api.addr_validate(&msg.vesting_contract_address)?;
|
||||
let node_families_contract_address = deps
|
||||
.api
|
||||
.addr_validate(&msg.node_families_contract_address)?;
|
||||
let state = default_initial_state(
|
||||
&msg,
|
||||
info.sender.clone(),
|
||||
rewarding_validator_address.clone(),
|
||||
vesting_contract_address,
|
||||
node_families_contract_address,
|
||||
);
|
||||
let starting_interval =
|
||||
Interval::init_interval(msg.epochs_in_interval, msg.epoch_duration, &env);
|
||||
@@ -301,6 +307,19 @@ pub fn execute(
|
||||
ExecuteMsg::MigrateVestedDelegation { mix_id } => {
|
||||
crate::vesting_migration::try_migrate_vested_delegation(deps, env, info, mix_id)
|
||||
}
|
||||
ExecuteMsg::AdminMigrateVestedMixNode { owner } => {
|
||||
crate::vesting_migration::try_admin_migrate_vested_mixnode(deps, info, owner)
|
||||
}
|
||||
ExecuteMsg::AdminMigrateVestedDelegation { mix_id, owner } => {
|
||||
crate::vesting_migration::try_admin_migrate_vested_delegation(
|
||||
deps, env, info, mix_id, owner,
|
||||
)
|
||||
}
|
||||
ExecuteMsg::AdminBatchMigrateVestedDelegations { entries } => {
|
||||
crate::vesting_migration::try_admin_batch_migrate_vested_delegations(
|
||||
deps, env, info, entries,
|
||||
)
|
||||
}
|
||||
|
||||
// legacy vesting
|
||||
ExecuteMsg::BondMixnodeOnBehalf { .. }
|
||||
@@ -629,7 +648,10 @@ pub fn migrate(
|
||||
let skip_state_updates = msg.unsafe_skip_state_updates.unwrap_or(false);
|
||||
|
||||
if !skip_state_updates {
|
||||
introduce_key_rotation_id(deps.branch())?;
|
||||
let addr = deps
|
||||
.api
|
||||
.addr_validate(&msg.node_families_contract_address)?;
|
||||
introduce_node_families_contract(deps.branch(), addr)?;
|
||||
}
|
||||
|
||||
// due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address
|
||||
@@ -668,6 +690,7 @@ mod tests {
|
||||
let init_msg = InstantiateMsg {
|
||||
rewarding_validator_address: deps.api.addr_make("foomp123").to_string(),
|
||||
vesting_contract_address: deps.api.addr_make("bar456").to_string(),
|
||||
node_families_contract_address: deps.api.addr_make("baz789").to_string(),
|
||||
rewarding_denom: "uatom".to_string(),
|
||||
epochs_in_interval: 1234,
|
||||
epoch_duration: Duration::from_secs(4321),
|
||||
@@ -708,6 +731,7 @@ mod tests {
|
||||
owner: Some(deps.api.addr_make("sender")),
|
||||
rewarding_validator_address: deps.api.addr_make("foomp123"),
|
||||
vesting_contract_address: deps.api.addr_make("bar456"),
|
||||
node_families_contract_address: deps.api.addr_make("baz789"),
|
||||
rewarding_denom: "uatom".into(),
|
||||
params: ContractStateParams {
|
||||
delegations_params: DelegationsParams {
|
||||
|
||||
@@ -71,7 +71,7 @@ pub(crate) fn query_current_nym_node_version(
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::support::tests::test_helpers;
|
||||
use cosmwasm_std::{coin, Addr};
|
||||
use cosmwasm_std::coin;
|
||||
use mixnet_contract_common::{ConfigScoreParams, DelegationsParams, OperatorsParams};
|
||||
|
||||
#[test]
|
||||
@@ -80,9 +80,10 @@ pub(crate) mod tests {
|
||||
|
||||
#[allow(deprecated)]
|
||||
let dummy_state = ContractState {
|
||||
owner: Some(Addr::unchecked("foomp")),
|
||||
rewarding_validator_address: Addr::unchecked("monitor"),
|
||||
vesting_contract_address: Addr::unchecked("foomp"),
|
||||
owner: Some(deps.api.addr_make("foomp")),
|
||||
rewarding_validator_address: deps.api.addr_make("monitor"),
|
||||
vesting_contract_address: deps.api.addr_make("foomp"),
|
||||
node_families_contract_address: deps.api.addr_make("bar"),
|
||||
rewarding_denom: "unym".to_string(),
|
||||
params: ContractStateParams {
|
||||
delegations_params: DelegationsParams {
|
||||
|
||||
@@ -156,6 +156,14 @@ pub(crate) fn vesting_contract_address(storage: &dyn Storage) -> Result<Addr, Mi
|
||||
.map(|state| state.vesting_contract_address)?)
|
||||
}
|
||||
|
||||
pub(crate) fn node_families_contract_address(
|
||||
storage: &dyn Storage,
|
||||
) -> Result<Addr, MixnetContractError> {
|
||||
Ok(CONTRACT_STATE
|
||||
.load(storage)
|
||||
.map(|state| state.node_families_contract_address)?)
|
||||
}
|
||||
|
||||
pub(crate) fn state_params(
|
||||
storage: &dyn Storage,
|
||||
) -> Result<ContractStateParams, MixnetContractError> {
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::support::helpers::{
|
||||
ensure_epoch_in_progress_state, ensure_no_existing_bond, ensure_operating_cost_within_range,
|
||||
ensure_profit_margin_within_range, validate_pledge,
|
||||
};
|
||||
use cosmwasm_std::{coin, Coin, DepsMut, Env, MessageInfo, Response};
|
||||
use cosmwasm_std::{coin, wasm_execute, Coin, DepsMut, Env, MessageInfo, Response};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::events::{
|
||||
new_nym_node_bonding_event, new_pending_cost_params_update_event,
|
||||
@@ -29,6 +29,7 @@ use mixnet_contract_common::{
|
||||
PendingIntervalEventKind,
|
||||
};
|
||||
use nym_contracts_common::signing::{MessageSignature, SigningPurpose};
|
||||
use nym_node_families_contract_common::msg::ExecuteMsg as NodeFamiliesExecuteMsg;
|
||||
use serde::Serialize;
|
||||
|
||||
pub fn try_add_nym_node(
|
||||
@@ -147,13 +148,24 @@ pub(crate) fn try_remove_nym_node(
|
||||
};
|
||||
interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?;
|
||||
|
||||
Ok(
|
||||
Response::new().add_event(new_pending_nym_node_unbonding_event(
|
||||
// send message to the node families contract to remove this node from any family it might be a member of
|
||||
let node_families_contract_addr =
|
||||
mixnet_params_storage::node_families_contract_address(deps.storage)?;
|
||||
let remove_from_family_exec = wasm_execute(
|
||||
node_families_contract_addr,
|
||||
&NodeFamiliesExecuteMsg::OnNymNodeUnbond {
|
||||
node_id: existing_bond.node_id,
|
||||
},
|
||||
vec![],
|
||||
)?;
|
||||
|
||||
Ok(Response::new()
|
||||
.add_message(remove_from_family_exec)
|
||||
.add_event(new_pending_nym_node_unbonding_event(
|
||||
&existing_bond.owner,
|
||||
existing_bond.identity(),
|
||||
existing_bond.node_id,
|
||||
)),
|
||||
)
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) fn try_update_node_config(
|
||||
|
||||
@@ -1,21 +1,39 @@
|
||||
// Copyright 2022-2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::interval::storage as interval_storage;
|
||||
use crate::nodes::storage as nymnodes_storage;
|
||||
use cosmwasm_std::DepsMut;
|
||||
use crate::constants::CONTRACT_STATE_KEY;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use cosmwasm_std::{Addr, DepsMut};
|
||||
use cw_storage_plus::Item;
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::KeyRotationState;
|
||||
use mixnet_contract_common::{ContractState, ContractStateParams};
|
||||
|
||||
pub fn introduce_node_families_contract(
|
||||
deps: DepsMut,
|
||||
node_families_contract_address: Addr,
|
||||
) -> Result<(), MixnetContractError> {
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct OldContractState {
|
||||
owner: Option<Addr>,
|
||||
rewarding_validator_address: Addr,
|
||||
vesting_contract_address: Addr,
|
||||
rewarding_denom: String,
|
||||
params: ContractStateParams,
|
||||
}
|
||||
|
||||
const OLD_CONTRACT_STATE: Item<OldContractState> = Item::new(CONTRACT_STATE_KEY);
|
||||
let old = OLD_CONTRACT_STATE.load(deps.storage)?;
|
||||
|
||||
#[allow(deprecated)]
|
||||
let updated = ContractState {
|
||||
owner: old.owner,
|
||||
rewarding_validator_address: old.rewarding_validator_address,
|
||||
vesting_contract_address: old.vesting_contract_address,
|
||||
rewarding_denom: old.rewarding_denom,
|
||||
params: old.params,
|
||||
node_families_contract_address,
|
||||
};
|
||||
mixnet_params_storage::CONTRACT_STATE.save(deps.storage, &updated)?;
|
||||
|
||||
pub fn introduce_key_rotation_id(deps: DepsMut) -> Result<(), MixnetContractError> {
|
||||
let current_epoch_id =
|
||||
interval_storage::current_interval(deps.storage)?.current_epoch_absolute_id();
|
||||
nymnodes_storage::KEY_ROTATION_STATE.save(
|
||||
deps.storage,
|
||||
&KeyRotationState {
|
||||
validity_epochs: 24,
|
||||
initial_epoch_id: current_epoch_id,
|
||||
},
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,16 +5,26 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::contract::{execute, instantiate, migrate, query};
|
||||
use cosmwasm_std::Decimal;
|
||||
use cosmwasm_std::testing::{message_info, mock_env};
|
||||
use cosmwasm_std::{coin, coins, Addr, Decimal, MessageInfo, StdError, StdResult};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::nym_node::{NodeDetailsResponse, NodeOwnershipResponse, Role};
|
||||
use mixnet_contract_common::reward_params::RewardedSetParams;
|
||||
use mixnet_contract_common::{
|
||||
ExecuteMsg, InitialRewardingParams, InstantiateMsg, MigrateMsg, QueryMsg,
|
||||
CurrentIntervalResponse, EpochId, ExecuteMsg, InitialRewardingParams, InstantiateMsg, Interval,
|
||||
MigrateMsg, MixnetContractQuerier, NodeCostParams, NodeId, NymNode, NymNodeBondingPayload,
|
||||
QueryMsg, RoleAssignment, SignableNymNodeBondingMsg, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT,
|
||||
DEFAULT_PROFIT_MARGIN_PERCENT,
|
||||
};
|
||||
use nym_contracts_common::signing::{ContractMessageContent, MessageSignature};
|
||||
use nym_contracts_common::Percent;
|
||||
use nym_contracts_common_testing::{
|
||||
mock_dependencies, ContractFn, PermissionedFn, QueryFn, TEST_DENOM,
|
||||
mock_dependencies, ArbitraryContractStorageReader, ArbitraryContractStorageWriter, BankExt,
|
||||
ChainOpts, ContractFn, ContractTester, PermissionedFn, QueryFn, RandExt, TEST_DENOM,
|
||||
};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::time::Duration;
|
||||
|
||||
pub use nym_contracts_common_testing::TestableNymContract;
|
||||
@@ -74,6 +84,10 @@ impl TestableNymContract for MixnetContract {
|
||||
InstantiateMsg {
|
||||
rewarding_validator_address: deps.api.addr_make("rewarder").to_string(),
|
||||
vesting_contract_address: deps.api.addr_make("vesting-contract").to_string(),
|
||||
node_families_contract_address: deps
|
||||
.api
|
||||
.addr_make("node-families-contract")
|
||||
.to_string(),
|
||||
rewarding_denom: TEST_DENOM.to_string(),
|
||||
epochs_in_interval: 720,
|
||||
epoch_duration: Duration::from_secs(60 * 60),
|
||||
@@ -87,3 +101,203 @@ impl TestableNymContract for MixnetContract {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EmbeddedMixnetContractExt:
|
||||
ChainOpts + ArbitraryContractStorageWriter + ArbitraryContractStorageReader + RandExt + BankExt
|
||||
{
|
||||
fn mixnet_contract_address(&self) -> StdResult<Addr>;
|
||||
|
||||
fn execute_mixnet_contract(&mut self, sender: MessageInfo, msg: &ExecuteMsg) -> StdResult<()> {
|
||||
let address = self.mixnet_contract_address()?;
|
||||
|
||||
self.execute_arbitrary_contract(address, sender, msg)
|
||||
.map_err(|err| {
|
||||
StdError::generic_err(format!("mixnet contract execution failure: {err}"))
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_from_mixnet_contract_storage<T: DeserializeOwned>(
|
||||
&self,
|
||||
key: impl AsRef<[u8]>,
|
||||
) -> StdResult<T> {
|
||||
let address = self.mixnet_contract_address()?;
|
||||
|
||||
self.must_read_value_from_contract_storage(address, key)
|
||||
}
|
||||
|
||||
fn write_to_mixnet_contract_storage(
|
||||
&mut self,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: impl AsRef<[u8]>,
|
||||
) -> StdResult<()> {
|
||||
let address = self.mixnet_contract_address()?;
|
||||
|
||||
<Self as ArbitraryContractStorageWriter>::set_contract_storage(self, address, key, value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_to_mixnet_contract_storage_value<T: Serialize>(
|
||||
&mut self,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: &T,
|
||||
) -> StdResult<()> {
|
||||
let address = self.mixnet_contract_address()?;
|
||||
|
||||
self.set_contract_storage_value(address, key, value)
|
||||
}
|
||||
|
||||
fn current_mixnet_epoch(&self) -> StdResult<EpochId> {
|
||||
let address = self.mixnet_contract_address()?;
|
||||
|
||||
Ok(self
|
||||
.deps()
|
||||
.querier
|
||||
.query_current_mixnet_interval(address.clone())?
|
||||
.current_epoch_absolute_id())
|
||||
}
|
||||
|
||||
fn advance_mixnet_epoch(&mut self) -> StdResult<()> {
|
||||
let interval_details: CurrentIntervalResponse = self.query_arbitrary_contract(
|
||||
self.mixnet_contract_address()?,
|
||||
&QueryMsg::GetCurrentIntervalDetails {},
|
||||
)?;
|
||||
let until_end = interval_details.time_until_current_epoch_end().as_secs();
|
||||
let timestamp = self.env().block.time.plus_seconds(until_end + 1);
|
||||
self.set_block_time(timestamp);
|
||||
self.next_block();
|
||||
|
||||
// this was hardcoded in mixnet init
|
||||
let mixnet_rewarder = self.addr_make("rewarder");
|
||||
let rewarder = message_info(&mixnet_rewarder, &[]);
|
||||
self.execute_mixnet_contract(rewarder.clone(), &ExecuteMsg::BeginEpochTransition {})?;
|
||||
self.execute_mixnet_contract(
|
||||
rewarder.clone(),
|
||||
&ExecuteMsg::ReconcileEpochEvents { limit: None },
|
||||
)?;
|
||||
|
||||
for role in [
|
||||
Role::ExitGateway,
|
||||
Role::EntryGateway,
|
||||
Role::Layer1,
|
||||
Role::Layer2,
|
||||
Role::Layer3,
|
||||
Role::Standby,
|
||||
] {
|
||||
self.execute_mixnet_contract(
|
||||
rewarder.clone(),
|
||||
&ExecuteMsg::AssignRoles {
|
||||
assignment: RoleAssignment {
|
||||
role,
|
||||
nodes: vec![],
|
||||
},
|
||||
},
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_mixnet_epoch(&mut self, epoch_id: EpochId) -> StdResult<()> {
|
||||
let address = self.mixnet_contract_address()?;
|
||||
|
||||
let interval = self
|
||||
.deps()
|
||||
.querier
|
||||
.query_current_mixnet_interval(address.clone())?;
|
||||
|
||||
let mut to_update = if interval.current_epoch_absolute_id() <= epoch_id {
|
||||
interval
|
||||
} else {
|
||||
Interval::init_interval(
|
||||
interval.epochs_in_interval(),
|
||||
interval.epoch_length(),
|
||||
&mock_env(),
|
||||
)
|
||||
};
|
||||
|
||||
let current = to_update.current_epoch_absolute_id();
|
||||
let diff = epoch_id - current;
|
||||
for _ in 0..diff {
|
||||
to_update = to_update.advance_epoch();
|
||||
}
|
||||
self.set_contract_storage_value(&address, b"ci", &to_update)
|
||||
}
|
||||
|
||||
fn bond_dummy_nymnode_for(&mut self, node_owner: &Addr) -> Result<NodeId, StdError> {
|
||||
let pledge = coins(100_000000, TEST_DENOM);
|
||||
let keypair = ed25519::KeyPair::new(self.raw_rng());
|
||||
let identity_key = keypair.public_key().to_base58_string();
|
||||
|
||||
let node = NymNode {
|
||||
host: "1.2.3.4".to_string(),
|
||||
custom_http_port: None,
|
||||
identity_key,
|
||||
};
|
||||
let cost_params = NodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT)
|
||||
.unwrap(),
|
||||
interval_operating_cost: coin(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, TEST_DENOM),
|
||||
};
|
||||
// initial signing nonce is 0 for a new address
|
||||
let signing_nonce = 0;
|
||||
|
||||
let payload = NymNodeBondingPayload::new(node.clone(), cost_params.clone());
|
||||
let content = ContractMessageContent::new(node_owner.clone(), pledge.clone(), payload);
|
||||
let msg = SignableNymNodeBondingMsg::new(signing_nonce, content);
|
||||
|
||||
let owner_signature = keypair.private_key().sign(msg.to_plaintext()?);
|
||||
let owner_signature = MessageSignature::from(owner_signature.to_bytes().as_ref());
|
||||
|
||||
self.execute_mixnet_contract(
|
||||
message_info(node_owner, &pledge),
|
||||
&ExecuteMsg::BondNymNode {
|
||||
node,
|
||||
cost_params,
|
||||
owner_signature,
|
||||
},
|
||||
)?;
|
||||
|
||||
let bond: NodeOwnershipResponse = self.query_arbitrary_contract(
|
||||
self.mixnet_contract_address()?,
|
||||
&QueryMsg::GetOwnedNymNode {
|
||||
address: node_owner.to_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(bond.details.unwrap().bond_information.node_id)
|
||||
}
|
||||
|
||||
fn bond_dummy_nymnode(&mut self) -> Result<NodeId, StdError> {
|
||||
let node_owner = self.generate_account_with_balance();
|
||||
self.bond_dummy_nymnode_for(&node_owner)
|
||||
}
|
||||
|
||||
fn unbond_nymnode(&mut self, node_id: NodeId) -> Result<(), StdError> {
|
||||
let bond: NodeDetailsResponse = self.query_arbitrary_contract(
|
||||
self.mixnet_contract_address()?,
|
||||
&QueryMsg::GetNymNodeDetails { node_id },
|
||||
)?;
|
||||
|
||||
let node_owner = bond.details.unwrap().bond_information.owner;
|
||||
|
||||
self.execute_mixnet_contract(
|
||||
message_info(&node_owner, &[]),
|
||||
&ExecuteMsg::UnbondNymNode {},
|
||||
)?;
|
||||
|
||||
self.advance_mixnet_epoch()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> EmbeddedMixnetContractExt for ContractTester<C>
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
fn mixnet_contract_address(&self) -> StdResult<Addr> {
|
||||
self.well_known_contracts
|
||||
.get(MixnetContract::NAME)
|
||||
.ok_or_else(|| StdError::generic_err("mixnet contract not part of the tester"))
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,24 +3,42 @@
|
||||
|
||||
use crate::delegations::storage as delegations_storage;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use crate::mixnet_contract_settings::storage::ADMIN;
|
||||
use crate::mixnodes::helpers::get_mixnode_details_by_owner;
|
||||
use crate::mixnodes::storage as mixnodes_storage;
|
||||
use crate::rewards::storage as rewards_storage;
|
||||
use crate::support::helpers::{
|
||||
ensure_bonded, ensure_epoch_in_progress_state, ensure_no_pending_pledge_changes,
|
||||
};
|
||||
use cosmwasm_std::{wasm_execute, DepsMut, Env, Event, MessageInfo, Response};
|
||||
use cosmwasm_std::{wasm_execute, Addr, DepsMut, Env, Event, MessageInfo, Response};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::{Delegation, NodeId};
|
||||
use mixnet_contract_common::{Delegation, NodeId, VestedDelegationMigrationEntry};
|
||||
use vesting_contract_common::messages::ExecuteMsg as VestingExecuteMsg;
|
||||
|
||||
pub(crate) fn try_migrate_vested_mixnode(
|
||||
deps: DepsMut<'_>,
|
||||
info: MessageInfo,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())?.ok_or(
|
||||
migrate_vested_mixnode_for_owner(deps, info.sender)
|
||||
}
|
||||
|
||||
pub(crate) fn try_admin_migrate_vested_mixnode(
|
||||
deps: DepsMut<'_>,
|
||||
info: MessageInfo,
|
||||
owner: String,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ADMIN.assert_admin(deps.as_ref(), &info.sender)?;
|
||||
let owner = deps.api.addr_validate(&owner)?;
|
||||
migrate_vested_mixnode_for_owner(deps, owner)
|
||||
}
|
||||
|
||||
fn migrate_vested_mixnode_for_owner(
|
||||
deps: DepsMut<'_>,
|
||||
owner: Addr,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
let mix_details = get_mixnode_details_by_owner(deps.storage, owner.clone())?.ok_or(
|
||||
MixnetContractError::NoAssociatedMixNodeBond {
|
||||
owner: info.sender.clone(),
|
||||
owner: owner.clone(),
|
||||
},
|
||||
)?;
|
||||
let mix_id = mix_details.mix_id();
|
||||
@@ -55,7 +73,7 @@ pub(crate) fn try_migrate_vested_mixnode(
|
||||
.add_message(wasm_execute(
|
||||
vesting_contract,
|
||||
&VestingExecuteMsg::TrackMigratedMixnode {
|
||||
owner: info.sender.into_string(),
|
||||
owner: owner.into_string(),
|
||||
},
|
||||
vec![],
|
||||
)?))
|
||||
@@ -66,6 +84,46 @@ pub(crate) fn try_migrate_vested_delegation(
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
mix_id: NodeId,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
migrate_vested_delegation_for_owner(deps, env, info.sender, mix_id)
|
||||
}
|
||||
|
||||
pub(crate) fn try_admin_migrate_vested_delegation(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
mix_id: NodeId,
|
||||
owner: String,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ADMIN.assert_admin(deps.as_ref(), &info.sender)?;
|
||||
let owner = deps.api.addr_validate(&owner)?;
|
||||
migrate_vested_delegation_for_owner(deps, env, owner, mix_id)
|
||||
}
|
||||
|
||||
pub(crate) fn try_admin_batch_migrate_vested_delegations(
|
||||
mut deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
entries: Vec<VestedDelegationMigrationEntry>,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ADMIN.assert_admin(deps.as_ref(), &info.sender)?;
|
||||
|
||||
let mut response = Response::new();
|
||||
for VestedDelegationMigrationEntry { mix_id, owner } in entries {
|
||||
let owner = deps.api.addr_validate(&owner)?;
|
||||
let sub = migrate_vested_delegation_for_owner(deps.branch(), env.clone(), owner, mix_id)?;
|
||||
response = response
|
||||
.add_submessages(sub.messages)
|
||||
.add_events(sub.events);
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn migrate_vested_delegation_for_owner(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
owner: Addr,
|
||||
mix_id: NodeId,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
let mut response = Response::new();
|
||||
|
||||
@@ -73,12 +131,16 @@ pub(crate) fn try_migrate_vested_delegation(
|
||||
|
||||
let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?;
|
||||
|
||||
let storage_key =
|
||||
Delegation::generate_storage_key(mix_id, &info.sender, Some(&vesting_contract));
|
||||
let storage_key = Delegation::generate_storage_key(mix_id, &owner, Some(&vesting_contract));
|
||||
let Some(vested_delegation) =
|
||||
delegations_storage::delegations().may_load(deps.storage, storage_key.clone())?
|
||||
else {
|
||||
return Err(MixnetContractError::NotAVestingDelegation);
|
||||
return Ok(Response::new().add_event(
|
||||
Event::new("migrate-vested-delegation-noop")
|
||||
.add_attribute("owner", owner.as_str())
|
||||
.add_attribute("mix_id", mix_id.to_string())
|
||||
.add_attribute("reason", "no_vested_delegation"),
|
||||
));
|
||||
};
|
||||
|
||||
// sanity check that's meant to blow up the contract
|
||||
@@ -88,7 +150,7 @@ pub(crate) fn try_migrate_vested_delegation(
|
||||
let mut updated_delegation = vested_delegation.clone();
|
||||
updated_delegation.proxy = None;
|
||||
|
||||
let new_storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None);
|
||||
let new_storage_key = Delegation::generate_storage_key(mix_id, &owner, None);
|
||||
|
||||
// remove the old (vested) delegation
|
||||
delegations_storage::delegations().remove(deps.storage, storage_key)?;
|
||||
@@ -205,7 +267,7 @@ pub(crate) fn try_migrate_vested_delegation(
|
||||
Ok(response.add_message(wasm_execute(
|
||||
vesting_contract,
|
||||
&VestingExecuteMsg::TrackMigratedDelegation {
|
||||
owner: info.sender.into_string(),
|
||||
owner: owner.into_string(),
|
||||
mix_id,
|
||||
},
|
||||
vec![],
|
||||
@@ -307,9 +369,10 @@ mod tests {
|
||||
|
||||
let sender = test.make_sender("owner-without-any-delegations");
|
||||
|
||||
// it simply fails for there is nothing to migrate
|
||||
let res = try_migrate_vested_delegation(test.deps_mut(), env, sender, 42).unwrap_err();
|
||||
assert_eq!(res, MixnetContractError::NotAVestingDelegation);
|
||||
// nothing to migrate -> idempotent no-op (so admin batches don't fail on stale entries)
|
||||
let res = try_migrate_vested_delegation(test.deps_mut(), env, sender, 42).unwrap();
|
||||
assert!(res.messages.is_empty());
|
||||
assert_eq!(res.events[0].ty, "migrate-vested-delegation-noop");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -333,10 +396,10 @@ mod tests {
|
||||
let sender = message_info(&delegation.owner, &[]);
|
||||
let mix_id = delegation.node_id;
|
||||
|
||||
// it also fails because the method is only allowed for vested delegations
|
||||
let res =
|
||||
try_migrate_vested_delegation(test.deps_mut(), env, sender, mix_id).unwrap_err();
|
||||
assert_eq!(res, MixnetContractError::NotAVestingDelegation);
|
||||
// liquid-only delegations are no-ops (nothing vested to migrate)
|
||||
let res = try_migrate_vested_delegation(test.deps_mut(), env, sender, mix_id).unwrap();
|
||||
assert!(res.messages.is_empty());
|
||||
assert_eq!(res.events[0].ty, "migrate-vested-delegation-noop");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -566,4 +629,329 @@ mod tests {
|
||||
compare_decimals(rewards, new_rewards_twin, Some("0.01".parse().unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod admin_migrating_vested_mixnode {
|
||||
use super::*;
|
||||
use crate::mixnodes::helpers::get_mixnode_details_by_id;
|
||||
use crate::support::tests::test_helpers::TestSetup;
|
||||
use cosmwasm_std::testing::message_info;
|
||||
use cosmwasm_std::{from_json, CosmosMsg, WasmMsg};
|
||||
|
||||
#[test]
|
||||
fn rejects_non_admin_caller() {
|
||||
let mut test = TestSetup::new();
|
||||
let owner = test.make_addr("owner");
|
||||
let mix_id = test.add_legacy_mixnode_with_legal_proxy(&owner, None);
|
||||
|
||||
let intruder = message_info(&test.make_addr("not-admin"), &[]);
|
||||
let owner_str = owner.to_string();
|
||||
let res =
|
||||
try_admin_migrate_vested_mixnode(test.deps_mut(), intruder, owner_str).unwrap_err();
|
||||
assert!(matches!(res, MixnetContractError::Admin(_)));
|
||||
|
||||
// bond is untouched
|
||||
let existing = get_mixnode_details_by_id(test.deps().storage, mix_id)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(existing.bond_information.proxy.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_can_migrate_someone_elses_vested_node() {
|
||||
let mut test = TestSetup::new();
|
||||
let owner = test.make_addr("owner");
|
||||
let mix_id = test.add_legacy_mixnode_with_legal_proxy(&owner, None);
|
||||
let admin = test.admin();
|
||||
|
||||
let info = message_info(&admin, &[]);
|
||||
let owner_str = owner.to_string();
|
||||
let res =
|
||||
try_admin_migrate_vested_mixnode(test.deps_mut(), info, owner_str.clone()).unwrap();
|
||||
|
||||
let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = &res.messages[0].msg else {
|
||||
panic!("no track message present")
|
||||
};
|
||||
assert_eq!(
|
||||
from_json::<VestingExecuteMsg>(msg).unwrap(),
|
||||
VestingExecuteMsg::TrackMigratedMixnode { owner: owner_str }
|
||||
);
|
||||
|
||||
// proxy was cleared on the bond owned by `owner` (not by the admin)
|
||||
let migrated = get_mixnode_details_by_id(test.deps().storage, mix_id)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(migrated.bond_information.proxy.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_owner_address() {
|
||||
let mut test = TestSetup::new();
|
||||
let admin = test.admin();
|
||||
|
||||
let info = message_info(&admin, &[]);
|
||||
let res =
|
||||
try_admin_migrate_vested_mixnode(test.deps_mut(), info, "not a bech32".to_string())
|
||||
.unwrap_err();
|
||||
assert!(matches!(res, MixnetContractError::StdErr { .. }));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod admin_migrating_vested_delegation {
|
||||
use super::*;
|
||||
use crate::delegations::storage::delegations;
|
||||
use crate::support::tests::test_helpers::TestSetup;
|
||||
use cosmwasm_std::testing::message_info;
|
||||
use cosmwasm_std::{from_json, CosmosMsg, Order, WasmMsg};
|
||||
|
||||
#[test]
|
||||
fn rejects_non_admin_caller() {
|
||||
let mut test = TestSetup::new_complex();
|
||||
let env = test.env();
|
||||
|
||||
let vested = delegations()
|
||||
.range(test.deps().storage, None, None, Order::Ascending)
|
||||
.filter_map(|d| d.map(|(_, del)| del).ok())
|
||||
.find(|d| d.proxy.is_some())
|
||||
.unwrap();
|
||||
|
||||
let intruder = message_info(&test.make_addr("not-admin"), &[]);
|
||||
let res = try_admin_migrate_vested_delegation(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
intruder,
|
||||
vested.node_id,
|
||||
vested.owner.to_string(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(res, MixnetContractError::Admin(_)));
|
||||
|
||||
// delegation is untouched
|
||||
assert!(delegations()
|
||||
.may_load(test.deps().storage, vested.storage_key())
|
||||
.unwrap()
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_can_migrate_someone_elses_vested_delegation() {
|
||||
let mut test = TestSetup::new_complex();
|
||||
let env = test.env();
|
||||
let admin = test.admin();
|
||||
|
||||
let vested = delegations()
|
||||
.range(test.deps().storage, None, None, Order::Ascending)
|
||||
.filter_map(|d| d.map(|(_, del)| del).ok())
|
||||
.find(|d| d.proxy.is_some())
|
||||
.unwrap();
|
||||
|
||||
// pick an owner that has no liquid twin so we exercise the simple branch
|
||||
let has_liquid_twin = delegations()
|
||||
.range(test.deps().storage, None, None, Order::Ascending)
|
||||
.filter_map(|d| d.map(|(_, del)| del).ok())
|
||||
.any(|d| {
|
||||
d.proxy.is_none()
|
||||
&& d.owner.as_str() == vested.owner.as_str()
|
||||
&& d.node_id == vested.node_id
|
||||
});
|
||||
assert!(!has_liquid_twin);
|
||||
|
||||
let old_key = vested.storage_key();
|
||||
let mut expected_liquid = vested.clone();
|
||||
expected_liquid.proxy = None;
|
||||
let new_key = expected_liquid.storage_key();
|
||||
|
||||
let info = message_info(&admin, &[]);
|
||||
let res = try_admin_migrate_vested_delegation(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
info,
|
||||
vested.node_id,
|
||||
vested.owner.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = &res.messages[0].msg else {
|
||||
panic!("no track message present")
|
||||
};
|
||||
assert_eq!(
|
||||
from_json::<VestingExecuteMsg>(msg).unwrap(),
|
||||
VestingExecuteMsg::TrackMigratedDelegation {
|
||||
owner: vested.owner.to_string(),
|
||||
mix_id: vested.node_id,
|
||||
}
|
||||
);
|
||||
|
||||
assert!(delegations()
|
||||
.may_load(test.deps().storage, old_key)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert_eq!(
|
||||
expected_liquid,
|
||||
delegations().load(test.deps().storage, new_key).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod admin_batch_migrating_vested_delegations {
|
||||
use super::*;
|
||||
use crate::delegations::storage::delegations;
|
||||
use crate::support::tests::test_helpers::TestSetup;
|
||||
use cosmwasm_std::testing::message_info;
|
||||
use cosmwasm_std::{CosmosMsg, Order, WasmMsg};
|
||||
|
||||
fn collect_vested(test: &TestSetup, n: usize) -> Vec<Delegation> {
|
||||
delegations()
|
||||
.range(test.deps().storage, None, None, Order::Ascending)
|
||||
.filter_map(|d| d.map(|(_, del)| del).ok())
|
||||
.filter(|d| d.proxy.is_some())
|
||||
.take(n)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_admin_caller() {
|
||||
let mut test = TestSetup::new_complex();
|
||||
let env = test.env();
|
||||
let vested = collect_vested(&test, 2);
|
||||
let entries = vested
|
||||
.iter()
|
||||
.map(|d| VestedDelegationMigrationEntry {
|
||||
mix_id: d.node_id,
|
||||
owner: d.owner.to_string(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let intruder = message_info(&test.make_addr("not-admin"), &[]);
|
||||
let res =
|
||||
try_admin_batch_migrate_vested_delegations(test.deps_mut(), env, intruder, entries)
|
||||
.unwrap_err();
|
||||
assert!(matches!(res, MixnetContractError::Admin(_)));
|
||||
|
||||
// nothing was touched
|
||||
for d in &vested {
|
||||
assert!(delegations()
|
||||
.may_load(test.deps().storage, d.storage_key())
|
||||
.unwrap()
|
||||
.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_can_migrate_multiple_entries_in_one_call() {
|
||||
let mut test = TestSetup::new_complex();
|
||||
let env = test.env();
|
||||
let admin = test.admin();
|
||||
let vested = collect_vested(&test, 3);
|
||||
assert_eq!(vested.len(), 3, "fixture must have ≥3 vested delegations");
|
||||
|
||||
let entries: Vec<_> = vested
|
||||
.iter()
|
||||
.map(|d| VestedDelegationMigrationEntry {
|
||||
mix_id: d.node_id,
|
||||
owner: d.owner.to_string(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let info = message_info(&admin, &[]);
|
||||
let res =
|
||||
try_admin_batch_migrate_vested_delegations(test.deps_mut(), env, info, entries)
|
||||
.unwrap();
|
||||
|
||||
// one TrackMigratedDelegation WasmMsg per entry
|
||||
let track_msgs: Vec<_> = res
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|sub| matches!(sub.msg, CosmosMsg::Wasm(WasmMsg::Execute { .. })))
|
||||
.collect();
|
||||
assert_eq!(track_msgs.len(), 3);
|
||||
|
||||
// each vested entry was removed and re-saved under the liquid key
|
||||
for d in &vested {
|
||||
assert!(delegations()
|
||||
.may_load(test.deps().storage, d.storage_key())
|
||||
.unwrap()
|
||||
.is_none());
|
||||
let liquid_key = Delegation::generate_storage_key(d.node_id, &d.owner, None);
|
||||
let liquid = delegations().load(test.deps().storage, liquid_key).unwrap();
|
||||
assert!(liquid.proxy.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_with_noop_entries_still_succeeds() {
|
||||
// batch contains: 1 valid vested, 1 stale (non-existent) — the stale one is a noop
|
||||
let mut test = TestSetup::new_complex();
|
||||
let env = test.env();
|
||||
let admin = test.admin();
|
||||
let vested = collect_vested(&test, 1);
|
||||
let real = &vested[0];
|
||||
|
||||
let entries = vec![
|
||||
VestedDelegationMigrationEntry {
|
||||
mix_id: real.node_id,
|
||||
owner: real.owner.to_string(),
|
||||
},
|
||||
VestedDelegationMigrationEntry {
|
||||
mix_id: 999_999,
|
||||
owner: test.make_addr("ghost").to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
let info = message_info(&admin, &[]);
|
||||
let res =
|
||||
try_admin_batch_migrate_vested_delegations(test.deps_mut(), env, info, entries)
|
||||
.unwrap();
|
||||
|
||||
// only the real entry dispatched a track message; the ghost was a noop
|
||||
let track_msgs: Vec<_> = res
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|sub| matches!(sub.msg, CosmosMsg::Wasm(WasmMsg::Execute { .. })))
|
||||
.collect();
|
||||
assert_eq!(track_msgs.len(), 1);
|
||||
assert!(res
|
||||
.events
|
||||
.iter()
|
||||
.any(|e| e.ty == "migrate-vested-delegation-noop"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_owner_address_propagates_as_error() {
|
||||
// a malformed entry causes the handler to return Err; on-chain this reverts
|
||||
// the entire batch atomically. (Unit tests use a raw `DepsMut` that does not
|
||||
// simulate chain-level rollback, so we only assert the error and ensure no
|
||||
// entry after the bad one was processed.)
|
||||
let mut test = TestSetup::new_complex();
|
||||
let env = test.env();
|
||||
let admin = test.admin();
|
||||
let vested = collect_vested(&test, 1);
|
||||
|
||||
let entries = vec![
|
||||
VestedDelegationMigrationEntry {
|
||||
mix_id: vested[0].node_id,
|
||||
owner: "not-a-bech32".to_string(),
|
||||
},
|
||||
VestedDelegationMigrationEntry {
|
||||
mix_id: vested[0].node_id,
|
||||
owner: vested[0].owner.to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
let info = message_info(&admin, &[]);
|
||||
let err =
|
||||
try_admin_batch_migrate_vested_delegations(test.deps_mut(), env, info, entries)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, MixnetContractError::StdErr { .. }));
|
||||
|
||||
// bailed out before reaching the second (valid) entry, so the vested record
|
||||
// is still in storage
|
||||
assert!(delegations()
|
||||
.may_load(test.deps().storage, vested[0].storage_key())
|
||||
.unwrap()
|
||||
.is_some());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
[alias]
|
||||
wasm = "build --release --lib --target wasm32-unknown-unknown"
|
||||
unit-test = "test --lib"
|
||||
schema = "run --bin schema --features=schema-gen"
|
||||
@@ -0,0 +1,62 @@
|
||||
[package]
|
||||
name = "node-families"
|
||||
description = "Nym Node Families contract"
|
||||
version = "0.1.0"
|
||||
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 = false
|
||||
|
||||
[[bin]]
|
||||
name = "schema"
|
||||
required-features = ["schema-gen"]
|
||||
|
||||
[lib]
|
||||
name = "node_families_contract"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
cw-storage-plus = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
cosmwasm-schema = { workspace = true, optional = true }
|
||||
cw-utils = { workspace = true }
|
||||
|
||||
nym-contracts-common = { workspace = true }
|
||||
nym-node-families-contract-common = { workspace = true }
|
||||
nym-mixnet-contract-common = { workspace = true }
|
||||
|
||||
# Optional deps activated by the `testable-node-families-contract` feature so
|
||||
# downstream crates can pull in `crate::testing` for their own test harnesses.
|
||||
nym-contracts-common-testing = { workspace = true, optional = true }
|
||||
nym-mixnet-contract = { workspace = true, optional = true, features = ["testable-mixnet-contract"] }
|
||||
nym-crypto = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
# make the testing helpers available for this crate's own unit tests via
|
||||
# `cfg(test)`; downstream crates instead pull these in through the
|
||||
# `testable-node-families-contract` feature.
|
||||
nym-contracts-common-testing = { workspace = true }
|
||||
nym-mixnet-contract = { workspace = true, features = ["testable-mixnet-contract"] }
|
||||
nym-crypto = { workspace = true, features = ["asymmetric"] }
|
||||
|
||||
[features]
|
||||
schema-gen = ["nym-node-families-contract-common/schema", "cosmwasm-schema"]
|
||||
testable-node-families-contract = [
|
||||
"nym-contracts-common-testing",
|
||||
"nym-mixnet-contract",
|
||||
"nym-crypto",
|
||||
"nym-crypto/asymmetric",
|
||||
]
|
||||
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,5 @@
|
||||
wasm:
|
||||
RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown
|
||||
|
||||
generate-schema:
|
||||
cargo schema
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,318 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ExecuteMsg",
|
||||
"description": "Execute messages accepted by the contract.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Replace the contract's runtime [`Config`]. Restricted to the contract admin.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"update_config"
|
||||
],
|
||||
"properties": {
|
||||
"update_config": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"config"
|
||||
],
|
||||
"properties": {
|
||||
"config": {
|
||||
"$ref": "#/definitions/Config"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Create a new family owned by the message sender. The configured `create_family_fee` must be attached as funds.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"create_family"
|
||||
],
|
||||
"properties": {
|
||||
"create_family": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Disband the family owned by the message sender. The family must have no current members; any still-pending invitations are revoked.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"disband_family"
|
||||
],
|
||||
"properties": {
|
||||
"disband_family": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "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.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"invite_to_family"
|
||||
],
|
||||
"properties": {
|
||||
"invite_to_family": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"validity_secs": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Revoke a still-pending invitation previously issued by the sender's family.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"revoke_family_invitation"
|
||||
],
|
||||
"properties": {
|
||||
"revoke_family_invitation": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Accept a pending invitation. The sender must control `node_id`.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"accept_family_invitation"
|
||||
],
|
||||
"properties": {
|
||||
"accept_family_invitation": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Reject a pending invitation. The sender must control `node_id`.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"reject_family_invitation"
|
||||
],
|
||||
"properties": {
|
||||
"reject_family_invitation": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Leave the family `node_id` currently belongs to. The sender must control `node_id`.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"leave_family"
|
||||
],
|
||||
"properties": {
|
||||
"leave_family": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Remove `node_id` from the family owned by the message sender.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kick_from_family"
|
||||
],
|
||||
"properties": {
|
||||
"kick_from_family": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "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.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"on_nym_node_unbond"
|
||||
],
|
||||
"properties": {
|
||||
"on_nym_node_unbond": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"Coin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Config": {
|
||||
"description": "Runtime configuration of the node families contract.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"create_family_fee",
|
||||
"default_invitation_validity_secs",
|
||||
"family_description_length_limit",
|
||||
"family_name_length_limit"
|
||||
],
|
||||
"properties": {
|
||||
"create_family_fee": {
|
||||
"description": "Fee charged on each successful `create_family` execution.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_invitation_validity_secs": {
|
||||
"description": "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.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"family_description_length_limit": {
|
||||
"description": "Maximum allowed length, in characters, of a family description.",
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"family_name_length_limit": {
|
||||
"description": "Maximum allowed length, in characters, of a family name.",
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Uint128": {
|
||||
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "InstantiateMsg",
|
||||
"description": "Message used to instantiate the node families contract.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"config",
|
||||
"mixnet_contract_address"
|
||||
],
|
||||
"properties": {
|
||||
"config": {
|
||||
"$ref": "#/definitions/Config"
|
||||
},
|
||||
"mixnet_contract_address": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Coin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Config": {
|
||||
"description": "Runtime configuration of the node families contract.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"create_family_fee",
|
||||
"default_invitation_validity_secs",
|
||||
"family_description_length_limit",
|
||||
"family_name_length_limit"
|
||||
],
|
||||
"properties": {
|
||||
"create_family_fee": {
|
||||
"description": "Fee charged on each successful `create_family` execution.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_invitation_validity_secs": {
|
||||
"description": "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.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"family_description_length_limit": {
|
||||
"description": "Maximum allowed length, in characters, of a family description.",
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"family_name_length_limit": {
|
||||
"description": "Maximum allowed length, in characters, of a family name.",
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Uint128": {
|
||||
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "MigrateMsg",
|
||||
"description": "Message passed to the contract's `migrate` entry point.",
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,620 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "QueryMsg",
|
||||
"description": "Query messages accepted by the contract.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Look up a single family by its id.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_family_by_id"
|
||||
],
|
||||
"properties": {
|
||||
"get_family_by_id": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Look up the (at most one) family owned by a given address.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_family_by_owner"
|
||||
],
|
||||
"properties": {
|
||||
"get_family_by_owner": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "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.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_family_by_name"
|
||||
],
|
||||
"properties": {
|
||||
"get_family_by_name": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_families_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_families_paged": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Look up which family — if any — a node currently belongs to.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_family_membership"
|
||||
],
|
||||
"properties": {
|
||||
"get_family_membership": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Page through every node currently in a given family.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_family_members_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_family_members_paged": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "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).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_all_family_members_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_all_family_members_paged": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Look up the pending invitation for a specific `(family_id, node_id)` pair.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_pending_invitation"
|
||||
],
|
||||
"properties": {
|
||||
"get_pending_invitation": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Page through every pending invitation issued by a given family.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_pending_invitations_for_family_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_pending_invitations_for_family_paged": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Page through every pending invitation issued for a given node.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_pending_invitations_for_node_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_pending_invitations_for_node_paged": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Page through every pending invitation across all families.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_all_pending_invitations_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_all_pending_invitations_paged": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Page through every archived (terminal-state) invitation issued by a given family.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_past_invitations_for_family_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_past_invitations_for_family_paged": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Page through every archived (terminal-state) invitation issued to a given node.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_past_invitations_for_node_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_past_invitations_for_node_paged": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Page through every archived (terminal-state) invitation across all families.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_all_past_invitations_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_all_past_invitations_paged": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Page through every archived membership record for a given family (nodes that used to belong to it but have since been removed).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_past_members_for_family_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_past_members_for_family_paged": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "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.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_past_members_for_node_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_past_members_for_node_paged": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "AllFamilyMembersPagedResponse",
|
||||
"description": "Response to [`QueryMsg::GetAllFamilyMembersPaged`](crate::QueryMsg::GetAllFamilyMembersPaged).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"members"
|
||||
],
|
||||
"properties": {
|
||||
"members": {
|
||||
"description": "The members on this page, in ascending [`NodeId`] order across every family.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/FamilyMemberRecord"
|
||||
}
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "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).",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyMemberRecord": {
|
||||
"description": "One entry in a [`FamilyMembersPagedResponse`] page — pairs a node id with its [`FamilyMembership`] record (notably its `joined_at` timestamp).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"membership",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"membership": {
|
||||
"description": "The membership record (carries `family_id` and `joined_at`).",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FamilyMembership"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_id": {
|
||||
"description": "The node currently in the family.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"FamilyMembership": {
|
||||
"description": "On-chain record of a node's current family membership.\n\nA 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.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id",
|
||||
"joined_at"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"description": "The family the node is currently a member of.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"joined_at": {
|
||||
"description": "Block timestamp (unix seconds) at which the node accepted its invitation and joined the family.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "AllPastFamilyInvitationsPagedResponse",
|
||||
"description": "Response to [`QueryMsg::GetAllPastInvitationsPaged`](crate::QueryMsg::GetAllPastInvitationsPaged).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"invitations"
|
||||
],
|
||||
"properties": {
|
||||
"invitations": {
|
||||
"description": "The archived invitations on this page, in ascending `((family_id, node_id), counter)` order across all terminal statuses.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PastFamilyInvitation"
|
||||
}
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Cursor to pass as `start_after` on the next call, or `None` if this page is empty (treat as end-of-list).",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations 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).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
"family_id",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"expires_at": {
|
||||
"description": "Block timestamp (unix seconds) after which the invitation is no longer valid.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"family_id": {
|
||||
"description": "The family that issued the invitation.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"description": "The node being invited.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"FamilyInvitationStatus": {
|
||||
"description": "Terminal status for an invitation that has been moved out of the pending set.\n\nNote: timed-out invitations are not represented here — they are simply left in the pending set (see `FamilyInvitation::expires_at`).",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Still awaiting a response. Recorded with a timestamp for completeness even though pending invitations live in a separate map.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"pending"
|
||||
],
|
||||
"properties": {
|
||||
"pending": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "The invitee accepted and joined the family at the given timestamp.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"accepted"
|
||||
],
|
||||
"properties": {
|
||||
"accepted": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "The invitee explicitly rejected the invitation at the given timestamp.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"rejected"
|
||||
],
|
||||
"properties": {
|
||||
"rejected": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "The family revoked the invitation at the given timestamp before it could be accepted or rejected.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"revoked"
|
||||
],
|
||||
"properties": {
|
||||
"revoked": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"PastFamilyInvitation": {
|
||||
"description": "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.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"invitation",
|
||||
"status"
|
||||
],
|
||||
"properties": {
|
||||
"invitation": {
|
||||
"description": "The original invitation as it was issued.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FamilyInvitation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"description": "What ultimately happened to it.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FamilyInvitationStatus"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PendingInvitationsPagedResponse",
|
||||
"description": "Response to [`QueryMsg::GetAllPendingInvitationsPaged`](crate::QueryMsg::GetAllPendingInvitationsPaged).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"invitations"
|
||||
],
|
||||
"properties": {
|
||||
"invitations": {
|
||||
"description": "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.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PendingFamilyInvitationDetails"
|
||||
}
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "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).",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations 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).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
"family_id",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"expires_at": {
|
||||
"description": "Block timestamp (unix seconds) after which the invitation is no longer valid.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"family_id": {
|
||||
"description": "The family that issued the invitation.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"description": "The node being invited.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PendingFamilyInvitationDetails": {
|
||||
"description": "A pending [`FamilyInvitation`] paired with whether it has already timed out at the time the query was served.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expired",
|
||||
"invitation"
|
||||
],
|
||||
"properties": {
|
||||
"expired": {
|
||||
"description": "`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.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"invitation": {
|
||||
"description": "The stored invitation as it was issued.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FamilyInvitation"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user