Compare commits

...

128 Commits

Author SHA1 Message Date
Tommy Verrall 1266f39446 thought process
- remove the storybook code as it's another thing to maintain
- remove the ci job for it
- will implement some other tests around it
2025-06-24 16:44:41 +02:00
Simon Wicky eb59615c56 StatsAPI qol : disable swagger try it out and remove debug level from nym_http_api_client (#5868) 2025-06-23 14:58:29 +02:00
Bogdan-Ștefan Neacşu 07c908c497 Return true remaining (#5866) 2025-06-23 11:53:39 +03:00
Jędrzej Stuczyński 6de0c4ce92 feat: initial performance contract (#5833)
* initialised basic structure for the performance contract

* shared code for contract testing

* unified common testing methods between performance and nym pool contracts

* impl of ExecuteMsg for the contract

* impl of QueryMsg for the contract

* setting initial authorised NMs during instantiation

* additional tests and fixes

* ibid

* scaffolding for client traits

* completed client traits

* clippy

* naive add performance contract to testnet manager

* placeholder values for the performance contract address

* introduced admin messages to purge old measurements from the storage

* introduced check ensuring performance data is only added to bonded nodes
2025-06-20 09:06:56 +01:00
benedettadavico 05d8b31e51 Merge branch 'remove/old-explorer' into develop 2025-06-18 15:34:40 +02:00
Georgio Nicolas 692fbf1392 Merge pull request #5828 from nymtech/georgio/dkg-crypsen-fixes
Security patches for the `dkg` crate
2025-06-18 10:48:37 +02:00
Andrej Mihajlov 0de4aea77b Merge pull request #5796 from nymtech/am/close-sqlite-pool
Close sqlite pool before moving or reopening databases
2025-06-17 19:01:25 +02:00
Georgio Nicolas a7cd8efc04 dkg: fix clippy suggestions 2025-06-17 16:37:50 +02:00
Georgio Nicolas 56aad75220 dkg: verify integrity of ciphertexts during decryption 2025-06-17 16:30:11 +02:00
Georgio Nicolas e2f2ab89ec dkg: add CryptoRng trait requirement 2025-06-17 16:30:11 +02:00
Georgio Nicolas 4d09b6f2e5 bte/proof_chunking.rs: Check for potential arithmentic overflows 2025-06-17 16:30:11 +02:00
dynco-nym b9339b8f0c Add /status endpoints (#5857)
* Add /status endpoints

* Bump package version

* pub use instead of import
2025-06-16 13:19:35 +02:00
Andrej Mihajlov 43a7360399 Merge pull request #5856 from nymtech/am/remove-surb-screaming-logs
Clear out screaming logs
2025-06-16 11:39:27 +02:00
Andrej Mihajlov 5f9f7f0fac Clear out screaming logs 2025-06-13 11:00:48 +02:00
Andrej Mihajlov df0e2fe489 Merge pull request #5853 from nymtech/am/path-display
Use display when printing paths
2025-06-13 10:54:12 +02:00
benedetta davico bc33cc4c8d Merge pull request #5855 from nymtech/fix-qa-removal 2025-06-13 09:40:56 +02:00
Simon Wicky a31597aca9 fix removal of qa env 2025-06-13 09:30:00 +02:00
Jack Wampler 378229b04e HTTP Discovery objects & network defaults (#5814)
add extended (optional) fields to the NetworkDiscovery and configure fallback hosts
2025-06-12 11:15:36 -06:00
Andrej Mihajlov fec196c097 Use display when printing paths 2025-06-12 17:17:00 +02:00
Andrej Mihajlov 1d7ffc1bb6 test: remove file after closing for a test 2025-06-12 15:39:26 +02:00
Andrej Mihajlov 0caa627960 Fix missing await on self.close_pool_inner() 2025-06-12 15:12:46 +02:00
import this d6b3d7fc0a [DOCs/operators]: Release notes for v2025.11 cheddar (#5852)
* bump up version

* add dev features

* add operator updates

* add updated stats

* update prebuild
2025-06-12 11:19:00 +00:00
dynco-nym ac273480f8 Fix CI version check (#5851)
* Fix version

* Test .rc version

* Undo cargo.toml version

* Remove comment

* Apply to statistics service
2025-06-12 11:17:56 +02:00
benedettadavico 79603d61d7 fix for QA 2025-06-12 10:02:40 +02:00
dynco-nym e8e9a70ef4 Feature/node status dvpn directory (#5829)
* wip - dvpn directory cache

* Endpoint & cache

* /gateways works
- SkimmedNode data still missing
- need to move probe models to monorepo

* Rest of the data for /gateways

* Revert before merge: pin deps to cheddar release

* Filter gw by country

* Return percent string instead of u8

* Filter by semver

* Bump package version

* Fix probe types

* Reorg

* Add exit, entry endpoints

* Different entry/exit selection criteria

* Date fix migration

* Unpin from cheddar

* Revert "Unpin from cheddar"

This reverts commit f17239075b.

* Validation with celes

* PR feedback

* Fix path

* Bump version

---------

Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
2025-06-12 09:56:31 +02:00
Tommy Verrall 0c52ee89c8 Merge pull request #5821 from nymtech/dependabot/npm_and_yarn/sdk/typescript/tests/integration-tests/mix-fetch/tar-fs-3.0.9
build(deps): bump tar-fs from 3.0.8 to 3.0.9 in /sdk/typescript/tests/integration-tests/mix-fetch
2025-06-12 08:43:47 +01:00
Tommy Verrall f324d45721 Merge pull request #5449 from indmind/patch-1
chore: fixed typo in API endpoint parameter
2025-06-12 08:42:50 +01:00
Tommy Verrall 470e88f46c Merge pull request #5843 from nymtech/dependabot/npm_and_yarn/wasm/mix-fetch/internal-dev/webpack-dev-server-5.2.1
build(deps-dev): bump webpack-dev-server from 4.13.2 to 5.2.1 in /wasm/mix-fetch/internal-dev
2025-06-12 08:41:20 +01:00
Tommy Verrall 42a5016822 Merge pull request #5845 from nymtech/remove/old-mock-nym-api-client
remove not used old mock-api
2025-06-12 08:40:35 +01:00
Tommy Verrall 579cff358d Merge pull request #5849 from nymtech/feature/remove-browser-extension
Updated browser extension piece removal
2025-06-12 08:38:38 +01:00
benedetta davico f95dda0f2f Merge pull request #5844 from nymtech/feature/remove-bity
remove bity dir
2025-06-12 09:37:19 +02:00
benedetta davico fc666fb984 Merge pull request #5848 from nymtech/remove/old-env-references
Remove/old env references
2025-06-12 09:37:08 +02:00
benedetta davico 1264fd9bfb Update ci-build.yml 2025-06-11 17:48:24 +02:00
Tommy Verrall 3e8451f292 updated browser extension piece
- keep all the internal-dev wasm pieces as future examples
- everything previously was going to be removed
- shows functioning wasm interaction with the js
2025-06-11 17:15:20 +02:00
benedetta davico 53f4582202 Merge pull request #5835 from nymtech/benny/node-version-test
Update publish-nym-binaries.yml
2025-06-11 16:39:18 +02:00
benedettadavico c7c6dcab65 remove old env references 2025-06-11 16:13:59 +02:00
benedettadavico 3422c49e85 remove qa env 2025-06-11 16:07:32 +02:00
benedettadavico deee0b8e14 remove bity integration from cargo toml 2025-06-11 16:05:03 +02:00
benedettadavico 3ac58e0c49 Clean up
remove old explorer references
2025-06-11 16:02:19 +02:00
Tommy Verrall 7243cb57b5 remove not used old mock-api 2025-06-11 15:58:01 +02:00
Tommy Verrall 0276bd7b0b Merge pull request #5840 from nymtech/remove-testnet-faucet
Removing test-net faucet
2025-06-11 14:47:08 +01:00
Tommy Verrall 457759bb57 Merge pull request #5841 from nymtech/feature/add-buy-locations
Amended the buy section
2025-06-11 14:20:59 +01:00
dependabot[bot] de0f8ee2d3 build(deps): bump next from 14.2.15 to 14.2.26 in /documentation/docs (#5772)
Bumps [next](https://github.com/vercel/next.js) from 14.2.15 to 14.2.26.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.15...v14.2.26)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 14.2.26
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-11 13:36:30 +01:00
dependabot[bot] ebf97ece9b build(deps-dev): bump webpack-dev-server in /wasm/mix-fetch/internal-dev
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.13.2 to 5.2.1.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.13.2...v5.2.1)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-version: 5.2.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-11 12:23:47 +00:00
dependabot[bot] 50a55f4bfb build(deps-dev): bump webpack-dev-server (#5826)
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.15.2 to 5.2.1.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.15.2...v5.2.1)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-version: 5.2.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-11 13:22:07 +01:00
Tommy Verrall 4ee5c6457b remove images dir 2025-06-11 13:18:01 +02:00
Tommy Verrall d7b5fce7aa amended the buy section
- change the wallet to include the buy options for nym
- remove legacy code
2025-06-11 13:15:37 +02:00
benedetta davico c3d9c1131b Merge pull request #5838 from nymtech/release/2025.11-cheddar
merge release/2025.11-cheddar to develop
2025-06-11 13:09:57 +02:00
Tommy Verrall 5bdfb1ba5c removing test-net faucet 2025-06-11 12:00:44 +02:00
benedettadavico 94e51f0047 remove bity dir 2025-06-11 10:28:44 +02:00
benedetta davico f313e95e2f Merge pull request #5837 from nymtech/yana/replace-mintscan
Replace mintscan with ping.pub
2025-06-11 10:14:36 +02:00
Yana 2b13ac99b4 Replace mintscan with ping.pub 2025-06-10 19:34:19 +03:00
benedetta davico ef220882d4 update the workflow file again with a temp fix
reference: https://github.com/softprops/action-gh-release/issues/628
2025-06-10 11:39:20 +02:00
benedetta davico 59e26178ee Update publish-nym-binaries.yml 2025-06-10 11:20:19 +02:00
benedetta davico 0d420fb0a5 remove explorer-api in workflow 2025-06-10 11:01:24 +02:00
benedettadavico fce195fdba update changelog 2025-06-10 10:28:47 +02:00
Jędrzej Stuczyński 554b1eb022 bugfix: fix swapped total and circulating supplies (#5822) 2025-06-09 08:41:21 +01:00
Andrej Mihajlov e52bd918fb Hide tokio behind feature 2025-06-06 15:00:40 +02:00
Andrej Mihajlov 9d82d6d111 Hide tokio and sqlx behind not(wasm32) 2025-06-06 13:34:56 +02:00
Andrej Mihajlov 3593631e4a Exclude sqlx-pool-guard from wasm builds 2025-06-06 13:24:04 +02:00
import this 5b67403fb9 [DOCs/operators]: Add auto scraping of staking_supply_scale_factor & update api outputs (#5832) 2025-06-06 09:57:48 +00:00
Bogdan-Ștefan Neacşu 3a528d3b89 No autoremoval of peers (#5831)
* No autoremoval

* Remove startup_timestamp
2025-06-06 12:48:34 +03:00
Bogdan-Ștefan Neacşu 466bb97bc7 Use the same client bandwidth for top up (#5813)
* Use the same client bandwidth for top up

* Fix clippy
2025-06-06 10:12:50 +03:00
Simon Wicky 0d78416454 [Stats API] IP from nginx headers if available (#5830)
* proper IP handling

* workflow doesn't like fancy versions
2025-06-06 09:08:58 +02:00
Simon Wicky 8ba58ba11e [Feature] Noise XKpsk3 integration (2025 version) (#5692)
* grand Noise squaheroo

* fix merge conflicts and adapt code for the key rotation changes (#5824)

* remove file that should have been ignored
2025-06-05 11:34:55 +02:00
Simon Wicky be16fddc75 [Stats API] Infallible network view (#5825)
* infallible network view and cheddar model for current compatibility

* bump patch version

* typo
2025-06-04 17:08:44 +02:00
benedettadavico e9bb9792ab bump binaries 2025-06-04 14:42:04 +02:00
dependabot[bot] a7d6cba11d build(deps-dev): bump http-proxy-middleware (#5810)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.4 to 2.0.9.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.9/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.4...v2.0.9)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-version: 2.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-04 13:42:41 +02:00
Andrej Mihajlov f5846d5bc2 Log all tracing output just in case 2025-06-04 11:40:56 +02:00
Bogdan-Ștefan Neacşu 88d4a9b111 Set cached storage counters to 0 (#5812)
* Set cached storage counters to 0

* u64 to i64 log possible error

* Check addition too
2025-06-04 12:11:46 +03:00
Andrej Mihajlov d7779df1b7 Include proc_pidinfo on iOS 2025-06-04 11:00:15 +02:00
dependabot[bot] a67ff33054 build(deps): bump tokio from 1.44.2 to 1.45.1 (#5798)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.44.2 to 1.45.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.44.2...tokio-1.45.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.45.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-04 10:21:00 +02:00
dependabot[bot] 61badfdcfe build(deps): bump undici in /.github/actions/nym-hash-releases/src (#5771)
Bumps [undici](https://github.com/nodejs/undici) from 5.28.5 to 5.29.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v5.28.5...v5.29.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 5.29.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-04 09:40:05 +02:00
dependabot[bot] 19dfbeb2b4 build(deps): bump cargo_metadata from 0.18.1 to 0.19.2 (#5765)
Bumps [cargo_metadata](https://github.com/oli-obk/cargo_metadata) from 0.18.1 to 0.19.2.
- [Release notes](https://github.com/oli-obk/cargo_metadata/releases)
- [Changelog](https://github.com/oli-obk/cargo_metadata/blob/main/CHANGELOG.md)
- [Commits](https://github.com/oli-obk/cargo_metadata/compare/0.18.1...0.19.2)

---
updated-dependencies:
- dependency-name: cargo_metadata
  dependency-version: 0.19.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-04 09:37:54 +02:00
dependabot[bot] 9f13616c24 build(deps): bump tempfile from 3.19.1 to 3.20.0 (#5764)
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.19.1 to 3.20.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.19.1...v3.20.0)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-version: 3.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-04 09:37:39 +02:00
Andrej Mihajlov 7fcc188041 Switch to tracing 2025-06-03 17:19:42 +02:00
Andrej Mihajlov b8c8d33c94 Use log here 2025-06-03 15:13:21 +02:00
Andrej Mihajlov 02909c03dd Expose database path 2025-06-03 14:49:49 +02:00
Jędrzej Stuczyński d8c84cc4d6 feat: key rotation (#5777)
* wip

* wip: wrap node's sphinx key with a manager

* wip: choosing correct key for packet processing

* further propagation of key rotation information

* attaching key rotation information to reply surbs

* added basic key rotation information to mixnet contract

* wip: introducing cached queries for key rotation info from nym api

* unified nym-api contract cache refreshing

* finish packet decoding

* multi api client + retrieving rotation id

* rotating sphinx key files

* logic for migrating config file

* wip: putting new sphinx keys to self described endpoints

* processing loop of KeyRotationController

* fixed sphinx key loading

* rotating bloomfilters

* wired up KeyRotationController

* flushing bloomfilters to disk and loading

* most of nym-node changes

* post rebase fixes

* fixes due to backwards compatible hostkeys

* split http state.rs file

* dont use deprecated fields

* fixed backwards compatible deserialisation of host information

* split up node describe cache

* added a dedicated CacheRefresher listener to perform full refresh outside the set interval

* controlling announced sphinx keys within nym-api

* retrieving rotation id when pulling topology

* split nym-nodes http handlers

* v2 nym-api endpoints to retrieve nodes with additional metadata information

* bug fixes...

* additional bugfixes and guards against stuck epoch

* testnet manager: set first nym-api as the rewarder

* fixed host information deserialisation

* fixed panic during first key rotation

* post rebase fixes

* clippy

* more guards against stuck epochs

* added helper method to reset node's sphinx key

* instantiate mixnet contract with custom key rotation validity

* additional bugfixes and debugging nym-api deadlock

* passing shutdown to nym apis client

* remove dead test

* post rebasing fixes

* missing MixnetQueryClient variants

* remove usage of deprecated methods in sdk example

* fix: incorrect method signature

* post rebasing fixes

* attempt to retrieve key rotation id before doing any config migration work

* ignore tests relying on networking behaviour

* allow networking failures in certain tests
2025-06-03 11:22:51 +01:00
Simon Wicky adbe0392ca Nym-statistics-api : Postgres schema and SSL handling + Dockerfile and GitHub action (#5817)
* add option for ssl mode

* add dockerfile and dev util

* add github workflow for nym-statistics api

* apply review comments

* ci check for version + removed checks from push
2025-06-03 12:06:00 +02:00
windy-ux 3c6567ae64 [DOCs]: redirectsl (#5816)
+ /docs/developers/tutorials/rust-sdk.html
2025-06-03 09:28:55 +00:00
Andrej Mihajlov 11262836d2 Clean up 2025-06-03 09:43:36 +02:00
Andrej Mihajlov f26fd5384d Improve windows 2025-06-03 09:43:36 +02:00
Andrej Mihajlov 085103b333 Cleanup 2025-06-03 09:43:36 +02:00
Andrej Mihajlov 574f7f1abd Revert 2025-06-03 09:43:36 +02:00
Andrej Mihajlov 31e161604a Use sqlite pool guard 2025-06-03 09:43:36 +02:00
Andrej Mihajlov e4e349bea8 Remove logs 2025-06-03 09:43:36 +02:00
Andrej Mihajlov 6391b7ed3a Document 2025-06-03 09:43:36 +02:00
Andrej Mihajlov c225511f95 Add Windows impl 2025-06-03 09:43:36 +02:00
Andrej Mihajlov 4eedbb235a Add Windows implementation 2025-06-03 09:43:36 +02:00
Andrej Mihajlov 548b8717b2 Update Linux impl 2025-06-03 09:43:36 +02:00
Andrej Mihajlov a215b3d0bf Open file watch 2025-06-03 09:43:36 +02:00
Andrej Mihajlov 03d5a133eb Close sqlite pool before erroring 2025-06-03 09:43:36 +02:00
dependabot[bot] b323c62a6e build(deps): bump tar-fs
Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 3.0.8 to 3.0.9.
- [Commits](https://github.com/mafintosh/tar-fs/compare/v3.0.8...v3.0.9)

---
updated-dependencies:
- dependency-name: tar-fs
  dependency-version: 3.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-03 06:50:43 +00:00
Jack Wampler 8384a411df Bug Fix for Wallet build (#5820)
revert url used for connection-tester
2025-06-02 14:19:43 -06:00
Jack Wampler c56ebd9ceb Url scheme warning log (#5819)
fix conditions for logging about url scheme
2025-06-02 09:11:16 -06:00
Jędrzej Stuczyński b081b20a83 chore: adjust heuristic for wireguard peer activity (#5818)
* chore: adjust heuristic for wireguard peer activity

* fixed incorrect delta_tx calculation + typo
2025-06-02 15:37:37 +01:00
Andrej Mihajlov 866d547745 Merge pull request #5795 from nymtech/am/update-sqlx-0.8.6
Update to sqlx 0.8.6
2025-06-02 13:23:09 +02:00
Andrej Mihajlov 64e3f066a7 Use type override to enforce i64 type instead of Option<i64> 2025-05-30 10:17:19 +02:00
Andrej Mihajlov 62520c9308 Update sqlx cache 2025-05-30 09:28:48 +02:00
Andrej Mihajlov e65d455c91 Switch counters to i64 since sqlx started giving it back 2025-05-30 09:28:48 +02:00
Andrej Mihajlov 9b9c82a02a Run unchecked as sqlx does not understand COALESCE on NULL value 2025-05-30 09:28:48 +02:00
Andrej Mihajlov 1a38a2503e Stick to OffsetDateTime 2025-05-30 09:28:48 +02:00
Andrej Mihajlov 318f293983 All count() calls return i64 from now on 2025-05-30 09:28:48 +02:00
Andrej Mihajlov 5f2aba19c2 Update to sqlx 0.8.6 2025-05-30 09:28:48 +02:00
Jack Wampler 814ee45b4d HTTP Client Retries, Fallbacks, and Redirects (#5789)
updates to nym HTTP api client with multiple features relating to request domains
2025-05-29 10:37:07 -06:00
dynco-nym 56ed915626 Replace chrono with time in NS API (#5811)
* Replace chrono with time in NS API

* Replace chrono in client

* Bump package version
2025-05-29 16:33:00 +02:00
Jędrzej Stuczyński 2de8f8bc21 feature: nympool contract (#5464)
* squashed nym-pool commits

initialised nym-pool contract and updated all bls12_381 to make it possible

create scaffolding for tests

ability to control the contract admin

introducing contract grants

grant type validation

basic grant operations + stubs for other messages

added queries

use transaction stubs

added expiration information to grant queries

setting initial grant state based on the current environment

allowance logic for attempting to spend part of a grant

implemented all remaining transactions

made public api for coin locking perform validation

tests for locked tokens storage

nympool storage tests

added messages for changing granter set

tests and fixes for sufficient tokens when inserting grants

tests for initial state + more bugfixes

queries tests

additional tests for transactions and fixes

post rebase fixes

updated contract dependencies

removed redundant wasm constructor

dont ask me why this suddenly became an issue - no clue

removed redundant wasm constructor

dont ask me why this suddenly became an issue - no clue

* missing schema + added nym_pool to the main Makefile
2025-05-29 10:31:01 +01:00
import this f04cb6f6a6 [DOCs/operators]: Release notes v2025.10-brie (#5808)
* finish release notes and operator updates

* add NSL update - ready for merge

* address review comment
2025-05-28 11:59:35 +00:00
dynco-nym 4c67f01efb Make address cache configurable (#5784)
* Make address cache configurable

* TestFixture
2025-05-28 10:41:12 +02:00
Simon Wicky b69c2e1e94 Nym Statistics API (#5800)
* move stats types from vpn-client to here

* base stats api

* change storage schema

* add link to nymAPI for whitelisting

* remove outdated comment

* more comments update

* example of chrono vs time

* Add build.rs
- exports DATABASE_URL so cargo check works
- exports SQLX_OFFLINE for CI
- added pg_up.sh which spawns PG container
  - required for cargo sqlx prepare

* fixes time vs chrono issue and cleaner build with docker

* add correct swagger types, with feature locking where relevant

* apply dynco suggestions

---------

Co-authored-by: dynco-nym <173912580+dynco-nym@users.noreply.github.com>
2025-05-28 10:23:11 +02:00
benedetta davico d27e3b49db Merge pull request #5806 from nymtech/release/2025.10-brie 2025-05-28 09:38:36 +02:00
Jędrzej Stuczyński 0b92a59f1a hack: temporarily use next.config.js instead of next.config.ts (#5805) 2025-05-27 11:41:51 +01:00
Jędrzej Stuczyński 474eff67fa chore: adjusted wallet storybook mocks to fix the build (#5804) 2025-05-27 11:38:13 +01:00
Jędrzej Stuczyński 4a1ce8154a chore: resolve 1.87 clippy warnings (#5802)
* Clippy in wallet & sdk

* Clippy in wallet

* Pin rust to 1.86 in builder

* apply changes from b7da75a18c

* missing nym-node features

* Box all the things

* additional boxes in the wallet

* post rebasing clippy

---------

Co-authored-by: dynco-nym <173912580+dynco-nym@users.noreply.github.com>
2025-05-27 11:08:36 +01:00
Bogdan-Ștefan Neacşu aca98ab04f Track wireguard credential retries (#5783)
* Add a cache for the credentials seen before on top-up

* Verify seen credentials on top ups

* Add warning log for timestamp subtraction

* Add unit test
2025-05-27 12:35:44 +03:00
Jędrzej Stuczyński f925c6caf0 QoL: RequestPath trait for http-api-client (#5788)
* qol: RequestPath trait for http-api-client

* additional test case

* applied the change to other trait methods
2025-05-27 10:30:13 +01:00
Jon Häggblad 9a62581272 Update codeowners 2025-05-23 08:54:25 +02:00
Drazen Urch ebb8e4ef19 Build and push nym-api action (#5793) 2025-05-22 19:12:29 +02:00
mfahampshire a0057eb223 add notice re sdks (#5792)
* add notice re sdks

* fix borked notice

* fix another borked notice
2025-05-22 10:25:25 +00:00
import this 39195d79f5 [DOCs/operators]: Hotfix - Round decimalds to common convention (#5791) 2025-05-21 16:02:09 +00:00
import this ede5ffaffc [DOCs/operaotrs]: Automate Rewards calculator default state value (#5790) 2025-05-21 09:47:04 +00:00
Bogdan-Ștefan Neacşu ed16505137 Fix contains ticketbook function that always returned true (#5787) 2025-05-20 17:18:06 +03:00
Simon Wicky 03bec90b83 swap a decode into a fromrow to please future postgres feature (#5785)
* swap a decode into a fromrow to please future postgres feature

* add missing feature and missing crate in log filter
2025-05-20 15:48:09 +02:00
import this add57b2c14 [DOCs/operators]: Rewards calculator quick tweak (#5786) 2025-05-20 13:26:55 +00:00
import this 927ca8970c [DOCs/operators]: Tokenomics cleanup (#5782)
* correcting APY to ROI

* cleanup and small edits

* add tooltip on ROI
2025-05-19 11:12:36 +00:00
Jack Wampler 47d222b13d more relaxed usage of reqwest accept-encoding (#5779) 2025-05-16 13:03:24 -06:00
indmind d511aac301 chore: fixed typo in API endpoint parameter 2025-02-11 05:39:00 -06:00
1167 changed files with 40463 additions and 116711 deletions
+1 -11
View File
@@ -14,7 +14,6 @@
# contracts
/contracts/mixnet @durch @jstuczyn
/contracts/vesting @durch @jstuczyn
/contracts/service-provider-directory @octol
# crypto code
/common/crypto/ @jstuczyn
@@ -22,14 +21,5 @@
/common/dkg/ @jstuczyn
/common/nymsphinx/ @jstuczyn
# rust sdk
/sdk/rust/ @octol
# nym-connect (rust)
/nym-connect/desktop/src-tauri/ @octol
# nym-wallet (rust)
/nym-wallet/src-tauri/ @octol
# documentation
/documentation @mfahampshire
/documentation @mfahampshire
+3 -3
View File
@@ -415,9 +415,9 @@
}
},
"node_modules/undici": {
"version": "5.28.5",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz",
"integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==",
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
"integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
"license": "MIT",
"dependencies": {
"@fastify/busboy": "^2.0.0"
+2 -2
View File
@@ -5,7 +5,6 @@ on:
paths:
- 'clients/**'
- 'common/**'
- 'explorer-api/**'
- 'gateway/**'
- 'integrations/**'
- 'nym-api/**'
@@ -13,6 +12,7 @@ on:
- 'nym-network-monitor/**'
- 'nym-node/**'
- 'nym-node-status-api/**'
- 'nym-statistics-api/**'
- 'nym-outfox/**'
- 'nym-validator-rewarder/**'
- 'nyx-chain-watcher/**'
@@ -38,7 +38,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ arc-ubuntu-22.04, custom-windows-11, custom-runner-mac-m1 ]
os: [ arc-ubuntu-22.04, custom-windows-11, custom-macos-15 ]
runs-on: ${{ matrix.os }}
env:
CARGO_TERM_COLOR: always
@@ -44,8 +44,10 @@ jobs:
echo "Tag is empty"
exit 1
fi
# first, list all tags for logging purposes
curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' )
# check if there's a matching tag
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' )
if [[ $exists = "true" ]]; then
echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo"
exit 1
@@ -53,5 +55,5 @@ jobs:
echo "Version '$TAG' doesn't exist on the remote"
else
echo "Unknown output '$exists'"
exit 1
exit 2
fi
@@ -0,0 +1,59 @@
name: ci-check-nym-stats-api-version
on:
pull_request:
paths:
- "nym-statistics-api/**"
env:
WORKING_DIRECTORY: "nym-statistics-api"
jobs:
check-if-tag-exists:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
- name: Check if git tag exists
run: |
TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
if [[ -z "$TAG" ]]; then
echo "Tag is empty"
exit 1
fi
git ls-remote --tags origin | awk '{print $2}'
if git ls-remote --tags origin | awk '{print $2}' | grep -q "refs/tags/$TAG$" ; then
echo "Tag '$TAG' ALREADY EXISTS on the remote"
exit 1
else
echo "Tag '$TAG' does not exist on the remote"
fi
- name: Check if harbor tag exists
run: |
TAG=${{ steps.get_version.outputs.result }}
registry=https://harbor.nymte.ch
repo_name=nym/nym-statistics-api
if [[ -z $TAG ]]; then
echo "Tag is empty"
exit 1
fi
# first, list all tags for logging purposes
curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq
# check if there's a matching tag
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' )
if [[ $exists = "true" ]]; then
echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo"
exit 1
elif [[ $exists = "false" ]]; then
echo "Version '$TAG' doesn't exist on the remote"
else
echo "Unknown output '$exists'"
exit 2
fi
@@ -56,6 +56,8 @@ jobs:
cp contracts/target/wasm32-unknown-unknown/release/cw3_flex_multisig.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_ecash.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_pool_contract.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_performance_contract.wasm $OUTPUT_DIR
- name: Deploy branch to CI www
continue-on-error: true
+1
View File
@@ -20,6 +20,7 @@ jobs:
runs-on: ubuntu-22.04
env:
CARGO_TERM_COLOR: always
RUSTUP_PERMIT_COPY_RENAME: 1
steps:
- uses: actions/checkout@v4
@@ -1,75 +0,0 @@
name: ci-nym-wallet-storybook
on:
pull_request:
paths:
- 'nym-wallet/**'
- '.github/workflows/ci-nym-wallet-storybook.yml'
jobs:
build:
runs-on: custom-linux
steps:
- uses: actions/checkout@v4
- 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 stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- 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/"
- name: Matrix - Node Install
run: npm install
working-directory: .github/workflows/support-files
- name: Matrix - Send Notification
env:
NYM_NOTIFICATION_KIND: nym-wallet
NYM_PROJECT_NAME: "nym-wallet"
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
NYM_CI_WWW_LOCATION: "wallet-${{ env.GITHUB_REF_SLUG }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "${GITHUB_REF##*/}"
IS_SUCCESS: "${{ job.status == 'success' }}"
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}"
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
+7 -5
View File
@@ -19,7 +19,11 @@ jobs:
if: ${{ (startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
strategy:
fail-fast: false
runs-on: arc-ubuntu-22.04
matrix:
include:
- os: arc-ubuntu-22.04
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
outputs:
release_id: ${{ steps.create-release.outputs.id }}
@@ -66,7 +70,6 @@ jobs:
with:
name: my-artifact
path: |
target/release/explorer-api
target/release/nym-client
target/release/nym-socks5-client
target/release/nym-api
@@ -75,14 +78,13 @@ jobs:
target/release/nymvisor
target/release/nym-node
retention-days: 30
- id: create-release
name: Upload to release based on tag name
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
if: github.event_name == 'release'
with:
files: |
target/release/explorer-api
target/release/nym-client
target/release/nym-socks5-client
target/release/nym-api
+51
View File
@@ -0,0 +1,51 @@
name: Build and upload Nym APU container to harbor.nymte.ch
on:
workflow_dispatch:
env:
WORKING_DIRECTORY: "."
CONTAINER_NAME: "nym-api"
jobs:
build-container:
runs-on: arc-ubuntu-22.04-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@v4
- 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
uses: mikefarah/yq@v4.45.4
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-api/Cargo.toml
- name: Remove existing tag if exists
run: |
echo "Checking if tag ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }} exists..."
if git rev-parse ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then
echo "Tag ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }} already exists"
git push --delete origin ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }}
git tag -d ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }}
fi
- name: Create tag
run: |
git tag -a ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}"
git push origin ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }}
- name: BuildAndPushImageOnHarbor
run: |
docker build -f nym-api.dockerfile ${{ env.WORKING_DIRECTORY }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
@@ -0,0 +1,42 @@
name: Build and upload Nym Statistics API container to harbor.nymte.ch
on:
workflow_dispatch:
env:
WORKING_DIRECTORY: "nym-statistics-api"
CONTAINER_NAME: "nym-statistics-api"
jobs:
build-container:
runs-on: arc-ubuntu-22.04-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@v4
- 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
uses: mikefarah/yq@v4.45.4
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
- name: Create tag
run: |
git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}"
git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
- name: BuildAndPushImageOnHarbor
run: |
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
@@ -5,8 +5,6 @@
>
> ➡️➡️➡️➡️➡️ **View output:**
>
> `storybook`: https://{{ env.NYM_CI_WWW_LOCATION }}.{{ env.NYM_CI_WWW_BASE }}
>
> `branch` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/tree/{{ env.GIT_BRANCH_NAME }}
>
> `commit` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/commit/{{ env.GITHUB_SHA }}
-1
View File
@@ -40,7 +40,6 @@ validator-config
validator-api-config.toml
dist
storybook-static
envs/qwerty.env
.parcel-cache
**/.DS_Store
cpu-cycles/libcpucycles/build
+24
View File
@@ -4,6 +4,30 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.11-cheddar] (2025-06-10)
- No autoremoval of peers ([#5831])
- Set cached storage counters to 0 ([#5812])
- hack: temporarily use next.config.js instead of next.config.ts ([#5805])
- chore: resolve 1.87 clippy warnings ([#5802])
- Nym Statistics API ([#5800])
- QoL: RequestPath trait for http-api-client ([#5788])
- Fix contains ticketbook function that always returned true ([#5787])
- swap a decode into a fromrow to please future postgres feature ([#5785])
- Make address cache configurable ([#5784])
- Track wireguard credential retries ([#5783])
[#5831]: https://github.com/nymtech/nym/pull/5831
[#5812]: https://github.com/nymtech/nym/pull/5812
[#5805]: https://github.com/nymtech/nym/pull/5805
[#5802]: https://github.com/nymtech/nym/pull/5802
[#5800]: https://github.com/nymtech/nym/pull/5800
[#5788]: https://github.com/nymtech/nym/pull/5788
[#5787]: https://github.com/nymtech/nym/pull/5787
[#5785]: https://github.com/nymtech/nym/pull/5785
[#5784]: https://github.com/nymtech/nym/pull/5784
[#5783]: https://github.com/nymtech/nym/pull/5783
## [2025.10-brie] (2025-05-27)
- Backport PR 5779 ([#5801])
Generated
+1545 -586
View File
File diff suppressed because it is too large Load Diff
+19 -12
View File
@@ -33,11 +33,14 @@ members = [
"common/commands",
"common/config",
"common/cosmwasm-smart-contracts/coconut-dkg",
"common/cosmwasm-smart-contracts/contracts-common", "common/cosmwasm-smart-contracts/easy_addr",
"common/cosmwasm-smart-contracts/contracts-common",
"common/cosmwasm-smart-contracts/contracts-common-testing",
"common/cosmwasm-smart-contracts/easy_addr",
"common/cosmwasm-smart-contracts/ecash-contract",
"common/cosmwasm-smart-contracts/group-contract",
"common/cosmwasm-smart-contracts/mixnet-contract",
"common/cosmwasm-smart-contracts/multisig-contract",
"common/cosmwasm-smart-contracts/multisig-contract", "common/cosmwasm-smart-contracts/nym-performance-contract",
"common/cosmwasm-smart-contracts/nym-pool-contract",
"common/cosmwasm-smart-contracts/vesting-contract",
"common/credential-storage",
"common/credential-utils",
@@ -64,6 +67,8 @@ members = [
"common/nym-id",
"common/nym-metrics",
"common/nym_offline_compact_ecash",
"common/nymnoise",
"common/nymnoise/keys",
"common/nymsphinx",
"common/nymsphinx/acknowledgements",
"common/nymsphinx/addressing",
@@ -97,7 +102,6 @@ members = [
"common/wireguard-types",
"documentation/autodoc",
"gateway",
"integrations/bity",
"nym-api",
"nym-api/nym-api-requests",
"nym-browser-extension/storage",
@@ -112,6 +116,7 @@ members = [
"nym-node/nym-node-metrics",
"nym-node/nym-node-requests",
"nym-outfox",
"nym-statistics-api",
"nym-validator-rewarder",
"nyx-chain-watcher",
"sdk/ffi/cpp",
@@ -122,6 +127,7 @@ members = [
"service-providers/common",
"service-providers/ip-packet-router",
"service-providers/network-requester",
"sqlx-pool-guard",
"tools/echo-server",
"tools/internal/contract-state-importer/importer-cli",
"tools/internal/contract-state-importer/importer-contract",
@@ -131,7 +137,7 @@ members = [
"tools/internal/testnet-manager",
"tools/internal/testnet-manager",
"tools/internal/testnet-manager/dkg-bypass-contract",
"tools/internal/testnet-manager/dkg-bypass-contract", "tools/internal/validator-status-check",
"tools/internal/validator-status-check",
"tools/nym-cli",
"tools/nym-id-cli",
"tools/nym-nr-query",
@@ -152,6 +158,7 @@ default-members = [
"nym-node",
"nym-node-status-api/nym-node-status-agent",
"nym-node-status-api/nym-node-status-api",
"nym-statistics-api",
"nym-validator-rewarder",
"nyx-chain-watcher",
"service-providers/authenticator",
@@ -198,7 +205,7 @@ bloomfilter = "3.0.1"
bs58 = "0.5.1"
bytecodec = "0.4.15"
bytes = "1.10.1"
cargo_metadata = "0.18.1"
cargo_metadata = "0.19.2"
celes = "2.6.0"
cfg-if = "1.0.0"
chacha20 = "0.9.0"
@@ -281,6 +288,7 @@ petgraph = "0.6.5"
pin-project = "1.1"
pin-project-lite = "0.2.16"
publicsuffix = "2.3.0"
proc_pidinfo = "0.1.3"
quote = "1"
rand = "0.8.5"
rand_chacha = "0.3"
@@ -305,8 +313,9 @@ serde_with = "3.9.0"
serde_yaml = "0.9.25"
sha2 = "0.10.9"
si-scale = "0.2.3"
snow = "0.9.6"
sphinx-packet = "=0.6.0"
sqlx = "0.7.4"
sqlx = "0.8.6"
strum = "0.26"
strum_macros = "0.26"
subtle-encoding = "0.5"
@@ -314,10 +323,10 @@ syn = "1"
sysinfo = "0.33.0"
tap = "1.0.1"
tar = "0.4.44"
tempfile = "3.19"
tempfile = "3.20"
thiserror = "2.0"
time = "0.3.41"
tokio = "1.44"
tokio = "1.45"
tokio-postgres = "0.7"
tokio-stream = "0.1.17"
tokio-test = "0.4.4"
@@ -344,7 +353,6 @@ utoipauto = "0.2"
uuid = "*"
vergen = { version = "=8.3.1", default-features = false }
walkdir = "2"
wasm-bindgen-test = "0.3.49"
x25519-dalek = "2.0.0"
zeroize = "1.7.0"
@@ -362,9 +370,6 @@ subtle = "2.5.0"
# cosmwasm-related
cosmwasm-schema = "=2.2.2"
cosmwasm-std = "=2.2.2"
# use 1.0.1 as that's the version used by cosmwasm-std 2.2.1
# (and ideally we don't want to pull the same dependency twice)
serde-json-wasm = "=1.0.1"
# same version as used by cosmwasm
cw-utils = "=2.0.0"
cw-storage-plus = "=2.0.0"
@@ -372,6 +377,7 @@ cw2 = { version = "=2.0.0" }
cw3 = { version = "=2.0.0" }
cw4 = { version = "=2.0.0" }
cw-controllers = { version = "=2.0.0" }
cw-multi-test = "=2.3.2"
# cosmrs-related
bip32 = { version = "0.5.3", default-features = false }
@@ -392,6 +398,7 @@ serde-wasm-bindgen = "0.6.5"
tsify = "0.4.5"
wasm-bindgen = "0.2.99"
wasm-bindgen-futures = "0.4.49"
wasm-bindgen-test = "0.3.49"
wasmtimer = "0.4.1"
web-sys = "0.3.76"
+1 -1
View File
@@ -133,7 +133,7 @@ clippy: sdk-wasm-lint
# Build contracts ready for deploy
# -----------------------------------------------------------------------------
CONTRACTS=vesting_contract mixnet_contract nym_ecash cw3_flex_multisig cw4_group nym_coconut_dkg
CONTRACTS=vesting_contract mixnet_contract nym_ecash cw3_flex_multisig cw4_group nym_coconut_dkg nym_pool_contract nym_performance_contract
CONTRACTS_WASM=$(addsuffix .wasm, $(CONTRACTS))
CONTRACTS_OUT_DIR=contracts/target/wasm32-unknown-unknown/release
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.56"
version = "1.1.57"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
@@ -2048,10 +2048,11 @@
}
},
"node_modules/http-proxy-middleware": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz",
"integrity": "sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==",
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-proxy": "^1.17.8",
"http-proxy": "^1.18.1",
@@ -6095,9 +6096,9 @@
}
},
"http-proxy-middleware": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz",
"integrity": "sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==",
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
"dev": true,
"requires": {
"@types/http-proxy": "^1.17.8",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.56"
version = "1.1.57"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
@@ -108,7 +108,7 @@ impl GatewayClient {
#[cfg(feature = "verify")]
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
// use gateways key as a ref to an x25519_dalek key
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
// TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)]
@@ -117,7 +117,7 @@ impl GatewayClient {
#[cfg(feature = "verify")]
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
// use gateways key as a ref to an x25519_dalek key
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
// TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)]
@@ -117,7 +117,7 @@ impl GatewayClient {
#[cfg(feature = "verify")]
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
// use gateways key as a ref to an x25519_dalek key
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
// TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)]
@@ -169,7 +169,7 @@ impl GatewayClient {
#[cfg(feature = "verify")]
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
// use gateways key as a ref to an x25519_dalek key
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
// TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)]
@@ -169,7 +169,7 @@ impl GatewayClient {
#[cfg(feature = "verify")]
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
// use gateways key as a ref to an x25519_dalek key
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
// TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)]
+3 -1
View File
@@ -44,7 +44,6 @@ nym-sphinx = { path = "../nymsphinx" }
nym-statistics-common = { path = "../statistics" }
nym-pemstore = { path = "../pemstore" }
nym-topology = { path = "../topology", features = ["persistence"] }
nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false }
nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
nym-task = { path = "../task" }
nym-credentials-interface = { path = "../credentials-interface" }
@@ -57,6 +56,9 @@ nym-client-core-surb-storage = { path = "./surb-storage" }
nym-client-core-gateways-storage = { path = "./gateways-storage" }
nym-ecash-time = { path = "../ecash-time" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies]
nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false }
### For serving prometheus metrics
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.hyper]
workspace = true
@@ -2,8 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::BadGateway;
use std::io;
use std::path::PathBuf;
use std::{io, path::PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
@@ -19,7 +18,6 @@ pub enum StorageError {
#[error("failed to perform sqlx migration: {source}")]
MigrationError {
#[source]
#[from]
source: sqlx::migrate::MigrateError,
},
@@ -32,7 +30,6 @@ pub enum StorageError {
#[error("failed to run the SQL query: {source}")]
QueryError {
#[source]
#[from]
source: sqlx::error::Error,
},
@@ -87,7 +87,7 @@ impl StorageManager {
sqlx::query!("SELECT EXISTS (SELECT 1 FROM registered_gateway WHERE gateway_id_bs58 = ?) AS 'exists'", gateway_id)
.fetch_one(&self.connection_pool)
.await
.map(|result| result.exists == Some(1))
.map(|result| result.exists == 1)
}
pub(crate) async fn maybe_get_registered_gateway(
@@ -456,7 +456,7 @@ where
log::error!("Could not authenticate and start up the gateway connection - {err}");
ClientCoreError::GatewayClientError {
gateway_id: details.gateway_id.to_base58_string(),
source: err,
source: Box::new(err),
}
};
@@ -1,20 +1,18 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::replies::reply_storage::{
fs_backend, CombinedReplyStorage, ReplyStorageBackend,
use crate::{
client::replies::reply_storage::{fs_backend, CombinedReplyStorage, ReplyStorageBackend},
config,
config::Config,
error::ClientCoreError,
};
use crate::config;
use crate::config::Config;
use crate::error::ClientCoreError;
use log::{error, info, trace};
use nym_bandwidth_controller::BandwidthController;
use nym_client_core_gateways_storage::OnDiskGatewaysDetails;
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_validator_client::nyxd;
use nym_validator_client::QueryHttpRpcNyxdClient;
use std::path::Path;
use std::{fs, io};
use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient};
use std::{io, path::Path};
use time::OffsetDateTime;
use url::Url;
@@ -22,11 +20,11 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
db_path: P,
surb_config: &config::ReplySurbs,
) -> Result<fs_backend::Backend, ClientCoreError> {
info!("creating fresh surb database");
info!("Creating fresh surb database");
let mut storage_backend = match fs_backend::Backend::init(db_path).await {
Ok(backend) => backend,
Err(err) => {
error!("failed to setup persistent storage backend for our reply needs: {err}");
error!("setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}");
return Err(ClientCoreError::SurbStorageError {
source: Box::new(err),
});
@@ -40,14 +38,15 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
surb_config.minimum_reply_surb_storage_threshold,
surb_config.maximum_reply_surb_storage_threshold,
);
storage_backend
.init_fresh(&mem_store)
.await
.map_err(|err| ClientCoreError::SurbStorageError {
source: Box::new(err),
})?;
Ok(storage_backend)
match storage_backend.init_fresh(&mem_store).await {
Ok(()) => Ok(storage_backend),
Err(err) => {
storage_backend.shutdown().await;
Err(ClientCoreError::SurbStorageError {
source: Box::new(err),
})
}
}
}
// fn setup_inactive_backend(surb_config: &config::ReplySurbs) -> fs_backend::Backend {
@@ -58,12 +57,11 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
// )
// }
fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
async fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
let db_path = db_path.as_ref();
debug_assert!(db_path.exists());
let now = OffsetDateTime::now_utc().unix_timestamp();
let suffix = format!("_{now}.corrupted");
let new_extension =
@@ -72,11 +70,15 @@ fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
} else {
suffix
};
let renamed = db_path.with_extension(new_extension);
let mut renamed = db_path.to_owned();
renamed.set_extension(new_extension);
fs::rename(db_path, renamed)
tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| {
error!(
"Failed to rename corrupt database file: {} to {}",
db_path.display(),
renamed.display()
);
})
}
pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
@@ -87,13 +89,12 @@ pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
// the existing one
let db_path = db_path.as_ref();
if db_path.exists() {
info!("loading existing surb database");
info!("Loading existing surb database");
match fs_backend::Backend::try_load(db_path, surb_config.fresh_sender_tags).await {
Ok(backend) => Ok(backend),
Err(err) => {
error!("failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
archive_corrupted_database(db_path)?;
error!("setup_fs_reply_surb_backend: Failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
archive_corrupted_database(db_path).await?;
setup_fresh_backend(db_path, surb_config).await
}
}
@@ -12,7 +12,7 @@ use crate::client::topology_control::{TopologyAccessor, TopologyReadPermit};
use nym_sphinx::acknowledgements::AckKey;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, RepliableMessage, ReplyMessage};
use nym_sphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey};
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
use nym_sphinx::chunking::fragment::{Fragment, FragmentIdentifier};
use nym_sphinx::message::NymMessage;
use nym_sphinx::params::{PacketSize, PacketType};
@@ -44,7 +44,10 @@ pub enum PreparationError {
}
impl PreparationError {
fn return_surbs(self, returned_surbs: Vec<ReplySurb>) -> SurbWrappedPreparationError {
fn return_surbs(
self,
returned_surbs: Vec<ReplySurbWithKeyRotation>,
) -> SurbWrappedPreparationError {
SurbWrappedPreparationError {
source: self,
returned_surbs: Some(returned_surbs),
@@ -58,7 +61,7 @@ pub struct SurbWrappedPreparationError {
#[source]
source: PreparationError,
returned_surbs: Option<Vec<ReplySurb>>,
returned_surbs: Option<Vec<ReplySurbWithKeyRotation>>,
}
impl<T> From<T> for SurbWrappedPreparationError
@@ -268,10 +271,10 @@ where
}
}
async fn generate_reply_surbs_with_keys(
async fn generate_reply_surbs(
&mut self,
amount: usize,
) -> Result<(Vec<ReplySurb>, Vec<SurbEncryptionKey>), PreparationError> {
) -> Result<Vec<ReplySurbWithKeyRotation>, PreparationError> {
let topology_permit = self.topology_access.get_read_permit().await;
let topology = self.get_topology(&topology_permit)?;
@@ -281,19 +284,14 @@ where
topology,
)?;
let reply_keys = reply_surbs
.iter()
.map(|s| *s.encryption_key())
.collect::<Vec<_>>();
Ok((reply_surbs, reply_keys))
Ok(reply_surbs)
}
pub(crate) async fn try_send_single_surb_message(
&mut self,
target: AnonymousSenderTag,
message: ReplyMessage,
reply_surb: ReplySurb,
reply_surb: ReplySurbWithKeyRotation,
is_extra_surb_request: bool,
) -> Result<(), SurbWrappedPreparationError> {
let msg = NymMessage::new_reply(message);
@@ -347,7 +345,7 @@ where
pub(crate) async fn try_request_additional_reply_surbs(
&mut self,
from: AnonymousSenderTag,
reply_surb: ReplySurb,
reply_surb: ReplySurbWithKeyRotation,
amount: u32,
) -> Result<(), SurbWrappedPreparationError> {
debug!("requesting {amount} reply SURBs from {from}");
@@ -387,7 +385,7 @@ where
&mut self,
target: AnonymousSenderTag,
fragments: Vec<FragmentWithMaxRetransmissions>,
reply_surbs: Vec<ReplySurb>,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
lane: TransmissionLane,
) -> Result<(), SurbWrappedPreparationError> {
// TODO: technically this is performing an unnecessary cloning, but in the grand scheme of things
@@ -404,7 +402,7 @@ where
&mut self,
target: AnonymousSenderTag,
fragments: Vec<(TransmissionLane, FragmentWithMaxRetransmissions)>,
reply_surbs: Vec<ReplySurb>,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
) -> Result<(), SurbWrappedPreparationError> {
let prepared_fragments = self
.prepare_reply_chunks_for_sending(
@@ -541,8 +539,12 @@ where
) -> Result<(), PreparationError> {
debug!("Sending additional reply SURBs with packet type {packet_type}");
let sender_tag = self.get_or_create_sender_tag(&recipient);
let (reply_surbs, reply_keys) =
self.generate_reply_surbs_with_keys(amount as usize).await?;
let reply_surbs = self.generate_reply_surbs(amount as usize).await?;
let reply_keys = reply_surbs
.iter()
.map(|s| *s.encryption_key())
.collect::<Vec<_>>();
let message = NymMessage::new_repliable(RepliableMessage::new_additional_surbs(
self.config.use_legacy_sphinx_format,
@@ -579,9 +581,12 @@ where
) -> Result<(), SurbWrappedPreparationError> {
debug!("Sending message with reply SURBs with packet type {packet_type}");
let sender_tag = self.get_or_create_sender_tag(&recipient);
let (reply_surbs, reply_keys) = self
.generate_reply_surbs_with_keys(num_reply_surbs as usize)
.await?;
let reply_surbs = self.generate_reply_surbs(num_reply_surbs as usize).await?;
let reply_keys = reply_surbs
.iter()
.map(|s| *s.encryption_key())
.collect::<Vec<_>>();
let message = NymMessage::new_repliable(RepliableMessage::new_data(
self.config.use_legacy_sphinx_format,
@@ -629,7 +634,7 @@ where
pub(crate) async fn prepare_reply_chunks_for_sending(
&mut self,
fragments: Vec<Fragment>,
reply_surbs: Vec<ReplySurb>,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
) -> Result<Vec<PreparedFragment>, SurbWrappedPreparationError> {
debug_assert_eq!(
fragments.len(),
@@ -665,7 +670,7 @@ where
pub(crate) async fn try_prepare_single_reply_chunk_for_sending(
&mut self,
reply_surb: ReplySurb,
reply_surb: ReplySurbWithKeyRotation,
chunk: Fragment,
) -> Result<PreparedFragment, SurbWrappedPreparationError> {
let topology_permit = self.topology_access.get_read_permit().await;
@@ -11,7 +11,7 @@ use futures::StreamExt;
use log::{debug, error, info, trace, warn};
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::anonymous_replies::ReplySurb;
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
use nym_sphinx::chunking::fragment::FragmentIdentifier;
use nym_task::connections::{ConnectionId, TransmissionLane};
use nym_task::TaskClient;
@@ -499,7 +499,7 @@ where
async fn handle_received_surbs(
&mut self,
from: AnonymousSenderTag,
reply_surbs: Vec<ReplySurb>,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
from_surb_request: bool,
) {
trace!("handling received surbs");
@@ -6,7 +6,7 @@ use futures::channel::{mpsc, oneshot};
use log::error;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::anonymous_replies::ReplySurb;
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
use nym_task::connections::{ConnectionId, TransmissionLane};
use std::sync::Weak;
@@ -81,7 +81,7 @@ impl ReplyControllerSender {
pub(crate) fn send_additional_surbs(
&self,
sender_tag: AnonymousSenderTag,
reply_surbs: Vec<ReplySurb>,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
from_surb_request: bool,
) -> Result<(), ReplyControllerSenderError> {
self.0
@@ -167,7 +167,7 @@ pub enum ReplyControllerMessage {
AdditionalSurbs {
sender_tag: AnonymousSenderTag,
reply_surbs: Vec<ReplySurb>,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
from_surb_request: bool,
},
@@ -4,7 +4,7 @@
use async_trait::async_trait;
use log::{debug, error, warn};
use nym_topology::provider_trait::TopologyProvider;
use nym_topology::NymTopology;
use nym_topology::{NymTopology, NymTopologyMetadata};
use nym_validator_client::UserAgent;
use rand::prelude::SliceRandom;
use rand::thread_rng;
@@ -89,55 +89,84 @@ impl NymApiTopologyProvider {
let rewarded_set_fut = self.validator_client.get_current_rewarded_set();
let topology = if self.config.use_extended_topology {
let all_nodes_fut = self.validator_client.get_all_basic_nodes();
let all_nodes_fut = self.validator_client.get_all_basic_nodes_with_metadata();
// Join rewarded_set_fut and all_nodes_fut concurrently
let (rewarded_set, all_nodes) = futures::try_join!(rewarded_set_fut, all_nodes_fut)
let (rewarded_set, all_nodes_res) = futures::try_join!(rewarded_set_fut, all_nodes_fut)
.inspect_err(|err| error!("failed to get network nodes: {err}"))
.ok()?;
let metadata = all_nodes_res.metadata;
let all_nodes = all_nodes_res.nodes;
debug!(
"there are {} nodes on the network (before filtering)",
all_nodes.len()
);
let mut topology = NymTopology::new_empty(rewarded_set);
topology.add_additional_nodes(all_nodes.iter().filter(|n| {
n.performance.round_to_integer() >= self.config.min_node_performance()
}));
let nodes_filtered = all_nodes
.into_iter()
.filter(|n| n.performance.round_to_integer() >= self.config.min_node_performance())
.collect::<Vec<_>>();
topology
NymTopology::new(
NymTopologyMetadata::new(metadata.rotation_id, metadata.absolute_epoch_id),
rewarded_set,
Vec::new(),
)
.with_skimmed_nodes(&nodes_filtered)
} else {
// if we're not using extended topology, we're only getting active set mixnodes and gateways
let mixnodes_fut = self
.validator_client
.get_all_basic_active_mixing_assigned_nodes();
.get_all_basic_active_mixing_assigned_nodes_with_metadata();
// TODO: we really should be getting ACTIVE gateways only
let gateways_fut = self.validator_client.get_all_basic_entry_assigned_nodes();
let gateways_fut = self
.validator_client
.get_all_basic_entry_assigned_nodes_v2();
let (rewarded_set, mixnodes, gateways) =
let (rewarded_set, mixnodes_res, gateways_res) =
futures::try_join!(rewarded_set_fut, mixnodes_fut, gateways_fut)
.inspect_err(|err| {
error!("failed to get network nodes: {err}");
})
.ok()?;
let metadata = mixnodes_res.metadata;
let mixnodes = mixnodes_res.nodes;
if gateways_res.metadata != metadata {
warn!("inconsistent nodes metadata between mixnodes and gateways calls! {metadata:?} and {:?}", gateways_res.metadata);
return None;
}
let gateways = gateways_res.nodes;
debug!(
"there are {} mixnodes and {} gateways in total (before performance filtering)",
mixnodes.len(),
gateways.len()
);
let mut topology = NymTopology::new_empty(rewarded_set);
topology.add_additional_nodes(mixnodes.iter().filter(|m| {
m.performance.round_to_integer() >= self.config.min_mixnode_performance
}));
topology.add_additional_nodes(gateways.iter().filter(|m| {
m.performance.round_to_integer() >= self.config.min_gateway_performance
}));
let mut nodes = Vec::new();
for mix in mixnodes {
if mix.performance.round_to_integer() >= self.config.min_mixnode_performance {
nodes.push(mix)
}
}
for gateway in gateways {
if gateway.performance.round_to_integer() >= self.config.min_gateway_performance {
nodes.push(gateway)
}
}
topology
NymTopology::new(
NymTopologyMetadata::new(metadata.rotation_id, metadata.absolute_epoch_id),
rewarded_set,
Vec::new(),
)
.with_skimmed_nodes(&nodes)
};
if !topology.is_minimally_routable() {
+10 -5
View File
@@ -18,7 +18,7 @@ pub enum ClientCoreError {
#[error("gateway client error ({gateway_id}): {source}")]
GatewayClientError {
gateway_id: String,
source: GatewayClientError,
source: Box<GatewayClientError>,
},
#[error("custom gateway client error: {source}")]
@@ -88,10 +88,7 @@ pub enum ClientCoreError {
},
#[error("failed to establish connection to gateway: {source}")]
GatewayConnectionFailure {
#[from]
source: tungstenite::Error,
},
GatewayConnectionFailure { source: Box<tungstenite::Error> },
#[cfg(target_arch = "wasm32")]
#[error("failed to establish gateway connection (wasm)")]
@@ -227,6 +224,14 @@ pub enum ClientCoreError {
HkdfDerivationError {},
}
impl From<tungstenite::Error> for ClientCoreError {
fn from(err: tungstenite::Error) -> ClientCoreError {
ClientCoreError::GatewayConnectionFailure {
source: Box::new(err),
}
}
}
/// Set of messages that the client can send to listeners via the task manager
#[derive(Debug)]
pub enum ClientCoreStatusMessage {
+3 -3
View File
@@ -107,7 +107,7 @@ pub async fn gateways_for_init<R: Rng>(
log::debug!("Fetching list of gateways from: {nym_api}");
let gateways = client.get_all_basic_entry_assigned_nodes().await?;
let gateways = client.get_all_basic_entry_assigned_nodes_v2().await?.nodes;
info!("nym api reports {} gateways", gateways.len());
log::trace!("Gateways: {:#?}", gateways);
@@ -329,7 +329,7 @@ pub(super) async fn register_with_gateway(
log::warn!("Failed to establish connection with gateway!");
ClientCoreError::GatewayClientError {
gateway_id: gateway_id.to_base58_string(),
source: err,
source: Box::new(err),
}
})?;
let auth_response = gateway_client
@@ -339,7 +339,7 @@ pub(super) async fn register_with_gateway(
log::warn!("Failed to register with the gateway {gateway_id}: {err}");
ClientCoreError::GatewayClientError {
gateway_id: gateway_id.to_base58_string(),
source: err,
source: Box::new(err),
}
})?;
+1 -1
View File
@@ -232,7 +232,7 @@ where
} => {
log::debug!("GatewaySetup::ReuseConnection");
Ok(reuse_gateway_connection(
authenticated_ephemeral_client,
*authenticated_ephemeral_client,
*gateway_details,
managed_keys,
))
+2 -2
View File
@@ -218,7 +218,7 @@ pub enum GatewaySetup {
ReuseConnection {
/// The authenticated ephemeral client that was created during `init`
authenticated_ephemeral_client: InitGatewayClient,
authenticated_ephemeral_client: Box<InitGatewayClient>,
// Details of this pre-initialised client (i.e. gateway and keys)
gateway_details: Box<GatewayRegistration>,
@@ -261,7 +261,7 @@ impl GatewaySetup {
pub fn try_reuse_connection(init_res: InitialisationResult) -> Result<Self, ClientCoreError> {
if let Some(authenticated_ephemeral_client) = init_res.authenticated_ephemeral_client {
Ok(GatewaySetup::ReuseConnection {
authenticated_ephemeral_client,
authenticated_ephemeral_client: Box::new(authenticated_ephemeral_client),
gateway_details: Box::new(init_res.gateway_registration),
client_keys: init_res.client_keys,
})
+12 -1
View File
@@ -17,15 +17,26 @@ nym-crypto = { path = "../../crypto", optional = true, default-features = false
nym-sphinx = { path = "../../nymsphinx" }
nym-task = { path = "../../task" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
workspace = true
features = ["fs"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
workspace = true
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
optional = true
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
path = "../../../sqlx-pool-guard"
[build-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
"sqlite",
"macros",
"migrate",
] }
[features]
fs-surb-storage = ["sqlx", "nym-crypto", "nym-crypto/hashing"]
@@ -0,0 +1,8 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
-- default value of 0 implies 'unknown' variant
ALTER TABLE reply_surb
ADD COLUMN encoded_key_rotation TINYINT NOT NULL DEFAULT 0;
@@ -1,8 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::io;
use std::path::PathBuf;
use std::{io, path::PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
@@ -30,7 +29,6 @@ pub enum StorageError {
#[error("failed to perform sqlx migration: {source}")]
MigrationError {
#[source]
#[from]
source: sqlx::migrate::MigrateError,
},
@@ -43,7 +41,6 @@ pub enum StorageError {
#[error("failed to run the SQL query: {source}")]
QueryError {
#[source]
#[from]
source: sqlx::error::Error,
},
@@ -15,9 +15,11 @@ use sqlx::{
};
use std::path::Path;
use sqlx_pool_guard::SqlitePoolGuard;
#[derive(Debug, Clone)]
pub struct StorageManager {
pub connection_pool: sqlx::SqlitePool,
connection_pool: SqlitePoolGuard,
}
// all SQL goes here
@@ -37,7 +39,7 @@ impl StorageManager {
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.synchronous(SqliteSynchronous::Normal)
.auto_vacuum(SqliteAutoVacuum::Incremental)
.filename(database_path)
.filename(&database_path)
.create_if_missing(fresh)
.disable_statement_logging();
@@ -49,11 +51,15 @@ impl StorageManager {
}
};
let connection_pool =
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
if let Err(err) = sqlx::migrate!("./fs_surbs_migrations")
.run(&connection_pool)
.run(&*connection_pool)
.await
{
error!("Failed to initialize SQLx database: {err}");
connection_pool.close().await;
return Err(err.into());
}
@@ -61,38 +67,43 @@ impl StorageManager {
Ok(StorageManager { connection_pool })
}
/// Close connection pool waiting for all connections to be closed.
pub async fn close_pool(&self) {
self.connection_pool.close().await;
}
#[allow(dead_code)]
pub async fn status_table_exists(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT name FROM sqlite_master WHERE type='table' AND name='status'")
.fetch_optional(&self.connection_pool)
.fetch_optional(&*self.connection_pool)
.await
.map(|r| r.is_some())
}
pub async fn create_status_table(&self) -> Result<(), sqlx::Error> {
sqlx::query!("INSERT INTO status(flush_in_progress, previous_flush_timestamp, client_in_use) VALUES (0, 0, 1)")
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn get_flush_status(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT flush_in_progress FROM status;")
.fetch_one(&self.connection_pool)
.fetch_one(&*self.connection_pool)
.await
.map(|r| r.flush_in_progress > 0)
}
pub async fn set_previous_flush_timestamp(&self, timestamp: i64) -> Result<(), sqlx::Error> {
sqlx::query!("UPDATE status SET previous_flush_timestamp = ?", timestamp)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn get_previous_flush_timestamp(&self) -> Result<i64, sqlx::Error> {
sqlx::query!("SELECT previous_flush_timestamp FROM status;")
.fetch_one(&self.connection_pool)
.fetch_one(&*self.connection_pool)
.await
.map(|r| r.previous_flush_timestamp)
}
@@ -100,14 +111,14 @@ impl StorageManager {
pub async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> {
let in_progress_int = i64::from(in_progress);
sqlx::query!("UPDATE status SET flush_in_progress = ?", in_progress_int)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn get_client_in_use_status(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT client_in_use FROM status;")
.fetch_one(&self.connection_pool)
.fetch_one(&*self.connection_pool)
.await
.map(|r| r.client_in_use > 0)
}
@@ -115,21 +126,21 @@ impl StorageManager {
pub async fn set_client_in_use_status(&self, in_use: bool) -> Result<(), sqlx::Error> {
let in_use_int = i64::from(in_use);
sqlx::query!("UPDATE status SET client_in_use = ?", in_use_int)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn delete_all_tags(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM sender_tag;")
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn get_tags(&self) -> Result<Vec<StoredSenderTag>, sqlx::Error> {
sqlx::query_as!(StoredSenderTag, "SELECT * FROM sender_tag;",)
.fetch_all(&self.connection_pool)
.fetch_all(&*self.connection_pool)
.await
}
@@ -141,21 +152,21 @@ impl StorageManager {
stored_tag.recipient,
stored_tag.tag
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn delete_all_reply_keys(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM reply_key;")
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn get_reply_keys(&self) -> Result<Vec<StoredReplyKey>, sqlx::Error> {
sqlx::query_as!(StoredReplyKey, "SELECT * FROM reply_key;",)
.fetch_all(&self.connection_pool)
.fetch_all(&*self.connection_pool)
.await
}
@@ -171,14 +182,14 @@ impl StorageManager {
stored_reply_key.reply_key,
stored_reply_key.sent_at_timestamp
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn get_surb_senders(&self) -> Result<Vec<StoredSurbSender>, sqlx::Error> {
sqlx::query_as!(StoredSurbSender, "SELECT * FROM reply_surb_sender;",)
.fetch_all(&self.connection_pool)
.fetch_all(&*self.connection_pool)
.await
}
@@ -193,7 +204,7 @@ impl StorageManager {
stored_surb_sender.tag,
stored_surb_sender.last_sent_timestamp
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?
.last_insert_rowid();
Ok(id)
@@ -205,20 +216,23 @@ impl StorageManager {
) -> Result<Vec<StoredReplySurb>, sqlx::Error> {
sqlx::query_as!(
StoredReplySurb,
"SELECT * FROM reply_surb WHERE reply_surb_sender_id = ?",
r#"
SELECT reply_surb_sender_id, reply_surb, encoded_key_rotation as "encoded_key_rotation: u8" FROM reply_surb
WHERE reply_surb_sender_id = ?
"#,
sender_id
)
.fetch_all(&self.connection_pool)
.fetch_all(&*self.connection_pool)
.await
}
pub async fn delete_all_reply_surb_data(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM reply_surb;")
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
sqlx::query!("DELETE FROM reply_surb_sender;")
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
@@ -230,12 +244,13 @@ impl StorageManager {
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO reply_surb(reply_surb_sender_id, reply_surb) VALUES (?, ?);
INSERT INTO reply_surb(reply_surb_sender_id, reply_surb, encoded_key_rotation) VALUES (?, ?, ?);
"#,
stored_reply_surb.reply_surb_sender_id,
stored_reply_surb.reply_surb
stored_reply_surb.reply_surb,
stored_reply_surb.encoded_key_rotation
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
@@ -249,7 +264,7 @@ impl StorageManager {
SELECT min_reply_surb_threshold as "min_reply_surb_threshold: u32", max_reply_surb_threshold as "max_reply_surb_threshold: u32" FROM reply_surb_storage_metadata;
"#,
)
.fetch_one(&self.connection_pool)
.fetch_one(&*self.connection_pool)
.await
}
@@ -263,7 +278,7 @@ impl StorageManager {
"#,
metadata.min_reply_surb_threshold,
metadata.max_reply_surb_threshold,
).execute(&self.connection_pool).await?;
).execute(&*self.connection_pool).await?;
Ok(())
}
}
@@ -1,18 +1,21 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::backend::fs_backend::manager::StorageManager;
use crate::backend::fs_backend::models::{
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender,
};
use crate::surb_storage::ReceivedReplySurbs;
use crate::{
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, UsedSenderTags,
backend::fs_backend::{
manager::StorageManager,
models::{
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag,
StoredSurbSender,
},
},
surb_storage::ReceivedReplySurbs,
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys,
UsedSenderTags,
};
use async_trait::async_trait;
use log::{debug, error, info, warn};
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use std::fs;
use std::path::{Path, PathBuf};
use time::OffsetDateTime;
@@ -41,15 +44,17 @@ impl Backend {
}
let manager = StorageManager::init(database_path, true).await?;
manager.create_status_table().await?;
let backend = Backend {
temporary_old_path: None,
database_path: owned_path,
manager,
};
Ok(backend)
match manager.create_status_table().await {
Ok(()) => Ok(Backend {
temporary_old_path: None,
database_path: owned_path,
manager,
}),
Err(err) => {
manager.close_pool().await;
Err(err.into())
}
}
}
pub async fn try_load<P: AsRef<Path>>(
@@ -64,7 +69,28 @@ impl Backend {
}
let manager = StorageManager::init(database_path, false).await?;
match Self::try_load_inner(&manager, fresh_sender_tags).await {
Ok(()) => Ok(Backend {
temporary_old_path: None,
database_path: owned_path,
manager,
}),
Err(e) => {
manager.close_pool().await;
Err(e)
}
}
}
/// Gracefully close sqlite connection pool and drop backend.
pub async fn shutdown(self) {
self.manager.close_pool().await
}
async fn try_load_inner(
manager: &StorageManager,
fresh_sender_tags: bool,
) -> Result<(), StorageError> {
// the database flush wasn't fully finished and thus the data is in inconsistent state
// (we don't really know what's properly saved or what's not)
if manager.get_flush_status().await? {
@@ -126,20 +152,11 @@ impl Backend {
manager.delete_all_tags().await?;
}
Ok(Backend {
temporary_old_path: None,
database_path: owned_path,
// manager: StorageManagerState::Storage(manager),
manager,
})
}
async fn close_pool(&mut self) {
self.manager.connection_pool.close().await;
Ok(())
}
async fn rotate(&mut self) -> Result<(), StorageError> {
self.close_pool().await;
self.manager.close_pool().await;
let new_extension = if let Some(existing_extension) =
self.database_path.extension().and_then(|ext| ext.to_str())
@@ -152,7 +169,8 @@ impl Backend {
let mut temp_old = self.database_path.clone();
temp_old.set_extension(new_extension);
fs::rename(&self.database_path, &temp_old)
tokio::fs::rename(&self.database_path, &temp_old)
.await
.map_err(|err| StorageError::DatabaseRenameError { source: err })?;
self.manager = StorageManager::init(&self.database_path, true).await?;
self.manager.create_status_table().await?;
@@ -161,9 +179,10 @@ impl Backend {
Ok(())
}
fn remove_old(&mut self) -> Result<(), StorageError> {
async fn remove_old(&mut self) -> Result<(), StorageError> {
if let Some(old_path) = self.temporary_old_path.take() {
fs::remove_file(old_path)
tokio::fs::remove_file(old_path)
.await
.map_err(|err| StorageError::DatabaseOldFileRemoveError { source: err })
} else {
warn!("the old database file doesn't seem to exist!");
@@ -335,7 +354,7 @@ impl ReplyStorageBackend for Backend {
self.dump_reply_surb_storage_metadata(surbs_ref).await?;
self.dump_reply_surbs(surbs_ref).await?;
self.remove_old()?;
self.remove_old().await?;
self.end_storage_flush().await
}
@@ -8,8 +8,10 @@ use nym_crypto::Digest;
use nym_sphinx::addressing::clients::{Recipient, RecipientBytes};
use nym_sphinx::anonymous_replies::encryption_key::EncryptionKeyDigest;
use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, SENDER_TAG_SIZE};
use nym_sphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey, SurbEncryptionKeySize};
use nym_sphinx::params::ReplySurbKeyDigestAlgorithm;
use nym_sphinx::anonymous_replies::{
ReplySurb, ReplySurbWithKeyRotation, SurbEncryptionKey, SurbEncryptionKeySize,
};
use nym_sphinx::params::{ReplySurbKeyDigestAlgorithm, SphinxKeyRotation};
#[derive(Debug, Clone)]
pub struct StoredSenderTag {
@@ -146,24 +148,40 @@ impl TryFrom<StoredSurbSender> for (AnonymousSenderTag, i64) {
pub struct StoredReplySurb {
pub reply_surb_sender_id: i64,
pub reply_surb: Vec<u8>,
// encodes only whether it's 'even', 'odd' or 'unknown' (default)
// and not the whole id because that's redundant
pub encoded_key_rotation: u8,
}
impl StoredReplySurb {
pub fn new(reply_surb_sender_id: i64, reply_surb: &ReplySurb) -> Self {
pub fn new(reply_surb_sender_id: i64, reply_surb: &ReplySurbWithKeyRotation) -> Self {
StoredReplySurb {
reply_surb_sender_id,
reply_surb: reply_surb.to_bytes(),
reply_surb: reply_surb.inner_reply_surb().to_bytes(),
encoded_key_rotation: reply_surb.key_rotation() as u8,
}
}
}
impl TryFrom<StoredReplySurb> for ReplySurb {
impl TryFrom<StoredReplySurb> for ReplySurbWithKeyRotation {
type Error = StorageError;
fn try_from(value: StoredReplySurb) -> Result<Self, Self::Error> {
ReplySurb::from_bytes(&value.reply_surb).map_err(|err| StorageError::CorruptedData {
details: format!("failed to recover the reply surb: {err}"),
})
let key_rotation =
SphinxKeyRotation::try_from(value.encoded_key_rotation).map_err(|err| {
StorageError::CorruptedData {
details: format!("stored key rotation was malformed: {err}"),
}
})?;
let reply_surb = ReplySurb::from_bytes(&value.reply_surb).map_err(|err| {
StorageError::CorruptedData {
details: format!("failed to recover the reply surb: {err}"),
}
})?;
Ok(reply_surb.with_key_rotation(key_rotation))
}
}
@@ -33,7 +33,6 @@ where
self.backend.load_surb_storage().await
}
// this will have to get enabled after merging develop
pub async fn flush_on_shutdown(
mut self,
mem_state: CombinedReplyStorage,
@@ -50,7 +49,6 @@ where
shutdown.recv().await;
info!("PersistentReplyStorage is flushing all reply-related data to underlying storage");
info!("you MUST NOT forcefully shutdown now or you risk data corruption!");
if let Err(err) = self.backend.flush_surb_storage(&mem_state).await {
error!("failed to flush our reply-related data to the persistent storage: {err}")
} else {
@@ -5,7 +5,7 @@ use dashmap::iter::Iter;
use dashmap::DashMap;
use log::trace;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::anonymous_replies::ReplySurb;
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
use std::collections::VecDeque;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
@@ -134,7 +134,7 @@ impl ReceivedReplySurbsMap {
&self,
target: &AnonymousSenderTag,
amount: usize,
) -> (Option<Vec<ReplySurb>>, usize) {
) -> (Option<Vec<ReplySurbWithKeyRotation>>, usize) {
if let Some(mut entry) = self.inner.data.get_mut(target) {
let surbs_left = entry.items_left();
if surbs_left < self.min_surb_threshold() + amount {
@@ -150,7 +150,7 @@ impl ReceivedReplySurbsMap {
pub fn get_reply_surb_ignoring_threshold(
&self,
target: &AnonymousSenderTag,
) -> Option<(Option<ReplySurb>, usize)> {
) -> Option<(Option<ReplySurbWithKeyRotation>, usize)> {
self.inner
.data
.get_mut(target)
@@ -160,7 +160,7 @@ impl ReceivedReplySurbsMap {
pub fn get_reply_surb(
&self,
target: &AnonymousSenderTag,
) -> Option<(Option<ReplySurb>, usize)> {
) -> Option<(Option<ReplySurbWithKeyRotation>, usize)> {
self.inner.data.get_mut(target).map(|mut entry| {
let surbs_left = entry.items_left();
if surbs_left < self.min_surb_threshold() {
@@ -171,7 +171,7 @@ impl ReceivedReplySurbsMap {
})
}
pub fn insert_surbs<I: IntoIterator<Item = ReplySurb>>(
pub fn insert_surbs<I: IntoIterator<Item = ReplySurbWithKeyRotation>>(
&self,
target: &AnonymousSenderTag,
surbs: I,
@@ -189,14 +189,14 @@ impl ReceivedReplySurbsMap {
pub struct ReceivedReplySurbs {
// in the future we'd probably want to put extra data here to indicate when the SURBs got received
// so we could invalidate entries from the previous key rotations
data: VecDeque<ReplySurb>,
data: VecDeque<ReplySurbWithKeyRotation>,
pending_reception: u32,
surbs_last_received_at_timestamp: i64,
}
impl ReceivedReplySurbs {
fn new(initial_surbs: VecDeque<ReplySurb>) -> Self {
fn new(initial_surbs: VecDeque<ReplySurbWithKeyRotation>) -> Self {
ReceivedReplySurbs {
data: initial_surbs,
pending_reception: 0,
@@ -206,7 +206,7 @@ impl ReceivedReplySurbs {
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
pub fn new_retrieved(
surbs: Vec<ReplySurb>,
surbs: Vec<ReplySurbWithKeyRotation>,
surbs_last_received_at_timestamp: i64,
) -> ReceivedReplySurbs {
ReceivedReplySurbs {
@@ -217,7 +217,7 @@ impl ReceivedReplySurbs {
}
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
pub fn surbs_ref(&self) -> &VecDeque<ReplySurb> {
pub fn surbs_ref(&self) -> &VecDeque<ReplySurbWithKeyRotation> {
&self.data
}
@@ -243,7 +243,10 @@ impl ReceivedReplySurbs {
self.pending_reception = 0;
}
pub fn get_reply_surbs(&mut self, amount: usize) -> (Option<Vec<ReplySurb>>, usize) {
pub fn get_reply_surbs(
&mut self,
amount: usize,
) -> (Option<Vec<ReplySurbWithKeyRotation>>, usize) {
if self.items_left() < amount {
(None, self.items_left())
} else {
@@ -252,11 +255,11 @@ impl ReceivedReplySurbs {
}
}
pub fn get_reply_surb(&mut self) -> (Option<ReplySurb>, usize) {
pub fn get_reply_surb(&mut self) -> (Option<ReplySurbWithKeyRotation>, usize) {
(self.pop_surb(), self.items_left())
}
fn pop_surb(&mut self) -> Option<ReplySurb> {
fn pop_surb(&mut self) -> Option<ReplySurbWithKeyRotation> {
self.data.pop_front()
}
@@ -265,7 +268,10 @@ impl ReceivedReplySurbs {
}
// realistically we're always going to be getting multiple surbs at once
pub fn insert_reply_surbs<I: IntoIterator<Item = ReplySurb>>(&mut self, surbs: I) {
pub fn insert_reply_surbs<I: IntoIterator<Item = ReplySurbWithKeyRotation>>(
&mut self,
surbs: I,
) {
let mut v = surbs.into_iter().collect::<VecDeque<_>>();
trace!("storing {} surbs in the storage", v.len());
self.data.append(&mut v);
@@ -21,8 +21,8 @@ use nym_crypto::asymmetric::ed25519;
use nym_gateway_requests::registration::handshake::client_handshake;
use nym_gateway_requests::{
BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersionExt,
SensitiveServerResponse, ServerResponse, SharedGatewayKey, SharedSymmetricKey,
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
GatewayRequestsError, SensitiveServerResponse, ServerResponse, SharedGatewayKey,
SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
};
use nym_sphinx::forwarding::packet::MixPacket;
use nym_statistics_common::clients::connection::ConnectionStatsEvent;
@@ -662,6 +662,7 @@ impl<C, St> GatewayClient<C, St> {
let supports_aes_gcm_siv = gw_protocol.supports_aes256_gcm_siv();
let supports_auth_v2 = gw_protocol.supports_authenticate_v2();
let supports_key_rotation_info = gw_protocol.supports_key_rotation_packet();
if !supports_aes_gcm_siv {
warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV");
@@ -669,6 +670,9 @@ impl<C, St> GatewayClient<C, St> {
if !supports_auth_v2 {
warn!("this gateway is on an old version that doesn't support authentication v2")
}
if !supports_key_rotation_info {
warn!("this gateway is on an old version that doesn't support key rotation packets")
}
if self.authenticated {
debug!("Already authenticated");
@@ -849,6 +853,22 @@ impl<C, St> GatewayClient<C, St> {
}
}
fn mix_packet_to_ws_message(&self, packet: MixPacket) -> Result<Message, GatewayRequestsError> {
// note: into_ws_message encrypts the requests and adds a MAC on it. Perhaps it should
// be more explicit in the naming?
let req = if self.negotiated_protocol.supports_key_rotation_packet() {
BinaryRequest::ForwardSphinxV2 { packet }
} else {
BinaryRequest::ForwardSphinx { packet }
};
req.into_ws_message(
self.shared_key
.as_ref()
.expect("no shared key present even though we're authenticated!"),
)
}
pub async fn batch_send_mix_packets(
&mut self,
packets: Vec<MixPacket>,
@@ -877,13 +897,7 @@ impl<C, St> GatewayClient<C, St> {
let messages: Result<Vec<_>, _> = packets
.into_iter()
.map(|mix_packet| {
BinaryRequest::ForwardSphinx { packet: mix_packet }.into_ws_message(
self.shared_key
.as_ref()
.expect("no shared key present even though we're authenticated!"),
)
})
.map(|mix_packet| self.mix_packet_to_ws_message(mix_packet))
.collect();
if let Err(err) = self
@@ -949,13 +963,8 @@ impl<C, St> GatewayClient<C, St> {
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
// note: into_ws_message encrypts the requests and adds a MAC on it. Perhaps it should
// be more explicit in the naming?
let msg = BinaryRequest::ForwardSphinx { packet: mix_packet }.into_ws_message(
self.shared_key
.as_ref()
.expect("no shared key present even though we're authenticated!"),
)?;
let msg = self.mix_packet_to_ws_message(mix_packet)?;
self.send_with_reconnection_on_failure(msg).await
}
@@ -56,7 +56,7 @@ pub(crate) async fn connect_async(
}
.map_err(|err| GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(),
source: err.into(),
source: Box::new(tungstenite::Error::from(err)),
})?;
#[cfg(unix)]
@@ -72,7 +72,7 @@ pub(crate) async fn connect_async(
Err(err) => {
stream = Err(GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(),
source: err.into(),
source: Box::new(tungstenite::Error::from(err)),
});
continue;
}
@@ -83,6 +83,6 @@ pub(crate) async fn connect_async(
.await
.map_err(|error| GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(),
source: error,
source: Box::new(error),
})
}
+12 -3
View File
@@ -25,7 +25,7 @@ pub enum GatewayClientError {
RequestError(#[from] GatewayRequestsError),
#[error("There was a network error: {0}")]
NetworkError(#[from] WsError),
NetworkError(Box<WsError>),
#[error("failed to upgrade our shared key - the gateway sent malformed response")]
FatalKeyUpgradeFailure,
@@ -41,7 +41,10 @@ pub enum GatewayClientError {
NetworkErrorWasm(#[from] JsError),
#[error("connection failed: {address}: {source}")]
NetworkConnectionFailed { address: String, source: WsError },
NetworkConnectionFailed {
address: String,
source: Box<WsError>,
},
#[error("no socket address for endpoint: {address}")]
NoEndpointForConnection { address: String },
@@ -127,10 +130,16 @@ pub enum GatewayClientError {
ShutdownInProgress,
}
impl From<WsError> for GatewayClientError {
fn from(error: WsError) -> Self {
GatewayClientError::NetworkError(Box::new(error))
}
}
impl GatewayClientError {
pub fn is_closed_connection(&self) -> bool {
match self {
GatewayClientError::NetworkError(ws_err) => match ws_err {
GatewayClientError::NetworkError(ws_err) => match ws_err.as_ref() {
WsError::AlreadyClosed | WsError::ConnectionClosed => true,
WsError::Io(io_err) => matches!(
io_err.kind(),
+2 -2
View File
@@ -28,7 +28,7 @@ pub(crate) fn cleanup_socket_message(
msg: Option<Result<Message, WsError>>,
) -> Result<Message, GatewayClientError> {
match msg {
Some(msg) => msg.map_err(GatewayClientError::NetworkError),
Some(msg) => msg.map_err(GatewayClientError::from),
None => Err(GatewayClientError::ConnectionAbruptlyClosed),
}
}
@@ -39,7 +39,7 @@ pub(crate) fn cleanup_socket_messages(
match msgs {
Some(msgs) => msgs
.into_iter()
.map(|msg| msg.map_err(GatewayClientError::NetworkError))
.map(|msg| msg.map_err(GatewayClientError::from))
.collect(),
None => Err(GatewayClientError::ConnectionAbruptlyClosed),
}
+6 -1
View File
@@ -16,9 +16,14 @@ tokio-util = { workspace = true, features = ["codec"], optional = true }
tokio-stream = { workspace = true }
# internal
nym-noise = { path = "../../nymnoise" }
nym-sphinx = { path = "../../nymsphinx" }
nym-task = { path = "../../task", optional = true }
[features]
default = ["client"]
client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"]
client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"]
[dev-dependencies]
nym-crypto = { path = "../../crypto" }
rand = { workspace = true }
+48 -25
View File
@@ -3,11 +3,11 @@
use dashmap::DashMap;
use futures::StreamExt;
use nym_sphinx::addressing::nodes::NymNodeRoutingAddress;
use nym_noise::config::NoiseConfig;
use nym_noise::upgrade_noise_initiator;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_sphinx::framing::codec::NymCodec;
use nym_sphinx::framing::packet::FramedNymPacket;
use nym_sphinx::params::PacketType;
use nym_sphinx::NymPacket;
use std::io;
use std::net::SocketAddr;
use std::ops::Deref;
@@ -49,23 +49,19 @@ impl Config {
pub trait SendWithoutResponse {
// Without response in this context means we will not listen for anything we might get back (not
// that we should get anything), including any possible io errors
fn send_without_response(
&self,
address: NymNodeRoutingAddress,
packet: NymPacket,
packet_type: PacketType,
) -> io::Result<()>;
fn send_without_response(&self, packet: MixPacket) -> io::Result<()>;
}
pub struct Client {
active_connections: ActiveConnections,
noise_config: NoiseConfig,
connections_count: Arc<AtomicUsize>,
config: Config,
}
#[derive(Default, Clone)]
pub struct ActiveConnections {
inner: Arc<DashMap<NymNodeRoutingAddress, ConnectionSender>>,
inner: Arc<DashMap<SocketAddr, ConnectionSender>>,
}
impl ActiveConnections {
@@ -82,7 +78,7 @@ impl ActiveConnections {
}
impl Deref for ActiveConnections {
type Target = DashMap<NymNodeRoutingAddress, ConnectionSender>;
type Target = DashMap<SocketAddr, ConnectionSender>;
fn deref(&self) -> &Self::Target {
&self.inner
}
@@ -104,6 +100,7 @@ impl ConnectionSender {
struct ManagedConnection {
address: SocketAddr,
noise_config: NoiseConfig,
message_receiver: ReceiverStream<FramedNymPacket>,
connection_timeout: Duration,
current_reconnection: Arc<AtomicU32>,
@@ -112,12 +109,14 @@ struct ManagedConnection {
impl ManagedConnection {
fn new(
address: SocketAddr,
noise_config: NoiseConfig,
message_receiver: mpsc::Receiver<FramedNymPacket>,
connection_timeout: Duration,
current_reconnection: Arc<AtomicU32>,
) -> Self {
ManagedConnection {
address,
noise_config,
message_receiver: ReceiverStream::new(message_receiver),
connection_timeout,
current_reconnection,
@@ -132,9 +131,21 @@ impl ManagedConnection {
Ok(stream_res) => match stream_res {
Ok(stream) => {
debug!("Managed to establish connection to {}", self.address);
// if we managed to connect, reset the reconnection count (whatever it might have been)
let noise_stream =
match upgrade_noise_initiator(stream, &self.noise_config).await {
Ok(noise_stream) => noise_stream,
Err(err) => {
error!("Failed to perform Noise handshake with {address} - {err}");
// we failed to finish the noise handshake - increase reconnection attempt
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
return;
}
};
// if we managed to connect AND do the noise handshake, reset the reconnection count (whatever it might have been)
self.current_reconnection.store(0, Ordering::Release);
Framed::new(stream, NymCodec)
debug!("Noise initiator handshake completed for {:?}", address);
Framed::new(noise_stream, NymCodec)
}
Err(err) => {
debug!("failed to establish connection to {address} (err: {err})",);
@@ -167,9 +178,14 @@ impl ManagedConnection {
}
impl Client {
pub fn new(config: Config, connections_count: Arc<AtomicUsize>) -> Client {
pub fn new(
config: Config,
noise_config: NoiseConfig,
connections_count: Arc<AtomicUsize>,
) -> Client {
Client {
active_connections: Default::default(),
noise_config,
connections_count,
config,
}
@@ -196,7 +212,7 @@ impl Client {
}
}
fn make_connection(&self, address: NymNodeRoutingAddress, pending_packet: FramedNymPacket) {
fn make_connection(&self, address: SocketAddr, pending_packet: FramedNymPacket) {
let (sender, receiver) = mpsc::channel(self.config.maximum_connection_buffer_size);
// this CAN'T fail because we just created the channel which has a non-zero capacity
@@ -224,6 +240,7 @@ impl Client {
let initial_connection_timeout = self.config.initial_connection_timeout;
let connections_count = self.connections_count.clone();
let noise_config = self.noise_config.clone();
tokio::spawn(async move {
// before executing the manager, wait for what was specified, if anything
if let Some(backoff) = backoff {
@@ -233,7 +250,8 @@ impl Client {
connections_count.fetch_add(1, Ordering::SeqCst);
ManagedConnection::new(
address.into(),
address,
noise_config,
receiver,
initial_connection_timeout,
current_reconnection_attempt,
@@ -246,18 +264,14 @@ impl Client {
}
impl SendWithoutResponse for Client {
fn send_without_response(
&self,
address: NymNodeRoutingAddress,
packet: NymPacket,
packet_type: PacketType,
) -> io::Result<()> {
trace!("Sending packet to {address:?}");
let framed_packet = FramedNymPacket::new(packet, packet_type);
fn send_without_response(&self, packet: MixPacket) -> io::Result<()> {
let address = packet.next_hop_address();
trace!("Sending packet to {address}");
let framed_packet = FramedNymPacket::from(packet);
let Some(sender) = self.active_connections.get_mut(&address) else {
// there was never a connection to begin with
debug!("establishing initial connection to {}", address);
debug!("establishing initial connection to {address}");
// it's not a 'big' error, but we did not manage to send the packet, but queue the packet
// for sending for as soon as the connection is created
self.make_connection(address, framed_packet);
@@ -302,8 +316,12 @@ impl SendWithoutResponse for Client {
#[cfg(test)]
mod tests {
use super::*;
use nym_crypto::asymmetric::x25519;
use nym_noise::config::NoiseNetworkView;
use rand::rngs::OsRng;
fn dummy_client() -> Client {
let mut rng = OsRng; //for test only, so we don't care if rng source isn't crypto grade
Client::new(
Config {
initial_reconnection_backoff: Duration::from_millis(10_000),
@@ -311,6 +329,11 @@ mod tests {
initial_connection_timeout: Duration::from_millis(1_500),
maximum_connection_buffer_size: 128,
},
NoiseConfig::new(
Arc::new(x25519::KeyPair::new(&mut rng)),
NoiseNetworkView::new_empty(),
Duration::from_millis(1_500),
),
Default::default(),
)
}
@@ -19,6 +19,7 @@ nym-vesting-contract-common = { path = "../../cosmwasm-smart-contracts/vesting-c
nym-ecash-contract-common = { path = "../../cosmwasm-smart-contracts/ecash-contract" }
nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" }
nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" }
nym-performance-contract-common = { path = "../../cosmwasm-smart-contracts/nym-performance-contract" }
nym-serde-helpers = { path = "../../serde-helpers", features = ["hex", "base64"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
@@ -16,8 +16,8 @@ async fn main() {
let prefix = "n";
let denom: Denom = "unym".parse().unwrap();
let signer_mnemonic: bip39::Mnemonic = "<MNEMONIC WITH FUNDS HERE>".parse().unwrap();
let validator = "https://qwerty-validator.qa.nymte.ch";
let to_address: AccountId = "n19kdst4srf76xgwe55jg32mpcpcyf6aqgp6qrdk".parse().unwrap();
let validator = "https://rpc.sandbox.nymtech.net";
let to_address: AccountId = "n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa".parse().unwrap();
let signer = DirectSecp256k1HdWallet::from_mnemonic(prefix, signer_mnemonic);
let signer_address = signer.try_derive_accounts().unwrap()[0].address().clone();
+106 -63
View File
@@ -25,7 +25,9 @@ use nym_api_requests::models::{
NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse,
};
use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated};
use nym_api_requests::nym_nodes::{NodesByAddressesResponse, SkimmedNode};
use nym_api_requests::nym_nodes::{
NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata,
};
use nym_coconut_dkg_common::types::EpochId;
use nym_http_api_client::UserAgent;
use nym_mixnet_contract_common::EpochRewardedSet;
@@ -46,6 +48,46 @@ use crate::rpc::http_client;
#[cfg(feature = "http-client")]
use crate::{DirectSigningHttpRpcValidatorClient, HttpRpcClient, QueryHttpRpcValidatorClient};
// a simple helper macro to define to repeatedly call a paged query until a full response is constructed
macro_rules! collect_paged_skimmed_v2 {
( $self:ident, $f: ident ) => {{
// unroll first loop iteration in order to obtain the metadata
let mut page = 0;
let res = $self
.nym_api
.$f(false, Some(page), None, $self.use_bincode)
.await?;
let mut nodes = res.nodes.data;
let metadata = res.metadata;
if res.nodes.pagination.total == nodes.len() {
return Ok(SkimmedNodesWithMetadata::new(nodes, metadata));
}
page += 1;
loop {
let mut res = $self
.nym_api
.$f(false, Some(page), None, $self.use_bincode)
.await?;
if metadata != res.metadata {
return Err(ValidatorClientError::InconsistentPagedMetadata);
}
nodes.append(&mut res.nodes.data);
if nodes.len() < res.nodes.pagination.total {
page += 1
} else {
break;
}
}
Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
}};
}
#[must_use]
#[derive(Debug, Clone)]
pub struct Config {
@@ -200,11 +242,11 @@ impl<C, S> Client<C, S> {
#[allow(deprecated)]
impl<C, S> Client<C, S> {
pub fn api_url(&self) -> &Url {
self.nym_api.current_url()
self.nym_api.current_url().as_ref()
}
pub fn change_nym_api(&mut self, new_endpoint: Url) {
self.nym_api.change_base_url(new_endpoint)
self.nym_api.change_base_urls(vec![new_endpoint.into()])
}
#[deprecated]
@@ -402,11 +444,11 @@ impl NymApiClient {
}
pub fn api_url(&self) -> &Url {
self.nym_api.current_url()
self.nym_api.current_url().as_ref()
}
pub fn change_nym_api(&mut self, new_endpoint: Url) {
self.nym_api.change_base_url(new_endpoint);
self.nym_api.change_base_urls(vec![new_endpoint.into()]);
}
#[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes instead")]
@@ -425,92 +467,93 @@ impl NymApiClient {
/// retrieve basic information for nodes are capable of operating as an entry gateway
/// this includes legacy gateways and nym-nodes
#[deprecated(note = "use get_all_basic_entry_assigned_nodes_with_metadata instead")]
pub async fn get_all_basic_entry_assigned_nodes(
&self,
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
// 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 nodes = Vec::new();
self.get_all_basic_entry_assigned_nodes_v2()
.await
.map(|res| res.nodes)
}
loop {
let mut res = self
.nym_api
.get_basic_entry_assigned_nodes(false, Some(page), None, self.use_bincode)
.await?;
nodes.append(&mut res.nodes.data);
if nodes.len() < res.nodes.pagination.total {
page += 1
} else {
break;
}
}
Ok(nodes)
pub async fn get_all_basic_entry_assigned_nodes_v2(
&self,
) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
collect_paged_skimmed_v2!(self, get_basic_entry_assigned_nodes_v2)
}
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
/// this includes legacy mixnodes and nym-nodes
#[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes_with_metadata instead")]
pub async fn get_all_basic_active_mixing_assigned_nodes(
&self,
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
// 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 nodes = Vec::new();
self.get_all_basic_active_mixing_assigned_nodes_with_metadata()
.await
.map(|res| res.nodes)
}
loop {
let mut res = self
.nym_api
.get_basic_active_mixing_assigned_nodes(false, Some(page), None, self.use_bincode)
.await?;
nodes.append(&mut res.nodes.data);
if nodes.len() < res.nodes.pagination.total {
page += 1
} else {
break;
}
}
Ok(nodes)
pub async fn get_all_basic_active_mixing_assigned_nodes_with_metadata(
&self,
) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
collect_paged_skimmed_v2!(self, get_basic_active_mixing_assigned_nodes_v2)
}
/// retrieve basic information for nodes are capable of operating as a mixnode
/// this includes legacy mixnodes and nym-nodes
#[deprecated(note = "use get_all_basic_mixing_capable_nodes_with_metadata instead")]
pub async fn get_all_basic_mixing_capable_nodes(
&self,
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
// 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 nodes = Vec::new();
self.get_all_basic_mixing_capable_nodes_with_metadata()
.await
.map(|res| res.nodes)
}
loop {
let mut res = self
.nym_api
.get_basic_mixing_capable_nodes(false, Some(page), None, self.use_bincode)
.await?;
nodes.append(&mut res.nodes.data);
if nodes.len() < res.nodes.pagination.total {
page += 1
} else {
break;
}
}
Ok(nodes)
pub async fn get_all_basic_mixing_capable_nodes_with_metadata(
&self,
) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
collect_paged_skimmed_v2!(self, get_basic_mixing_capable_nodes_v2)
}
/// retrieve basic information for all bonded nodes on the network
#[deprecated(note = "use get_all_basic_nodes_with_metadata instead")]
pub async fn get_all_basic_nodes(&self) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
self.get_all_basic_nodes_with_metadata()
.await
.map(|res| res.nodes)
}
pub async fn get_all_basic_nodes_with_metadata(
&self,
) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
collect_paged_skimmed_v2!(self, get_basic_nodes_v2)
}
/// retrieve expanded information for all bonded nodes on the network
pub async fn get_all_expanded_nodes(
&self,
) -> Result<SemiSkimmedNodesWithMetadata, ValidatorClientError> {
// Unroll the first iteration to get the metadata
let mut page = 0;
let mut nodes = Vec::new();
let res = self
.nym_api
.get_expanded_nodes(false, Some(page), None)
.await?;
let mut nodes = res.nodes.data;
let metadata = res.metadata;
if res.nodes.pagination.total == nodes.len() {
return Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata));
}
page += 1;
loop {
let mut res = self
.nym_api
.get_basic_nodes(false, Some(page), None, self.use_bincode)
.get_expanded_nodes(false, Some(page), None)
.await?;
nodes.append(&mut res.nodes.data);
@@ -521,7 +564,7 @@ impl NymApiClient {
}
}
Ok(nodes)
Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata))
}
pub async fn health(&self) -> Result<ApiHealthResponse, ValidatorClientError> {
@@ -22,6 +22,9 @@ pub enum ValidatorClientError {
#[error("nyxd request failed: {0}")]
NyxdError(#[from] crate::nyxd::error::NyxdError),
#[error("the response metadata has changed between pages")]
InconsistentPagedMetadata,
#[error("No validator API url has been provided")]
NoAPIUrlAvailable,
}
@@ -14,11 +14,12 @@ use nym_api_requests::ecash::models::{
use nym_api_requests::ecash::VerificationKeyResponse;
use nym_api_requests::models::{
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainStatusResponse,
LegacyDescribedMixNode, NodePerformanceResponse, NodeRefreshBody, NymNodeDescription,
PerformanceHistoryResponse, RewardedSetResponse,
KeyRotationInfoResponse, LegacyDescribedMixNode, NodePerformanceResponse, NodeRefreshBody,
NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
};
use nym_api_requests::nym_nodes::{
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponse,
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1,
PaginatedCachedNodesResponseV2,
};
use nym_api_requests::pagination::PaginatedResponse;
pub use nym_api_requests::{
@@ -34,7 +35,7 @@ pub use nym_api_requests::{
MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse,
StakeSaturationResponse, UptimeResponse,
},
nym_nodes::{CachedNodesResponse, SkimmedNode},
nym_nodes::{CachedNodesResponse, SemiSkimmedNode, SkimmedNode},
NymNetworkDetailsResponse,
};
use nym_contracts_common::IdentityKey;
@@ -62,7 +63,7 @@ pub trait NymApiClientExt: ApiClient {
async fn health(&self) -> Result<ApiHealthResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::API_STATUS_ROUTES,
routes::HEALTH,
],
@@ -75,7 +76,7 @@ pub trait NymApiClientExt: ApiClient {
async fn build_information(&self) -> Result<BinaryBuildInformationOwned, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::API_STATUS_ROUTES,
routes::BUILD_INFORMATION,
],
@@ -87,7 +88,7 @@ pub trait NymApiClientExt: ApiClient {
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
self.get_json(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS)
self.get_json(&[routes::V1_API_VERSION, routes::MIXNODES], NO_PARAMS)
.await
}
@@ -96,7 +97,7 @@ pub trait NymApiClientExt: ApiClient {
async fn get_mixnodes_detailed(&self) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS,
routes::MIXNODES,
routes::DETAILED,
@@ -111,7 +112,7 @@ pub trait NymApiClientExt: ApiClient {
async fn get_gateways_detailed(&self) -> Result<Vec<GatewayBondAnnotated>, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS,
routes::GATEWAYS,
routes::DETAILED,
@@ -128,7 +129,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<Vec<GatewayBondAnnotated>, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS,
routes::GATEWAYS,
routes::DETAILED_UNFILTERED,
@@ -145,7 +146,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS,
routes::MIXNODES,
routes::DETAILED_UNFILTERED,
@@ -158,7 +159,7 @@ pub trait NymApiClientExt: ApiClient {
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_gateways(&self) -> Result<Vec<GatewayBond>, NymAPIError> {
self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS)
self.get_json(&[routes::V1_API_VERSION, routes::GATEWAYS], NO_PARAMS)
.await
}
@@ -166,7 +167,7 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_gateways_described(&self) -> Result<Vec<LegacyDescribedGateway>, NymAPIError> {
self.get_json(
&[routes::API_VERSION, routes::GATEWAYS, routes::DESCRIBED],
&[routes::V1_API_VERSION, routes::GATEWAYS, routes::DESCRIBED],
NO_PARAMS,
)
.await
@@ -176,7 +177,7 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_mixnodes_described(&self) -> Result<Vec<LegacyDescribedMixNode>, NymAPIError> {
self.get_json(
&[routes::API_VERSION, routes::MIXNODES, routes::DESCRIBED],
&[routes::V1_API_VERSION, routes::MIXNODES, routes::DESCRIBED],
NO_PARAMS,
)
.await
@@ -201,7 +202,7 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::NYM_NODES_ROUTES,
routes::NYM_NODES_PERFORMANCE_HISTORY,
&*node_id.to_string(),
@@ -229,7 +230,7 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::NYM_NODES_ROUTES,
routes::NYM_NODES_DESCRIBED,
],
@@ -256,7 +257,7 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::NYM_NODES_ROUTES,
routes::NYM_NODES_BONDED,
],
@@ -270,7 +271,7 @@ pub trait NymApiClientExt: ApiClient {
async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"mixnodes",
@@ -286,7 +287,7 @@ pub trait NymApiClientExt: ApiClient {
async fn get_basic_gateways(&self) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"gateways",
@@ -301,7 +302,7 @@ pub trait NymApiClientExt: ApiClient {
async fn get_rewarded_set(&self) -> Result<RewardedSetResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::NYM_NODES_ROUTES,
routes::NYM_NODES_REWARDED_SET,
],
@@ -312,6 +313,7 @@ pub trait NymApiClientExt: ApiClient {
/// retrieve basic information for nodes are capable of operating as an entry gateway
/// this includes legacy gateways and nym-nodes
#[deprecated(note = "use get_basic_entry_assigned_nodes_v2")]
#[instrument(level = "debug", skip(self))]
async fn get_basic_entry_assigned_nodes(
&self,
@@ -319,7 +321,7 @@ pub trait NymApiClientExt: ApiClient {
page: Option<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
@@ -340,7 +342,49 @@ pub trait NymApiClientExt: ApiClient {
self.get_response(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"skimmed",
"entry-gateways",
"all",
],
&params,
)
.await
}
/// retrieve basic information for nodes are capable of operating as an entry gateway
/// this includes legacy gateways and nym-nodes
#[instrument(level = "debug", skip(self))]
async fn get_basic_entry_assigned_nodes_v2(
&self,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
params.push(("no_legacy", "true".to_string()))
}
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()))
}
if use_bincode {
params.push(("output", "bincode".to_string()))
}
self.get_response(
&[
routes::V2_API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"skimmed",
@@ -354,6 +398,7 @@ pub trait NymApiClientExt: ApiClient {
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
/// this includes legacy mixnodes and nym-nodes
#[deprecated(note = "use get_basic_active_mixing_assigned_nodes_v2")]
#[instrument(level = "debug", skip(self))]
async fn get_basic_active_mixing_assigned_nodes(
&self,
@@ -361,7 +406,7 @@ pub trait NymApiClientExt: ApiClient {
page: Option<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
@@ -382,7 +427,7 @@ pub trait NymApiClientExt: ApiClient {
self.get_response(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"skimmed",
@@ -397,13 +442,13 @@ pub trait NymApiClientExt: ApiClient {
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
/// this includes legacy mixnodes and nym-nodes
#[instrument(level = "debug", skip(self))]
async fn get_basic_mixing_capable_nodes(
async fn get_basic_active_mixing_assigned_nodes_v2(
&self,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
@@ -424,7 +469,50 @@ pub trait NymApiClientExt: ApiClient {
self.get_response(
&[
routes::API_VERSION,
routes::V2_API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"skimmed",
"mixnodes",
"active",
],
&params,
)
.await
}
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
/// this includes legacy mixnodes and nym-nodes
#[deprecated(note = "use get_basic_mixing_capable_nodes_v2")]
#[instrument(level = "debug", skip(self))]
async fn get_basic_mixing_capable_nodes(
&self,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
params.push(("no_legacy", "true".to_string()))
}
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()))
}
if use_bincode {
params.push(("output", "bincode".to_string()))
}
self.get_response(
&[
routes::V1_API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"skimmed",
@@ -436,14 +524,16 @@ pub trait NymApiClientExt: ApiClient {
.await
}
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
/// this includes legacy mixnodes and nym-nodes
#[instrument(level = "debug", skip(self))]
async fn get_basic_nodes(
async fn get_basic_mixing_capable_nodes_v2(
&self,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
@@ -464,10 +554,122 @@ pub trait NymApiClientExt: ApiClient {
self.get_response(
&[
routes::API_VERSION,
routes::V2_API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"skimmed",
"mixnodes",
"all",
],
&params,
)
.await
}
#[deprecated(note = "use get_basic_nodes_v2")]
#[instrument(level = "debug", skip(self))]
async fn get_basic_nodes(
&self,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
params.push(("no_legacy", "true".to_string()))
}
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()))
}
if use_bincode {
params.push(("output", "bincode".to_string()))
}
self.get_response(
&[
routes::V1_API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"skimmed",
],
&params,
)
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_basic_nodes_v2(
&self,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
params.push(("no_legacy", "true".to_string()))
}
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()))
}
if use_bincode {
params.push(("output", "bincode".to_string()))
}
self.get_response(
&[
routes::V2_API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"skimmed",
],
&params,
)
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_expanded_nodes(
&self,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedCachedNodesResponseV2<SemiSkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
params.push(("no_legacy", "true".to_string()))
}
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::V2_API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"semi-skimmed",
],
&params,
)
@@ -478,7 +680,7 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
self.get_json(
&[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE],
&[routes::V1_API_VERSION, routes::MIXNODES, routes::ACTIVE],
NO_PARAMS,
)
.await
@@ -489,7 +691,7 @@ pub trait NymApiClientExt: ApiClient {
async fn get_active_mixnodes_detailed(&self) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS,
routes::MIXNODES,
routes::ACTIVE,
@@ -504,7 +706,7 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_rewarded_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
self.get_json(
&[routes::API_VERSION, routes::MIXNODES, routes::REWARDED],
&[routes::V1_API_VERSION, routes::MIXNODES, routes::REWARDED],
NO_PARAMS,
)
.await
@@ -518,7 +720,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<MixnodeStatusReportResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS,
routes::MIXNODE,
&mix_id.to_string(),
@@ -537,7 +739,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<GatewayStatusReportResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS,
routes::GATEWAY,
identity,
@@ -556,7 +758,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<MixnodeUptimeHistoryResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS,
routes::MIXNODE,
&mix_id.to_string(),
@@ -575,7 +777,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<GatewayUptimeHistoryResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS,
routes::GATEWAY,
identity,
@@ -593,7 +795,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS,
routes::MIXNODES,
routes::REWARDED,
@@ -614,7 +816,7 @@ pub trait NymApiClientExt: ApiClient {
if let Some(since) = since {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS_ROUTES,
routes::GATEWAY,
identity,
@@ -626,7 +828,7 @@ pub trait NymApiClientExt: ApiClient {
} else {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS_ROUTES,
routes::GATEWAY,
identity,
@@ -647,7 +849,7 @@ pub trait NymApiClientExt: ApiClient {
if let Some(since) = since {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODE,
&mix_id.to_string(),
@@ -659,7 +861,7 @@ pub trait NymApiClientExt: ApiClient {
} else {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODE,
&mix_id.to_string(),
@@ -679,7 +881,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<MixnodeStatusResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODE,
&mix_id.to_string(),
@@ -698,7 +900,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<RewardEstimationResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODE,
&mix_id.to_string(),
@@ -718,7 +920,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<RewardEstimationResponse, NymAPIError> {
self.post_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODE,
&mix_id.to_string(),
@@ -738,7 +940,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<StakeSaturationResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODE,
&mix_id.to_string(),
@@ -758,7 +960,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<nym_api_requests::models::InclusionProbabilityResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODE,
&mix_id.to_string(),
@@ -776,7 +978,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<NodePerformanceResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::NYM_NODES_ROUTES,
routes::NYM_NODES_PERFORMANCE,
&node_id.to_string(),
@@ -792,7 +994,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<AnnotationResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::NYM_NODES_ROUTES,
routes::NYM_NODES_ANNOTATION,
&node_id.to_string(),
@@ -806,7 +1008,7 @@ pub trait NymApiClientExt: ApiClient {
async fn get_mixnode_avg_uptime(&self, mix_id: NodeId) -> Result<UptimeResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODE,
&mix_id.to_string(),
@@ -821,7 +1023,11 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_mixnodes_blacklisted(&self) -> Result<Vec<NodeId>, NymAPIError> {
self.get_json(
&[routes::API_VERSION, routes::MIXNODES, routes::BLACKLISTED],
&[
routes::V1_API_VERSION,
routes::MIXNODES,
routes::BLACKLISTED,
],
NO_PARAMS,
)
.await
@@ -831,7 +1037,11 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_gateways_blacklisted(&self) -> Result<Vec<IdentityKey>, NymAPIError> {
self.get_json(
&[routes::API_VERSION, routes::GATEWAYS, routes::BLACKLISTED],
&[
routes::V1_API_VERSION,
routes::GATEWAYS,
routes::BLACKLISTED,
],
NO_PARAMS,
)
.await
@@ -844,7 +1054,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<BlindedSignatureResponse, NymAPIError> {
self.post_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::ECASH_ROUTES,
routes::ECASH_BLIND_SIGN,
],
@@ -861,7 +1071,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<EcashTicketVerificationResponse, NymAPIError> {
self.post_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::ECASH_ROUTES,
routes::VERIFY_ECASH_TICKET,
],
@@ -878,7 +1088,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<EcashBatchTicketRedemptionResponse, NymAPIError> {
self.post_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::ECASH_ROUTES,
routes::BATCH_REDEEM_ECASH_TICKETS,
],
@@ -903,7 +1113,7 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::ECASH_ROUTES,
routes::PARTIAL_EXPIRATION_DATE_SIGNATURES,
],
@@ -924,7 +1134,7 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::ECASH_ROUTES,
routes::PARTIAL_COIN_INDICES_SIGNATURES,
],
@@ -948,7 +1158,7 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::ECASH_ROUTES,
routes::GLOBAL_EXPIRATION_DATE_SIGNATURES,
],
@@ -969,7 +1179,7 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::ECASH_ROUTES,
routes::GLOBAL_COIN_INDICES_SIGNATURES,
],
@@ -989,7 +1199,7 @@ pub trait NymApiClientExt: ApiClient {
};
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::ECASH_ROUTES,
ecash::MASTER_VERIFICATION_KEY,
],
@@ -1005,7 +1215,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<(), NymAPIError> {
self.post_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::NYM_NODES_ROUTES,
routes::NYM_NODES_REFRESH_DESCRIBED,
],
@@ -1022,7 +1232,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<IssuedTicketbooksForResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::ECASH_ROUTES,
routes::ECASH_ISSUED_TICKETBOOKS_FOR,
&expiration_date.to_string(),
@@ -1039,7 +1249,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<IssuedTicketbooksForCountResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::ECASH_ROUTES,
routes::ECASH_ISSUED_TICKETBOOKS_FOR_COUNT,
&expiration_date.to_string(),
@@ -1056,7 +1266,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<IssuedTicketbooksChallengeCommitmentResponse, NymAPIError> {
self.post_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::ECASH_ROUTES,
routes::ECASH_ISSUED_TICKETBOOKS_CHALLENGE_COMMITMENT,
],
@@ -1073,7 +1283,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<IssuedTicketbooksDataResponse, NymAPIError> {
self.post_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
routes::ECASH_ROUTES,
routes::ECASH_ISSUED_TICKETBOOKS_DATA,
],
@@ -1089,7 +1299,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<NodesByAddressesResponse, NymAPIError> {
self.post_json(
&[
routes::API_VERSION,
routes::V1_API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
routes::nym_nodes::BY_ADDRESSES,
@@ -1103,7 +1313,7 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_network_details(&self) -> Result<NymNetworkDetailsResponse, NymAPIError> {
self.get_json(
&[routes::API_VERSION, routes::NETWORK, routes::DETAILS],
&[routes::V1_API_VERSION, routes::NETWORK, routes::DETAILS],
NO_PARAMS,
)
.await
@@ -1112,7 +1322,24 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_chain_status(&self) -> Result<ChainStatusResponse, NymAPIError> {
self.get_json(
&[routes::API_VERSION, routes::NETWORK, routes::CHAIN_STATUS],
&[
routes::V1_API_VERSION,
routes::NETWORK,
routes::CHAIN_STATUS,
],
NO_PARAMS,
)
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_key_rotation_info(&self) -> Result<KeyRotationInfoResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::EPOCH,
routes::KEY_ROTATION_INFO,
],
NO_PARAMS,
)
.await
@@ -1,9 +1,8 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_network_defaults::NYM_API_VERSION;
pub const API_VERSION: &str = NYM_API_VERSION;
pub const V1_API_VERSION: &str = "v1";
pub const V2_API_VERSION: &str = "v2";
pub const MIXNODES: &str = "mixnodes";
pub const GATEWAYS: &str = "gateways";
pub const DESCRIBED: &str = "described";
@@ -79,3 +78,11 @@ pub const SERVICE_PROVIDERS: &str = "services";
pub const DETAILS: &str = "details";
pub const CHAIN_STATUS: &str = "chain-status";
pub const NETWORK: &str = "network";
pub const EPOCH: &str = "epoch";
pub use epoch_routes::*;
pub mod epoch_routes {
pub const CURRENT: &str = "current";
pub const KEY_ROTATION_INFO: &str = "key-rotation-info";
}
@@ -12,8 +12,8 @@ use nym_mixnet_contract_common::gateway::{PreassignedGatewayIdsResponse, Preassi
use nym_mixnet_contract_common::nym_node::{
EpochAssignmentResponse, NodeDetailsByIdentityResponse, NodeDetailsResponse,
NodeOwnershipResponse, NodeRewardingDetailsResponse, PagedNymNodeBondsResponse,
PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse,
StakeSaturationResponse, UnbondedNodeResponse, UnbondedNymNode,
PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, RewardedSetMetadata, Role,
RolesMetadataResponse, StakeSaturationResponse, UnbondedNodeResponse, UnbondedNymNode,
};
use nym_mixnet_contract_common::reward_params::WorkFactor;
use nym_mixnet_contract_common::{
@@ -28,12 +28,12 @@ use nym_mixnet_contract_common::{
ContractBuildInformation, ContractState, ContractStateParams, CurrentIntervalResponse,
CurrentNymNodeVersionResponse, Delegation, EpochEventId, EpochRewardedSet, EpochStatus,
GatewayBond, GatewayBondResponse, GatewayOwnershipResponse, HistoricalNymNodeVersionEntry,
IdentityKey, IdentityKeyRef, IntervalEventId, MixNodeBond, MixNodeDetails,
MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeId,
NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails, NymNodeVersionHistoryResponse,
PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedGatewayResponse,
PagedMixnodeBondsResponse, PagedNodeDelegationsResponse, PendingEpochEvent,
PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEvent,
IdentityKey, IdentityKeyRef, IntervalEventId, KeyRotationIdResponse, KeyRotationState,
MixNodeBond, MixNodeDetails, MixOwnershipResponse, MixnodeDetailsByIdentityResponse,
MixnodeDetailsResponse, NodeId, NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails,
NymNodeVersionHistoryResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse,
PagedGatewayResponse, PagedMixnodeBondsResponse, PagedNodeDelegationsResponse,
PendingEpochEvent, PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEvent,
PendingIntervalEventResponse, PendingIntervalEventsResponse, QueryMsg as MixnetQueryMsg,
RewardedSet, UnbondedMixnode,
};
@@ -546,6 +546,16 @@ pub trait MixnetQueryClient {
})
.await
}
async fn get_key_rotation_state(&self) -> Result<KeyRotationState, NyxdError> {
self.query_mixnet_contract(MixnetQueryMsg::GetKeyRotationState {})
.await
}
async fn get_key_rotation_id(&self) -> Result<KeyRotationIdResponse, NyxdError> {
self.query_mixnet_contract(MixnetQueryMsg::GetKeyRotationId {})
.await
}
}
// extension trait to the query client to deal with the paged queries
@@ -673,12 +683,20 @@ pub trait MixnetQueryClientExt: MixnetQueryClient {
async fn get_rewarded_set(&self) -> Result<EpochRewardedSet, NyxdError> {
let error_response = |message| Err(NyxdError::extension_query_failure("mixnet", message));
// bypass for catch 22 for fresh contracts. we can't refresh cache because there's no rewarded set,
// but we can't set the rewarded set because we didn't refresh the cache
let metadata = self.get_rewarded_set_metadata().await?;
if !metadata.metadata.fully_assigned {
let is_default = metadata.metadata == RewardedSetMetadata::default();
if !metadata.metadata.fully_assigned && !is_default {
return error_response("the rewarded set hasn't been fully assigned for this epoch");
}
let expected_epoch_id = metadata.metadata.epoch_id;
if is_default {
return Ok(Default::default());
}
// if we have to query those things more frequently, we could do it concurrently,
// but as it stands now, it happens so infrequently it might as well be sequential
let entry = self.get_role_assignment(Role::EntryGateway).await?;
@@ -955,6 +973,8 @@ mod tests {
QueryMsg::GetNymNodeVersionHistory { limit, start_after } => client
.get_nym_node_version_history_paged(start_after, limit)
.ignore(),
QueryMsg::GetKeyRotationState {} => client.get_key_rotation_state().ignore(),
QueryMsg::GetKeyRotationId {} => client.get_key_rotation_id().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 performance_query_client;
pub mod vesting_query_client;
// signing clients
@@ -21,6 +22,7 @@ pub mod ecash_signing_client;
pub mod group_signing_client;
pub mod mixnet_signing_client;
pub mod multisig_signing_client;
pub mod performance_signing_client;
pub mod vesting_signing_client;
// re-export query traits
@@ -29,6 +31,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 performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient};
pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient};
// re-export signing traits
@@ -37,6 +40,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 performance_signing_client::PerformanceSigningClient;
pub use vesting_signing_client::VestingSigningClient;
// helper for providing blanket implementation for query clients
@@ -44,6 +48,7 @@ pub trait NymContractsProvider {
// main
fn mixnet_contract_address(&self) -> Option<&AccountId>;
fn vesting_contract_address(&self) -> Option<&AccountId>;
fn performance_contract_address(&self) -> Option<&AccountId>;
// coconut-related
fn ecash_contract_address(&self) -> Option<&AccountId>;
@@ -56,6 +61,7 @@ pub trait NymContractsProvider {
pub struct TypedNymContracts {
pub mixnet_contract_address: Option<AccountId>,
pub vesting_contract_address: Option<AccountId>,
pub performance_contract_address: Option<AccountId>,
pub ecash_contract_address: Option<AccountId>,
pub group_contract_address: Option<AccountId>,
@@ -76,6 +82,10 @@ impl TryFrom<NymContracts> for TypedNymContracts {
.vesting_contract_address
.map(|addr| addr.parse())
.transpose()?,
performance_contract_address: value
.performance_contract_address
.map(|addr| addr.parse())
.transpose()?,
ecash_contract_address: value
.ecash_contract_address
.map(|addr| addr.parse())
@@ -0,0 +1,265 @@
// Copyright 2025 - 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;
pub use nym_performance_contract_common::{
msg::QueryMsg as PerformanceQueryMsg, types::NetworkMonitorResponse,
};
use nym_performance_contract_common::{
EpochId, EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse,
FullHistoricalPerformancePagedResponse, HistoricalPerformance, NetworkMonitorInformation,
NetworkMonitorsPagedResponse, NodeId, NodeMeasurement, NodeMeasurementsResponse,
NodePerformance, NodePerformancePagedResponse, NodePerformanceResponse, RetiredNetworkMonitor,
RetiredNetworkMonitorsPagedResponse,
};
use serde::Deserialize;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait PerformanceQueryClient {
async fn query_performance_contract<T>(
&self,
query: PerformanceQueryMsg,
) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>;
async fn admin(&self) -> Result<cw_controllers::AdminResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::Admin {})
.await
}
async fn get_node_performance(
&self,
epoch_id: EpochId,
node_id: NodeId,
) -> Result<NodePerformanceResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::NodePerformance { epoch_id, node_id })
.await
}
async fn get_node_performance_paged(
&self,
node_id: NodeId,
start_after: Option<EpochId>,
limit: Option<u32>,
) -> Result<NodePerformancePagedResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::NodePerformancePaged {
node_id,
start_after,
limit,
})
.await
}
async fn get_node_measurements(
&self,
epoch_id: EpochId,
node_id: NodeId,
) -> Result<NodeMeasurementsResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::NodeMeasurements { epoch_id, node_id })
.await
}
async fn get_epoch_measurements_paged(
&self,
epoch_id: EpochId,
start_after: Option<NodeId>,
limit: Option<u32>,
) -> Result<EpochMeasurementsPagedResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::EpochMeasurementsPaged {
epoch_id,
start_after,
limit,
})
.await
}
async fn get_epoch_performance_paged(
&self,
epoch_id: EpochId,
start_after: Option<NodeId>,
limit: Option<u32>,
) -> Result<EpochPerformancePagedResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::EpochPerformancePaged {
epoch_id,
start_after,
limit,
})
.await
}
async fn get_full_historical_performance_paged(
&self,
start_after: Option<(EpochId, NodeId)>,
limit: Option<u32>,
) -> Result<FullHistoricalPerformancePagedResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::FullHistoricalPerformancePaged {
start_after,
limit,
})
.await
}
async fn get_network_monitor(
&self,
address: &AccountId,
) -> Result<NetworkMonitorResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::NetworkMonitor {
address: address.to_string(),
})
.await
}
async fn get_network_monitors_paged(
&self,
start_after: Option<String>,
limit: Option<u32>,
) -> Result<NetworkMonitorsPagedResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::NetworkMonitorsPaged {
start_after,
limit,
})
.await
}
async fn get_retired_network_monitors_paged(
&self,
start_after: Option<String>,
limit: Option<u32>,
) -> Result<RetiredNetworkMonitorsPagedResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::RetiredNetworkMonitorsPaged {
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 PagedPerformanceQueryClient: PerformanceQueryClient {
async fn get_all_node_performance(
&self,
node_id: NodeId,
) -> Result<Vec<EpochNodePerformance>, NyxdError> {
collect_paged!(self, get_node_performance_paged, performance, node_id)
}
async fn get_all_epoch_measurements(
&self,
node_id: NodeId,
) -> Result<Vec<NodeMeasurement>, NyxdError> {
collect_paged!(self, get_epoch_measurements_paged, measurements, node_id)
}
async fn get_all_epoch_performance(
&self,
epoch_id: EpochId,
) -> Result<Vec<NodePerformance>, NyxdError> {
collect_paged!(self, get_epoch_performance_paged, performance, epoch_id)
}
async fn get_all_full_historical_performance(
&self,
) -> Result<Vec<HistoricalPerformance>, NyxdError> {
collect_paged!(self, get_full_historical_performance_paged, performance)
}
async fn get_all_network_monitors(&self) -> Result<Vec<NetworkMonitorInformation>, NyxdError> {
collect_paged!(self, get_network_monitors_paged, info)
}
async fn get_all_retired_network_monitors(
&self,
) -> Result<Vec<RetiredNetworkMonitor>, NyxdError> {
collect_paged!(self, get_retired_network_monitors_paged, info)
}
}
#[async_trait]
impl<T> PagedPerformanceQueryClient for T where T: PerformanceQueryClient {}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> PerformanceQueryClient for C
where
C: CosmWasmClient + NymContractsProvider + Send + Sync,
{
async fn query_performance_contract<T>(
&self,
query: PerformanceQueryMsg,
) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>,
{
let performance_contract_address = &self
.performance_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("performance contract"))?;
self.query_contract_smart(performance_contract_address, &query)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
fn all_query_variants_are_covered<C: PerformanceQueryClient + Send + Sync>(
client: C,
msg: PerformanceQueryMsg,
) {
match msg {
PerformanceQueryMsg::Admin {} => client.admin().ignore(),
PerformanceQueryMsg::NodePerformance { epoch_id, node_id } => {
client.get_node_performance(epoch_id, node_id).ignore()
}
PerformanceQueryMsg::NodePerformancePaged {
node_id,
start_after,
limit,
} => client
.get_node_performance_paged(node_id, start_after, limit)
.ignore(),
PerformanceQueryMsg::NodeMeasurements { epoch_id, node_id } => {
client.get_node_measurements(epoch_id, node_id).ignore()
}
PerformanceQueryMsg::EpochMeasurementsPaged {
epoch_id,
start_after,
limit,
} => client
.get_epoch_measurements_paged(epoch_id, start_after, limit)
.ignore(),
PerformanceQueryMsg::EpochPerformancePaged {
epoch_id,
start_after,
limit,
} => client
.get_epoch_performance_paged(epoch_id, start_after, limit)
.ignore(),
PerformanceQueryMsg::FullHistoricalPerformancePaged { start_after, limit } => client
.get_full_historical_performance_paged(start_after, limit)
.ignore(),
PerformanceQueryMsg::NetworkMonitor { address } => client
.get_network_monitor(&address.parse().unwrap())
.ignore(),
PerformanceQueryMsg::NetworkMonitorsPaged { start_after, limit } => client
.get_network_monitors_paged(start_after, limit)
.ignore(),
PerformanceQueryMsg::RetiredNetworkMonitorsPaged { start_after, limit } => client
.get_retired_network_monitors_paged(start_after, limit)
.ignore(),
};
}
}
@@ -0,0 +1,217 @@
// Copyright 2025 - 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::cosmwasm_client::ContractResponseData;
use crate::nyxd::error::NyxdError;
use crate::nyxd::{Fee, SigningCosmWasmClient};
use crate::signing::signer::OfflineSigner;
use async_trait::async_trait;
use nym_performance_contract_common::{
EpochId, ExecuteMsg as PerformanceExecuteMsg, NodeId, NodePerformance,
RemoveEpochMeasurementsResponse,
};
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait PerformanceSigningClient {
async fn execute_performance_contract(
&self,
fee: Option<Fee>,
msg: PerformanceExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError>;
async fn update_admin(
&self,
admin: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_performance_contract(
fee,
PerformanceExecuteMsg::UpdateAdmin { admin },
"PerformanceContract::UpdateAdmin".to_string(),
vec![],
)
.await
}
async fn submit_performance(
&self,
epoch: EpochId,
data: NodePerformance,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_performance_contract(
fee,
PerformanceExecuteMsg::Submit { epoch, data },
"PerformanceContract::Submit".to_string(),
vec![],
)
.await
}
async fn batch_submit_performance(
&self,
epoch: EpochId,
data: Vec<NodePerformance>,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_performance_contract(
fee,
PerformanceExecuteMsg::BatchSubmit { epoch, data },
"PerformanceContract::BatchSubmit".to_string(),
vec![],
)
.await
}
async fn authorise_network_monitor(
&self,
address: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_performance_contract(
fee,
PerformanceExecuteMsg::AuthoriseNetworkMonitor { address },
"PerformanceContract::AuthoriseNetworkMonitor".to_string(),
vec![],
)
.await
}
async fn retire_network_monitor(
&self,
address: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_performance_contract(
fee,
PerformanceExecuteMsg::RetireNetworkMonitor { address },
"PerformanceContract::RetireNetworkMonitor".to_string(),
vec![],
)
.await
}
async fn remove_node_measurements(
&self,
epoch_id: EpochId,
node_id: NodeId,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_performance_contract(
fee,
PerformanceExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id },
"PerformanceContract::RemoveNodeMeasurements".to_string(),
vec![],
)
.await
}
async fn partial_remove_epoch_measurements(
&self,
epoch_id: EpochId,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_performance_contract(
fee,
PerformanceExecuteMsg::RemoveEpochMeasurements { epoch_id },
"PerformanceContract::RemoveEpochMeasurements".to_string(),
vec![],
)
.await
}
async fn remove_epoch_measurements(
&self,
epoch_id: EpochId,
fee: Option<Fee>,
) -> Result<(), NyxdError> {
loop {
let execute_res = self
.partial_remove_epoch_measurements(epoch_id, fee.clone())
.await?;
let response = execute_res
.parse_singleton_json_contract_response::<RemoveEpochMeasurementsResponse>()?;
if !response.additional_entries_to_remove_remaining {
break;
}
}
Ok(())
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> PerformanceSigningClient for C
where
C: SigningCosmWasmClient + NymContractsProvider + Sync,
NyxdError: From<<Self as OfflineSigner>::Error>,
{
async fn execute_performance_contract(
&self,
fee: Option<Fee>,
msg: PerformanceExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError> {
let performance_contract_address = &self
.performance_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("performance contract"))?;
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
let signer_address = &self.signer_addresses()?[0];
self.execute(
signer_address,
performance_contract_address,
&msg,
fee,
memo,
funds,
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
use nym_performance_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: PerformanceSigningClient + Send + Sync>(
client: C,
msg: PerformanceExecuteMsg,
) {
match msg {
PerformanceExecuteMsg::UpdateAdmin { admin } => {
client.update_admin(admin, None).ignore()
}
PerformanceExecuteMsg::Submit { epoch, data } => {
client.submit_performance(epoch, data, None).ignore()
}
PerformanceExecuteMsg::BatchSubmit { epoch, data } => {
client.batch_submit_performance(epoch, data, None).ignore()
}
PerformanceExecuteMsg::AuthoriseNetworkMonitor { address } => {
client.authorise_network_monitor(address, None).ignore()
}
PerformanceExecuteMsg::RetireNetworkMonitor { address } => {
client.retire_network_monitor(address, None).ignore()
}
ExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id } => client
.remove_node_measurements(epoch_id, node_id, None)
.ignore(),
ExecuteMsg::RemoveEpochMeasurements { epoch_id } => client
.partial_remove_epoch_measurements(epoch_id, None)
.ignore(),
};
}
}
@@ -12,6 +12,8 @@ use tendermint_rpc::endpoint::broadcast;
use tracing::error;
pub use cosmrs::abci::MsgResponse;
use cosmwasm_std::from_json;
use serde::de::DeserializeOwned;
pub fn parse_singleton_u32_from_contract_response(b: Vec<u8>) -> Result<u32, NyxdError> {
if b.len() != 4 {
@@ -73,6 +75,11 @@ pub fn parse_msg_responses(data: Bytes) -> Vec<MsgResponse> {
// requires there's a single response message
pub trait ContractResponseData: Sized {
fn parse_singleton_json_contract_response<T: DeserializeOwned>(&self) -> Result<T, NyxdError> {
let b = self.to_singleton_contract_data()?;
from_json(&b).map_err(|err| err.into())
}
fn parse_singleton_u32_contract_data(&self) -> Result<u32, NyxdError> {
let b = self.to_singleton_contract_data()?;
parse_singleton_u32_from_contract_response(b)
@@ -138,6 +138,14 @@ impl NyxdClient<HttpClient> {
config,
})
}
pub fn connect_to_default_env<U>(endpoint: U) -> Result<QueryHttpRpcNyxdClient, NyxdError>
where
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
{
let config = Config::try_from_nym_network_details(&NymNetworkDetails::new_from_env())?;
Self::connect(config, endpoint)
}
}
impl NyxdClient<ReqwestRpcClient> {
@@ -268,6 +276,10 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
self.config.contracts.vesting_contract_address.as_ref()
}
fn performance_contract_address(&self) -> Option<&AccountId> {
self.config.contracts.performance_contract_address.as_ref()
}
fn ecash_contract_address(&self) -> Option<&AccountId> {
self.config.contracts.ecash_contract_address.as_ref()
}
@@ -60,7 +60,7 @@ pub async fn send(args: Args, client: &SigningClient) {
"Nodesguru: https://nym.explorers.guru/transaction/{}",
&res.hash
);
println!("Mintscan: https://www.mintscan.io/nyx/txs/{}", &res.hash);
println!("Mintscan: https://ping.pub/nyx/tx/{}", &res.hash);
println!("Transaction result code: {}", &res.tx_result.code.value());
println!("Transaction hash: {}", &res.hash);
}
@@ -95,7 +95,7 @@ pub async fn send_multiple(args: Args, client: &SigningClient) {
"Nodesguru: https://nym.explorers.guru/transaction/{}",
&res.hash
);
println!("Mintscan: https://www.mintscan.io/nyx/txs/{}", &res.hash);
println!("Mintscan: https://ping.pub/nyx/tx/{}", &res.hash);
println!("Transaction result code: {}", &res.tx_result.code.value());
println!("Transaction hash: {}", &res.hash);
@@ -157,6 +157,7 @@ pub async fn generate(args: Args) {
minimum: args.minimum_interval_operating_cost.amount.into(),
maximum: args.maximum_interval_operating_cost.amount.into(),
},
key_validity_in_epochs: None,
};
debug!("instantiate_msg: {:?}", instantiate_msg);
@@ -76,7 +76,7 @@ pub async fn execute(args: Args, client: SigningClient) {
&res.transaction_hash
);
println!(
"Mintscan: https://www.mintscan.io/nyx/txs/{}",
"Mintscan: https://ping.pub/nyx/tx/{}",
&res.transaction_hash
);
println!("Transaction hash: {}", &res.transaction_hash);
+1 -2
View File
@@ -48,8 +48,7 @@ pub mod nym_config {
log::trace!("Loading from file: {:#?}", filepath.as_ref().to_owned());
let config_contents = fs::read_to_string(filepath)?;
toml::from_str(&config_contents)
.map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err))
toml::from_str(&config_contents).map_err(io::Error::other)
}
}
}
+1 -2
View File
@@ -151,8 +151,7 @@ where
let content = fs::read_to_string(path)?;
// TODO: should we be preserving original error type instead?
deserialize_config_from_toml_str(&content)
.map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err))
deserialize_config_from_toml_str(&content).map_err(io::Error::other)
}
//
@@ -0,0 +1,24 @@
[package]
name = "nym-contracts-common-testing"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
[dependencies]
anyhow = { workspace = true }
cosmwasm-std = { workspace = true }
cw-storage-plus = { workspace = true }
serde = { workspace = true }
rand_chacha = { workspace = true }
rand = { workspace = true }
cw-multi-test = { workspace = true }
[lints]
workspace = true
@@ -0,0 +1,127 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::testing::{message_info, MockApi, MockQuerier, MockStorage};
use cosmwasm_std::{
coins, Addr, BankMsg, CosmosMsg, Empty, Env, MemoryStorage, MessageInfo, Order, OwnedDeps,
Response, StdResult, Storage,
};
use cw_storage_plus::{KeyDeserialize, Map, Prefix, PrimaryKey};
use rand::{RngCore, SeedableRng};
use rand_chacha::ChaCha20Rng;
use serde::de::DeserializeOwned;
use serde::Serialize;
pub const TEST_DENOM: &str = "unym";
pub const TEST_PREFIX: &str = "n";
pub fn mock_api() -> MockApi {
MockApi::default().with_prefix(TEST_PREFIX)
}
pub fn mock_dependencies() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
OwnedDeps {
storage: MockStorage::default(),
api: mock_api(),
querier: MockQuerier::default(),
custom_query_type: Default::default(),
}
}
pub fn test_rng() -> ChaCha20Rng {
let dummy_seed = [42u8; 32];
rand_chacha::ChaCha20Rng::from_seed(dummy_seed)
}
pub fn deps_with_balance(env: &Env) -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
let mut deps = mock_dependencies();
deps.querier = MockQuerier::<Empty>::new(&[(
env.contract.address.as_str(),
coins(100000000000, TEST_DENOM).as_slice(),
)]);
deps
}
pub fn generate_sorted_addresses(n: usize) -> Vec<Addr> {
let mut rng = test_rng();
let mut addrs = Vec::with_capacity(n);
for i in 0..n {
addrs.push(mock_api().addr_make(&format!("addr{i}{}", rng.next_u64())));
}
addrs.sort();
addrs
}
pub fn addr<S: AsRef<str>>(raw: S) -> Addr {
mock_api().addr_make(raw.as_ref())
}
pub fn sender<S: AsRef<str>>(raw: S) -> MessageInfo {
message_info(&addr(raw), &[])
}
pub trait ExtractBankMsg {
fn unwrap_bank_msg(self) -> Option<BankMsg>;
}
impl ExtractBankMsg for Response {
fn unwrap_bank_msg(self) -> Option<BankMsg> {
for msg in self.messages {
match msg.msg {
CosmosMsg::Bank(bank_msg) => return Some(bank_msg),
_ => continue,
}
}
None
}
}
pub trait FullReader<'a> {
type Key;
type Value: Serialize + DeserializeOwned;
fn all_values(&self, store: &dyn Storage) -> StdResult<Vec<Self::Value>>;
fn all_key_values(&self, store: &dyn Storage) -> StdResult<Vec<(Self::Key, Self::Value)>>;
}
impl<'a, K, T> FullReader<'a> for Map<K, T>
where
T: Serialize + DeserializeOwned,
K: PrimaryKey<'a> + KeyDeserialize,
K::Output: 'static,
{
type Key = K::Output;
type Value = T;
fn all_values(&self, store: &dyn Storage) -> StdResult<Vec<Self::Value>> {
self.range(store, None, None, Order::Ascending)
.map(|record| record.map(|r| r.1))
.collect()
}
fn all_key_values(&self, store: &dyn Storage) -> StdResult<Vec<(Self::Key, Self::Value)>> {
self.range(store, None, None, Order::Ascending).collect()
}
}
impl<'a, K, T, B> FullReader<'a> for Prefix<K, T, B>
where
K: KeyDeserialize + 'static,
T: Serialize + DeserializeOwned,
B: PrimaryKey<'a>,
{
type Key = K::Output;
type Value = T;
fn all_values(&self, store: &dyn Storage) -> StdResult<Vec<Self::Value>> {
self.range(store, None, None, Order::Ascending)
.map(|record| record.map(|r| r.1))
.collect()
}
fn all_key_values(&self, store: &dyn Storage) -> StdResult<Vec<(Self::Key, Self::Value)>> {
self.range(store, None, None, Order::Ascending).collect()
}
}
@@ -0,0 +1,13 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// those are all used exclusively for testing thus unwraps, et al. are allowed
#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]
#![allow(clippy::panic)]
pub mod helpers;
pub mod tester;
pub use helpers::*;
pub use tester::*;
@@ -0,0 +1,239 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{ContractTester, TestableNymContract};
use cosmwasm_std::testing::{message_info, mock_env};
use cosmwasm_std::{
from_json, Addr, Coin, ContractInfo, Deps, DepsMut, Env, MessageInfo, Response, StdResult,
Storage, Timestamp,
};
use cw_multi_test::{next_block, AppResponse, Executor};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::any::type_name;
use std::fmt::Debug;
pub trait ContractOpts {
type ExecuteMsg;
type QueryMsg;
type ContractError;
fn deps(&self) -> Deps<'_>;
fn deps_mut(&mut self) -> DepsMut<'_>;
fn env(&self) -> Env;
fn addr_make(&self, input: &str) -> Addr;
fn deps_mut_env(&mut self) -> (DepsMut<'_>, Env) {
let env = self.env().clone();
(self.deps_mut(), env)
}
fn storage(&self) -> &dyn Storage;
fn storage_mut(&mut self) -> &mut dyn Storage;
fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T>;
fn set_contract_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>);
fn unchecked_read_from_contract_storage<T: DeserializeOwned>(
&self,
key: impl AsRef<[u8]>,
) -> T {
let typ = type_name::<T>();
self.read_from_contract_storage(key)
.unwrap_or_else(|| panic!("value of type {typ} not present in the storage"))
}
fn execute_raw(
&mut self,
sender: Addr,
message: Self::ExecuteMsg,
) -> Result<Response, Self::ContractError> {
self.execute_raw_with_balance(sender, &[], message)
}
fn execute_raw_with_balance(
&mut self,
sender: Addr,
coins: &[Coin],
message: Self::ExecuteMsg,
) -> Result<Response, Self::ContractError>;
}
impl<C> ContractOpts for ContractTester<C>
where
C: TestableNymContract,
{
type ExecuteMsg = C::ExecuteMsg;
type QueryMsg = C::QueryMsg;
type ContractError = C::ContractError;
fn deps(&self) -> Deps<'_> {
Deps {
storage: &self.storage,
api: self.app.api(),
querier: self.app.wrap(),
}
}
fn deps_mut(&mut self) -> DepsMut<'_> {
DepsMut {
storage: &mut self.storage,
api: self.app.api(),
querier: self.app.wrap(),
}
}
fn env(&self) -> Env {
Env {
block: self.app.block_info(),
contract: ContractInfo {
address: self.contract_address.clone(),
},
..mock_env()
}
}
fn addr_make(&self, input: &str) -> Addr {
self.app.api().addr_make(input)
}
fn storage(&self) -> &dyn Storage {
&self.storage
}
fn storage_mut(&mut self) -> &mut dyn Storage {
&mut self.storage
}
fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T> {
let raw = self.deps().storage.get(key.as_ref())?;
from_json(&raw).ok()
}
fn set_contract_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) {
self.deps_mut().storage.set(key.as_ref(), value.as_ref());
}
fn execute_raw_with_balance(
&mut self,
sender: Addr,
coins: &[Coin],
message: C::ExecuteMsg,
) -> Result<Response, C::ContractError> {
let env = self.env();
let info = message_info(&sender, coins);
C::execute()(self.deps_mut(), env, info, message)
}
}
pub trait ChainOpts: ContractOpts {
fn set_contract_balance(&mut self, balance: Coin);
fn next_block(&mut self);
fn set_block_time(&mut self, time: Timestamp);
fn execute_msg(
&mut self,
sender: Addr,
message: &Self::ExecuteMsg,
) -> anyhow::Result<AppResponse> {
self.execute_msg_with_balance(sender, &[], message)
}
fn execute_msg_with_balance(
&mut self,
sender: Addr,
coins: &[Coin],
message: &Self::ExecuteMsg,
) -> anyhow::Result<AppResponse>;
fn execute_arbitrary_contract<T: Serialize + Debug>(
&mut self,
contract: Addr,
sender: MessageInfo,
message: &T,
) -> anyhow::Result<AppResponse>;
fn query_arbitrary_contract<Q: Serialize + Debug, T: DeserializeOwned>(
&self,
contract: Addr,
message: &Q,
) -> StdResult<T>;
fn query<T: DeserializeOwned>(&self, message: &Self::QueryMsg) -> StdResult<T>;
}
impl<C> ChainOpts for ContractTester<C>
where
C: TestableNymContract,
{
fn set_contract_balance(&mut self, balance: Coin) {
let contract_address = &self.contract_address;
self.app
.router()
.bank
.init_balance(
&mut self.storage.inner_storage(),
contract_address,
vec![balance],
)
.unwrap();
}
fn next_block(&mut self) {
self.app.update_block(next_block)
}
fn set_block_time(&mut self, time: Timestamp) {
self.app.update_block(|b| b.time = time)
}
fn execute_msg(
&mut self,
sender: Addr,
message: &C::ExecuteMsg,
) -> anyhow::Result<AppResponse> {
self.execute_msg_with_balance(sender, &[], message)
}
fn execute_msg_with_balance(
&mut self,
sender: Addr,
coins: &[Coin],
message: &C::ExecuteMsg,
) -> anyhow::Result<AppResponse> {
self.app
.execute_contract(sender, self.contract_address.clone(), message, coins)
}
fn execute_arbitrary_contract<T: Serialize + Debug>(
&mut self,
contract: Addr,
sender: MessageInfo,
message: &T,
) -> anyhow::Result<AppResponse> {
let coins = &sender.funds;
let sender = sender.sender;
self.app.execute_contract(sender, contract, message, coins)
}
fn query_arbitrary_contract<Q: Serialize + Debug, T: DeserializeOwned>(
&self,
contract: Addr,
message: &Q,
) -> StdResult<T> {
self.app.wrap().query_wasm_smart(contract, message)
}
fn query<T: DeserializeOwned>(&self, message: &C::QueryMsg) -> StdResult<T> {
self.app
.wrap()
.query_wasm_smart(self.contract_address.as_str(), message)
}
}
@@ -0,0 +1,305 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{
CommonStorageKeys, ContractOpts, ContractTester, StorageWrapper, TestableNymContract,
TEST_DENOM,
};
use cosmwasm_std::testing::message_info;
use cosmwasm_std::{
coin, coins, from_json, to_json_vec, Addr, Coin, MessageInfo, StdError, StdResult, Storage,
};
use cw_multi_test::Executor;
use cw_storage_plus::{Key, Path, PrimaryKey};
use rand::RngCore;
use rand_chacha::ChaCha20Rng;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::any::type_name;
use std::ops::Deref;
pub trait StorageReader {
fn common_key(&self, key: CommonStorageKeys) -> Option<&[u8]>;
fn read_common_value<T: DeserializeOwned>(&self, key: CommonStorageKeys) -> Option<T> {
self.read_from_contract_storage(self.common_key(key)?)
}
fn unchecked_read_common_value<T: DeserializeOwned>(&self, key: CommonStorageKeys) -> T {
self.unchecked_read_from_contract_storage(
self.common_key(key)
.unwrap_or_else(|| panic!("no key set for {key:?}")),
)
}
fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T>;
fn unchecked_read_from_contract_storage<T: DeserializeOwned>(
&self,
key: impl AsRef<[u8]>,
) -> T {
let typ = type_name::<T>();
self.read_from_contract_storage(key)
.unwrap_or_else(|| panic!("value of type {typ} not present in the storage"))
}
}
// technically it shouldn't rely on `StorageReader` and `common_key` should be extracted
// but this makes it a tad easier and it's only testing code so it's fine
pub trait StorageWriter: StorageReader {
fn set_common_value<T: Serialize>(
&mut self,
key: CommonStorageKeys,
value: &T,
) -> StdResult<()> {
let key = self
.common_key(key)
.ok_or(StdError::not_found("key not found"))?
.to_vec();
self.set_storage_value(key, value)
}
fn set_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>);
fn set_storage_value<T: Serialize>(
&mut self,
key: impl AsRef<[u8]>,
value: &T,
) -> StdResult<()> {
self.set_storage(key, &to_json_vec(value)?);
Ok(())
}
}
pub trait ArbitraryContractStorageReader {
fn may_read_from_contract_storage(
&self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
) -> Option<Vec<u8>>;
fn must_read_from_contract_storage(
&self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
) -> StdResult<Vec<u8>> {
let key = key.as_ref();
self.may_read_from_contract_storage(address, key)
.ok_or(StdError::not_found(format!("no data under {key:?}")))
}
fn may_read_value_from_contract_storage<T: DeserializeOwned>(
&self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
) -> StdResult<Option<T>> {
let Some(bytes) = self.may_read_from_contract_storage(address, key) else {
return Ok(None);
};
from_json(&bytes).map(Some)
}
fn must_read_value_from_contract_storage<T: DeserializeOwned>(
&self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
) -> StdResult<T> {
let bytes = self.must_read_from_contract_storage(address, key)?;
from_json(&bytes)
}
}
pub trait ArbitraryContractStorageWriter {
fn set_contract_storage(
&mut self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
value: impl AsRef<[u8]>,
);
fn set_contract_storage_value<T: Serialize>(
&mut self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
value: &T,
) -> StdResult<()> {
self.set_contract_storage(address, key, &to_json_vec(value)?);
Ok(())
}
// attempts to write to an arbitrary contract `cw_storage_plus::Map`
fn set_contract_map_value<'a, K, T>(
&mut self,
address: impl Into<String>,
namespace: impl AsRef<[u8]>,
key: K,
value: &T,
) -> StdResult<()>
where
K: PrimaryKey<'a>,
T: Serialize + DeserializeOwned,
{
let key_path: Path<T> = Path::new(
namespace.as_ref(),
&key.key().iter().map(Key::as_ref).collect::<Vec<_>>(),
);
let storage_key = key_path.deref();
self.set_contract_storage_value(address, storage_key, value)
}
}
// contract that has an admin
pub trait AdminExt: StorageReader + StorageWriter {
fn admin(&self) -> Option<Addr> {
self.read_common_value(CommonStorageKeys::Admin)
}
fn update_admin(&mut self, admin: &Option<Addr>) -> StdResult<()> {
self.set_common_value(CommonStorageKeys::Admin, admin)
}
fn admin_unchecked(&self) -> Addr {
self.admin().expect("no admin set")
}
fn admin_msg(&self) -> MessageInfo {
message_info(&self.admin_unchecked(), &[])
}
}
// contract that operates on some specific coin denom
pub trait DenomExt: StorageReader {
fn denom(&self) -> String {
self.unchecked_read_common_value(CommonStorageKeys::Denom)
}
fn coin(&self, amount: u128) -> Coin {
coin(amount, self.denom())
}
fn coins(&self, amount: u128) -> Vec<Coin> {
coins(amount, self.denom())
}
}
pub trait RandExt {
fn raw_rng(&mut self) -> &mut ChaCha20Rng;
fn generate_account(&mut self) -> Addr;
fn generate_account_with_balance(&mut self) -> Addr
where
Self: BankExt;
}
pub trait BankExt {
fn send_tokens(&mut self, to: Addr, amount: Coin) -> anyhow::Result<()>;
}
impl<T> AdminExt for T where T: StorageReader + StorageWriter {}
impl<T> DenomExt for T where T: StorageReader {}
impl<C: TestableNymContract> StorageReader for ContractTester<C> {
fn common_key(&self, key: CommonStorageKeys) -> Option<&[u8]> {
self.common_storage_keys.get(&key).map(|v| &**v)
}
fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T> {
<Self as ContractOpts>::read_from_contract_storage(self, key)
}
}
impl<C: TestableNymContract> StorageWriter for ContractTester<C> {
fn set_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) {
<Self as ContractOpts>::set_contract_storage(self, key, value)
}
}
impl<C: TestableNymContract> BankExt for ContractTester<C> {
fn send_tokens(&mut self, to: Addr, amount: Coin) -> anyhow::Result<()> {
self.app
.send_tokens(self.master_address.clone(), to, &[amount])?;
Ok(())
}
}
impl<C: TestableNymContract> RandExt for ContractTester<C> {
fn raw_rng(&mut self) -> &mut ChaCha20Rng {
&mut self.rng
}
fn generate_account(&mut self) -> Addr {
self.app
.api()
.addr_make(&format!("foomp{}", self.rng.next_u64()))
}
fn generate_account_with_balance(&mut self) -> Addr
where
Self: BankExt,
{
let addr = self.generate_account();
let million = 1_000_000_000_000;
self.send_tokens(addr.clone(), coin(million, TEST_DENOM))
.unwrap();
addr
}
}
impl ArbitraryContractStorageReader for StorageWrapper {
fn may_read_from_contract_storage(
&self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
) -> Option<Vec<u8>> {
self.contract_storage_wrapper(&Addr::unchecked(address))
.get(key.as_ref())
}
}
impl ArbitraryContractStorageWriter for StorageWrapper {
fn set_contract_storage(
&mut self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
value: impl AsRef<[u8]>,
) {
// yeah, we're unnecessarily cloning a Rc pointer, but this is a test code, so this inefficiency is fine
let mut wrapped_storage = self
.clone()
.contract_storage_wrapper(&Addr::unchecked(address));
wrapped_storage.set(key.as_ref(), value.as_ref());
}
}
impl<C> ArbitraryContractStorageReader for ContractTester<C>
where
C: TestableNymContract,
{
fn may_read_from_contract_storage(
&self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
) -> Option<Vec<u8>> {
self.storage
.as_inner_storage()
.may_read_from_contract_storage(address, key)
}
}
impl<C> ArbitraryContractStorageWriter for ContractTester<C>
where
C: TestableNymContract,
{
fn set_contract_storage(
&mut self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
value: impl AsRef<[u8]>,
) {
self.storage
.as_inner_storage_mut()
.set_contract_storage(address, key, value);
}
}
@@ -0,0 +1,276 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{mock_api, test_rng, TEST_DENOM};
use cosmwasm_std::testing::MockApi;
use cosmwasm_std::{
coin, coins, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Order, QuerierWrapper,
Record, Response, Storage,
};
use cw_multi_test::{App, AppBuilder, BankKeeper, Contract, ContractWrapper, Executor};
use rand_chacha::ChaCha20Rng;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::collections::HashMap;
use std::fmt::{Debug, Display};
use std::marker::PhantomData;
pub use basic_traits::*;
pub use extensions::*;
pub use crate::tester::storage_wrapper::{ContractStorageWrapper, StorageWrapper};
mod basic_traits;
mod extensions;
mod storage_wrapper;
// copied from cw-multi-test (but removed generics for custom messages and querier for we don't need them for now)
pub type ContractFn<T, E> =
fn(deps: DepsMut, env: Env, info: MessageInfo, msg: T) -> Result<Response, E>;
pub type QueryFn<T, E> = fn(deps: Deps, env: Env, msg: T) -> Result<Binary, E>;
pub type PermissionedFn<T, E> = fn(deps: DepsMut, env: Env, msg: T) -> Result<Response, E>;
pub type ContractClosure<T, E> = Box<dyn Fn(DepsMut, Env, MessageInfo, T) -> Result<Response, E>>;
pub type QueryClosure<T, E> = Box<dyn Fn(Deps, Env, T) -> Result<Binary, E>>;
pub trait TestableNymContract {
const NAME: &'static str;
type InitMsg: DeserializeOwned + Serialize + Debug + 'static;
type ExecuteMsg: DeserializeOwned + Serialize + Debug + 'static;
type QueryMsg: DeserializeOwned + Serialize + Debug + 'static;
type MigrateMsg: DeserializeOwned + Serialize + Debug + 'static;
type ContractError: Display + Debug + Send + Sync + 'static;
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError>;
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError>;
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError>;
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError>;
fn base_init_msg() -> Self::InitMsg;
// // for now we don't care about custom queriers
// fn contract_wrapper() -> ContractWrapper<
// Self::ExecuteMsg,
// Self::InitMsg,
// Self::QueryMsg,
// Self::ContractError,
// anyhow::Error,
// anyhow::Error,
// Empty,
// Empty,
// Empty,
// Self::ContractError,
// Self::ContractError,
// Self::MigrateMsg,
// Self::ContractError,
// > {
// ContractWrapper::new(Self::execute(), Self::instantiate(), Self::query())
// .with_migrate(Self::migrate())
// }
fn dyn_contract() -> Box<dyn Contract<Empty>> {
Box::new(
ContractWrapper::new(Self::execute(), Self::instantiate(), Self::query())
.with_migrate(Self::migrate()),
)
}
fn init() -> ContractTester<Self>
where
Self: Sized,
{
ContractTesterBuilder::new()
.instantiate::<Self>(None)
.build()
}
}
pub struct ContractTesterBuilder<C> {
contract: PhantomData<C>,
master_address: Addr,
app: App<BankKeeper, MockApi, StorageWrapper>,
storage: StorageWrapper,
pub well_known_contracts: HashMap<&'static str, Addr>,
}
impl<C> ContractTesterBuilder<C> {
#[allow(clippy::new_without_default)]
pub fn new() -> Self
where
C: TestableNymContract,
{
let storage = StorageWrapper::new();
let api = mock_api();
let master_address = api.addr_make("master-owner");
let app = AppBuilder::new()
.with_api(api)
.with_storage(storage.clone())
.build(|router, _api, storage| {
router
.bank
.init_balance(
storage,
&master_address,
coins(1000000000000000, TEST_DENOM),
)
.unwrap()
});
ContractTesterBuilder {
contract: Default::default(),
master_address,
app,
storage,
well_known_contracts: Default::default(),
}
}
pub fn instantiate<D: TestableNymContract>(
mut self,
custom_init_msg: Option<D::InitMsg>,
) -> ContractTesterBuilder<C> {
let code_id = self.app.store_code(D::dyn_contract());
let contract_address = self
.app
.instantiate_contract(
code_id,
self.master_address.clone(),
&custom_init_msg.unwrap_or(D::base_init_msg()),
&[],
D::NAME,
Some(self.master_address.to_string()),
)
.unwrap();
// send some tokens to the contract
self.app
.send_tokens(
self.master_address.clone(),
contract_address.clone(),
&[coin(100000000, TEST_DENOM)],
)
.unwrap();
self.well_known_contracts.insert(D::NAME, contract_address);
self
}
pub fn build(self) -> ContractTester<C>
where
C: TestableNymContract,
{
if !self.well_known_contracts.contains_key(C::NAME) {
panic!("{} contract has not been instantiated", C::NAME);
}
let contract_address = self.well_known_contracts[C::NAME].clone();
ContractTester {
contract: self.contract,
app: self.app,
rng: test_rng(),
master_address: self.master_address,
storage: self.storage.contract_storage_wrapper(&contract_address),
contract_address,
common_storage_keys: Default::default(),
well_known_contracts: self.well_known_contracts,
}
}
pub fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper {
self.storage.contract_storage_wrapper(contract)
}
pub fn api(&self) -> MockApi {
*self.app.api()
}
pub fn querier(&self) -> QuerierWrapper {
self.app.wrap()
}
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
pub enum CommonStorageKeys {
Admin,
Denom,
}
pub struct ContractTester<C: TestableNymContract> {
contract: PhantomData<C>,
pub app: App<BankKeeper, MockApi, StorageWrapper>,
pub rng: ChaCha20Rng,
pub contract_address: Addr,
pub master_address: Addr,
pub(crate) storage: ContractStorageWrapper,
pub common_storage_keys: HashMap<CommonStorageKeys, Vec<u8>>,
// TODO: limitation: doesn't allow multiple contracts of the same type (but that's fine for the time being)
pub well_known_contracts: HashMap<&'static str, Addr>,
}
impl<C> ContractTester<C>
where
C: TestableNymContract,
{
pub fn insert_common_storage_key(&mut self, key: CommonStorageKeys, value: impl AsRef<[u8]>) {
self.common_storage_keys
.insert(key, value.as_ref().to_vec());
}
pub fn with_common_storage_key(
mut self,
key: CommonStorageKeys,
value: impl AsRef<[u8]>,
) -> Self {
self.insert_common_storage_key(key, value);
self
}
}
impl<C> Storage for ContractTester<C>
where
C: TestableNymContract,
{
fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
self.storage.get(key)
}
fn range<'a>(
&'a self,
start: Option<&[u8]>,
end: Option<&[u8]>,
order: Order,
) -> Box<dyn Iterator<Item = Record> + 'a> {
self.storage.range(start, end, order)
}
fn range_keys<'a>(
&'a self,
start: Option<&[u8]>,
end: Option<&[u8]>,
order: Order,
) -> Box<dyn Iterator<Item = Vec<u8>> + 'a> {
self.storage.range_keys(start, end, order)
}
fn range_values<'a>(
&'a self,
start: Option<&[u8]>,
end: Option<&[u8]>,
order: Order,
) -> Box<dyn Iterator<Item = Vec<u8>> + 'a> {
self.storage.range_values(start, end, order)
}
fn set(&mut self, key: &[u8], value: &[u8]) {
self.storage.set(key, value)
}
fn remove(&mut self, key: &[u8]) {
self.storage.remove(key)
}
}
@@ -0,0 +1,184 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::storage_keys::to_length_prefixed_nested;
use cosmwasm_std::testing::MockStorage;
use cosmwasm_std::{Addr, MemoryStorage, Order, Record, Storage};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug, Clone)]
pub struct StorageWrapper(Rc<RefCell<MemoryStorage>>);
impl StorageWrapper {
pub fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper {
ContractStorageWrapper {
address: contract.clone(),
inner: self.clone(),
}
}
pub(super) fn new() -> Self {
StorageWrapper(Rc::new(RefCell::new(MockStorage::new())))
}
}
#[derive(Debug, Clone)]
pub struct ContractStorageWrapper {
address: Addr,
inner: StorageWrapper,
}
impl ContractStorageWrapper {
pub fn inner_storage(&self) -> StorageWrapper {
self.inner.clone()
}
pub fn as_inner_storage(&self) -> &StorageWrapper {
&self.inner
}
pub fn as_inner_storage_mut(&mut self) -> &mut StorageWrapper {
&mut self.inner
}
#[must_use = "this returns the result of the operation, without modifying the original"]
pub fn change_contract(&self, contract: &Addr) -> Self {
ContractStorageWrapper {
address: contract.clone(),
inner: self.inner.clone(),
}
}
}
impl Storage for StorageWrapper {
fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
self.0.borrow().get(key)
}
fn range<'a>(
&'a self,
start: Option<&[u8]>,
end: Option<&[u8]>,
order: Order,
) -> Box<dyn Iterator<Item = Record> + 'a> {
// hehe, that's nasty
let vals = self.0.borrow().range(start, end, order).collect::<Vec<_>>();
Box::new(vals.into_iter())
}
fn set(&mut self, key: &[u8], value: &[u8]) {
self.0.borrow_mut().set(key, value);
}
fn remove(&mut self, key: &[u8]) {
self.0.borrow_mut().remove(key);
}
}
impl ContractStorageWrapper {
fn contract_namespace(&self) -> Vec<u8> {
let mut name = b"contract_data/".to_vec();
name.extend_from_slice(self.address.as_bytes());
name
}
fn prefix(&self) -> Vec<u8> {
to_length_prefixed_nested(&[b"wasm", &self.contract_namespace()])
}
#[inline]
fn trim(namespace: &[u8], key: &[u8]) -> Vec<u8> {
key[namespace.len()..].to_vec()
}
/// Returns a new vec of same length and last byte incremented by one
/// If last bytes are 255, we handle overflow up the chain.
/// If all bytes are 255, this returns wrong data - but that is never possible as a namespace
fn namespace_upper_bound(input: &[u8]) -> Vec<u8> {
let mut copy = input.to_vec();
// zero out all trailing 255, increment first that is not such
for i in (0..input.len()).rev() {
if copy[i] == 255 {
copy[i] = 0;
} else {
copy[i] += 1;
break;
}
}
copy
}
#[inline]
fn concat(namespace: &[u8], key: &[u8]) -> Vec<u8> {
let mut k = namespace.to_vec();
k.extend_from_slice(key);
k
}
fn get_with_prefix(storage: &dyn Storage, namespace: &[u8], key: &[u8]) -> Option<Vec<u8>> {
storage.get(&Self::concat(namespace, key))
}
fn set_with_prefix(storage: &mut dyn Storage, namespace: &[u8], key: &[u8], value: &[u8]) {
storage.set(&Self::concat(namespace, key), value);
}
fn remove_with_prefix(storage: &mut dyn Storage, namespace: &[u8], key: &[u8]) {
storage.remove(&Self::concat(namespace, key));
}
fn range_with_prefix<'a>(
storage: &'a dyn Storage,
namespace: &[u8],
start: Option<&[u8]>,
end: Option<&[u8]>,
order: Order,
) -> Box<dyn Iterator<Item = Record> + 'a> {
// prepare start, end with prefix
let start = match start {
Some(s) => Self::concat(namespace, s),
None => namespace.to_vec(),
};
let end = match end {
Some(e) => Self::concat(namespace, e),
// end is updating last byte by one
None => Self::namespace_upper_bound(namespace),
};
// get iterator from storage
let base_iterator = storage.range(Some(&start), Some(&end), order);
// make a copy for the closure to handle lifetimes safely
let prefix = namespace.to_vec();
let mapped = base_iterator.map(move |(k, v)| (Self::trim(&prefix, &k), v));
Box::new(mapped)
}
}
impl Storage for ContractStorageWrapper {
fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
let prefix = self.prefix();
Self::get_with_prefix(&self.inner, &prefix, key)
}
fn range<'a>(
&'a self,
start: Option<&[u8]>,
end: Option<&[u8]>,
order: Order,
) -> Box<dyn Iterator<Item = Record> + 'a> {
let prefix = self.prefix();
Self::range_with_prefix(&self.inner, &prefix, start, end, order)
}
fn set(&mut self, key: &[u8], value: &[u8]) {
let prefix = self.prefix();
Self::set_with_prefix(&mut self.inner, &prefix, key, value);
}
fn remove(&mut self, key: &[u8]) {
let prefix = self.prefix();
Self::remove_with_prefix(&mut self.inner, &prefix, key);
}
}
@@ -18,6 +18,7 @@ serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true }
serde_json = { workspace = true }
[build-dependencies]
@@ -35,7 +35,7 @@ pub enum ContractsCommonError {
/// Percent represents a value between 0 and 100%
/// (i.e. between 0.0 and 1.0)
#[cw_serde]
#[derive(Copy, Default, PartialOrd)]
#[derive(Copy, Default, PartialOrd, Ord, Eq)]
pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal);
impl Percent {
@@ -80,6 +80,44 @@ impl Percent {
pub fn checked_pow(&self, exp: u32) -> Result<Self, OverflowError> {
self.0.checked_pow(exp).map(Percent)
}
// truncate provided percent to only have 2 decimal places,
// e.g. convert "0.1234567" into "0.12"
// the purpose of it is to reduce storage space, in particular for the performance contract
// since that extra precision gains us nothing
#[must_use = "this returns the result of the operation, without modifying the original"]
pub fn round_to_two_decimal_places(&self) -> Self {
let raw = self.0;
const DECIMAL_FRACTIONAL: Uint128 = Uint128::new(1_000_000_000_000_000_000u128); // 1*10**18
const THRESHOLD: Decimal = Decimal::permille(5); // 0.005
// in case it ever changes since it's not exposed in the public API
debug_assert_eq!(
DECIMAL_FRACTIONAL,
Uint128::new(10).pow(Decimal::DECIMAL_PLACES)
);
let int = (raw.atomics() * Uint128::new(100)) / DECIMAL_FRACTIONAL;
#[allow(clippy::unwrap_used)]
let floored = Decimal::from_atomics(int, 2).unwrap();
let diff = raw - floored;
let rounded = if diff >= THRESHOLD {
// ceil
floored + Decimal::percent(1)
} else {
floored
};
Percent(rounded)
}
#[must_use = "this returns the result of the operation, without modifying the original"]
pub fn average(&self, other: &Self) -> Self {
let sum = self.0 + other.0;
let inner = Decimal::from_ratio(sum.numerator(), sum.denominator() * Uint128::new(2));
Percent(inner)
}
}
impl Display for Percent {
@@ -334,6 +372,7 @@ mod tests {
}
#[test]
#[cfg(feature = "naive_float")]
fn naive_float_conversion() {
// around 15 decimal places is the maximum precision we can handle
// which is still way more than enough for what we use it for
@@ -347,4 +386,41 @@ mod tests {
assert!(converted.0 - converted.0 < epsilon);
}
#[test]
fn rounding_percent() {
let test_cases = vec![
("0", "0"),
("0.1", "0.1"),
("0.12", "0.12"),
("0.12", "0.123"),
("0.12", "0.123456789"),
("0.13", "0.125"),
("0.13", "0.126"),
("0.13", "0.126436545676"),
("0.99", "0.99"),
("0.99", "0.994"),
("1", "0.999"),
("1", "0.995"),
];
for (expected, input) in test_cases {
let expected: Percent = expected.parse().unwrap();
let pre_truncated: Percent = input.parse().unwrap();
assert_eq!(expected, pre_truncated.round_to_two_decimal_places())
}
}
#[test]
fn calculating_average() -> anyhow::Result<()> {
fn p(raw: &str) -> Percent {
raw.parse().unwrap()
}
assert_eq!(p("0.1").average(&p("0.1")), p("0.1"));
assert_eq!(p("0.1").average(&p("0.2")), p("0.15"));
assert_eq!(p("1").average(&p("0")), p("0.5"));
assert_eq!(p("0.123").average(&p("0.456")), p("0.2895"));
Ok(())
}
}
@@ -23,7 +23,6 @@ semver = { workspace = true, features = ["serde"] }
schemars = { workspace = true }
thiserror = { workspace = true }
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.5.0" }
serde-json-wasm = { workspace = true }
humantime-serde = { workspace = true }
utoipa = { workspace = true, optional = true }
@@ -40,3 +39,6 @@ contract-testing = []
utoipa = ["dep:utoipa"]
schema = ["cw2"]
generate-ts = ['ts-rs']
[lints]
workspace = true
@@ -193,6 +193,9 @@ pub enum MixnetContractError {
#[error("attempted to perform the operation with 0 coins. This is not allowed")]
ZeroCoinAmount,
#[error("key rotation validity below minimum value")]
TooShortRotationInterval,
#[error("this validator ({current_validator}) is not the one responsible for advancing this epoch. It's responsibility of {chosen_validator}.")]
RewardingValidatorMismatch {
current_validator: Addr,
@@ -3,7 +3,7 @@
use crate::{IdentityKey, NodeId, SphinxKey};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Coin};
use cosmwasm_std::{to_json_string, Addr, Coin};
use std::cmp::Ordering;
use std::fmt::Display;
@@ -154,7 +154,7 @@ pub struct GatewayConfigUpdate {
impl GatewayConfigUpdate {
pub fn to_inline_json(&self) -> String {
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
}
}
@@ -358,7 +358,7 @@ impl Interval {
self.total_elapsed_epochs
}
pub const fn current_epoch_absolute_id(&self) -> u32 {
pub const fn current_epoch_absolute_id(&self) -> EpochId {
// since we count epochs starting from 0, if n epochs have elapsed, the current one has absolute id of n
self.total_elapsed_epochs
}
@@ -0,0 +1,155 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::EpochId;
use cosmwasm_schema::cw_serde;
pub type KeyRotationId = u32;
#[cw_serde]
#[derive(Copy)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct KeyRotationState {
/// Defines how long each key rotation is valid for (in terms of epochs)
pub validity_epochs: u32,
/// Records the initial epoch_id when the key rotation has been introduced (0 for fresh contracts).
/// It is used for determining when rotation is meant to advance.
#[cfg_attr(feature = "utoipa", schema(value_type = u32))]
pub initial_epoch_id: EpochId,
}
impl KeyRotationState {
pub fn key_rotation_id(&self, current_epoch_id: EpochId) -> KeyRotationId {
let diff = current_epoch_id.saturating_sub(self.initial_epoch_id);
diff / self.validity_epochs
}
pub fn next_rotation_starting_epoch_id(&self, current_epoch_id: EpochId) -> EpochId {
self.current_rotation_starting_epoch_id(current_epoch_id) + self.validity_epochs
}
pub fn current_rotation_starting_epoch_id(&self, current_epoch_id: EpochId) -> EpochId {
let current_rotation_id = self.key_rotation_id(current_epoch_id);
self.initial_epoch_id + self.validity_epochs * current_rotation_id
}
}
#[cw_serde]
pub struct KeyRotationIdResponse {
pub rotation_id: KeyRotationId,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_rotation_id() {
let state = KeyRotationState {
validity_epochs: 24,
initial_epoch_id: 0,
};
assert_eq!(0, state.key_rotation_id(0));
assert_eq!(0, state.key_rotation_id(23));
assert_eq!(1, state.key_rotation_id(24));
assert_eq!(1, state.key_rotation_id(47));
assert_eq!(2, state.key_rotation_id(48));
let state = KeyRotationState {
validity_epochs: 12,
initial_epoch_id: 0,
};
assert_eq!(0, state.key_rotation_id(0));
assert_eq!(0, state.key_rotation_id(11));
assert_eq!(1, state.key_rotation_id(12));
assert_eq!(1, state.key_rotation_id(23));
assert_eq!(2, state.key_rotation_id(24));
let state = KeyRotationState {
validity_epochs: 24,
initial_epoch_id: 10000,
};
assert_eq!(0, state.key_rotation_id(123));
assert_eq!(0, state.key_rotation_id(10000));
assert_eq!(0, state.key_rotation_id(10001));
assert_eq!(0, state.key_rotation_id(10023));
assert_eq!(1, state.key_rotation_id(10024));
assert_eq!(1, state.key_rotation_id(10047));
assert_eq!(2, state.key_rotation_id(10048));
assert_eq!(2, state.key_rotation_id(10060));
}
#[test]
fn next_rotation_starting_epoch_id() {
let state = KeyRotationState {
validity_epochs: 24,
initial_epoch_id: 0,
};
assert_eq!(24, state.next_rotation_starting_epoch_id(0));
assert_eq!(24, state.next_rotation_starting_epoch_id(23));
assert_eq!(48, state.next_rotation_starting_epoch_id(24));
assert_eq!(48, state.next_rotation_starting_epoch_id(47));
assert_eq!(72, state.next_rotation_starting_epoch_id(48));
let state = KeyRotationState {
validity_epochs: 12,
initial_epoch_id: 0,
};
assert_eq!(12, state.next_rotation_starting_epoch_id(0));
assert_eq!(12, state.next_rotation_starting_epoch_id(11));
assert_eq!(24, state.next_rotation_starting_epoch_id(12));
assert_eq!(24, state.next_rotation_starting_epoch_id(23));
assert_eq!(36, state.next_rotation_starting_epoch_id(24));
let state = KeyRotationState {
validity_epochs: 24,
initial_epoch_id: 10000,
};
assert_eq!(10024, state.next_rotation_starting_epoch_id(123));
assert_eq!(10024, state.next_rotation_starting_epoch_id(10000));
assert_eq!(10024, state.next_rotation_starting_epoch_id(10001));
assert_eq!(10024, state.next_rotation_starting_epoch_id(10023));
assert_eq!(10048, state.next_rotation_starting_epoch_id(10024));
assert_eq!(10048, state.next_rotation_starting_epoch_id(10047));
assert_eq!(10072, state.next_rotation_starting_epoch_id(10048));
assert_eq!(10072, state.next_rotation_starting_epoch_id(10060));
}
#[test]
fn current_rotation_starting_epoch_id() {
let state = KeyRotationState {
validity_epochs: 24,
initial_epoch_id: 0,
};
assert_eq!(0, state.current_rotation_starting_epoch_id(0));
assert_eq!(0, state.current_rotation_starting_epoch_id(23));
assert_eq!(24, state.current_rotation_starting_epoch_id(24));
assert_eq!(24, state.current_rotation_starting_epoch_id(47));
assert_eq!(48, state.current_rotation_starting_epoch_id(48));
let state = KeyRotationState {
validity_epochs: 12,
initial_epoch_id: 0,
};
assert_eq!(0, state.current_rotation_starting_epoch_id(0));
assert_eq!(0, state.current_rotation_starting_epoch_id(11));
assert_eq!(12, state.current_rotation_starting_epoch_id(12));
assert_eq!(12, state.current_rotation_starting_epoch_id(23));
assert_eq!(24, state.current_rotation_starting_epoch_id(24));
let state = KeyRotationState {
validity_epochs: 24,
initial_epoch_id: 10000,
};
assert_eq!(10000, state.current_rotation_starting_epoch_id(123));
assert_eq!(10000, state.current_rotation_starting_epoch_id(10000));
assert_eq!(10000, state.current_rotation_starting_epoch_id(10001));
assert_eq!(10000, state.current_rotation_starting_epoch_id(10023));
assert_eq!(10024, state.current_rotation_starting_epoch_id(10024));
assert_eq!(10024, state.current_rotation_starting_epoch_id(10047));
assert_eq!(10048, state.current_rotation_starting_epoch_id(10048));
assert_eq!(10048, state.current_rotation_starting_epoch_id(10060));
}
}
@@ -1,10 +1,6 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
#![warn(clippy::todo)]
mod config_score;
pub mod constants;
pub mod delegation;
@@ -13,6 +9,7 @@ pub mod events;
pub mod gateway;
pub mod helpers;
pub mod interval;
pub mod key_rotation;
pub mod mixnode;
pub mod msg;
pub mod nym_node;
@@ -37,6 +34,7 @@ pub use gateway::{
pub use interval::{
CurrentIntervalResponse, EpochId, EpochState, EpochStatus, Interval, IntervalId,
};
pub use key_rotation::*;
pub use mixnode::{
LegacyMixLayer, MixNode, MixNodeBond, MixNodeConfigUpdate, MixNodeDetails,
MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeCostParams,
@@ -16,7 +16,7 @@ use crate::{
Percent, ProfitMarginRange, SphinxKey,
};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128};
use cosmwasm_std::{to_json_string, Addr, Coin, Decimal, StdResult, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
@@ -170,6 +170,11 @@ impl NodeRewarding {
}
}
// we panic here as opposed to returning an error as this is undefined behaviour,
// because the pledge amount has decreased (i.e. slashing has occurred) which
// should not be possible under any situation. at this point we don't know how many other things
// might have failed so we have to bail
#[allow(clippy::panic)]
pub fn pending_detailed_operator_reward(&self, original_pledge: &Coin) -> StdResult<Decimal> {
let initial_dec = original_pledge.amount.into_base_decimal()?;
if initial_dec > self.operator {
@@ -189,6 +194,11 @@ impl NodeRewarding {
Ok(truncate_reward(delegator_reward, &delegation.amount.denom))
}
// we panic here as opposed to returning an error as this is undefined behaviour,
// because the pledge amount has decreased (i.e. slashing has occurred) which
// should not be possible under any situation. at this point we don't know how many other things
// might have failed so we have to bail
#[allow(clippy::panic)]
pub fn withdraw_operator_reward(
&mut self,
original_pledge: &Coin,
@@ -594,7 +604,7 @@ pub struct NodeCostParams {
impl NodeCostParams {
pub fn to_inline_json(&self) -> String {
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
}
}
@@ -763,7 +773,7 @@ pub struct MixNodeConfigUpdate {
impl MixNodeConfigUpdate {
pub fn to_inline_json(&self) -> String {
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
}
}
@@ -35,6 +35,7 @@ use crate::{
PreassignedGatewayIdsResponse,
},
interval::{CurrentIntervalResponse, EpochStatus},
key_rotation::{KeyRotationIdResponse, KeyRotationState},
mixnode::{
MixOwnershipResponse, MixStakeSaturationResponse, MixnodeDetailsByIdentityResponse,
MixnodeDetailsResponse, MixnodeRewardingDetailsResponse, PagedMixnodeBondsResponse,
@@ -81,6 +82,18 @@ pub struct InstantiateMsg {
#[serde(default)]
pub interval_operating_cost: OperatingCostRange,
#[serde(default)]
pub key_validity_in_epochs: Option<u32>,
}
impl InstantiateMsg {
// needs to give us enough time to pre-announce key for following epoch
// and have an overlap with the preceding epoch
pub const MIN_KEY_ROTATION_VALIDITY: u32 = 3;
pub fn key_validity_in_epochs(&self) -> u32 {
self.key_validity_in_epochs.unwrap_or(24)
}
}
#[cw_serde]
@@ -857,6 +870,15 @@ pub enum QueryMsg {
/// Cosmos address used for the query of the signing nonce.
address: String,
},
// sphinx key rotation-related
#[cfg_attr(feature = "schema", returns(KeyRotationState))]
/// Gets the current state config of the key rotation (i.e. starting epoch id and validity duration)
GetKeyRotationState {},
/// Gets the current key rotation id
#[cfg_attr(feature = "schema", returns(KeyRotationIdResponse))]
GetKeyRotationId {},
}
#[cw_serde]
@@ -5,7 +5,7 @@ use crate::helpers::IntoBaseDecimal;
use crate::nym_node::Role;
use crate::{error::MixnetContractError, Percent};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Decimal;
use cosmwasm_std::{to_json_string, Decimal};
pub type Performance = Percent;
pub type WorkFactor = Decimal;
@@ -84,7 +84,7 @@ pub struct IntervalRewardParams {
impl IntervalRewardParams {
pub fn to_inline_json(&self) -> String {
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
}
}
@@ -410,7 +410,7 @@ impl IntervalRewardingParamsUpdate {
}
pub fn to_inline_json(&self) -> String {
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
}
}
@@ -151,6 +151,9 @@ impl Simulator {
}
}
// this code is not meant to be used in production systems, only in tests
// so a panic due to inconsistent arguments is fine
#[allow(clippy::panic)]
pub fn simulate_epoch(
&mut self,
node_params: &BTreeMap<NodeId, NodeRewardingParameters>,
@@ -0,0 +1,29 @@
[package]
name = "nym-performance-contract-common"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
[dependencies]
thiserror = { workspace = true }
serde = { workspace = true }
schemars = { workspace = true }
cosmwasm-std = { workspace = true }
cosmwasm-schema = { workspace = true }
cw-controllers = { workspace = true }
nym-contracts-common = { path = "../contracts-common" }
[features]
schema = []
[lints]
workspace = true
@@ -0,0 +1,13 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod storage_keys {
pub const CONTRACT_ADMIN: &str = "contract-admin";
pub const INITIAL_EPOCH_ID: &str = "initial-epoch-id";
pub const MIXNET_CONTRACT: &str = "mixnet-contract";
pub const AUTHORISED_COUNT: &str = "authorised-count";
pub const AUTHORISED: &str = "authorised";
pub const RETIRED: &str = "retired";
pub const PERFORMANCE_RESULTS: &str = "performance-results";
pub const SUBMISSION_METADATA: &str = "submission-metadata";
}
@@ -0,0 +1,39 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{EpochId, NodeId};
use cosmwasm_std::Addr;
use cw_controllers::AdminError;
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum NymPerformanceContractError {
#[error("could not perform contract migration: {comment}")]
FailedMigration { comment: String },
#[error(transparent)]
Admin(#[from] AdminError),
#[error(transparent)]
StdErr(#[from] cosmwasm_std::StdError),
#[error("{address} is already an authorised network monitor")]
AlreadyAuthorised { address: Addr },
#[error("{address} is not an authorised network monitor")]
NotAuthorised { address: Addr },
#[error("attempted to submit performance data for epoch {epoch_id} and node {node_id} whilst last submitted was {last_epoch_id} for node {last_node_id}")]
StalePerformanceSubmission {
epoch_id: EpochId,
node_id: NodeId,
last_epoch_id: EpochId,
last_node_id: NodeId,
},
#[error("the batch performance data has not been sorted")]
UnsortedBatchSubmission,
#[error("node {node_id} does not appear to be bonded")]
NodeNotBonded { node_id: NodeId },
}
@@ -0,0 +1,2 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,12 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod constants;
pub mod error;
pub mod helpers;
pub mod msg;
pub mod types;
pub use error::*;
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
pub use types::*;
@@ -0,0 +1,121 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{EpochId, NodeId, NodePerformance};
use cosmwasm_schema::cw_serde;
#[cfg(feature = "schema")]
use crate::types::{
EpochMeasurementsPagedResponse, EpochPerformancePagedResponse,
FullHistoricalPerformancePagedResponse, NetworkMonitorResponse, NetworkMonitorsPagedResponse,
NodeMeasurementsResponse, NodePerformancePagedResponse, NodePerformanceResponse,
RetiredNetworkMonitorsPagedResponse,
};
#[cw_serde]
pub struct InstantiateMsg {
pub mixnet_contract_address: String,
pub authorised_network_monitors: Vec<String>,
}
#[cw_serde]
pub enum ExecuteMsg {
/// Change the admin
UpdateAdmin { admin: String },
/// Attempt to submit performance data of a particular node for given epoch
Submit {
epoch: EpochId,
data: NodePerformance,
},
/// Attempt to submit performance data of a batch of nodes for given epoch
BatchSubmit {
epoch: EpochId,
data: Vec<NodePerformance>,
},
/// Attempt to authorise new network monitor for submitting performance data
AuthoriseNetworkMonitor { address: String },
/// Attempt to retire an existing network monitor and forbid it from submitting any future performance data
RetireNetworkMonitor { address: String },
/// An admin method to remove submitted node measurements. Used as an escape hatch should
/// the data stored get too unwieldy.
RemoveNodeMeasurements { epoch_id: EpochId, node_id: NodeId },
/// An admin method to remove submitted nodes measurements. Used as an escape hatch should
/// the data stored get too unwieldy. Note: it is expected to get called multiple times
/// until the response indicates all the epoch data has been removed.
RemoveEpochMeasurements { epoch_id: EpochId },
}
#[cw_serde]
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
pub enum QueryMsg {
#[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))]
Admin {},
/// Returns performance of particular node for the provided epoch
#[cfg_attr(feature = "schema", returns(NodePerformanceResponse))]
NodePerformance { epoch_id: EpochId, node_id: NodeId },
/// Returns historical performance for particular node
#[cfg_attr(feature = "schema", returns(NodePerformancePagedResponse))]
NodePerformancePaged {
node_id: NodeId,
start_after: Option<EpochId>,
limit: Option<u32>,
},
/// Returns all submitted measurements for the particular node
#[cfg_attr(feature = "schema", returns(NodeMeasurementsResponse))]
NodeMeasurements { epoch_id: EpochId, node_id: NodeId },
/// Returns (paged) measurements for particular epoch
#[cfg_attr(feature = "schema", returns(EpochMeasurementsPagedResponse))]
EpochMeasurementsPaged {
epoch_id: EpochId,
start_after: Option<NodeId>,
limit: Option<u32>,
},
/// Returns (paged) performance for particular epoch
#[cfg_attr(feature = "schema", returns(EpochPerformancePagedResponse))]
EpochPerformancePaged {
epoch_id: EpochId,
start_after: Option<NodeId>,
limit: Option<u32>,
},
/// Returns full (paged) historical performance of the whole network
#[cfg_attr(feature = "schema", returns(FullHistoricalPerformancePagedResponse))]
FullHistoricalPerformancePaged {
start_after: Option<(EpochId, NodeId)>,
limit: Option<u32>,
},
/// Returns information about particular network monitor
#[cfg_attr(feature = "schema", returns(NetworkMonitorResponse))]
NetworkMonitor { address: String },
/// Returns information about all network monitors
#[cfg_attr(feature = "schema", returns(NetworkMonitorsPagedResponse))]
NetworkMonitorsPaged {
start_after: Option<String>,
limit: Option<u32>,
},
/// Returns information about all retired network monitors
#[cfg_attr(feature = "schema", returns(RetiredNetworkMonitorsPagedResponse))]
RetiredNetworkMonitorsPaged {
start_after: Option<String>,
limit: Option<u32>,
},
}
#[cw_serde]
pub struct MigrateMsg {
//
}
@@ -0,0 +1,242 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Env};
use nym_contracts_common::Percent;
pub type EpochId = u32;
pub type NodeId = u32;
#[cw_serde]
pub struct NetworkMonitorDetails {
pub address: Addr,
pub authorised_by: Addr,
pub authorised_at_height: u64,
}
impl NetworkMonitorDetails {
pub fn retire(self, env: &Env, sender: &Addr) -> RetiredNetworkMonitor {
RetiredNetworkMonitor {
details: self,
retired_by: sender.clone(),
retired_at_height: env.block.height,
}
}
}
#[cw_serde]
pub struct RetiredNetworkMonitor {
pub details: NetworkMonitorDetails,
pub retired_by: Addr,
pub retired_at_height: u64,
}
#[cw_serde]
#[derive(Copy)]
pub struct NodePerformance {
#[serde(rename = "n")]
pub node_id: NodeId,
// note: value is rounded to 2 decimal places.
#[serde(rename = "p")]
pub performance: Percent,
}
#[cw_serde]
pub struct NetworkMonitorSubmissionMetadata {
pub last_submitted_epoch_id: EpochId,
pub last_submitted_node_id: NodeId,
}
// the internal values are always sorted
#[cw_serde]
pub struct NodeResults(Vec<Percent>);
impl NodeResults {
pub fn new(initial: Percent) -> NodeResults {
NodeResults(vec![initial.round_to_two_decimal_places()])
}
// ASSUMPTION: number of NM will be relatively small, so loading the whole vector of values
// to insert new one and resave is cheap
pub fn insert_new(&mut self, result: Percent) {
let result = result.round_to_two_decimal_places();
let pos = self.0.binary_search(&result).unwrap_or_else(|e| e);
self.0.insert(pos, result);
}
// SAFETY: there are no codepaths that allow constructing empty struct
pub fn median(&self) -> Percent {
let len = self.0.len();
if len % 2 == 1 {
// odd number of elements: return the middle one
self.0[len / 2]
} else {
// even number: average the two middle elements
let mid1 = self.0[len / 2 - 1];
let mid2 = self.0[len / 2];
mid1.average(&mid2).round_to_two_decimal_places()
}
}
pub fn inner(&self) -> &[Percent] {
&self.0
}
}
#[cw_serde]
pub struct NodePerformanceResponse {
pub performance: Option<Percent>,
}
#[cw_serde]
pub struct NodeMeasurementsResponse {
pub measurements: Option<NodeResults>,
}
#[cw_serde]
#[derive(Copy)]
pub struct EpochNodePerformance {
pub epoch: EpochId,
pub performance: Option<Percent>,
}
#[cw_serde]
pub struct NodePerformancePagedResponse {
pub node_id: NodeId,
pub performance: Vec<EpochNodePerformance>,
pub start_next_after: Option<EpochId>,
}
#[cw_serde]
pub struct EpochPerformancePagedResponse {
pub epoch_id: EpochId,
pub performance: Vec<NodePerformance>,
pub start_next_after: Option<NodeId>,
}
#[cw_serde]
pub struct NodeMeasurement {
pub node_id: NodeId,
pub measurements: NodeResults,
}
#[cw_serde]
pub struct EpochMeasurementsPagedResponse {
pub epoch_id: EpochId,
pub measurements: Vec<NodeMeasurement>,
pub start_next_after: Option<NodeId>,
}
#[cw_serde]
#[derive(Copy)]
pub struct HistoricalPerformance {
pub epoch_id: EpochId,
pub node_id: NodeId,
pub performance: Percent,
}
#[cw_serde]
pub struct FullHistoricalPerformancePagedResponse {
pub performance: Vec<HistoricalPerformance>,
pub start_next_after: Option<(EpochId, NodeId)>,
}
#[cw_serde]
pub struct NetworkMonitorInformation {
pub details: NetworkMonitorDetails,
pub current_submission_metadata: NetworkMonitorSubmissionMetadata,
}
#[cw_serde]
pub struct NetworkMonitorResponse {
pub info: Option<NetworkMonitorInformation>,
}
#[cw_serde]
pub struct NetworkMonitorsPagedResponse {
pub info: Vec<NetworkMonitorInformation>,
pub start_next_after: Option<String>,
}
#[cw_serde]
pub struct RetiredNetworkMonitorsPagedResponse {
pub info: Vec<RetiredNetworkMonitor>,
pub start_next_after: Option<String>,
}
#[cw_serde]
pub struct RemoveEpochMeasurementsResponse {
pub additional_entries_to_remove_remaining: bool,
}
#[cw_serde]
#[derive(Default)]
pub struct BatchSubmissionResult {
pub accepted_scores: u64,
pub non_existent_nodes: Vec<NodeId>,
}
#[cfg(test)]
mod tests {
use super::*;
fn p(raw: impl AsRef<str>) -> Percent {
raw.as_ref().parse().unwrap()
}
fn ps(raw: &[&str]) -> Vec<Percent> {
raw.iter().map(p).collect()
}
#[test]
fn node_results_insertion() {
let initial = NodeResults::new(p("0.5"));
let mut smaller = initial.clone();
let mut greater = initial.clone();
smaller.insert_new(p("0.4"));
greater.insert_new(p("0.6"));
assert_eq!(smaller.0, ps(&["0.4", "0.5"]));
assert_eq!(greater.0, ps(&["0.5", "0.6"]));
let mut another = NodeResults(ps(&["0.1", "0.4", "0.5", "0.6", "0.6", "1.0"]));
another.insert_new(p("0.6"));
another.insert_new(p("0.2"));
another.insert_new(p("0.7"));
another.insert_new(p("0.3"));
another.insert_new(p("0.3"));
another.insert_new(p("0.55"));
assert_eq!(
another.0,
ps(&[
"0.1", "0.2", "0.3", "0.3", "0.4", "0.5", "0.55", "0.6", "0.6", "0.6", "0.7", "1.0"
])
);
}
#[test]
fn node_results_median() {
let results = NodeResults(ps(&["0.1"]));
assert_eq!(results.median(), p("0.1"));
let results = NodeResults(ps(&["0.1", "0.2"]));
assert_eq!(results.median(), p("0.15"));
let results = NodeResults(ps(&["0.1", "0.2", "0.3"]));
assert_eq!(results.median(), p("0.2"));
let results = NodeResults(ps(&["0.1", "0.2", "0.3", "0.4"]));
assert_eq!(results.median(), p("0.25"));
let results = NodeResults(ps(&["0.1", "0.2", "0.3", "0.4", "0.5"]));
assert_eq!(results.median(), p("0.3"));
let results = NodeResults(ps(&["0", "0", "1", "1", "1", "1", "1"]));
assert_eq!(results.median(), p("1"));
}
}
@@ -0,0 +1,27 @@
[package]
name = "nym-pool-contract-common"
version = "0.1.0"
description = "Common library for the Nym Pool contract"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
[dependencies]
thiserror = { workspace = true }
serde = { workspace = true }
schemars = { workspace = true }
cosmwasm-std = { workspace = true }
cosmwasm-schema = { workspace = true }
cw-controllers = { workspace = true }
[dev-dependencies]
time = { workspace = true, features = ["macros"] }
[features]
schema = []
@@ -0,0 +1,11 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod storage_keys {
pub const CONTRACT_ADMIN: &str = "contract-admin";
pub const POOL_DENOMINATION: &str = "pool_denom";
pub const GRANTERS: &str = "granters";
pub const GRANTS: &str = "grants";
pub const TOTAL_LOCKED: &str = "total_locked";
pub const LOCKED_GRANTEES: &str = "locked_grantees";
}
@@ -0,0 +1,98 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{Coin, Uint128};
use cw_controllers::AdminError;
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum NymPoolContractError {
#[error("could not perform contract migration: {comment}")]
FailedMigration { comment: String },
#[error(transparent)]
Admin(#[from] AdminError),
#[error(transparent)]
StdErr(#[from] cosmwasm_std::StdError),
#[error("this sender is not authorised to revoke this grant. its neither the admin or the original (and still whitelisted) granter")]
UnauthorizedGrantRevocation,
#[error("the specified address is already a whitelisted granter")]
AlreadyAGranter,
#[error("{addr} is not a permitted granter")]
InvalidGranter { addr: String },
#[error("invalid coin denomination. got {got}, but expected {expected}")]
InvalidDenom { expected: String, got: String },
#[error("there already exists an active grant for {grantee}. it was granted by {granter} at block height {created_at_height}")]
GrantAlreadyExist {
granter: String,
grantee: String,
created_at_height: u64,
},
#[error("could not find any active grants for {grantee}")]
GrantNotFound { grantee: String },
#[error("the provided timestamp value ({timestamp}) is set in the past. the current block timestamp is {current_block_timestamp}")]
TimestampInThePast {
timestamp: u64,
current_block_timestamp: u64,
},
#[error("there are not enough tokens to process this request. {available} are available, but {required} is needed.")]
InsufficientTokens { available: Coin, required: Coin },
#[error("the period length can't be zero")]
ZeroAllowancePeriod,
#[error("the provided coin value is zero")]
ZeroAmount,
#[error("the periodic spend limit of {periodic} was set to be higher than the total spend limit {total_limit}")]
PeriodicGrantOverSpendLimit { periodic: Coin, total_limit: Coin },
#[error("the accumulation spend limit of {accumulation} was set to be lower than the periodic grant amount of {periodic_grant}")]
AccumulationBelowGrantAmount {
accumulation: Coin,
periodic_grant: Coin,
},
#[error("the accumulation spend limit of {accumulation} was set to be higher than the total spend limit of {total_limit}")]
AccumulationOverSpendLimit {
accumulation: Coin,
total_limit: Coin,
},
#[error("the specified delayed allowance would never be available. it would become active at {available_timestamp} yet it expires at {expiration_timestamp}")]
UnattainableDelayedAllowance {
expiration_timestamp: u64,
available_timestamp: u64,
},
#[error("could not unlock {requested} tokens from {grantee}. it only has {locked} locked")]
InsufficientLockedTokens {
grantee: String,
locked: Uint128,
requested: Uint128,
},
#[error("attempted to spend more tokens than permitted by the current allowance")]
SpendingAboveAllowance,
#[error("attempted to send an empty allowance usage request")]
EmptyUsageRequest,
#[error("the associated grant has already expired")]
GrantExpired,
#[error("the associated grant hasn't expired yet")]
GrantNotExpired,
#[error("this grant is not available yet. it will become usable at {available_at_timestamp}")]
GrantNotYetAvailable { available_at_timestamp: u64 },
}
@@ -0,0 +1,12 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod constants;
pub mod error;
pub mod msg;
pub mod types;
mod utils;
pub use error::*;
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
pub use types::*;

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