Compare commits

..

78 Commits

Author SHA1 Message Date
dependabot[bot] 5d7f51f9f2 build(deps): bump softprops/action-gh-release from 2 to 3
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-20 03:31:34 +00:00
dependabot[bot] 483bb6f477 build(deps): bump pnpm/action-setup from 4.2.0 to 5.0.0 (#6571)
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 4.2.0 to 5.0.0.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v4.2.0...v5.0.0)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 16:37:52 +01:00
dependabot[bot] a68355a75a Bump tauri from 2.10.3 to 2.11.1 in /nym-wallet (#6742)
Bumps [tauri](https://github.com/tauri-apps/tauri) from 2.10.3 to 2.11.1.
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.10.3...tauri-v2.11.1)

---
updated-dependencies:
- dependency-name: tauri
  dependency-version: 2.11.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 16:36:30 +01:00
dependabot[bot] 1572d8e5c2 Bump rand from 0.8.5 to 0.8.6 in /contracts (#6702)
Bumps [rand](https://github.com/rust-random/rand) from 0.8.5 to 0.8.6.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/0.8.6/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.8.5...0.8.6)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.8.6
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 15:49:54 +01:00
dependabot[bot] fd76c5ca4d build(deps): bump microsoft/setup-msbuild from 2 to 3 (#6602)
Bumps [microsoft/setup-msbuild](https://github.com/microsoft/setup-msbuild) from 2 to 3.
- [Release notes](https://github.com/microsoft/setup-msbuild/releases)
- [Commits](https://github.com/microsoft/setup-msbuild/compare/v2...v3)

---
updated-dependencies:
- dependency-name: microsoft/setup-msbuild
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 15:49:24 +01:00
dependabot[bot] f94589c2d1 build(deps): bump tar from 0.4.44 to 0.4.45 (#6595)
Bumps [tar](https://github.com/alexcrichton/tar-rs) from 0.4.44 to 0.4.45.
- [Commits](https://github.com/alexcrichton/tar-rs/compare/0.4.44...0.4.45)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 0.4.45
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 15:49:14 +01:00
dependabot[bot] 1c40499829 build(deps): bump quinn-proto from 0.11.12 to 0.11.14 (#6549)
Bumps [quinn-proto](https://github.com/quinn-rs/quinn) from 0.11.12 to 0.11.14.
- [Release notes](https://github.com/quinn-rs/quinn/releases)
- [Commits](https://github.com/quinn-rs/quinn/compare/quinn-proto-0.11.12...quinn-proto-0.11.14)

---
updated-dependencies:
- dependency-name: quinn-proto
  dependency-version: 0.11.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 15:48:31 +01:00
dependabot[bot] f8a4d5f1ff build(deps): bump quinn-proto from 0.11.10 to 0.11.14 in /nym-wallet (#6548)
Bumps [quinn-proto](https://github.com/quinn-rs/quinn) from 0.11.10 to 0.11.14.
- [Release notes](https://github.com/quinn-rs/quinn/releases)
- [Commits](https://github.com/quinn-rs/quinn/compare/quinn-proto-0.11.10...quinn-proto-0.11.14)

---
updated-dependencies:
- dependency-name: quinn-proto
  dependency-version: 0.11.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 15:48:20 +01:00
dependabot[bot] 42807890af build(deps): bump docker/login-action from 3 to 4 (#6518)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 15:48:09 +01:00
dependabot[bot] 5aa576b596 build(deps): bump actions/download-artifact from 7 to 8 (#6497)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 15:47:59 +01:00
dependabot[bot] 0215ad9294 build(deps): bump actions/upload-artifact from 6 to 7 (#6496)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 15:47:26 +01:00
ZM 227e6a10e1 fix(ecash): cast usize to u64 in to_bytes() for 32-bit platform compatibility (#6528)
VerificationKeyAuth::to_bytes() and SecretKeyAuth::to_bytes() used
usize::to_le_bytes() to serialize vector lengths, producing 4 bytes on
32-bit and 8 bytes on 64-bit. Since from_bytes() always reads 8 bytes
(u64), this caused ZK proof challenge hash mismatches when a 32-bit
client's proof was verified by a 64-bit gateway, resulting in
"the provided ticket failed to get verified" on all 32-bit platforms.
2026-05-19 15:17:24 +01:00
Jędrzej Stuczyński d3b6a270de chore: expose admin method for migrating vesting delegations/mixnodes (#6795)
* chore: expose admin method for migrating vesting delegations/mixnodes

* don't error out on vested delegation no longer existing - perform a noop instead

* cargo fmt

* add message for batch migration
2026-05-19 15:13:03 +01:00
mfahampshire e12ada0105 Point mobile reference section at nymvpn setup (#6776) 2026-05-19 13:00:51 +00:00
Simon Wicky 71d50d79c2 fix clippy 1.95 lints (#6794) 2026-05-19 14:21:12 +02:00
Jędrzej Stuczyński a21a01cf1a node families (#6715)
* start node families topic branch

* start node families topic branch

* initialise node families contract

* define contract storage

* registering new family in storage

* accepting family invitation

* add_pending_invitation

* revoke_pending_invitation

* remove_family_member

* reject_pending_invitation

* disband_family

* added unit tests for the storage methods

* added restriction on uniquness of family names

* update rustc version for node families contract common

* clippy

* basic queries by id

* query_families_paged

* change family membership storage and expose query for all members of a family

* queries for pending invitations

* queries for past invitations

* queries for past data per node

* queries for past family members

* query_past_members_for_node_paged

* queries for family by name and by owner

* fixup family name normalisation

* fixed incorrect lower bound for queries for past data

* implement contract and storage initialisation

* stubbing tx messages that are to be exposed by the contract

* handler for updating config

* removed partial fee return

* wip: create family

* move mixnet contract interaction traits to shared location

* store original family name alongside the normalised variant

* prevent family creation if owner has a node in another family

* try_disband_family

* try_invite_to_family + shared helpers

* try_revoke_family_invitation

* accept_family_invitation

* stub method for node unbonding

* try_reject_family_invitation

* unit tests for family name normalisation

* try_leave_family

* try_kick_from_family

* fix outdated comments and add paid fee event attribute

* feat: NMv3: leave family upon node unbonding

* NF contract handling of unbonding

* lints

* init node families contract when creating performance contract tester

* clippy

* avoid self-dep in the contract dev deps

* introduced client traits for interacting with the node families contract

* add node families contract to cache refresher

* added query for all node family members (globally) and started scaffolding nym-api caches

* docs and cache -> api conversion

* calculating average node age based on individual timestamps

* wire up node families cache

* http stubs

* filled in the implementation

* route tests + extracting shared code

* review fixes

* feat: expose family information for all dvpn gateway endpoints within NS API

* expose family information for explorer v3 route

* clippy

* review comments and optimise db family update

* feat: Node Families: expose stake information inside DVpnGateway

* chore: update lock files after rebase

* chore: sort workspace members

* explicitly require providing node families contract address for mixnet contract migration

* fix missing node families contract address env export

* dont swallow cache overwrite failures in fixture

* pin network-defaults rustc version due to contracts dep

* further version pinning

* chore: update mixnet contract schema
2026-05-19 10:36:20 +01:00
Jack Wampler 362f84b5f6 Handle Rate Limit Challenge Response (#6786)
rotate urls on HTTP response error indicating API rate limiting
2026-05-18 08:47:41 -06:00
benedettadavico daed9cd15b Merge branch 'release/2026.9-venaco' into develop 2026-05-16 06:27:52 +02:00
Jack Wampler a53ca71bd2 Re-order default API urls for network details (#6767) 2026-05-15 09:46:33 -06:00
mfahampshire a70e68c7bd Max/smolmix docs (#6716)
* Smolmix documentation

* Add smolmix docs: landing page, tutorials, and developer page links

* Add Exit Gateway services page (NR vs IPR) and link from existing docs

* Update auto-generated command and API outputs

* Reorg of tutorials and architecture pages

* License information + remove TODO from docs.rs visibile comment + reorg
readme

* Add versions file for doc-wide versioning

* Relative -> absolute links

* Relative -> absolute links

* Update license + add old tutorial code as examples

* Streamline smolmix docs

* Clippy

* Clean up doc comments

* Last pass

* Add larger file download to list

* set new versions

* Clippy

* Remove blake pin from docs + add version range to root Cargo.toml

* Format example logging

* Remove crate blocked component

* Loose whitespace

* Add doc verification script for inline mdx

* Formatting

* Components regen

* Reorg + tighten text

* Voicing cohesion pass + remove bloated examples

* Voicing cont.

* Reduce max download size

* Small suggested clarifications

* Max/docs voicing consistency (#6769)

* Reduce max download size

* voicing consistency across docs

* New landing order w smolmix

* Tweaks

* Final tweaks
2026-05-13 11:19:44 +00:00
import this fdebed7c38 Bugfix: nym-node-cly.py argument mismatch fix and sync up with NTM updates (#6743)
* fix argument missmatch and sync args with recent NTM update

* fix wg_enabled check & name consistency

* correct env.os saving persisting vars logic

* fix naming issue
2026-05-12 11:52:46 +02:00
benedetta davico f576a4ee2d Merge pull request #6764 from nymtech/bdq/add-ci-build-NM-agents
add ci for NM agent binary
2026-05-12 10:40:23 +02:00
benedettadavico a9aafd785e publish NM agent binary 2026-05-12 10:34:34 +02:00
Jędrzej Stuczyński 2d72b1b201 feat: introduce shared contract caches within Nym API (#6760)
it has been extracted from the mix stress testing branch and it is going to be used within node families branch
2026-05-11 13:02:37 +01:00
Jędrzej Stuczyński 412657f773 chore: removed dead code for redundant mixnet-vesting integration tests (#6759) 2026-05-11 10:03:56 +01:00
Andrej Mihajlov b501ddd534 Migrate to hickory 0.26.1 (#6751)
* Migrate to hickory 0.26.1
2026-05-08 12:25:07 -06:00
Tommy Verrall e9f6d1d47a Merge pull request #6738 from nymtech/dependabot/cargo/nym-wallet/openssl-0.10.79
Bump openssl from 0.10.72 to 0.10.79 in /nym-wallet
2026-05-06 19:27:31 +02:00
Tommy Verrall 52b4490e80 Merge pull request #6741 from nymtech/fix/issue-windows
Fix windows open log viewer
2026-05-06 17:13:12 +02:00
Tommy Verrall 7b30c83f9a Linting
- Add changelog for future release notes
2026-05-06 17:03:25 +02:00
Tommy Verrall 4aabb4ed56 Fix windows open log viewer
- There was tendency where webview would just freeze on windows, lets ensure this doesn't happen.
2026-05-06 16:55:58 +02:00
Tommy Verrall b14c28a462 Add environment variable for Windows signing 2026-05-06 16:25:13 +02:00
Tommy Verrall 664782c0c6 Merge pull request #6740 from nymtech/feature/nym-wallet-delegation-log-hardening
Delegation query cache, log webview streaming, HTTPS webviews
2026-05-06 16:02:38 +02:00
Tommy Verrall aeb2f1f0f6 Okay mr Rabbit
- PR comments by mr rabbit were valid
2026-05-06 15:45:15 +02:00
Tommy Verrall 268ba36700 Update changelog 2026-05-06 15:41:55 +02:00
Tommy Verrall c4df05157a Tighten delegation summary query keys and rewards context
- Remove redeemAllRewards / TRewardsTransaction from rewards context
- Use a dedicated React Query key when no client address is set
2026-05-06 15:35:02 +02:00
Tommy Verrall 09548a9aa9 Delegation query cache, log webview streaming, HTTPS webviews
- Use_https_scheme(true) on log window builder
- Delegation data is loaded and refreshed via TanStack Query
2026-05-06 15:19:13 +02:00
Tommy Verrall 78b796bf24 Merge pull request #6681 from nymtech/feature/wallet-investigation
Nym Wallet: deps updates, clipboard/updater/, icon, polishing...
2026-05-06 15:02:41 +02:00
import this f5ab7b3eb6 [DOCs/operators]: Release notes for v2026.9-venaco (#6739)
* add changelog notes

* bump up version

* semi-atomated data update

* fix spacing
2026-05-06 14:12:24 +02:00
Tommy Verrall 9cf679dadb Fix default workspace packages 2026-05-06 10:38:28 +02:00
Tommy Verrall 97a382520c Pin ESLint 8 and align @typescript-eslint to restore yarn lint
- Fix CI yarn lint after ESLint 9 switched to flat config by default while the repo still uses legacy .eslintrc / eslintConfig. Add Yarn resolutions for eslint@8.57.1 and a single @typescript-eslint@5.62.0 line so parser and typescript-estree stay in sync
2026-05-06 10:21:39 +02:00
Tommy Verrall f87ce06865 Pin ESLint 8 and align @typescript-eslint to restore yarn lint
- Fix CI yarn lint after ESLint 9 switched to flat config by default while the repo still uses legacy .eslintrc / eslintConfig. Add Yarn resolutions for eslint@8.57.1 and a single @typescript-eslint@5.62.0 line so parser and typescript-estree stay in sync
2026-05-06 10:20:09 +02:00
Tommy Verrall 6095215a73 Merge branch 'develop' into feature/wallet-investigation 2026-05-06 09:49:30 +02:00
benedetta davico 8c6ff79cd1 Merge pull request #6736 from nymtech/master
Keep branches synced
2026-05-06 08:23:38 +02:00
dependabot[bot] 16678537f7 Bump openssl from 0.10.72 to 0.10.79 in /nym-wallet
Bumps [openssl](https://github.com/rust-openssl/rust-openssl) from 0.10.72 to 0.10.79.
- [Release notes](https://github.com/rust-openssl/rust-openssl/releases)
- [Commits](https://github.com/rust-openssl/rust-openssl/compare/openssl-v0.10.72...openssl-v0.10.79)

---
updated-dependencies:
- dependency-name: openssl
  dependency-version: 0.10.79
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-06 06:03:47 +00:00
benedetta davico ae877e3867 Merge pull request #6735 from nymtech/release/2026.9-venaco
Merge release/2026.9-venaco to master
2026-05-06 08:00:15 +02:00
benedetta davico 21479bfb80 Merge pull request #6734 from nymtech/release/2026.9-venaco
Merge release/2026.9-venaco to develop
2026-05-06 08:00:08 +02:00
benedetta davico db8edfe752 Merge pull request #6729 from nymtech/bdq/add-workflows
add workflows for NM3
2026-05-05 10:01:27 +02:00
benedettadavico 73edf28f39 add workflows for NM3 2026-05-05 08:26:23 +02:00
benedetta davico d23a42f7f5 credential proxy pool (#6726)
* fix?

* version

* unit test

* additional logs for stalled deposits

---------

Co-authored-by: benedettadavico <benedettadavico@users.noreply.github.com>
Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
2026-05-01 09:21:28 +01:00
mfahampshire d0f2c08cd1 Move pricing into table format (#6722) 2026-05-01 07:17:30 +00:00
benedetta davico 5599987d89 Merge pull request #6723 from nymtech/bdq/bump-cred-proxy
Bump cred proxy version
2026-04-30 18:01:50 +02:00
rachyandco a93763d73b Merge pull request #6688 from nymtech/fix/dns-ttl-reduce-cache-and-shuffle-ips
Title: fix(dns): shuffle resolved IPs
2026-04-30 16:21:22 +02:00
benedetta davico 8e8b6f4467 Bump version to 0.3.1 in Cargo.toml 2026-04-30 15:00:53 +02:00
mfahampshire 7feeed41d5 tweak subheading (#6721) 2026-04-30 09:09:49 +00:00
mfahampshire e9a20653b8 Components autogenerate + BYON section in NymVPNCLI docs (#6719) 2026-04-30 08:56:29 +00:00
Jędrzej Stuczyński 9438691506 Merge pull request #6718 from nymtech/chore/ipr-clippy
chore: made sphinx version threshold assertion a compile time check
2026-04-30 09:43:44 +01:00
p17o 84a4924e77 nym-node-setup: plumb HOST_SSH_PORT through tunnel manager, CLI, and env setup (#6633)
* network-tunnel-manager: make SSH port configurable

* Rename SSH_PORT to HOST_SSH_PORT.

* setup: plumb HOST_SSH_PORT through env and CLI

* setup-env-vars: persist HOST_SSH_PORT in env.sh

---------

Co-authored-by: p17o <p17o>
2026-04-30 07:10:21 +00:00
rachyandco 49277310ba Apply suggestion from @jmwample
Co-authored-by: Jack Wampler <jmwample@users.noreply.github.com>
2026-04-30 05:57:09 +02:00
rachyandco 944d2eb7d5 Apply suggestion from @jmwample
Co-authored-by: Jack Wampler <jmwample@users.noreply.github.com>
2026-04-30 05:56:43 +02:00
rachyandco bfaf17540e Apply suggestion from @jmwample
Co-authored-by: Jack Wampler <jmwample@users.noreply.github.com>
2026-04-30 05:56:09 +02:00
rachyandco 6dbc4efbd9 Apply suggestion from @jmwample
Co-authored-by: Jack Wampler <jmwample@users.noreply.github.com>
2026-04-30 05:55:39 +02:00
p17o cabbeaf1bf Handle split IPv4/IPv6 uplinks in network-tunnel-manager (#6640)
* Handle separate IPv4 and IPv6 uplink interfaces in network-tunnel-manager

* check_forward_chain() now checks IPv6 and is less brittle overall; missing IPv6 uplink detection now degrades to a loud warning plus partial IPv4-only setup rather than hard-failing early

* fix typos; fix UDP port 4443 being configured but not tested

---------

Co-authored-by: p17o <p17o>
2026-04-29 15:42:29 +00:00
benedetta davico a99b8348d7 Merge pull request #6697 from nymtech/release/2026.8-urda
merge release/2026.8-urda to master
2026-04-21 13:14:31 +02:00
Rachyandco bf85e9eb79 fix(dns): reduce positive TTL to 60s and shuffle resolved IPs
The 1800s minimum TTL defeated CDN failover mechanisms (e.g. Fastly
  publishes 30–60s A-record TTLs specifically to signal when edge nodes
  are removed). Dead IPs were cached for up to 30 minutes with no
  way for the client to recover without a restart.

  - Drop DEFAULT_POSITIVE_LOOKUP_CACHE_TTL from 1800s to 60s so that
    CDN-signalled failovers take effect within a minute
  - Shuffle resolved IPs on each lookup so retries cycle through all
    available edge nodes rather than hitting the same dead address
  - Add invalidate_preresolve_entry / invalidate_preresolve_for API
    for callers that want targeted per-host cache eviction on hard
    connection failures
2026-04-19 23:03:26 +02:00
Tommy Verrall 3f1e04ebd4 Fix ubuntu CI issues - update readme 2026-04-17 19:41:13 +02:00
Tommy Verrall d4c5131bcb Attempt at windows CI 2026-04-17 19:21:42 +02:00
Tommy Verrall ef1c1b50d5 More CI fixes 2026-04-17 18:49:41 +02:00
Tommy Verrall 23b745d353 Fix windows workflow 2026-04-17 18:41:25 +02:00
Tommy Verrall 3dc94cc85a Send Max UX, shared address helper, CI and desktop packaging
- Wallet CI, Tauri, webpack, routes
- Send and Sahred UI
- Wallet app build and readme
2026-04-17 14:41:20 +02:00
Tommy Verrall a4c4345257 More linting 2026-04-17 13:30:17 +02:00
Tommy Verrall a0fb92cf17 More clippy and linting 2026-04-17 12:36:20 +02:00
Tommy Verrall 52cc77356e Adjust lefthook for usage on GUI's like GitKraken 2026-04-17 12:19:58 +02:00
Tommy Verrall a671084f4e Linting and fixing CI 2026-04-17 12:18:57 +02:00
Tommy Verrall 3ae986acc8 Tauri prod CSP for Emotion/MUI and window maximize ACL
- Tauri was injecting nonces/hashes into style-src, which disables
'unsafe-inline' and blocked Emotion/MUI runtime <style> tags.
- Grant core:window:allow-maximize so frontend maximize() passes ACL.
- Add node-status and explorer helpers plus chart mappers; Jest coverage
- NodeOperatorInsights on BondedNymNode; optional API moniker/location
- Shared MUI Emotion cache (speedy: false) and CacheProvider wiring
- SendInputModal: amount/recipient validation timing; memoized fee check
- AuthLayout refresh; NodeTable overflow-x; Bonding error title typo fix
2026-04-17 12:05:01 +02:00
Tommy Verrall 754994ba01 Removing the misleading log tag and adding a brief comment. 2026-04-17 12:05:01 +02:00
Tommy Verrall 33b181b26b Fix routing for main window, loading modal, and error polish 2026-04-17 12:05:01 +02:00
Tommy Verrall 809559e6dc Nym Wallet: deps updates, clipboard/updater/, icon, polishing...
This rolls together desktop wallet hardening, UX polish, and operational fixes we have been carrying in the branch. The goal is safer defaults, less noisy background behaviour.

Security
- Tighten the Tauri CSP for production and keep connect-src aligned with real needs.
- Add a safe URL opener path (allowlisted schemes / validation) so user-influenced links do not become an open redirect surface.
- Replace unwrap usage in mixnet account flows with proper errors and propagation.
- Add an internal threat-model note so future changes keep the same assumptions explicit.

Clipboard and desktop
- Add a window-level Tauri clipboard hook for normal inputs, with clear exclusions for
  currency fields, auth-sensitive paste, and opt-in replace-paste fields.
- Wire an Edit menu (cut, copy, paste, select all) where it helps, and keep behaviour
  consistent with the hook.
- Deduplicate clipboard field props and satisfy ESLint on optional paste handlers.
Updater and vesting operations
- Treat legacy static updater JSON (missing per-platform signatures) as a soft failure with a clear warning, instead of erroring the version check IPC
- Cut vesting polling spam when the chain has no vesting account for the address, and map vesting "no account" to a dedicated BackendError for stable handling on the client.
- Move high-frequency vesting query logs to debug and keep removed-query stubs at warn.

Icons and first-run chrome
- Regenerate macOS/Windows icon assets from a padded 1024 master so dock and switcher visual weight matches other apps; add a small script to regenerate from app-icon-source.png.
- Default the app to dark mode, paint the HTML shell and webview background in the same dark base colour

Housekeeping
- Mock app context defaults to dark for consistency with the new baseline.
Validation run locally where relevant: Rust check, TypeScript check, ESLint, and icon
regeneration script smoke run.

- Remove storybook and old webdriver tests too
2026-04-17 12:05:00 +02:00
benedetta davico 00cc2f215a Merge pull request #6647 from nymtech/release/2026.7-tola
Merge release/2026.7-tola
2026-04-09 14:18:23 +02:00
532 changed files with 33306 additions and 14727 deletions
+2 -2
View File
@@ -25,14 +25,14 @@ jobs:
echo "file2=$(ls nym-vpn*.deb)" >> $GITHUB_ENV
- name: Upload nym-repo-setup
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: ${{ env.file1 }}
path: ppa/packages/nym-repo-setup*.deb
retention-days: 10
- name: Upload nym-vpn
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: ${{ env.file2 }}
path: ppa/packages/nym-vpn*.deb
+4 -1
View File
@@ -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/"
+4 -1
View File
@@ -28,7 +28,7 @@ jobs:
run: sudo apt-get install -y rsync
- uses: rlespinasse/github-slug-action@v3.x
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v5.0.0
with:
version: 9
- uses: actions/setup-node@v4
@@ -61,6 +61,9 @@ jobs:
cd ${{ github.workspace }}/sdk/typescript/packages/sdk && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/mix-fetch && typedoc --skipErrorChecking
- name: Verify doc versions
run: ${{ github.workspace }}/documentation/scripts/verify-doc-versions.sh
working-directory: ${{ github.workspace }}
- name: Install project dependencies
run: pnpm i
- name: Generate llms-full.txt
@@ -0,0 +1,41 @@
name: ci-nym-wallet-frontend
on:
pull_request:
paths:
- 'nym-wallet/**'
- '.github/workflows/ci-nym-wallet-frontend.yml'
jobs:
types-lint:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
with:
node-version-file: nym-wallet/.nvmrc
cache: yarn
cache-dependency-path: yarn.lock
- name: Install dependencies
run: yarn install --network-timeout 100000
- name: Build TypeScript packages (wallet depends on @nymproject/types, etc.)
run: yarn build:types
- name: Build @nymproject/mui-theme and @nymproject/react (wallet imports subpaths)
run: yarn build:packages
- name: Typecheck nym-wallet
run: yarn --cwd nym-wallet tsc
- name: Lint nym-wallet
run: yarn --cwd nym-wallet lint
- name: Yarn audit (workspace lockfile; informational)
run: yarn audit --level critical
continue-on-error: true
- name: Unit tests (nym-wallet)
run: yarn --cwd nym-wallet test
+16
View File
@@ -41,6 +41,9 @@ jobs:
sed -i.bak '1s/^/\[profile.dev\]\ndebug = false\n\n/' Cargo.toml
git diff
- name: Ensure nym-wallet/dist exists for Tauri
run: mkdir -p nym-wallet/dist
- name: Build all binaries
uses: actions-rs/cargo@v1
with:
@@ -71,3 +74,16 @@ jobs:
with:
command: clippy
args: --manifest-path nym-wallet/Cargo.toml --workspace --all-features --all-targets -- -D warnings
- name: Install cargo-audit
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-audit --locked
- name: Cargo audit (nym-wallet workspace)
uses: actions-rs/cargo@v1
with:
command: audit
working-directory: nym-wallet
continue-on-error: true
@@ -1,53 +0,0 @@
name: ci-nym-wallet-storybook
on:
pull_request:
paths:
- 'nym-wallet/**'
- '.github/workflows/ci-nym-wallet-storybook.yml'
jobs:
build:
runs-on: arc-linux-latest-dind
steps:
- uses: actions/checkout@v6
- name: Install rsync
run: sudo apt-get install rsync
continue-on-error: true
- uses: rlespinasse/github-slug-action@v3.x
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup yarn
run: npm install -g yarn
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Build dependencies
run: yarn && yarn build
- name: Build storybook
run: yarn storybook:build
working-directory: ./nym-wallet
- name: Deploy branch to CI www (storybook)
continue-on-error: true
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
ARGS: "-rltgoDzvO --delete"
SOURCE: "nym-wallet/storybook-static/"
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/wallet-${{ env.GITHUB_REF_SLUG }}
EXCLUDE: "/dist/, /node_modules/"
@@ -21,7 +21,7 @@ jobs:
uses: actions/checkout@v6
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
if: matrix.os == 'ubuntu-22.04'
- name: Install rust toolchain
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
find . -name Cargo.toml -exec cargo deny --manifest-path {} check \
advisories -A advisory-not-detected --hide-inclusion-graph \; &> \
>(uniq &> .github/workflows/support-files/notifications/deny.message )
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: report
path: .github/workflows/support-files/notifications/deny.message
+2 -2
View File
@@ -66,7 +66,7 @@ jobs:
args: --workspace --release ${{ env.CARGO_FEATURES }}
- name: Upload Artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: my-artifact
path: |
@@ -81,7 +81,7 @@ jobs:
- id: create-release
name: Upload to release based on tag name
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda
if: github.event_name == 'release'
with:
files: |
+3 -3
View File
@@ -27,21 +27,21 @@ jobs:
run: make contracts
- name: Upload Mixnet Contract Artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: mixnet_contract.wasm
path: contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm
retention-days: 5
- name: Upload Vesting Contract Artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: vesting_contract.wasm
path: contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm
retention-days: 5
- name: Upload to release based on tag name
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
if: github.event_name == 'release'
with:
files: |
@@ -26,7 +26,7 @@ jobs:
- name: Node
uses: actions/setup-node@v4
with:
node-version: 21
node-version: 22.13.0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
@@ -108,7 +108,7 @@ jobs:
cd -
- name: Upload Artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: nym-wallet.app.tar.gz
path: nym-wallet/target/x86_64-apple-darwin/release/bundle/macos/nym-wallet.app.tar.gz
@@ -121,7 +121,7 @@ jobs:
- id: create-release
name: Upload to release based on tag name
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
if: github.event_name == 'release'
with:
files: |
@@ -30,7 +30,7 @@ jobs:
- name: Node
uses: actions/setup-node@v4
with:
node-version: 21
node-version: 22.13.0
cache: 'yarn'
- name: Install Rust toolchain
@@ -72,6 +72,41 @@ jobs:
find target/release/bundle -type d -name "*appimage*" -o -name "*AppImage*" || echo "No AppImage directories found"
find target/release/bundle -name "*.AppImage" -o -name "*.appimage" || echo "No AppImage files found"
fi
- name: Inspect AppImage (hook + bundled graphics libs)
shell: bash
run: |
set -euo pipefail
APPIMAGE_REL=$(find target/release/bundle -name '*.AppImage' | head -n 1)
if [ -z "${APPIMAGE_REL}" ]; then
echo "No AppImage under target/release/bundle"
exit 1
fi
APPIMAGE_ABS="${GITHUB_WORKSPACE}/nym-wallet/${APPIMAGE_REL}"
chmod +x "${APPIMAGE_ABS}"
EXTRACT_DIR=$(mktemp -d)
cd "${EXTRACT_DIR}"
"${APPIMAGE_ABS}" --appimage-extract
# Tauri only stages appimage "files" under /usr/ into the AppDir; paths like /apprun-hooks/ never reach the image.
# Wayland + WEBKIT_DISABLE_DMABUF_RENDERER defaults are applied in main() instead (see configure_linux_wayland_defaults).
HOOK=$(find squashfs-root -name '99-nym-wayland.sh' 2>/dev/null | head -n 1)
if [ -n "${HOOK}" ]; then
echo "Found legacy apprun hook at ${HOOK}"
else
echo "No apprun-hooks/99-nym-wayland.sh (expected): Wayland defaults are set in-process."
fi
find squashfs-root/usr/lib -maxdepth 6 \
\( -name 'libwayland-client.so*' -o -name 'libEGL.so*' -o -name 'libgbm.so*' \) \
2>/dev/null | sort > "${GITHUB_WORKSPACE}/nym-wallet/appimage-bundled-graphics-libs.txt"
wc -l "${GITHUB_WORKSPACE}/nym-wallet/appimage-bundled-graphics-libs.txt"
head -50 "${GITHUB_WORKSPACE}/nym-wallet/appimage-bundled-graphics-libs.txt" || true
- name: Upload AppImage graphics lib inventory
uses: actions/upload-artifact@v6
with:
name: nym-wallet-appimage-lib-inventory
path: nym-wallet/appimage-bundled-graphics-libs.txt
retention-days: 30
- name: Create AppImage tarball if needed
run: |
@@ -97,7 +132,7 @@ jobs:
fi
- name: Upload Artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: nym-wallet-appimage.tar.gz
path: |
@@ -107,7 +142,7 @@ jobs:
- id: create-release
name: Upload to release based on tag name
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
if: github.event_name == 'release'
with:
files: |
+66 -29
View File
@@ -26,6 +26,9 @@ jobs:
outputs:
release_tag: ${{ github.ref_name }}
env:
SIGN_WINDOWS: ${{ github.event_name == 'release' || inputs.sign }}
steps:
- uses: actions/checkout@v6
@@ -35,50 +38,84 @@ jobs:
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v2
uses: microsoft/setup-msbuild@v3
# No cache:yarn here: setup-node needs yarn on PATH to populate the cache, but this runner
# only gets yarn from the step below.
- name: Node
uses: actions/setup-node@v4
with:
node-version: 21
node-version: 22.13.0
- name: Install Yarn (classic)
shell: bash
run: npm install -g yarn@1.22.22
- name: Strip Authenticode thumbprint (avoid signtool on runner)
working-directory: nym-wallet/src-tauri
if: ${{ env.SIGN_WINDOWS == 'true' || (github.event_name == 'workflow_dispatch' && !inputs.sign) }}
shell: bash
run: |
set -euo pipefail
if ! command -v yq >/dev/null 2>&1; then
echo "yq is required on this runner to edit tauri.conf.json"
exit 1
fi
yq eval --inplace '
del(.bundle.windows.certificateThumbprint) |
del(.bundle.windows.digestAlgorithm) |
del(.bundle.windows.timestampUrl)
' tauri.conf.json
- name: Download EV CodeSignTool from ssl.com
working-directory: nym-wallet/src-tauri
if: ${{ inputs.sign }}
if: env.SIGN_WINDOWS == 'true'
shell: bash
run: |
curl -L0 https://www.ssl.com/download/codesigntool-for-linux-and-macos/ -o codesigntool.zip
unzip codesigntool.zip
- name: Get EV certificate credential id
working-directory: nym-wallet/src-tauri
if: ${{ inputs.sign }}
if: env.SIGN_WINDOWS == 'true'
id: get_credential_ids
shell: bash
run: |
echo "SSL_COM_CREDENTIAL_ID=$(./CodeSignTool.sh get_credential_ids -username=${{ secrets.SSL_COM_USERNAME }} -password=${{ secrets.SSL_COM_PASSWORD }} | sed -n '1!p' | sed 's/- //')" >> "$GITHUB_OUTPUT"
- name: Add custom sign command to tauri.conf.json
working-directory: nym-wallet/src-tauri
if: ${{ inputs.sign }}
if: env.SIGN_WINDOWS == 'true'
shell: bash
env:
SSL_SIGN_USER: ${{ secrets.SSL_COM_USERNAME }}
SSL_SIGN_PASS: ${{ secrets.SSL_COM_PASSWORD }}
SSL_SIGN_CRED: ${{ steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
SSL_SIGN_TOTP: ${{ secrets.SSL_COM_TOTP_SECRET }}
run: |
yq eval --inplace '.bundle.windows +=
{
"signCommand": {
"cmd": "C:\Program Files\Git\bin\bash.EXE",
"args": [
"/c/actions-runner/_work/nym/nym/nym-wallet/src-tauri/CodeSignTool.sh",
"sign",
"-username ${{ secrets.SSL_COM_USERNAME }}",
"-password ${{ secrets.SSL_COM_PASSWORD }}",
"-credential_id ${{ steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}",
"-totp_secret ${{ secrets.SSL_COM_TOTP_SECRET }}",
"-program_name NymWallet",
"-input_file_path",
"%1",
"-override"
]
set -euo pipefail
if ! command -v cygpath >/dev/null 2>&1; then
echo "cygpath not found; install Git for Windows or use bash from Git SDK"
exit 1
fi
export SCRIPT_UNIX="$(cygpath -u "$GITHUB_WORKSPACE/nym-wallet/src-tauri/CodeSignTool.sh")"
yq eval --inplace '
.bundle.windows += {
"signCommand": {
"cmd": "C:/Program Files/Git/bin/bash.exe",
"args": [
strenv(SCRIPT_UNIX),
"sign",
("-username " + strenv(SSL_SIGN_USER)),
("-password " + strenv(SSL_SIGN_PASS)),
("-credential_id " + strenv(SSL_SIGN_CRED)),
("-totp_secret " + strenv(SSL_SIGN_TOTP)),
"-program_name NymWallet",
"-input_file_path",
"%1",
"-override"
]
}
}
}' tauri.conf.json
' tauri.conf.json
- name: Install project dependencies
shell: bash
run: cd .. && yarn --network-timeout 100000
@@ -93,10 +130,10 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
SSL_COM_USERNAME: ${{ inputs.sign && secrets.SSL_COM_USERNAME }}
SSL_COM_PASSWORD: ${{ inputs.sign && secrets.SSL_COM_PASSWORD }}
SSL_COM_CREDENTIAL_ID: ${{ inputs.sign && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
SSL_COM_TOTP_SECRET: ${{ inputs.sign && secrets.SSL_COM_TOTP_SECRET }}
SSL_COM_USERNAME: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_USERNAME }}
SSL_COM_PASSWORD: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_PASSWORD }}
SSL_COM_CREDENTIAL_ID: ${{ env.SIGN_WINDOWS == 'true' && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
SSL_COM_TOTP_SECRET: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_TOTP_SECRET }}
run: |
echo "Starting build process..."
yarn build
@@ -128,7 +165,7 @@ jobs:
find . -name "*.msi" -type f
- name: Upload Artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: nym-wallet.msi
path: |
@@ -139,7 +176,7 @@ jobs:
- id: create-release
name: Upload to release based on tag name
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
if: github.event_name == 'release'
with:
files: |
@@ -167,4 +204,4 @@ jobs:
needs: publish-tauri
with:
release_tag: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
secrets: inherit
secrets: inherit
@@ -76,7 +76,7 @@ jobs:
apk/nyms5-arch64-release.apk
- name: Upload APKs
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: nyms5-apk-arch64
path: |
@@ -91,12 +91,12 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
- name: Download binary artifact
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: nyms5-apk-arch64
path: apk
- name: Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
files: |
apk/nyms5-arch64-debug.apk
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
@@ -0,0 +1,61 @@
name: Build and upload Network Monitor Agent container to harbor.nymte.ch
on:
workflow_dispatch:
inputs:
release_image:
description: 'Tag image as a release (prefix with golden-)'
required: true
default: false
type: boolean
env:
WORKING_DIRECTORY: "nym-network-monitor-v3/nym-network-monitor-agent"
CONTAINER_NAME: "network-monitor-agent"
jobs:
build-container:
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
password: ${{ secrets.HARBOR_ROBOT_SECRET }}
- name: Checkout repo
uses: actions/checkout@v6
- name: Configure git identity
run: |
git config --global user.email "lawrence@nymtech.net"
git config --global user.name "Lawrence Stalder"
- name: Get version from Cargo.toml
id: get_version
run: |
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml)
echo "result=$VERSION" >> $GITHUB_OUTPUT
- name: Set GIT_TAG variable
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
- name: Initialize RELEASE_TAG
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
- name: Set RELEASE_TAG for release
if: github.event.inputs.release_image == 'true'
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
- name: Set IMAGE_NAME_AND_TAGS variable
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
- name: New env vars
run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
- name: Build and push image to Harbor
run: |
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }}
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
@@ -0,0 +1,57 @@
name: Build and upload Network Monitor Orchestrator container to harbor.nymte.ch
on:
workflow_dispatch:
inputs:
release_image:
description: 'Tag image as a release (prefix with golden-)'
required: true
default: false
type: boolean
env:
WORKING_DIRECTORY: "nym-network-monitor-v3/nym-network-monitor-orchestrator"
CONTAINER_NAME: "network-monitor-orchestrator"
jobs:
build-container:
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
password: ${{ secrets.HARBOR_ROBOT_SECRET }}
- name: Checkout repo
uses: actions/checkout@v6
- name: Configure git identity
run: |
git config --global user.email "lawrence@nymtech.net"
git config --global user.name "Lawrence Stalder"
- name: Get version from Cargo.toml
id: get_version
run: |
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml)
echo "result=$VERSION" >> $GITHUB_OUTPUT
- name: Initialize RELEASE_TAG
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
- name: Set RELEASE_TAG for release
if: github.event.inputs.release_image == 'true'
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
- name: Set IMAGE_NAME_AND_TAGS variable
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
- name: Log image name
run: echo "RELEASE_TAG='$RELEASE_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
- name: Build and push image to Harbor
run: |
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }}
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
@@ -18,7 +18,7 @@ jobs:
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
@@ -11,7 +11,7 @@ jobs:
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Login to Harbor
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
@@ -11,7 +11,7 @@ jobs:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
+1 -1
View File
@@ -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
+2
View File
@@ -27,6 +27,7 @@ v6-topology.json
/explorer/public/downloads/mixmining.json
/explorer/public/downloads/topology.json
/nym-wallet/dist/*
/nym-wallet/appimage-bundled-graphics-libs.txt
/clients/validator/examples/nym-driver-example/current-contract.txt
validator-api/v4.json
validator-api/v6.json
@@ -77,3 +78,4 @@ CLAUDE.md
/notes
/target-otel
test-tutorials/
Generated
+220 -45
View File
@@ -57,7 +57,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
"cpufeatures 0.2.17",
]
[[package]]
@@ -253,7 +253,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2 0.10.6",
"cpufeatures",
"cpufeatures 0.2.17",
"password-hash",
]
@@ -1309,7 +1309,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
"cpufeatures 0.2.17",
]
[[package]]
name = "chacha20"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core 0.10.1",
]
[[package]]
@@ -1319,7 +1330,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
dependencies = [
"aead",
"chacha20",
"chacha20 0.9.1",
"cipher",
"poly1305",
"zeroize",
@@ -1831,6 +1842,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.3.0"
@@ -2095,7 +2115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"curve25519-dalek-derive",
"digest 0.10.7",
"fiat-crypto",
@@ -2799,18 +2819,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "enum-as-inner"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "env_filter"
version = "0.1.3"
@@ -3271,6 +3279,7 @@ dependencies = [
"cfg-if",
"libc",
"r-efi",
"rand_core 0.10.1",
"wasip2",
"wasip3",
]
@@ -3630,26 +3639,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0"
[[package]]
name = "hickory-proto"
version = "0.25.2"
name = "hickory-net"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502"
checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183"
dependencies = [
"async-trait",
"bytes",
"cfg-if",
"data-encoding",
"enum-as-inner",
"futures-channel",
"futures-io",
"futures-util",
"h2 0.4.11",
"hickory-proto",
"http 1.3.1",
"idna",
"ipnet",
"once_cell",
"rand 0.9.2",
"ring",
"jni 0.22.4",
"rand 0.10.1",
"rustls 0.23.37",
"thiserror 2.0.12",
"tinyvec",
@@ -3657,31 +3665,56 @@ dependencies = [
"tokio-rustls 0.26.2",
"tracing",
"url",
"webpki-roots 0.26.11",
"webpki-roots 1.0.2",
]
[[package]]
name = "hickory-proto"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643"
dependencies = [
"data-encoding",
"idna",
"ipnet",
"jni 0.22.4",
"once_cell",
"prefix-trie",
"rand 0.10.1",
"ring",
"thiserror 2.0.12",
"tinyvec",
"tracing",
"url",
]
[[package]]
name = "hickory-resolver"
version = "0.25.2"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a"
checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c"
dependencies = [
"cfg-if",
"futures-util",
"hickory-net",
"hickory-proto",
"ipconfig",
"ipnet",
"jni 0.22.4",
"moka",
"ndk-context",
"once_cell",
"parking_lot",
"rand 0.9.2",
"rand 0.10.1",
"resolv-conf",
"rustls 0.23.37",
"smallvec",
"system-configuration 0.7.0",
"thiserror 2.0.12",
"tokio",
"tokio-rustls 0.26.2",
"tracing",
"webpki-roots 0.26.11",
"webpki-roots 1.0.2",
]
[[package]]
@@ -4409,6 +4442,9 @@ name = "ipnet"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
dependencies = [
"serde",
]
[[package]]
name = "ipnetwork"
@@ -4503,19 +4539,68 @@ dependencies = [
"cesu8",
"cfg-if",
"combine",
"jni-sys",
"jni-sys 0.3.0",
"log",
"thiserror 1.0.69",
"walkdir",
"windows-sys 0.45.0",
]
[[package]]
name = "jni"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
dependencies = [
"cfg-if",
"combine",
"jni-macros",
"jni-sys 0.4.1",
"log",
"simd_cesu8",
"thiserror 2.0.12",
"walkdir",
"windows-link 0.2.1",
]
[[package]]
name = "jni-macros"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
dependencies = [
"proc-macro2",
"quote",
"rustc_version 0.4.1",
"simd_cesu8",
"syn 2.0.106",
]
[[package]]
name = "jni-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jni-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
dependencies = [
"jni-sys-macros",
]
[[package]]
name = "jni-sys-macros"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
dependencies = [
"quote",
"syn 2.0.106",
]
[[package]]
name = "jobserver"
version = "0.1.33"
@@ -4582,7 +4667,7 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
dependencies = [
"cpufeatures",
"cpufeatures 0.2.17",
]
[[package]]
@@ -5359,6 +5444,12 @@ dependencies = [
"version_check",
]
[[package]]
name = "ndk-context"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
[[package]]
name = "netlink-packet-core"
version = "0.8.1"
@@ -5677,6 +5768,7 @@ dependencies = [
"nym-http-api-client",
"nym-http-api-common",
"nym-mixnet-contract-common",
"nym-node-families-contract-common",
"nym-node-requests",
"nym-node-tester-utils",
"nym-pemstore",
@@ -6273,7 +6365,7 @@ dependencies = [
[[package]]
name = "nym-credential-proxy"
version = "0.3.0"
version = "0.3.2-rc"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -6926,6 +7018,7 @@ dependencies = [
"bytes",
"cfg-if",
"encoding_rs",
"fastrand",
"hickory-resolver",
"http 1.3.1",
"inventory",
@@ -7516,6 +7609,21 @@ dependencies = [
"zeroize",
]
[[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 0.8.22",
"serde",
"thiserror 2.0.12",
]
[[package]]
name = "nym-node-metrics"
version = "1.21.0"
@@ -7596,6 +7704,7 @@ dependencies = [
"humantime",
"itertools 0.14.0",
"moka",
"nym-api-requests",
"nym-bin-common",
"nym-contracts-common",
"nym-credential-proxy-lib",
@@ -7768,7 +7877,7 @@ name = "nym-outfox"
version = "1.21.0"
dependencies = [
"blake3",
"chacha20",
"chacha20 0.9.1",
"chacha20poly1305",
"fastrand",
"sphinx-packet",
@@ -8480,6 +8589,7 @@ dependencies = [
"nym-mixnet-contract-common",
"nym-multisig-contract-common",
"nym-network-defaults",
"nym-node-families-contract-common",
"nym-performance-contract-common",
"nym-serde-helpers",
"nym-vesting-contract-common",
@@ -9450,7 +9560,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
"cpufeatures",
"cpufeatures 0.2.17",
"opaque-debug 0.3.1",
"universal-hash",
]
@@ -9462,7 +9572,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"opaque-debug 0.3.1",
"universal-hash",
]
@@ -9541,6 +9651,17 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "prefix-trie"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90f561214012d3fc240a1f9c817cc4d57f5310910d066069c1b093f766bb5966"
dependencies = [
"either",
"ipnet",
"num-traits",
]
[[package]]
name = "pretty_assertions"
version = "1.4.1"
@@ -9762,9 +9883,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
version = "0.11.12"
version = "0.11.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
dependencies = [
"aws-lc-rs",
"bytes",
@@ -9838,6 +9959,17 @@ dependencies = [
"rand_core 0.9.3",
]
[[package]]
name = "rand"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"chacha20 0.10.0",
"getrandom 0.4.1",
"rand_core 0.10.1",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
@@ -9876,6 +10008,12 @@ dependencies = [
"getrandom 0.3.3",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_distr"
version = "0.4.3"
@@ -10005,7 +10143,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"sync_wrapper 0.1.2",
"system-configuration",
"system-configuration 0.5.1",
"tokio",
"tokio-rustls 0.24.1",
"tower-service",
@@ -10428,7 +10566,7 @@ checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni",
"jni 0.21.1",
"log",
"once_cell",
"rustls 0.23.37",
@@ -11015,7 +11153,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest 0.10.7",
]
@@ -11027,7 +11165,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
dependencies = [
"block-buffer 0.9.0",
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest 0.9.0",
"opaque-debug 0.3.1",
]
@@ -11039,7 +11177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest 0.10.7",
]
@@ -11120,6 +11258,22 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simd_cesu8"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
dependencies = [
"rustc_version 0.4.1",
"simdutf8",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "siphasher"
version = "0.3.11"
@@ -11721,7 +11875,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"system-configuration-sys",
"system-configuration-sys 0.5.0",
]
[[package]]
name = "system-configuration"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
dependencies = [
"bitflags 2.9.1",
"core-foundation 0.9.4",
"system-configuration-sys 0.6.0",
]
[[package]]
@@ -11734,6 +11899,16 @@ dependencies = [
"libc",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "tagptr"
version = "0.2.0"
@@ -11775,9 +11950,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tar"
version = "0.4.44"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973"
dependencies = [
"filetime",
"libc",
+23 -20
View File
@@ -31,7 +31,6 @@ members = [
"common/client-libs/mixnet-client",
"common/client-libs/validator-client",
"common/commands",
"common/nym-common",
"common/config",
"common/cosmwasm-smart-contracts/coconut-dkg",
"common/cosmwasm-smart-contracts/contracts-common",
@@ -41,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"]
@@ -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"
@@ -280,8 +281,8 @@ getrandom03 = { package = "getrandom", version = "=0.3.3" }
glob = "0.3"
handlebars = "3.5.5"
hex = "0.4.3"
hickory-proto = "0.25.2"
hickory-resolver = "0.25.2"
hickory-proto = "0.26.1"
hickory-resolver = "0.26.1"
hkdf = "0.12.3"
hmac = "0.12.1"
http = "1"
@@ -325,7 +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" }
@@ -359,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"
@@ -410,7 +411,7 @@ libcrux-chacha20poly1305 = "0.0.7"
libcrux-psq = "0.0.8"
libcrux-ml-kem = "0.0.8"
libcrux-sha3 = "0.0.8"
libcrux-traits = "0.0.8"
libcrux-traits = "0.0.6"
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
nym-api-requests = { version = "1.21.0", path = "nym-api/nym-api-requests" }
@@ -472,6 +473,7 @@ 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" }
@@ -617,3 +619,4 @@ exit = "deny"
panic = "deny"
unimplemented = "deny"
unreachable = "deny"
+2
View File
@@ -472,6 +472,7 @@ impl Handler {
fn prepare_reconstructed_binary(
reconstructed_messages: Vec<ReconstructedMessage>,
) -> Vec<Result<WsMessage, WsError>> {
#[allow(clippy::result_large_err)] // TODO : remove this once tungstenite is updated
reconstructed_messages
.into_iter()
.map(ServerResponse::Received)
@@ -484,6 +485,7 @@ fn prepare_reconstructed_binary(
fn prepare_reconstructed_text(
reconstructed_messages: Vec<ReconstructedMessage>,
) -> Vec<Result<WsMessage, WsError>> {
#[allow(clippy::result_large_err)] // TODO : remove this once tungstenite is updated
reconstructed_messages
.into_iter()
.map(ServerResponse::Received)
@@ -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],
&params,
)
.await
}
async fn get_all_node_families(&self) -> Result<Vec<NodeFamily>, NymAPIError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut families = Vec::new();
loop {
let mut res = self.get_node_families(Some(page), None).await?;
families.append(&mut res.data);
if families.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(families)
}
#[deprecated]
#[tracing::instrument(level = "debug", skip_all)]
async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNodeV1>, NymAPIError> {
@@ -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())
@@ -0,0 +1,441 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::collect_paged;
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::error::NyxdError;
use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use cosmrs::AccountId;
use serde::Deserialize;
use nym_mixnet_contract_common::NodeId;
pub use nym_node_families_contract_common::{
msg::QueryMsg as NodeFamiliesQueryMsg, AllFamilyMembersPagedResponse,
AllPastFamilyInvitationsPagedResponse, FamiliesPagedResponse, FamilyMemberRecord,
FamilyMembersPagedResponse, GlobalPastFamilyInvitationCursor, NodeFamily,
NodeFamilyByNameResponse, NodeFamilyByOwnerResponse, NodeFamilyId,
NodeFamilyMembershipResponse, NodeFamilyResponse, PastFamilyInvitation,
PastFamilyInvitationCursor, PastFamilyInvitationForNodeCursor,
PastFamilyInvitationsForNodePagedResponse, PastFamilyInvitationsPagedResponse,
PastFamilyMember, PastFamilyMemberCursor, PastFamilyMemberForNodeCursor,
PastFamilyMembersForNodePagedResponse, PastFamilyMembersPagedResponse,
PendingFamilyInvitationDetails, PendingFamilyInvitationResponse,
PendingFamilyInvitationsPagedResponse, PendingInvitationsForNodePagedResponse,
PendingInvitationsPagedResponse,
};
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait NodeFamiliesQueryClient {
async fn query_node_families_contract<T>(
&self,
query: NodeFamiliesQueryMsg,
) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>;
async fn get_family_by_id(
&self,
family_id: NodeFamilyId,
) -> Result<NodeFamilyResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyById { family_id })
.await
}
async fn get_family_by_owner(
&self,
owner: &AccountId,
) -> Result<NodeFamilyByOwnerResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyByOwner {
owner: owner.to_string(),
})
.await
}
async fn get_family_by_name(
&self,
name: String,
) -> Result<NodeFamilyByNameResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyByName { name })
.await
}
async fn get_families_paged(
&self,
start_after: Option<NodeFamilyId>,
limit: Option<u32>,
) -> Result<FamiliesPagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamiliesPaged {
start_after,
limit,
})
.await
}
async fn get_family_membership(
&self,
node_id: NodeId,
) -> Result<NodeFamilyMembershipResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyMembership { node_id })
.await
}
async fn get_family_members_paged(
&self,
family_id: NodeFamilyId,
start_after: Option<NodeId>,
limit: Option<u32>,
) -> Result<FamilyMembersPagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyMembersPaged {
family_id,
start_after,
limit,
})
.await
}
async fn get_all_family_members_paged(
&self,
start_after: Option<NodeId>,
limit: Option<u32>,
) -> Result<AllFamilyMembersPagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetAllFamilyMembersPaged {
start_after,
limit,
})
.await
}
async fn get_pending_invitation(
&self,
family_id: NodeFamilyId,
node_id: NodeId,
) -> Result<PendingFamilyInvitationResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPendingInvitation {
family_id,
node_id,
})
.await
}
async fn get_pending_invitations_for_family_paged(
&self,
family_id: NodeFamilyId,
start_after: Option<NodeId>,
limit: Option<u32>,
) -> Result<PendingFamilyInvitationsPagedResponse, NyxdError> {
self.query_node_families_contract(
NodeFamiliesQueryMsg::GetPendingInvitationsForFamilyPaged {
family_id,
start_after,
limit,
},
)
.await
}
async fn get_pending_invitations_for_node_paged(
&self,
node_id: NodeId,
start_after: Option<NodeFamilyId>,
limit: Option<u32>,
) -> Result<PendingInvitationsForNodePagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPendingInvitationsForNodePaged {
node_id,
start_after,
limit,
})
.await
}
async fn get_all_pending_invitations_paged(
&self,
start_after: Option<(NodeFamilyId, NodeId)>,
limit: Option<u32>,
) -> Result<PendingInvitationsPagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetAllPendingInvitationsPaged {
start_after,
limit,
})
.await
}
async fn get_past_invitations_for_family_paged(
&self,
family_id: NodeFamilyId,
start_after: Option<PastFamilyInvitationCursor>,
limit: Option<u32>,
) -> Result<PastFamilyInvitationsPagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastInvitationsForFamilyPaged {
family_id,
start_after,
limit,
})
.await
}
async fn get_past_invitations_for_node_paged(
&self,
node_id: NodeId,
start_after: Option<PastFamilyInvitationForNodeCursor>,
limit: Option<u32>,
) -> Result<PastFamilyInvitationsForNodePagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastInvitationsForNodePaged {
node_id,
start_after,
limit,
})
.await
}
async fn get_all_past_invitations_paged(
&self,
start_after: Option<GlobalPastFamilyInvitationCursor>,
limit: Option<u32>,
) -> Result<AllPastFamilyInvitationsPagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetAllPastInvitationsPaged {
start_after,
limit,
})
.await
}
async fn get_past_members_for_family_paged(
&self,
family_id: NodeFamilyId,
start_after: Option<PastFamilyMemberCursor>,
limit: Option<u32>,
) -> Result<PastFamilyMembersPagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastMembersForFamilyPaged {
family_id,
start_after,
limit,
})
.await
}
async fn get_past_members_for_node_paged(
&self,
node_id: NodeId,
start_after: Option<PastFamilyMemberForNodeCursor>,
limit: Option<u32>,
) -> Result<PastFamilyMembersForNodePagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastMembersForNodePaged {
node_id,
start_after,
limit,
})
.await
}
}
// extension trait to the query client to deal with the paged queries
// (it didn't feel appropriate to combine it with the existing trait)
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait PagedNodeFamiliesQueryClient: NodeFamiliesQueryClient {
async fn get_all_families(&self) -> Result<Vec<NodeFamily>, NyxdError> {
collect_paged!(self, get_families_paged, families)
}
async fn get_all_family_members_for_family(
&self,
family_id: NodeFamilyId,
) -> Result<Vec<FamilyMemberRecord>, NyxdError> {
collect_paged!(self, get_family_members_paged, members, family_id)
}
async fn get_all_family_members(&self) -> Result<Vec<FamilyMemberRecord>, NyxdError> {
collect_paged!(self, get_all_family_members_paged, members)
}
async fn get_all_pending_invitations_for_family(
&self,
family_id: NodeFamilyId,
) -> Result<Vec<PendingFamilyInvitationDetails>, NyxdError> {
collect_paged!(
self,
get_pending_invitations_for_family_paged,
invitations,
family_id
)
}
async fn get_all_pending_invitations_for_node(
&self,
node_id: NodeId,
) -> Result<Vec<PendingFamilyInvitationDetails>, NyxdError> {
collect_paged!(
self,
get_pending_invitations_for_node_paged,
invitations,
node_id
)
}
async fn get_all_pending_invitations(
&self,
) -> Result<Vec<PendingFamilyInvitationDetails>, NyxdError> {
collect_paged!(self, get_all_pending_invitations_paged, invitations)
}
async fn get_all_past_invitations_for_family(
&self,
family_id: NodeFamilyId,
) -> Result<Vec<PastFamilyInvitation>, NyxdError> {
collect_paged!(
self,
get_past_invitations_for_family_paged,
invitations,
family_id
)
}
async fn get_all_past_invitations_for_node(
&self,
node_id: NodeId,
) -> Result<Vec<PastFamilyInvitation>, NyxdError> {
collect_paged!(
self,
get_past_invitations_for_node_paged,
invitations,
node_id
)
}
async fn get_all_past_invitations(&self) -> Result<Vec<PastFamilyInvitation>, NyxdError> {
collect_paged!(self, get_all_past_invitations_paged, invitations)
}
async fn get_all_past_members_for_family(
&self,
family_id: NodeFamilyId,
) -> Result<Vec<PastFamilyMember>, NyxdError> {
collect_paged!(self, get_past_members_for_family_paged, members, family_id)
}
async fn get_all_past_members_for_node(
&self,
node_id: NodeId,
) -> Result<Vec<PastFamilyMember>, NyxdError> {
collect_paged!(self, get_past_members_for_node_paged, members, node_id)
}
}
#[async_trait]
impl<T> PagedNodeFamiliesQueryClient for T where T: NodeFamiliesQueryClient {}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> NodeFamiliesQueryClient for C
where
C: CosmWasmClient + NymContractsProvider + Send + Sync,
{
async fn query_node_families_contract<T>(
&self,
query: NodeFamiliesQueryMsg,
) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>,
{
let node_families_contract_address = &self
.node_families_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("node families contract"))?;
self.query_contract_smart(node_families_contract_address, &query)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
use nym_node_families_contract_common::QueryMsg;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
fn all_query_variants_are_covered<C: NodeFamiliesQueryClient + Send + Sync>(
client: C,
msg: NodeFamiliesQueryMsg,
) {
match msg {
NodeFamiliesQueryMsg::GetFamilyById { family_id } => {
client.get_family_by_id(family_id).ignore()
}
NodeFamiliesQueryMsg::GetFamilyByOwner { owner } => {
client.get_family_by_owner(&owner.parse().unwrap()).ignore()
}
NodeFamiliesQueryMsg::GetFamilyByName { name } => {
client.get_family_by_name(name).ignore()
}
NodeFamiliesQueryMsg::GetFamiliesPaged { start_after, limit } => {
client.get_families_paged(start_after, limit).ignore()
}
NodeFamiliesQueryMsg::GetFamilyMembership { node_id } => {
client.get_family_membership(node_id).ignore()
}
NodeFamiliesQueryMsg::GetFamilyMembersPaged {
family_id,
start_after,
limit,
} => client
.get_family_members_paged(family_id, start_after, limit)
.ignore(),
NodeFamiliesQueryMsg::GetAllFamilyMembersPaged { start_after, limit } => client
.get_all_family_members_paged(start_after, limit)
.ignore(),
NodeFamiliesQueryMsg::GetPendingInvitation { family_id, node_id } => {
client.get_pending_invitation(family_id, node_id).ignore()
}
NodeFamiliesQueryMsg::GetPendingInvitationsForFamilyPaged {
family_id,
start_after,
limit,
} => client
.get_pending_invitations_for_family_paged(family_id, start_after, limit)
.ignore(),
NodeFamiliesQueryMsg::GetPendingInvitationsForNodePaged {
node_id,
start_after,
limit,
} => client
.get_pending_invitations_for_node_paged(node_id, start_after, limit)
.ignore(),
NodeFamiliesQueryMsg::GetAllPendingInvitationsPaged { start_after, limit } => client
.get_all_pending_invitations_paged(start_after, limit)
.ignore(),
NodeFamiliesQueryMsg::GetPastInvitationsForFamilyPaged {
family_id,
start_after,
limit,
} => client
.get_past_invitations_for_family_paged(family_id, start_after, limit)
.ignore(),
NodeFamiliesQueryMsg::GetPastInvitationsForNodePaged {
node_id,
start_after,
limit,
} => client
.get_past_invitations_for_node_paged(node_id, start_after, limit)
.ignore(),
NodeFamiliesQueryMsg::GetAllPastInvitationsPaged { start_after, limit } => client
.get_all_past_invitations_paged(start_after, limit)
.ignore(),
NodeFamiliesQueryMsg::GetPastMembersForFamilyPaged {
family_id,
start_after,
limit,
} => client
.get_past_members_for_family_paged(family_id, start_after, limit)
.ignore(),
QueryMsg::GetPastMembersForNodePaged {
node_id,
start_after,
limit,
} => client
.get_past_members_for_node_paged(node_id, start_after, limit)
.ignore(),
};
}
}
@@ -0,0 +1,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>,
}
@@ -13,8 +13,9 @@ use std::sync::Arc;
use std::time::Duration;
use time::OffsetDateTime;
use tokio::sync::Mutex as AsyncMutex;
use tokio::time::Instant;
use tokio_util::sync::CancellationToken;
use tracing::{debug, info, instrument, warn};
use tracing::{debug, error, info, instrument, warn};
use uuid::Uuid;
pub use helpers::{BufferedDeposit, PerformedDeposits, make_deposits_request, split_deposits};
@@ -146,9 +147,14 @@ impl DepositsBuffer {
// if we're here, we know we're below the threshold
fn maybe_refill_deposits(&self) {
if let Some(mut guard) = self.inner.deposits_refill_task.try_get_new_task_guard() {
if let Some((mut guard, completion_guard)) =
self.inner.deposits_refill_task.try_get_new_task_guard()
{
let this = self.clone();
*guard = Some(tokio::spawn(async move { this.refill_deposits().await }));
*guard = Some(tokio::spawn(async move {
let _completion_guard = completion_guard;
this.refill_deposits().await
}));
}
}
@@ -179,6 +185,8 @@ impl DepositsBuffer {
requested_on: OffsetDateTime,
client_pubkey: PublicKeyUser,
) -> Result<BufferedDeposit, CredentialProxyError> {
let wait_start = Instant::now();
let mut i = 0;
loop {
tokio::time::sleep(Duration::from_millis(500)).await;
if let Some(buffered_deposit) = self.inner.unused_deposits.lock().await.pop() {
@@ -195,6 +203,15 @@ impl DepositsBuffer {
// make sure there's always a task working in the background in case deposits get used up too quickly
self.maybe_refill_deposits()
}
i += 1;
let elapsed = wait_start.elapsed();
if elapsed > Duration::from_secs(5) && i % 10 == 0 {
warn!("we've been waiting for over 5s to make a deposit - something is wrong!")
} else if elapsed > Duration::from_secs(10) && i % 5 == 0 {
error!(
"we've been waiting for over 10s to make a deposit - something is SERIOUSLY wrong!"
)
}
}
}
@@ -3,12 +3,22 @@
use crate::error::CredentialProxyError;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex as StdMutex, MutexGuard};
use std::sync::{Arc, Mutex as StdMutex, MutexGuard};
use tokio::task::JoinHandle;
use tracing::{debug, error};
pub(super) type RefillTaskResult = Result<(), CredentialProxyError>;
pub(super) struct InProgressGuard {
in_progress: Arc<AtomicBool>,
}
impl Drop for InProgressGuard {
fn drop(&mut self) {
self.in_progress.store(false, Ordering::SeqCst);
}
}
#[derive(Default)]
pub(super) struct RefillTask {
// note that we can only have a single transaction in progress (or it'd mess up with our sequence numbers)
@@ -16,7 +26,7 @@ pub(super) struct RefillTask {
// we'll have to increase the number of deposits per transaction
join_handle: StdMutex<Option<JoinHandle<RefillTaskResult>>>,
in_progress: AtomicBool,
in_progress: Arc<AtomicBool>,
}
impl RefillTask {
@@ -28,9 +38,15 @@ impl RefillTask {
.is_ok()
}
/// Returns `None` if a refill is already in progress. On success, returns the
/// join-handle guard (to store the new `JoinHandle` into) and an [`InProgressGuard`]
/// that **must be moved into the spawned task** — it resets the flag when dropped.
pub(super) fn try_get_new_task_guard(
&self,
) -> Option<MutexGuard<'_, Option<JoinHandle<RefillTaskResult>>>> {
) -> Option<(
MutexGuard<'_, Option<JoinHandle<RefillTaskResult>>>,
InProgressGuard,
)> {
// sanity check for concurrent request
if !self.try_set_in_progress() {
debug!("another task has already started deposit refill request");
@@ -48,7 +64,11 @@ impl RefillTask {
}
}
Some(guard)
let completion_guard = InProgressGuard {
in_progress: Arc::clone(&self.in_progress),
};
Some((guard, completion_guard))
}
pub(super) fn take_task_join_handle(&self) -> Option<JoinHandle<RefillTaskResult>> {
@@ -56,3 +76,34 @@ impl RefillTask {
self.join_handle.lock().expect("mutex got poisoned").take()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn in_progress_resets_after_guard_drop() {
let task = RefillTask::default();
let (guard, completion_guard) = task.try_get_new_task_guard().unwrap();
drop(guard);
assert!(task.try_get_new_task_guard().is_none());
drop(completion_guard);
assert!(task.try_get_new_task_guard().is_some());
}
#[test]
fn in_progress_resets_on_panic() {
let task = RefillTask::default();
let (guard, completion_guard) = task.try_get_new_task_guard().unwrap();
drop(guard);
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _g = completion_guard;
panic!("simulated refill task panic");
}));
assert!(task.try_get_new_task_guard().is_some());
}
}
+3 -1
View File
@@ -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
+1
View File
@@ -36,6 +36,7 @@ thiserror = { workspace = true }
tracing = { workspace = true }
itertools = { workspace = true }
inventory = { workspace = true }
fastrand = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros", "time"] }
rustls = { workspace=true }
# used for decoding text responses (they were already implicitly included)
+78 -128
View File
@@ -55,9 +55,8 @@ use std::{
use hickory_resolver::{
TokioResolver,
config::{NameServerConfig, NameServerConfigGroup, ResolverConfig, ResolverOpts},
lookup_ip::LookupIpIntoIter,
name_server::TokioConnectionProvider,
config::{CLOUDFLARE, NameServerConfig, QUAD9, ResolverConfig, ResolverOpts},
net::{NetError, runtime::TokioRuntimeProvider},
};
use once_cell::sync::OnceCell;
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
@@ -113,7 +112,7 @@ pub enum ResolveError {
#[error("invalid name: {0}")]
InvalidNameError(String),
#[error("hickory-dns resolver error: {0}")]
ResolveError(#[from] hickory_resolver::ResolveError),
ResolveError(#[from] NetError),
#[error("high level lookup timed out")]
Timeout,
#[error("hostname not found in static lookup table")]
@@ -123,7 +122,10 @@ pub enum ResolveError {
impl ResolveError {
/// Returns true if the error is a timeout.
pub fn is_timeout(&self) -> bool {
matches!(self, ResolveError::Timeout)
matches!(
self,
ResolveError::Timeout | ResolveError::ResolveError(NetError::Timeout)
)
}
}
@@ -167,18 +169,17 @@ impl Resolve for HickoryDnsResolver {
fn resolve(&self, name: Name) -> Resolving {
let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
let use_shared = self.use_shared;
let resolver = if use_system {
match self
.system_resolver
let result = if use_system {
self.system_resolver
.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(use_shared))
{
Ok(r) => r.clone(),
Err(e) => return Box::pin(return_err(e)),
}
} else {
self.state
.get_or_init(|| HickoryDnsResolver::new_resolver(use_shared))
.clone()
.get_or_try_init(|| HickoryDnsResolver::new_resolver(use_shared))
};
let resolver = match result {
Ok(r) => r.clone(),
Err(err) => return Box::pin(return_err(err)),
};
let maybe_static = self.static_base.clone();
@@ -227,9 +228,11 @@ async fn resolve(
let primary_err = match resolve_fut.await {
Err(_) => ResolveError::Timeout,
Ok(Ok(lookup)) => {
let addrs: Addrs = Box::new(SocketAddrs {
iter: lookup.into_iter(),
});
// Shuffle so that successive connection attempts cycle through all
// returned IPs rather than always hitting the same first address.
let mut ips = Vec::from_iter(lookup.iter());
fastrand::shuffle(&mut ips);
let addrs: Addrs = Box::new(ips.into_iter().map(|ip| SocketAddr::new(ip, 0)));
return Ok(addrs);
}
Ok(Err(e)) => {
@@ -256,18 +259,6 @@ async fn resolve(
Err(primary_err)
}
struct SocketAddrs {
iter: LookupIpIntoIter,
}
impl Iterator for SocketAddrs {
type Item = SocketAddr;
fn next(&mut self) -> Option<Self::Item> {
self.iter.next().map(|ip_addr| SocketAddr::new(ip_addr, 0))
}
}
impl HickoryDnsResolver {
/// Returns an instance of the shared resolver.
pub fn shared() -> Self {
@@ -288,7 +279,7 @@ impl HickoryDnsResolver {
.clone()
} else {
self.state
.get_or_init(|| HickoryDnsResolver::new_resolver(self.use_shared))
.get_or_try_init(|| HickoryDnsResolver::new_resolver(self.use_shared))?
.clone()
};
@@ -311,11 +302,11 @@ impl HickoryDnsResolver {
}
}
fn new_resolver(use_shared: bool) -> TokioResolver {
fn new_resolver(use_shared: bool) -> Result<TokioResolver, ResolveError> {
// using a closure here is slightly gross, but this makes sure that if the
// lazy-init returns an error it can be handled by the client
if use_shared {
SHARED_RESOLVER.state.get_or_init(new_resolver).clone()
SHARED_RESOLVER.state.get_or_try_init(new_resolver).cloned()
} else {
new_resolver()
}
@@ -367,7 +358,7 @@ impl HickoryDnsResolver {
/// Clear entries from the static table that would return entries during the pre-resolve stage.
/// This means that all lookups will attempt to use the network resolver again before the static
/// table is consulted.
///
///
/// Entries elevated to pre-resolve from fallback (added from default or using
/// [`set_fallback`]`) will have their cache timeout cleared. Entries added directly to
/// pre-resolve (using [`Self::set_static_preresolve`]) will be removed.
@@ -438,20 +429,7 @@ impl HickoryDnsResolver {
/// Get the list of currently available nameserver configs.
pub fn all_configured_name_servers(&self) -> Vec<NameServerConfig> {
default_nameserver_group().to_vec()
}
/// Get the list of currently used nameserver configs.
pub fn active_name_servers(&self) -> Vec<NameServerConfig> {
if !self.use_shared {
return self
.state
.get()
.map(|r| r.config().name_servers().to_vec())
.unwrap_or(self.all_configured_name_servers());
}
SHARED_RESOLVER.active_name_servers()
default_nameserver_group()
}
/// Do a trial resolution using each nameserver individually to test which are working and which
@@ -477,65 +455,60 @@ impl HickoryDnsResolver {
///
/// Caches successfully resolved addresses for 30 minutes to prevent continual use of remote lookup.
/// This resolver is intended to be used for OUR API endpoints that do not rapidly rotate IPs.
fn new_resolver() -> TokioResolver {
fn new_resolver() -> Result<TokioResolver, ResolveError> {
let name_servers = default_nameserver_group_ipv4_only();
configure_and_build_resolver(name_servers)
}
fn configure_and_build_resolver<G>(name_servers: G) -> TokioResolver
where
G: Into<NameServerConfigGroup>,
{
fn configure_and_build_resolver(
name_servers: Vec<NameServerConfig>,
) -> Result<TokioResolver, ResolveError> {
let options = HickoryDnsResolver::default_options();
let name_servers: NameServerConfigGroup = name_servers.into();
info!("building new configured resolver");
debug!("configuring resolver with {options:?}, {name_servers:?}");
let config = ResolverConfig::from_parts(None, Vec::new(), name_servers);
let mut resolver_builder =
TokioResolver::builder_with_config(config, TokioConnectionProvider::default());
TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
resolver_builder = resolver_builder.with_options(options);
resolver_builder.build()
Ok(resolver_builder.build()?)
}
fn filter_ipv4(nameservers: impl AsRef<[NameServerConfig]>) -> Vec<NameServerConfig> {
fn filter_ipv4(nameservers: impl IntoIterator<Item = NameServerConfig>) -> Vec<NameServerConfig> {
nameservers
.as_ref()
.iter()
.filter(|ns| ns.socket_addr.is_ipv4())
.cloned()
.into_iter()
.filter(|ns| ns.ip.is_ipv4())
.collect()
}
#[allow(unused)]
fn filter_ipv6(nameservers: impl AsRef<[NameServerConfig]>) -> Vec<NameServerConfig> {
fn filter_ipv6(nameservers: impl IntoIterator<Item = NameServerConfig>) -> Vec<NameServerConfig> {
nameservers
.as_ref()
.iter()
.filter(|ns| ns.socket_addr.is_ipv6())
.cloned()
.into_iter()
.filter(|ns| ns.ip.is_ipv6())
.collect()
}
#[allow(unused)]
fn default_nameserver_group() -> NameServerConfigGroup {
let mut name_servers = NameServerConfigGroup::quad9_tls();
name_servers.merge(NameServerConfigGroup::quad9_https());
name_servers.merge(NameServerConfigGroup::cloudflare_tls());
name_servers.merge(NameServerConfigGroup::cloudflare_https());
name_servers
fn default_nameserver_group() -> Vec<NameServerConfig> {
QUAD9
.tls()
.chain(QUAD9.https())
.chain(CLOUDFLARE.tls())
.chain(CLOUDFLARE.https())
.collect()
}
fn default_nameserver_group_ipv4_only() -> NameServerConfigGroup {
filter_ipv4(&default_nameserver_group() as &[NameServerConfig]).into()
fn default_nameserver_group_ipv4_only() -> Vec<NameServerConfig> {
filter_ipv4(default_nameserver_group())
}
#[allow(unused)]
fn default_nameserver_group_ipv6_only() -> NameServerConfigGroup {
filter_ipv6(&default_nameserver_group() as &[NameServerConfig]).into()
fn default_nameserver_group_ipv6_only() -> Vec<NameServerConfig> {
filter_ipv6(default_nameserver_group())
}
/// Create a new resolver with the default configuration, which reads from the system DNS config
@@ -550,7 +523,7 @@ fn new_resolver_system() -> Result<TokioResolver, ResolveError> {
resolver_builder = resolver_builder.with_options(options);
Ok(resolver_builder.build())
Ok(resolver_builder.build()?)
}
fn new_default_static_fallback() -> StaticResolver {
@@ -577,7 +550,7 @@ async fn trial_nameservers_inner(
async fn trial_lookup(name_server: NameServerConfig, query: &str) -> Result<(), ResolveError> {
debug!("running ns trial {name_server:?} query={query}");
let resolver = configure_and_build_resolver(vec![name_server]);
let resolver = configure_and_build_resolver(vec![name_server])?;
match tokio::time::timeout(DEFAULT_OVERALL_LOOKUP_TIMEOUT, resolver.ipv4_lookup(query)).await {
Ok(Ok(_)) => Ok(()),
@@ -590,8 +563,10 @@ async fn trial_lookup(name_server: NameServerConfig, query: &str) -> Result<(),
mod test {
use super::*;
use itertools::Itertools;
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::{
collections::HashMap,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
};
/// IP addresses guaranteed to fail attempts to resolve
///
@@ -670,26 +645,16 @@ mod test {
let mut ns_ips = GUARANTEED_BROKEN_IPS_1.to_vec();
ns_ips.push(good_cf_ip);
let broken_ns_https = NameServerConfigGroup::from_ips_https(
&ns_ips,
443,
"cloudflare-dns.com".to_string(),
true,
);
let domain = Arc::<str>::from("cloudflare-dns.com");
let path = Arc::<str>::from("/dns-query");
let broken_ns_https = GUARANTEED_BROKEN_IPS_1
.iter()
.chain([&good_cf_ip])
.map(|ip| NameServerConfig::https(*ip, domain.clone(), Some(path.clone())))
.collect::<Vec<_>>();
let inner = configure_and_build_resolver(broken_ns_https);
// create a new resolver that won't mess with the shared resolver used by other tests
let resolver = HickoryDnsResolver {
use_shared: false,
state: Arc::new(OnceCell::with_value(inner)),
static_base: Some(Default::default()),
..Default::default()
};
let name_servers = resolver.state.get().unwrap().config().name_servers();
for (ns, result) in trial_nameservers_inner(name_servers).await {
if ns.socket_addr.ip() == good_cf_ip {
for (ns, result) in trial_nameservers_inner(&broken_ns_https).await {
if ns.ip == good_cf_ip {
assert!(result.is_ok())
} else {
assert!(result.is_err())
@@ -705,21 +670,20 @@ mod test {
fn build_broken_resolver() -> Result<TokioResolver, ResolveError> {
info!("building new faulty resolver");
let mut broken_ns_group = NameServerConfigGroup::from_ips_tls(
GUARANTEED_BROKEN_IPS_1,
853,
"cloudflare-dns.com".to_string(),
true,
);
let broken_ns_https = NameServerConfigGroup::from_ips_https(
GUARANTEED_BROKEN_IPS_1,
443,
"cloudflare-dns.com".to_string(),
true,
);
broken_ns_group.merge(broken_ns_https);
let domain = Arc::<str>::from("cloudflare-dns.com");
let path = Arc::<str>::from("/dns-query");
let broken_ns_group = GUARANTEED_BROKEN_IPS_1
.iter()
.map(|ip| NameServerConfig::tls(*ip, domain.clone()))
.chain(
GUARANTEED_BROKEN_IPS_1
.iter()
.map(|ip| NameServerConfig::https(*ip, domain.clone(), Some(path.clone())))
.collect::<Vec<_>>(),
)
.collect::<Vec<_>>();
Ok(configure_and_build_resolver(broken_ns_group))
configure_and_build_resolver(broken_ns_group)
}
#[tokio::test]
@@ -740,7 +704,7 @@ mod test {
build_broken_resolver()?;
let domain = "ifconfig.me";
let result = resolver.resolve_str(domain).await;
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
assert!(result.is_err_and(|e| e.is_timeout()));
let duration = time_start.elapsed();
assert!(duration < resolver.overall_dns_timeout + Duration::from_secs(1));
@@ -774,25 +738,11 @@ mod test {
// unsuccessful lookup - primary times out, and not in static table
let domain = "non-existent.nymtech.net";
let result = resolver.resolve_str(domain).await;
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
assert!(result.is_err_and(|e| e.is_timeout()));
Ok(())
}
#[test]
fn default_resolver_uses_ipv4_only_nameservers() {
let resolver = HickoryDnsResolver::thread_resolver();
resolver
.active_name_servers()
.iter()
.all(|cfg| cfg.socket_addr.is_ipv4());
SHARED_RESOLVER
.active_name_servers()
.iter()
.all(|cfg| cfg.socket_addr.is_ipv4());
}
#[tokio::test]
#[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
// This test impacts the state of the shared resolver and as such is disabled to avoid
+59 -21
View File
@@ -141,9 +141,7 @@
use http::header::USER_AGENT;
pub use inventory;
pub use reqwest;
pub use reqwest::ClientBuilder as ReqwestClientBuilder;
pub use reqwest::StatusCode;
pub use reqwest::{self, ClientBuilder as ReqwestClientBuilder, StatusCode};
use std::error::Error;
pub mod registry;
@@ -152,19 +150,21 @@ use crate::path::RequestPath;
use async_trait::async_trait;
use bytes::Bytes;
use cfg_if::cfg_if;
use http::HeaderMap;
use http::header::{ACCEPT, CONTENT_TYPE};
use http::{
HeaderMap,
header::{ACCEPT, CONTENT_TYPE},
};
use itertools::Itertools;
use mime::Mime;
use reqwest::header::HeaderValue;
use reqwest::{RequestBuilder, Response};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use reqwest::{RequestBuilder, Response, header::HeaderValue};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
#[cfg(not(target_arch = "wasm32"))]
use std::io::ErrorKind;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use std::{
fmt::Display,
sync::atomic::{AtomicUsize, Ordering},
time::Duration,
};
use thiserror::Error;
use tracing::{debug, instrument, warn};
@@ -1173,7 +1173,16 @@ impl ApiClientCore for Client {
};
match response {
Ok(resp) => return Ok(resp),
Ok(resp) => {
// Check if the response includes a rate limit error from the vercel API
if is_http_rate_limit_err(&resp) {
warn!("encountered vercel rate limit error for {}", url.as_str());
// if we have multiple urls, update to the next
self.maybe_rotate_hosts(Some(url.clone()));
}
return Ok(resp);
}
Err(err) => {
#[cfg(target_arch = "wasm32")]
let is_network_err = err.is_timeout();
@@ -1226,17 +1235,39 @@ impl ApiClientCore for Client {
}
}
const VERCEL_CHALLENGE_HEADER: &str = "x-vercel-mitigated";
const VERCEL_CHALLENGE_VALUE: &[u8] = b"challenge";
/// Check for Rate Limit challenge response from the vercel API
pub(crate) fn is_http_rate_limit_err(resp: &Response) -> bool {
let status = resp.status() == StatusCode::FORBIDDEN;
let header = resp
.headers()
.get(VERCEL_CHALLENGE_HEADER)
.is_some_and(|v| v.as_bytes() == VERCEL_CHALLENGE_VALUE);
let content_type = resp
.headers()
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<Mime>().ok())
.is_some_and(|mime_type| {
mime_type.type_() == mime::TEXT && mime_type.subtype() == mime::HTML
});
status && header && content_type
}
#[cfg(not(target_arch = "wasm32"))]
const MAX_ERR_SOURCE_ITERATIONS: usize = 4;
/// This functions attempts to check the error returned by reqwest to see if
/// rotating host informtion (for clients with mutliple hosts defined) could be
/// helpful. This looks for situations where the error could plausibly be caused
/// by a network adversary, or where rotating to an equival hostname might help.
/// This functions attempts to check the error returned by reqwest to see if rotating host
/// information (for clients with multiple hosts defined) could be helpful. This looks for
/// situations where the error could plausibly be caused by a network adversary, or where rotating
/// to an equivalent hostname might help.
///
/// For example --> NetworkUnreachable will not be helped by rotating domains,
/// but ConnectionReset might be caused by a network adversary blocking by SNI
/// which could possibly benefit from rotating domains.
/// For example --> NetworkUnreachable will not be helped by rotating domains, but ConnectionReset
/// might be caused by a network adversary blocking by SNI which could possibly benefit from
/// rotating domains.
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
if err.is_timeout() {
@@ -1274,7 +1305,7 @@ pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
} else if let Some(_tls_err) = e.downcast_ref::<rustls::Error>() {
// try downcast to TLS error
return true;
} else if let Some(resolve_err) = e.downcast_ref::<hickory_resolver::ResolveError>() {
} else if let Some(resolve_err) = e.downcast_ref::<hickory_resolver::net::NetError>() {
// try downcast to DNS error
return resolve_err.is_nx_domain();
} else {
@@ -1697,6 +1728,13 @@ where
decode_raw_response(&headers, full)
} else if res.status() == StatusCode::NOT_FOUND {
Err(HttpClientError::NotFound { url: Box::new(url) })
} else if is_http_rate_limit_err(&res) {
Err(HttpClientError::EndpointFailure {
url: Box::new(url),
status,
headers: Box::new(headers),
error: String::from("received vercel rate limit challenge response"),
})
} else {
let Ok(plaintext) = res.text().await else {
return Err(HttpClientError::RequestFailure {
@@ -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")]
+4
View File
@@ -24,6 +24,10 @@ pub const SPHINX_STREAM_VERSION_THRESHOLD: u8 = v9::VERSION;
mod tests {
use super::*;
const _: () = {
assert!(SPHINX_STREAM_VERSION_THRESHOLD > MAX_NON_STREAM_VERSION);
};
#[test]
fn stream_transport_threshold_is_consistent() {
assert_eq!(MAX_NON_STREAM_VERSION, 8);
+3 -1
View File
@@ -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
+5 -5
View File
@@ -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/",
+15
View File
@@ -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);
+1
View File
@@ -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";
@@ -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())
+3 -1
View File
@@ -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
+2 -3
View File
@@ -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;
}
+78 -261
View File
@@ -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
View File
@@ -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;
+4 -1
View File
@@ -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,
+93
View File
@@ -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",
+6
View File
@@ -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,
+26 -2
View File
@@ -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> {
+17 -5
View File
@@ -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(
+32 -14
View File
@@ -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()
}
}
+405 -17
View File
@@ -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());
}
}
}
+4
View File
@@ -0,0 +1,4 @@
[alias]
wasm = "build --release --lib --target wasm32-unknown-unknown"
unit-test = "test --lib"
schema = "run --bin schema --features=schema-gen"
+62
View File
@@ -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
+5
View File
@@ -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
}
]
}

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