Compare commits

...

42 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
benedettadavico 3ac58e0c49 Clean up
remove old explorer references
2025-06-11 16:02:19 +02: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
Andrej Mihajlov f5846d5bc2 Log all tracing output just in case 2025-06-04 11:40:56 +02:00
Andrej Mihajlov d7779df1b7 Include proc_pidinfo on iOS 2025-06-04 11:00:15 +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
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
446 changed files with 8391 additions and 100912 deletions
@@ -57,6 +57,7 @@ jobs:
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,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
+5 -1
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 }}
@@ -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 }}
Generated
+177 -9
View File
@@ -1923,6 +1923,26 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "cw-multi-test"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533b31c94b9e10e77e2468a2b1559aa506505d18c4e52eb64cbfc624ca876ad2"
dependencies = [
"anyhow",
"bech32",
"cosmwasm-schema",
"cosmwasm-std",
"cw-storage-plus",
"cw-utils",
"itertools 0.14.0",
"prost 0.13.5",
"schemars",
"serde",
"sha2 0.10.9",
"thiserror 2.0.12",
]
[[package]]
name = "cw-storage-plus"
version = "2.0.0"
@@ -5306,6 +5326,7 @@ dependencies = [
"nym-sphinx 0.1.0",
"nym-task 0.1.0",
"sqlx",
"sqlx-pool-guard",
"thiserror 2.0.12",
"time",
"tokio",
@@ -5450,6 +5471,7 @@ dependencies = [
name = "nym-contracts-common"
version = "0.5.0"
dependencies = [
"anyhow",
"bs58",
"cosmwasm-schema",
"cosmwasm-std",
@@ -5477,6 +5499,19 @@ dependencies = [
"vergen",
]
[[package]]
name = "nym-contracts-common-testing"
version = "0.1.0"
dependencies = [
"anyhow",
"cosmwasm-std",
"cw-multi-test",
"cw-storage-plus",
"rand 0.8.5",
"rand_chacha 0.3.1",
"serde",
]
[[package]]
name = "nym-cpp-ffi"
version = "0.1.2"
@@ -5573,6 +5608,7 @@ dependencies = [
"nym-ecash-time 0.1.0",
"serde",
"sqlx",
"sqlx-pool-guard",
"thiserror 2.0.12",
"tokio",
"zeroize",
@@ -6286,7 +6322,6 @@ dependencies = [
"schemars",
"semver 1.0.26",
"serde",
"serde-json-wasm",
"serde_repr",
"thiserror 2.0.12",
"time",
@@ -6649,7 +6684,7 @@ dependencies = [
[[package]]
name = "nym-node-status-api"
version = "3.0.0"
version = "3.1.0"
dependencies = [
"ammonia",
"anyhow",
@@ -6891,6 +6926,19 @@ dependencies = [
"tracing",
]
[[package]]
name = "nym-performance-contract-common"
version = "0.1.0"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"nym-contracts-common 0.5.0",
"schemars",
"serde",
"thiserror 2.0.12",
]
[[package]]
name = "nym-pool-contract-common"
version = "0.1.0"
@@ -7437,7 +7485,7 @@ dependencies = [
[[package]]
name = "nym-statistics-api"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -7710,6 +7758,7 @@ dependencies = [
"nym-mixnet-contract-common 0.6.0",
"nym-multisig-contract-common 0.1.0",
"nym-network-defaults 0.1.0",
"nym-performance-contract-common",
"nym-serde-helpers 0.1.0",
"nym-vesting-contract-common 0.7.0",
"prost 0.13.5",
@@ -8691,6 +8740,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "proc_pidinfo"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af53dad2390f8df98dda1e4188322bdf2f91c86cf6001f51d10d64451edf463a"
dependencies = [
"libc",
]
[[package]]
name = "prometheus"
version = "0.14.0"
@@ -10278,6 +10336,19 @@ dependencies = [
"whoami",
]
[[package]]
name = "sqlx-pool-guard"
version = "0.1.0"
dependencies = [
"proc_pidinfo",
"sqlx",
"tempfile",
"tokio",
"tracing",
"tracing-subscriber",
"windows 0.61.1",
]
[[package]]
name = "sqlx-postgres"
version = "0.8.6"
@@ -10733,6 +10804,7 @@ dependencies = [
"nym-mixnet-contract-common 0.6.0",
"nym-multisig-contract-common 0.1.0",
"nym-pemstore 0.3.0",
"nym-performance-contract-common",
"nym-validator-client 0.1.0",
"nym-vesting-contract-common 0.7.0",
"rand 0.8.5",
@@ -12292,6 +12364,28 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.61.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
dependencies = [
"windows-collections",
"windows-core 0.61.2",
"windows-future",
"windows-link",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [
"windows-core 0.61.2",
]
[[package]]
name = "windows-core"
version = "0.52.0"
@@ -12326,6 +12420,30 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement 0.60.0",
"windows-interface 0.59.1",
"windows-link",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
[[package]]
name = "windows-future"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core 0.61.2",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.57.0"
@@ -12348,6 +12466,17 @@ dependencies = [
"syn 2.0.98",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "windows-interface"
version = "0.57.0"
@@ -12371,10 +12500,31 @@ dependencies = [
]
[[package]]
name = "windows-link"
version = "0.1.0"
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "windows-link"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-numerics"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core 0.61.2",
"windows-link",
]
[[package]]
name = "windows-registry"
@@ -12382,7 +12532,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
dependencies = [
"windows-result 0.3.1",
"windows-result 0.3.4",
"windows-strings 0.3.1",
"windows-targets 0.53.0",
]
@@ -12407,9 +12557,9 @@ dependencies = [
[[package]]
name = "windows-result"
version = "0.3.1"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
]
@@ -12433,6 +12583,15 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
@@ -12531,6 +12690,15 @@ dependencies = [
"windows_x86_64_msvc 0.53.0",
]
[[package]]
name = "windows-threading"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
dependencies = [
"windows-link",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
+5 -5
View File
@@ -34,11 +34,12 @@ members = [
"common/config",
"common/cosmwasm-smart-contracts/coconut-dkg",
"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",
@@ -126,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",
@@ -135,7 +137,6 @@ 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/nym-cli",
"tools/nym-id-cli",
@@ -287,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"
@@ -368,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"
@@ -378,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 }
+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 nym_pool_contract
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
@@ -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,
},
@@ -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 -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"]
@@ -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)
@@ -211,17 +222,17 @@ impl StorageManager {
"#,
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(())
@@ -239,7 +250,7 @@ impl StorageManager {
stored_reply_surb.reply_surb,
stored_reply_surb.encoded_key_rotation
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
@@ -253,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
}
@@ -267,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
}
@@ -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 {
@@ -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 }
@@ -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)
@@ -276,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()
}
@@ -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)
}
}
@@ -11,7 +11,7 @@ use std::rc::Rc;
pub struct StorageWrapper(Rc<RefCell<MemoryStorage>>);
impl StorageWrapper {
pub(super) fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper {
pub fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper {
ContractStorageWrapper {
address: contract.clone(),
inner: self.clone(),
@@ -24,7 +24,7 @@ impl StorageWrapper {
}
#[derive(Debug, Clone)]
pub(crate) struct ContractStorageWrapper {
pub struct ContractStorageWrapper {
address: Addr,
inner: StorageWrapper,
}
@@ -33,6 +33,22 @@ 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 {
@@ -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 }
@@ -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())
}
}
@@ -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};
@@ -604,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())
}
}
@@ -773,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())
}
}
@@ -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())
}
}
@@ -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"));
}
}
+9 -2
View File
@@ -20,6 +20,8 @@ nym-credentials = { path = "../credentials" }
nym-compact-ecash = { path = "../nym_offline_compact_ecash" }
nym-ecash-time = { path = "../ecash-time" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
path = "../../sqlx-pool-guard"
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
workspace = true
@@ -31,8 +33,13 @@ features = ["rt-multi-thread", "net", "signal", "fs"]
[build-dependencies]
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
"sqlite",
"macros",
"migrate",
] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
[features]
persistent-storage = ["bincode", "serde"]
persistent-storage = ["bincode", "serde"]
@@ -7,10 +7,11 @@ use crate::models::{
};
use nym_ecash_time::Date;
use sqlx::{Executor, Sqlite, Transaction};
use sqlx_pool_guard::SqlitePoolGuard;
#[derive(Clone)]
pub struct SqliteEcashTicketbookManager {
connection_pool: sqlx::SqlitePool,
connection_pool: SqlitePoolGuard,
}
impl SqliteEcashTicketbookManager {
@@ -19,7 +20,7 @@ impl SqliteEcashTicketbookManager {
/// # Arguments
///
/// * `connection_pool`: database connection pool to use.
pub fn new(connection_pool: sqlx::SqlitePool) -> Self {
pub fn new(connection_pool: SqlitePoolGuard) -> Self {
SqliteEcashTicketbookManager { connection_pool }
}
@@ -33,7 +34,7 @@ impl SqliteEcashTicketbookManager {
"DELETE FROM ecash_ticketbook WHERE expiration_date <= ?",
deadline
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
@@ -60,7 +61,7 @@ impl SqliteEcashTicketbookManager {
data,
expiration_date,
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
@@ -90,7 +91,7 @@ impl SqliteEcashTicketbookManager {
epoch_id,
total_tickets,
used_tickets,
).execute(&self.connection_pool).await?;
).execute(&*self.connection_pool).await?;
Ok(())
}
@@ -105,7 +106,7 @@ impl SqliteEcashTicketbookManager {
"#,
)
.bind(data)
.fetch_optional(&self.connection_pool)
.fetch_optional(&*self.connection_pool)
.await?
.is_some();
@@ -121,7 +122,7 @@ impl SqliteEcashTicketbookManager {
FROM ecash_ticketbook
"#,
)
.fetch_all(&self.connection_pool)
.fetch_all(&*self.connection_pool)
.await
}
@@ -143,7 +144,7 @@ impl SqliteEcashTicketbookManager {
ticketbook_id,
expected_current_total_spent
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?
.rows_affected();
Ok(affected > 0)
@@ -153,7 +154,7 @@ impl SqliteEcashTicketbookManager {
&self,
) -> Result<Vec<StoredPendingTicketbook>, sqlx::Error> {
sqlx::query_as("SELECT * FROM pending_issuance")
.fetch_all(&self.connection_pool)
.fetch_all(&*self.connection_pool)
.await
}
@@ -165,7 +166,7 @@ impl SqliteEcashTicketbookManager {
"DELETE FROM pending_issuance WHERE deposit_id = ?",
pending_id
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
@@ -182,7 +183,7 @@ impl SqliteEcashTicketbookManager {
"#,
epoch_id
)
.fetch_optional(&self.connection_pool)
.fetch_optional(&*self.connection_pool)
.await
}
@@ -208,7 +209,7 @@ impl SqliteEcashTicketbookManager {
serialisation_revision,
epoch_id
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
@@ -225,7 +226,7 @@ impl SqliteEcashTicketbookManager {
"#,
epoch_id
)
.fetch_optional(&self.connection_pool)
.fetch_optional(&*self.connection_pool)
.await
}
@@ -251,7 +252,7 @@ impl SqliteEcashTicketbookManager {
serialisation_revision,
epoch_id,
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
@@ -269,7 +270,7 @@ impl SqliteEcashTicketbookManager {
"#,
expiration_date
)
.fetch_optional(&self.connection_pool)
.fetch_optional(&*self.connection_pool)
.await
}
@@ -298,7 +299,7 @@ impl SqliteEcashTicketbookManager {
serialisation_revision,
expiration_date
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
@@ -37,6 +37,7 @@ use sqlx::{
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
ConnectOptions,
};
use sqlx_pool_guard::SqlitePoolGuard;
use std::path::Path;
use zeroize::Zeroizing;
@@ -54,15 +55,15 @@ impl PersistentStorage {
/// * `database_path`: path to the database.
pub async fn init<P: AsRef<Path>>(database_path: P) -> Result<Self, StorageError> {
debug!(
"Attempting to connect to database {:?}",
database_path.as_ref().as_os_str()
"Attempting to connect to database {}",
database_path.as_ref().display()
);
let opts = sqlx::sqlite::SqliteConnectOptions::new()
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.synchronous(SqliteSynchronous::Normal)
.auto_vacuum(SqliteAutoVacuum::Incremental)
.filename(database_path)
.filename(&database_path)
.create_if_missing(true)
.disable_statement_logging();
@@ -74,13 +75,17 @@ impl PersistentStorage {
}
};
if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await {
let connection_pool =
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
if let Err(err) = sqlx::migrate!("./migrations").run(&*connection_pool).await {
error!("Failed to perform migration on the SQLx database: {err}");
connection_pool.close().await;
return Err(err.into());
}
Ok(PersistentStorage {
storage_manager: SqliteEcashTicketbookManager::new(connection_pool.clone()),
storage_manager: SqliteEcashTicketbookManager::new(connection_pool),
})
}
}
@@ -88,7 +88,8 @@ impl BandwidthStorageManager {
debug!(available = available_bi2, required = required_bi2);
self.consume_bandwidth(required_bandwidth).await?;
Ok(available_bandwidth)
let remaining_bandwidth = self.client_bandwidth.available().await;
Ok(remaining_bandwidth)
}
async fn expire_bandwidth(&mut self) -> Result<()> {
+14 -7
View File
@@ -14,6 +14,7 @@ use nym_dkg::bte::{
};
use nym_dkg::interpolation::polynomial::Polynomial;
use nym_dkg::{combine_shares, Dealing, NodeIndex, Share, Threshold};
use rand::CryptoRng;
use rand_core::{RngCore, SeedableRng};
use std::collections::BTreeMap;
@@ -31,7 +32,7 @@ pub fn precomputing_g2_generator_for_miller_loop(c: &mut Criterion) {
}
fn prepare_keys(
mut rng: impl RngCore,
mut rng: impl RngCore + CryptoRng,
nodes: usize,
) -> (BTreeMap<NodeIndex, PublicKey>, Vec<DecryptionKey>) {
let params = setup();
@@ -50,7 +51,7 @@ fn prepare_keys(
}
fn prepare_resharing(
mut rng: impl RngCore,
mut rng: impl RngCore + CryptoRng,
params: &Params,
nodes: usize,
threshold: Threshold,
@@ -68,7 +69,7 @@ fn prepare_resharing(
for (i, ref mut dk) in dks.iter_mut().enumerate() {
let shares = first_dealings
.iter()
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
.map(|dealing| decrypt_share(params, dk, i, &dealing.ciphertexts, None).unwrap())
.collect();
let recovered_secret =
@@ -154,7 +155,9 @@ pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criteri
|b| {
b.iter(|| {
assert!(dealing.verify(&params, threshold, &receivers, None).is_ok());
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap());
black_box(
decrypt_share(&params, first_key, 0, &dealing.ciphertexts, None).unwrap(),
);
})
},
);
@@ -237,7 +240,9 @@ pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criter
|b| {
b.iter(|| {
assert!(dealing.verify(&params, threshold, &receivers, None).is_ok());
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap());
black_box(
decrypt_share(&params, first_key, 0, &dealing.ciphertexts, None).unwrap(),
);
})
},
);
@@ -320,7 +325,9 @@ pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Crite
|b| {
b.iter(|| {
assert!(dealing.verify(&params, threshold, &receivers, None).is_ok());
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap());
black_box(
decrypt_share(&params, first_key, 0, &dealing.ciphertexts, None).unwrap(),
);
})
},
);
@@ -547,7 +554,7 @@ pub fn share_decryption(c: &mut Criterion) {
let (ciphertexts, _) = encrypt_shares(&[(&share, pk.public_key())], &params, &mut rng);
c.bench_function("single share decryption", |b| {
b.iter(|| black_box(decrypt_share(&dk, 0, &ciphertexts, None)))
b.iter(|| black_box(decrypt_share(&params, &dk, 0, &ciphertexts, None)))
});
}
+43 -10
View File
@@ -9,6 +9,7 @@ use crate::{Chunk, ChunkedShare, Share};
use bls12_381::{G1Affine, G1Projective, G2Prepared, G2Projective, Gt, Scalar};
use ff::Field;
use group::{Curve, Group, GroupEncoding};
use rand::CryptoRng;
use rand_core::RngCore;
use std::collections::HashMap;
use std::ops::Neg;
@@ -191,7 +192,7 @@ impl HazmatRandomness {
pub fn encrypt_shares(
shares: &[(&Share, &PublicKey)],
params: &Params,
mut rng: impl RngCore,
mut rng: impl RngCore + CryptoRng,
) -> (Ciphertexts, HazmatRandomness) {
let g1 = G1Projective::generator();
@@ -262,6 +263,7 @@ pub fn encrypt_shares(
}
pub fn decrypt_share(
params: &Params,
dk: &DecryptionKey,
// in the case of multiple receivers, specifies which index of ciphertext chunks should be used
i: usize,
@@ -270,6 +272,10 @@ pub fn decrypt_share(
) -> Result<Share, DkgError> {
let mut plaintext = ChunkedShare::default();
if !ciphertext.verify_integrity(params) {
return Err(DkgError::FailedCiphertextIntegrityCheck);
}
if i >= ciphertext.ciphertext_chunks.len() {
return Err(DkgError::UnavailableCiphertext(i));
}
@@ -461,10 +467,22 @@ mod tests {
let (ciphertext, hazmat) = encrypt_shares(shares, &params, &mut rng);
verify_hazmat_rand(&ciphertext, &hazmat);
let recovered1 =
decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap();
let recovered2 =
decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap();
let recovered1 = decrypt_share(
&params,
&decryption_key1,
0,
&ciphertext,
Some(lookup_table),
)
.unwrap();
let recovered2 = decrypt_share(
&params,
&decryption_key2,
1,
&ciphertext,
Some(lookup_table),
)
.unwrap();
assert_eq!(m1, recovered1);
assert_eq!(m2, recovered2);
}
@@ -490,10 +508,22 @@ mod tests {
let (ciphertext, hazmat) = encrypt_shares(shares, &params, &mut rng);
verify_hazmat_rand(&ciphertext, &hazmat);
let recovered1 =
decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap();
let recovered2 =
decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap();
let recovered1 = decrypt_share(
&params,
&decryption_key1,
0,
&ciphertext,
Some(lookup_table),
)
.unwrap();
let recovered2 = decrypt_share(
&params,
&decryption_key2,
1,
&ciphertext,
Some(lookup_table),
)
.unwrap();
assert_eq!(m1, recovered1);
assert_eq!(m2, recovered2);
}
@@ -574,7 +604,10 @@ mod tests {
#[test]
fn ciphertexts_roundtrip() {
fn random_ciphertexts(mut rng: impl RngCore, num_receivers: usize) -> Ciphertexts {
fn random_ciphertexts(
mut rng: impl RngCore + CryptoRng,
num_receivers: usize,
) -> Ciphertexts {
Ciphertexts {
rr: (0..NUM_CHUNKS)
.map(|_| G1Projective::random(&mut rng))
+6 -2
View File
@@ -9,11 +9,15 @@ use bls12_381::{G1Projective, G2Projective, Scalar};
use ff::Field;
use group::GroupEncoding;
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use rand::CryptoRng;
use rand_core::RngCore;
use zeroize::Zeroize;
// produces public key and a decryption key for the root of the tree
pub fn keygen(params: &Params, mut rng: impl RngCore) -> (DecryptionKey, PublicKeyWithProof) {
pub fn keygen(
params: &Params,
mut rng: impl RngCore + CryptoRng,
) -> (DecryptionKey, PublicKeyWithProof) {
let g1 = G1Projective::generator();
let g2 = G2Projective::generator();
@@ -244,7 +248,7 @@ pub struct KeyPair {
}
impl KeyPair {
pub fn new(params: &Params, rng: impl RngCore) -> Self {
pub fn new(params: &Params, rng: impl RngCore + CryptoRng) -> Self {
let (dk, pk) = keygen(params, rng);
Self {
private_key: dk,
+90 -20
View File
@@ -10,7 +10,7 @@ use crate::utils::{deserialize_scalar, RandomOracleBuilder};
use bls12_381::{G1Projective, Scalar};
use ff::Field;
use group::{Group, GroupEncoding};
use rand::Rng;
use rand::{CryptoRng, Rng};
use rand_core::{RngCore, SeedableRng};
const CHUNKING_ORACLE_DOMAIN: &[u8] =
@@ -28,6 +28,7 @@ const SECURITY_PARAMETER: usize = 256;
/// ceil(SECURITY_PARAMETER / PARALLEL_RUNS) in the paper
const NUM_CHALLENGE_BITS: usize = SECURITY_PARAMETER.div_ceil(PARALLEL_RUNS);
const EE: usize = 1 << NUM_CHALLENGE_BITS;
// type alias for ease of use
type FirstChallenge = Vec<Vec<Vec<u64>>>;
@@ -94,7 +95,7 @@ impl ProofOfChunking {
// Scalar(-1) would in reality be Scalar(q - 1), which is greater than Scalar(1) and opposite to
// what we wanted.
pub fn construct(
mut rng: impl RngCore,
mut rng: impl RngCore + CryptoRng,
instance: Instance,
witness_r: &[Scalar; NUM_CHUNKS],
witnesses_s: &[Share],
@@ -110,21 +111,20 @@ impl ProofOfChunking {
// define bounds for the blinding factors
let n = instance.public_keys.len();
let m = NUM_CHUNKS;
let ee = 1 << NUM_CHALLENGE_BITS;
// CHUNK_MAX corresponds to paper's B
let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64;
let zz = (2 * (PARALLEL_RUNS as u64))
.checked_mul(ss)
.expect("overflow in Z = 2 * l * S");
// ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1))
// Z = 2 * l * S
let (ss, zz): (u64, u64) = compute_ss_zz(n, m)?;
let ss_scalar = Scalar::from(ss);
// rather than generating blinding factors in [-S, Z-1] directly,
// do it via [0, Z - 1 + S + 1] and deal with the shift later.
let combined_upper_range = (zz - 1)
.checked_add(ss + 1)
.expect("overflow in Z - 1 + S + 1");
// combined_upper_range = Z - 1 + S + 1
let combined_upper_range = zz.checked_add(ss).ok_or(DkgError::ArithmeticOverflow {
info: "ProofOfChunking::construct | Z - 1 + S + 1",
})?;
let mut betas = Vec::with_capacity(PARALLEL_RUNS);
let mut bs = Vec::with_capacity(PARALLEL_RUNS);
@@ -178,12 +178,23 @@ impl ProofOfChunking {
// I think this part is more readable with a range loop
#[allow(clippy::needless_range_loop)]
for l in 0..PARALLEL_RUNS {
let mut sum = 0;
let mut sum: u64 = 0;
for (i, witness_i) in witnesses_s.iter().enumerate() {
for (j, witness_ij) in witness_i.to_chunks().chunks.iter().enumerate() {
debug_assert!(std::mem::size_of::<Chunk>() <= std::mem::size_of::<u64>());
sum += first_challenge[i][j][l] * (*witness_ij as u64)
// sum += first_challenge[i][j][l] * (*witness_ij as u64)
sum = sum
.checked_add(
first_challenge[i][j][l]
.checked_mul(*witness_ij as u64)
.ok_or(DkgError::ArithmeticOverflow {
info: "ProofOfChunking::construct | first_challenge[i][j][l] * witness_ij",
})?,
)
.ok_or(DkgError::ArithmeticOverflow {
info: "ProofOfChunking::construct | sum + (first_challenge[i][j][l] * witness_ij)",
})?;
}
}
@@ -191,7 +202,18 @@ impl ProofOfChunking {
continue 'retry_loop;
}
// shifted_blinding_factors[l] - ss restores it to "proper" [-S, Z - 1] range
let response = sum + shifted_blinding_factors[l] - ss;
// let response = sum + shifted_blinding_factors[l] - ss;
let response = sum
.checked_add(shifted_blinding_factors[l])
.ok_or(DkgError::ArithmeticOverflow {
info:
"ProofOfChunking::construct | sum + (shifted_blinding_factors[l] - ss)",
})?
.checked_sub(ss)
.ok_or(DkgError::ArithmeticUnderflow {
info: "ProofOfChunking::construct | shifted_blinding_factors[l] - ss",
})?;
if response < zz {
responses_chunks.push(response)
} else {
@@ -276,11 +298,13 @@ impl ProofOfChunking {
ensure_len!(&self.responses_r, n);
ensure_len!(&self.responses_chunks, PARALLEL_RUNS);
let ee = 1 << NUM_CHALLENGE_BITS;
// ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1))
// Z = 2 * l * S
// CHUNK_MAX corresponds to paper's B
let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64;
let zz = 2 * (PARALLEL_RUNS as u64) * ss;
let zz: u64 = match compute_ss_zz(n, m) {
Ok((_, zz_res)) => zz_res,
_ => return false,
};
for response_chunk in &self.responses_chunks {
if response_chunk >= &zz {
@@ -411,7 +435,7 @@ impl ProofOfChunking {
random_oracle_builder.update(lambda_e.to_be_bytes());
let mut oracle = rand_chacha::ChaCha20Rng::from_seed(random_oracle_builder.finalize());
let range_max_excl = 1 << NUM_CHALLENGE_BITS;
let range_max_excl = EE as u64;
(0..n)
.map(|_| {
@@ -637,6 +661,50 @@ impl ProofOfChunking {
}
}
fn compute_ss_zz(n: usize, m: usize) -> Result<(u64, u64), DkgError> {
// let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64;
// CHUNK_MAX corresponds to paper's B
let ee = EE;
let ss = n
.checked_mul(m)
.ok_or(DkgError::ArithmeticOverflow {
info: "ProofOfChunking::compute_ss_zz | n * m",
})?
.checked_mul(
CHUNK_SIZE
.checked_sub(1)
.ok_or(DkgError::ArithmeticUnderflow {
info: "ProofOfChunking::compute_ss_zz | (CHUNK_SIZE - 1)",
})?
.checked_mul(ee.checked_sub(1).ok_or(DkgError::ArithmeticUnderflow {
info: "ProofOfChunking::compute_ss_zz | (ee - 1)",
})?)
.ok_or(DkgError::ArithmeticOverflow {
info: "ProofOfChunking::compute_ss_zz | (CHUNK_SIZE - 1) * (ee - 1)",
})?,
)
.ok_or(DkgError::ArithmeticOverflow {
info: "ProofOfChunking::compute_ss_zz | ss_lhs * ss_rhs",
})? as u64;
// let zz = 2 * PARALLEL_RUNS as u64 * ss;
// Z = 2 * l * S
let zz = 2u64
.checked_mul(PARALLEL_RUNS as u64)
.ok_or(DkgError::ArithmeticOverflow {
info: "ProofOfChunking::compute_ss_zz | 2 * l",
})?
.checked_mul(ss)
.ok_or(DkgError::ArithmeticOverflow {
info: "ProofOfChunking::compute_ss_zz | (2 * l) * S",
})?;
Ok((ss, zz))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -652,7 +720,9 @@ mod tests {
ciphertext_chunks: Vec<[G1Projective; NUM_CHUNKS]>,
}
fn setup(mut rng: impl RngCore) -> (OwnedInstance, [Scalar; NUM_CHUNKS], Vec<Share>) {
fn setup(
mut rng: impl RngCore + CryptoRng,
) -> (OwnedInstance, [Scalar; NUM_CHUNKS], Vec<Share>) {
let g1 = G1Projective::generator();
let mut pks = Vec::with_capacity(NODES);
+6 -1
View File
@@ -5,6 +5,7 @@ use crate::utils::hash_to_scalar;
use bls12_381::{G1Projective, Scalar};
use ff::Field;
use group::GroupEncoding;
use rand::CryptoRng;
use rand_core::RngCore;
use zeroize::Zeroize;
@@ -20,7 +21,11 @@ pub struct ProofOfDiscreteLog {
}
impl ProofOfDiscreteLog {
pub fn construct(mut rng: impl RngCore, public: &G1Projective, witness: &Scalar) -> Self {
pub fn construct(
mut rng: impl RngCore + CryptoRng,
public: &G1Projective,
witness: &Scalar,
) -> Self {
let mut rand_x = Scalar::random(&mut rng);
let rand_commitment = G1Projective::generator() * rand_x;
let challenge = Self::compute_challenge(public, &rand_commitment);
+4 -2
View File
@@ -9,6 +9,7 @@ use crate::{NodeIndex, Share};
use bls12_381::{G1Projective, G2Projective, Scalar};
use ff::Field;
use group::GroupEncoding;
use rand::CryptoRng;
use rand_core::RngCore;
use std::collections::BTreeMap;
@@ -87,7 +88,7 @@ pub struct ProofOfSecretSharing {
impl ProofOfSecretSharing {
pub fn construct(
mut rng: impl RngCore,
mut rng: impl RngCore + CryptoRng,
instance: Instance,
witness_r: &Scalar,
witnesses_s: &[Share],
@@ -309,13 +310,14 @@ mod tests {
use super::*;
use crate::interpolation::polynomial::Polynomial;
use group::Group;
use rand::CryptoRng;
use rand_core::SeedableRng;
const NODES: u64 = 50;
const THRESHOLD: u64 = 40;
fn setup(
mut rng: impl RngCore,
mut rng: impl RngCore + CryptoRng,
) -> (
BTreeMap<NodeIndex, PublicKey>,
PublicCoefficients,
+4 -3
View File
@@ -13,6 +13,7 @@ use crate::utils::deserialize_g2;
use crate::{NodeIndex, Share, Threshold};
use bls12_381::{G2Projective, Scalar};
use group::GroupEncoding;
use rand::CryptoRng;
use rand_core::RngCore;
use std::collections::BTreeMap;
use zeroize::Zeroize;
@@ -94,7 +95,7 @@ impl Dealing {
// I'm not a big fan of this function signature, but I'm not clear on how to improve it while
// allowing the dealer to skip decryption of its own share if it was also one of the receivers
pub fn create(
mut rng: impl RngCore,
mut rng: impl RngCore + CryptoRng + CryptoRng,
params: &Params,
dealer_index: NodeIndex,
threshold: Threshold,
@@ -484,7 +485,7 @@ mod tests {
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
let shares = dealings
.values()
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
.map(|dealing| decrypt_share(&params, dk, i, &dealing.ciphertexts, None).unwrap())
.collect();
derived_secrets.push(
combine_shares(shares, &receivers.keys().copied().collect::<Vec<_>>()).unwrap(),
@@ -593,7 +594,7 @@ mod tests {
for (i, (dk, _)) in full_keys.iter().enumerate() {
let shares = dealings
.values()
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
.map(|dealing| decrypt_share(&params, dk, i, &dealing.ciphertexts, None).unwrap())
.collect();
let recovered_secret = combine_shares(shares, &dealer_indices).unwrap();
+6
View File
@@ -99,6 +99,12 @@ pub enum DkgError {
"The reshared dealing has different public constant coefficient than its prior variant"
)]
InvalidResharing,
#[error("Arithmetic Overflow: {info}")]
ArithmeticOverflow { info: &'static str },
#[error("Arithmetic Underflow: {info}")]
ArithmeticUnderflow { info: &'static str },
}
impl DkgError {
+2 -1
View File
@@ -6,6 +6,7 @@ use crate::utils::deserialize_g2;
use bls12_381::{G2Projective, Scalar};
use ff::Field;
use group::GroupEncoding;
use rand::CryptoRng;
use rand_core::RngCore;
use std::ops::{Add, Index, IndexMut};
use zeroize::Zeroize;
@@ -120,7 +121,7 @@ impl Polynomial {
// for polynomial of degree n, we generate n+1 values
// (for example for degree 1, like y = x + 2, we need [2,1])
/// Creates new pseudorandom polynomial of specified degree.
pub fn new_random(mut rng: impl RngCore, degree: u64) -> Self {
pub fn new_random(mut rng: impl RngCore + CryptoRng + CryptoRng, degree: u64) -> Self {
Polynomial {
coefficients: (0..=degree).map(|_| Scalar::random(&mut rng)).collect(),
}
+8 -7
View File
@@ -53,11 +53,12 @@ fn single_sender() {
// make sure each share is actually decryptable (even though proofs say they must be, perform this sanity check)
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
let _recovered = decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap();
let _recovered = decrypt_share(&params, dk, i, &dealing.ciphertexts, None).unwrap();
}
// and for good measure, check that the dealer's share matches decryption result
let recovered_dealer = decrypt_share(&full_keys[0].0, 0, &dealing.ciphertexts, None).unwrap();
let recovered_dealer =
decrypt_share(&params, &full_keys[0].0, 0, &dealing.ciphertexts, None).unwrap();
assert_eq!(recovered_dealer, dealer_share.unwrap());
}
@@ -115,7 +116,7 @@ fn full_threshold_secret_sharing() {
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
let shares = dealings
.values()
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
.map(|dealing| decrypt_share(&params, dk, i, &dealing.ciphertexts, None).unwrap())
.collect();
// we know dealer_share matches, but it would be inconvenient to try to put them in here,
@@ -189,7 +190,7 @@ fn full_threshold_secret_resharing() {
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
let shares = first_dealings
.values()
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
.map(|dealing| decrypt_share(&params, dk, i, &dealing.ciphertexts, None).unwrap())
.collect();
let recovered_secret =
@@ -240,7 +241,7 @@ fn full_threshold_secret_resharing() {
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
let shares = resharing_dealings
.values()
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
.map(|dealing| decrypt_share(&params, dk, i, &dealing.ciphertexts, None).unwrap())
.collect();
let recovered_secret =
@@ -305,7 +306,7 @@ fn full_threshold_secret_resharing_left_party() {
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
let shares = first_dealings
.values()
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
.map(|dealing| decrypt_share(&params, dk, i, &dealing.ciphertexts, None).unwrap())
.collect();
let recovered_secret =
@@ -369,7 +370,7 @@ fn full_threshold_secret_resharing_left_party() {
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
let shares = resharing_dealings
.values()
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
.map(|dealing| decrypt_share(&params, dk, i, &dealing.ciphertexts, None).unwrap())
.collect();
let recovered_secret = combine_shares(shares, &node_indices).unwrap();
+2 -2
View File
@@ -33,8 +33,8 @@ impl PersistentStatsStorage {
/// * `database_path`: path to the database.
pub async fn init<P: AsRef<Path> + Send>(database_path: P) -> Result<Self, StatsStorageError> {
debug!(
"Attempting to connect to database {:?}",
database_path.as_ref().as_os_str()
"Attempting to connect to database {}",
database_path.as_ref().display()
);
// TODO: we can inject here more stuff based on our gateway global config
+2 -2
View File
@@ -82,8 +82,8 @@ impl GatewayStorage {
message_retrieval_limit: i64,
) -> Result<Self, GatewayStorageError> {
debug!(
"Attempting to connect to database {:?}",
database_path.as_ref().as_os_str()
"Attempting to connect to database {}",
database_path.as_ref().display()
);
// TODO: we can inject here more stuff based on our gateway global config
-1
View File
@@ -23,7 +23,6 @@ fn main() {
"REWARDING_VALIDATOR_ADDRESS",
"NYM_API",
"NYXD_WS",
"EXPLORER_API",
"NYM_VPN_API",
];
+35 -5
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
#[cfg(feature = "network")]
use crate::{DenomDetails, ValidatorDetails};
use crate::{ApiUrlConst, DenomDetails, ValidatorDetails};
pub const NETWORK_NAME: &str = "mainnet";
@@ -17,6 +17,11 @@ pub const MIXNET_CONTRACT_ADDRESS: &str =
"n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr";
pub const VESTING_CONTRACT_ADDRESS: &str =
"n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw";
// \/ TODO: this has to be updated once the contract is deployed
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "";
// /\ TODO: this has to be updated once the contract is deployed
pub const ECASH_CONTRACT_ADDRESS: &str =
"n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun";
pub const GROUP_CONTRACT_ADDRESS: &str =
@@ -29,10 +34,37 @@ pub const COCONUT_DKG_CONTRACT_ADDRESS: &str =
pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy";
pub const NYXD_URL: &str = "https://rpc.nymtech.net";
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket";
pub const EXPLORER_API: &str = "https://explorer.nymtech.net/api/";
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
#[cfg(feature = "network")]
pub const NYM_APIS: &[ApiUrlConst] = &[
ApiUrlConst {
url: NYM_API,
front_hosts: None,
},
ApiUrlConst {
url: "https://nym-fronntdoor.vercel.app/api/",
front_hosts: Some(&["vercel.app", "vercel.com"]),
},
ApiUrlConst {
url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
},
];
pub const NYM_VPN_API: &str = "https://nymvpn.com/api/";
#[cfg(feature = "network")]
pub const NYM_VPN_APIS: &[ApiUrlConst] = &[
ApiUrlConst {
url: NYM_VPN_API,
front_hosts: Some(&["vercel.app", "vercel.com"]),
},
ApiUrlConst {
url: "https://nymvpn-frontdoor.global.ssl.fastly.net/api/",
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
},
];
// I'm making clippy mad on purpose, because that url HAS TO be updated and deployed before merging
pub const EXIT_POLICY_URL: &str =
@@ -123,7 +155,6 @@ pub fn export_to_env() {
set_var_to_default(var_names::NYXD, NYXD_URL);
set_var_to_default(var_names::NYM_API, NYM_API);
set_var_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS);
set_var_to_default(var_names::EXPLORER_API, EXPLORER_API);
set_var_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL);
set_var_to_default(var_names::NYM_VPN_API, NYM_VPN_API);
}
@@ -165,6 +196,5 @@ pub fn export_to_env_if_not_set() {
set_var_conditionally_to_default(var_names::NYXD, NYXD_URL);
set_var_conditionally_to_default(var_names::NYM_API, NYM_API);
set_var_conditionally_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS);
set_var_conditionally_to_default(var_names::EXPLORER_API, EXPLORER_API);
set_var_conditionally_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL);
}
+40 -11
View File
@@ -20,6 +20,8 @@ pub struct ChainDetails {
pub struct NymContracts {
pub mixnet_contract_address: Option<String>,
pub vesting_contract_address: Option<String>,
#[serde(default)]
pub performance_contract_address: Option<String>,
pub ecash_contract_address: Option<String>,
pub group_contract_address: Option<String>,
pub multisig_contract_address: Option<String>,
@@ -35,8 +37,38 @@ pub struct NymNetworkDetails {
pub chain_details: ChainDetails,
pub endpoints: Vec<ValidatorDetails>,
pub contracts: NymContracts,
pub explorer_api: Option<String>,
pub nym_vpn_api_url: Option<String>,
pub nym_api_urls: Option<Vec<ApiUrl>>,
pub nym_vpn_api_urls: Option<Vec<ApiUrl>>,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct ApiUrl {
/// Expects a string formatted Url
///
/// see https://docs.rs/url/latest/url/struct.Url.html
pub url: String,
/// Optional alternative equivalent hostnames. Each entry must parse as valid Host
///
/// see https://docs.rs/url/latest/url/enum.Host.html
pub front_hosts: Option<Vec<String>>,
}
pub struct ApiUrlConst<'a> {
pub url: &'a str,
pub front_hosts: Option<&'a [&'a str]>,
}
impl From<ApiUrlConst<'_>> for ApiUrl {
fn from(value: ApiUrlConst) -> Self {
ApiUrl {
url: value.url.to_string(),
front_hosts: value
.front_hosts
.map(|slice| slice.iter().map(|s| s.to_string()).collect()),
}
}
}
// by default we assume the same defaults as mainnet, i.e. same prefixes and denoms
@@ -65,8 +97,9 @@ impl NymNetworkDetails {
},
endpoints: Default::default(),
contracts: Default::default(),
explorer_api: Default::default(),
nym_vpn_api_url: Default::default(),
nym_api_urls: Default::default(),
nym_vpn_api_urls: Default::default(),
}
}
@@ -124,7 +157,6 @@ impl NymNetworkDetails {
.with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS))
.with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
.with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS))
.with_explorer_api(get_optional_env(var_names::EXPLORER_API))
.with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API))
}
@@ -145,6 +177,9 @@ impl NymNetworkDetails {
contracts: NymContracts {
mixnet_contract_address: parse_optional_str(mainnet::MIXNET_CONTRACT_ADDRESS),
vesting_contract_address: parse_optional_str(mainnet::VESTING_CONTRACT_ADDRESS),
performance_contract_address: parse_optional_str(
mainnet::PERFORMANCE_CONTRACT_ADDRESS,
),
ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS),
group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS),
multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS),
@@ -152,8 +187,9 @@ impl NymNetworkDetails {
mainnet::COCONUT_DKG_CONTRACT_ADDRESS,
),
},
explorer_api: parse_optional_str(mainnet::EXPLORER_API),
nym_vpn_api_url: parse_optional_str(mainnet::NYM_VPN_API),
nym_api_urls: None,
nym_vpn_api_urls: None,
}
}
@@ -193,7 +229,6 @@ impl NymNetworkDetails {
set_optional_var(var_names::MULTISIG_CONTRACT_ADDRESS, self.contracts.multisig_contract_address);
set_optional_var(var_names::COCONUT_DKG_CONTRACT_ADDRESS, self.contracts.coconut_dkg_contract_address);
set_optional_var(var_names::EXPLORER_API, self.explorer_api);
set_optional_var(var_names::NYM_VPN_API, self.nym_vpn_api_url);
}
@@ -297,12 +332,6 @@ impl NymNetworkDetails {
self
}
#[must_use]
pub fn with_explorer_api<S: Into<String>>(mut self, endpoint: Option<S>) -> Self {
self.explorer_api = endpoint.map(Into::into);
self
}
#[must_use]
pub fn with_nym_vpn_api_url<S: Into<String>>(mut self, endpoint: Option<S>) -> Self {
self.nym_vpn_api_url = endpoint.map(Into::into);
-1
View File
@@ -22,7 +22,6 @@ pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS";
pub const NYXD: &str = "NYXD";
pub const NYM_API: &str = "NYM_API";
pub const NYXD_WEBSOCKET: &str = "NYXD_WS";
pub const EXPLORER_API: &str = "EXPLORER_API";
pub const EXIT_POLICY_URL: &str = "EXIT_POLICY";
pub const NYM_VPN_API: &str = "NYM_VPN_API";
pub const CLIENT_STATS_COLLECTION_PROVIDER: &str = "CLIENT_STATS_COLLECTION_PROVIDER";
+50 -7
View File
@@ -31,9 +31,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "anyhow"
version = "1.0.97"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "ark-bls12-381"
@@ -1133,6 +1133,19 @@ dependencies = [
"vergen",
]
[[package]]
name = "nym-contracts-common-testing"
version = "0.1.0"
dependencies = [
"anyhow",
"cosmwasm-std",
"cw-multi-test",
"cw-storage-plus",
"rand",
"rand_chacha",
"serde",
]
[[package]]
name = "nym-crypto"
version = "0.4.0"
@@ -1214,7 +1227,9 @@ dependencies = [
"cw2",
"easy-addr",
"nym-contracts-common",
"nym-contracts-common-testing",
"nym-crypto",
"nym-mixnet-contract",
"nym-mixnet-contract-common",
"nym-vesting-contract-common",
"rand",
@@ -1238,7 +1253,6 @@ dependencies = [
"schemars",
"semver",
"serde",
"serde-json-wasm",
"serde_repr",
"thiserror 2.0.12",
"time",
@@ -1276,6 +1290,38 @@ dependencies = [
"zeroize",
]
[[package]]
name = "nym-performance-contract"
version = "0.1.0"
dependencies = [
"anyhow",
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"cw-storage-plus",
"cw2",
"nym-contracts-common",
"nym-contracts-common-testing",
"nym-crypto",
"nym-mixnet-contract",
"nym-mixnet-contract-common",
"nym-performance-contract-common",
"serde",
]
[[package]]
name = "nym-performance-contract-common"
version = "0.1.0"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"nym-contracts-common",
"schemars",
"serde",
"thiserror 2.0.12",
]
[[package]]
name = "nym-pool-contract"
version = "0.1.0"
@@ -1284,14 +1330,11 @@ dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"cw-multi-test",
"cw-storage-plus",
"cw2",
"nym-contracts-common",
"nym-contracts-common-testing",
"nym-pool-contract-common",
"rand",
"rand_chacha",
"serde",
]
[[package]]
+2 -1
View File
@@ -9,6 +9,7 @@ members = [
"multisig/cw3-flex-multisig",
"multisig/cw4-group",
"vesting",
"performance",
]
[workspace.package]
@@ -64,4 +65,4 @@ dbg_macro = "deny"
exit = "deny"
panic = "deny"
unimplemented = "deny"
unreachable = "deny"
unreachable = "deny"
+12 -6
View File
@@ -26,13 +26,13 @@ name = "mixnet_contract"
crate-type = ["cdylib", "rlib"]
[dependencies]
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.6.0" }
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.7.0" }
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", version = "0.5.0" }
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common" }
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common" }
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing", optional = true }
cosmwasm-schema = { workspace = true, optional = true }
cosmwasm-std = { workspace = true }
cw-controllers = { workspace = true }
cw2 = { workspace = true }
cw-storage-plus = { workspace = true }
@@ -41,16 +41,22 @@ bs58 = { workspace = true }
serde = { workspace = true, default-features = false, features = ["derive"] }
semver = { workspace = true }
[dev-dependencies]
anyhow.workspace = true
rand_chacha = "0.3"
rand = "0.8.5"
rand_chacha = { workspace = true }
rand = { workspace = true }
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
easy-addr = { path = "../../common/cosmwasm-smart-contracts/easy_addr" }
# activate the `testable-mixnet-contract` in tests (weird workaround, but it does the trick)
nym-mixnet-contract = { path = ".", features = ["testable-mixnet-contract"] }
nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing" }
[features]
default = []
contract-testing = ["mixnet-contract-common/contract-testing"]
testable-mixnet-contract = ["nym-contracts-common-testing"]
schema-gen = ["mixnet-contract-common/schema", "cosmwasm-schema"]
[lints]
+2 -1
View File
@@ -649,7 +649,7 @@ pub fn migrate(
mod tests {
use super::*;
use crate::rewards::storage as rewards_storage;
use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env};
use cosmwasm_std::testing::{message_info, mock_env};
use cosmwasm_std::{Decimal, Uint128};
use mixnet_contract_common::reward_params::{
IntervalRewardParams, RewardedSetParams, RewardingParams,
@@ -657,6 +657,7 @@ mod tests {
use mixnet_contract_common::{
InitialRewardingParams, OperatingCostRange, Percent, ProfitMarginRange,
};
use nym_contracts_common_testing::mock_dependencies;
use std::time::Duration;
#[test]
+3
View File
@@ -22,3 +22,6 @@ mod support;
#[cfg(feature = "contract-testing")]
mod testing;
mod vesting_migration;
#[cfg(feature = "testable-mixnet-contract")]
pub mod testable_mixnet_contract;
@@ -130,20 +130,22 @@ pub mod tests {
use crate::mixnet_contract_settings::queries::query_rewarding_validator_address;
use crate::mixnet_contract_settings::storage::rewarding_denom;
use crate::support::tests::test_helpers;
use cosmwasm_std::testing::{message_info, MockApi};
use cosmwasm_std::testing::message_info;
use cosmwasm_std::{Coin, Uint128};
use cw_controllers::AdminError::NotAdmin;
use mixnet_contract_common::OperatorsParamsUpdate;
use nym_contracts_common_testing::mock_api;
#[test]
fn update_contract_rewarding_validator_address() {
let mut deps = test_helpers::init_contract();
let mock_api = mock_api();
let info = message_info(&deps.api.addr_make("not-the-creator"), &[]);
let res = try_update_rewarding_validator_address(
deps.as_mut(),
info,
MockApi::default().addr_make("not-the-creator").to_string(),
mock_api.addr_make("not-the-creator").to_string(),
);
assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {})));
@@ -151,14 +153,14 @@ pub mod tests {
let res = try_update_rewarding_validator_address(
deps.as_mut(),
info,
MockApi::default().addr_make("new-good-address").to_string(),
mock_api.addr_make("new-good-address").to_string(),
);
assert_eq!(
res,
Ok(
Response::default().add_event(new_rewarding_validator_address_update_event(
MockApi::default().addr_make("rewarder"),
MockApi::default().addr_make("new-good-address")
mock_api.addr_make("rewarder"),
mock_api.addr_make("new-good-address")
))
)
);
@@ -166,7 +168,7 @@ pub mod tests {
let state = storage::CONTRACT_STATE.load(&deps.storage).unwrap();
assert_eq!(
state.rewarding_validator_address,
MockApi::default().addr_make("new-good-address")
mock_api.addr_make("new-good-address")
);
assert_eq!(
+12 -48
View File
@@ -51,11 +51,11 @@ pub mod test_helpers {
use crate::support::helpers::ensure_no_existing_bond;
use crate::support::tests;
use crate::support::tests::fixtures::{
good_gateway_pledge, good_mixnode_pledge, good_node_plegge, TEST_COIN_DENOM,
good_gateway_pledge, good_mixnode_pledge, good_node_plegge,
};
use crate::support::tests::{legacy, test_helpers};
use crate::testable_mixnet_contract::MixnetContract;
use cosmwasm_std::testing::message_info;
use cosmwasm_std::testing::mock_dependencies;
use cosmwasm_std::testing::mock_env;
use cosmwasm_std::testing::MockApi;
use cosmwasm_std::testing::MockQuerier;
@@ -74,22 +74,24 @@ pub mod test_helpers {
use mixnet_contract_common::nym_node::{RewardedSetMetadata, Role};
use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData};
use mixnet_contract_common::reward_params::{
NodeRewardingParameters, Performance, RewardedSetParams, RewardingParams, WorkFactor,
NodeRewardingParameters, Performance, RewardingParams, WorkFactor,
};
use mixnet_contract_common::rewarding::simulator::simulated_node::SimulatedNode;
use mixnet_contract_common::rewarding::simulator::Simulator;
use mixnet_contract_common::rewarding::RewardDistribution;
use mixnet_contract_common::{
ContractStateParamsUpdate, Delegation, EpochEventId, EpochState, EpochStatus, ExecuteMsg,
Gateway, GatewayBondingPayload, IdentityKey, InitialRewardingParams, InstantiateMsg,
Interval, MixNode, MixNodeBond, MixNodeDetails, MixnodeBondingPayload, NodeId, NymNode,
NymNodeBond, NymNodeBondingPayload, NymNodeDetails, OperatingCostRange,
OperatorsParamsUpdate, Percent, ProfitMarginRange, RoleAssignment,
SignableGatewayBondingMsg, SignableMixNodeBondingMsg, SignableNymNodeBondingMsg,
Gateway, GatewayBondingPayload, IdentityKey, Interval, MixNode, MixNodeBond,
MixNodeDetails, MixnodeBondingPayload, NodeId, NymNode, NymNodeBond, NymNodeBondingPayload,
NymNodeDetails, OperatingCostRange, OperatorsParamsUpdate, ProfitMarginRange,
RoleAssignment, SignableGatewayBondingMsg, SignableMixNodeBondingMsg,
SignableNymNodeBondingMsg,
};
use nym_contracts_common::signing::{
ContractMessageContent, MessageSignature, SignableMessage, SigningAlgorithm, SigningPurpose,
};
use nym_contracts_common_testing::TestableNymContract;
use nym_contracts_common_testing::{mock_api, mock_dependencies};
use nym_crypto::asymmetric::ed25519;
use nym_crypto::asymmetric::ed25519::KeyPair;
use rand::distributions::WeightedIndex;
@@ -100,13 +102,12 @@ pub mod test_helpers {
use std::collections::HashMap;
use std::fmt::Debug;
use std::str::FromStr;
use std::time::Duration;
pub(crate) fn 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(MockApi::default().addr_make(&format!("addr{i}{}", rng.next_u64())));
addrs.push(mock_api().addr_make(&format!("addr{i}{}", rng.next_u64())));
}
addrs.sort();
addrs
@@ -1820,46 +1821,9 @@ pub mod test_helpers {
SignableGatewayBondingMsg::new(nonce, content)
}
fn intial_rewarded_set_params() -> RewardedSetParams {
RewardedSetParams {
entry_gateways: 50,
exit_gateways: 70,
mixnodes: 120,
standby: 50,
}
}
fn initial_rewarding_params() -> InitialRewardingParams {
let reward_pool = 250_000_000_000_000u128;
let staking_supply = 100_000_000_000_000u128;
InitialRewardingParams {
initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens)
initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M
staking_supply_scale_factor: Percent::hundred(),
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(),
interval_pool_emission: Percent::from_percentage_value(2).unwrap(),
rewarded_set_params: intial_rewarded_set_params(),
}
}
pub fn init_contract() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
let mut deps = mock_dependencies();
let msg = InstantiateMsg {
rewarding_validator_address: deps.api.addr_make("rewarder").to_string(),
vesting_contract_address: deps.api.addr_make("vesting-contract").to_string(),
rewarding_denom: TEST_COIN_DENOM.to_string(),
epochs_in_interval: 720,
epoch_duration: Duration::from_secs(60 * 60),
initial_rewarding_params: initial_rewarding_params(),
current_nym_node_version: "1.1.10".to_string(),
version_score_weights: Default::default(),
version_score_params: Default::default(),
profit_margin: Default::default(),
interval_operating_cost: Default::default(),
key_validity_in_epochs: None,
};
let msg = MixnetContract::base_init_msg();
let env = mock_env();
let info = sender("creator");
instantiate(deps.as_mut(), env, info, msg).unwrap();
@@ -0,0 +1,89 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// fine in test code
#![allow(clippy::unwrap_used)]
use crate::contract::{execute, instantiate, migrate, query};
use cosmwasm_std::Decimal;
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::reward_params::RewardedSetParams;
use mixnet_contract_common::{
ExecuteMsg, InitialRewardingParams, InstantiateMsg, MigrateMsg, QueryMsg,
};
use nym_contracts_common::Percent;
use nym_contracts_common_testing::{
mock_dependencies, ContractFn, PermissionedFn, QueryFn, TEST_DENOM,
};
use std::time::Duration;
pub use nym_contracts_common_testing::TestableNymContract;
pub struct MixnetContract;
fn initial_rewarded_set_params() -> RewardedSetParams {
RewardedSetParams {
entry_gateways: 50,
exit_gateways: 70,
mixnodes: 120,
standby: 50,
}
}
fn initial_rewarding_params() -> InitialRewardingParams {
let reward_pool = 250_000_000_000_000u128;
let staking_supply = 100_000_000_000_000u128;
InitialRewardingParams {
initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens)
initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M
staking_supply_scale_factor: Percent::hundred(),
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(),
interval_pool_emission: Percent::from_percentage_value(2).unwrap(),
rewarded_set_params: initial_rewarded_set_params(),
}
}
impl TestableNymContract for MixnetContract {
const NAME: &'static str = "mixnet-contract";
type InitMsg = InstantiateMsg;
type ExecuteMsg = ExecuteMsg;
type QueryMsg = QueryMsg;
type MigrateMsg = MigrateMsg;
type ContractError = MixnetContractError;
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
instantiate
}
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
execute
}
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
query
}
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
migrate
}
fn base_init_msg() -> Self::InitMsg {
let deps = mock_dependencies();
InstantiateMsg {
rewarding_validator_address: deps.api.addr_make("rewarder").to_string(),
vesting_contract_address: deps.api.addr_make("vesting-contract").to_string(),
rewarding_denom: TEST_DENOM.to_string(),
epochs_in_interval: 720,
epoch_duration: Duration::from_secs(60 * 60),
initial_rewarding_params: initial_rewarding_params(),
current_nym_node_version: "1.1.10".to_string(),
version_score_weights: Default::default(),
version_score_params: Default::default(),
profit_margin: Default::default(),
interval_operating_cost: Default::default(),
key_validity_in_epochs: None,
}
}
}
+1 -5
View File
@@ -25,13 +25,9 @@ cosmwasm-schema = { workspace = true, optional = true }
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
nym-pool-contract-common = { path = "../../common/cosmwasm-smart-contracts/nym-pool-contract" }
[dev-dependencies]
anyhow = { workspace = true }
serde = { workspace = true }
rand_chacha = { workspace = true }
rand = { workspace = true }
cw-multi-test = { workspace = true }
nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing" }
[features]
schema-gen = ["nym-pool-contract-common/schema", "cosmwasm-schema"]
+1 -1
View File
@@ -193,8 +193,8 @@ mod tests {
#[cfg(test)]
mod setting_initial_grants {
use super::*;
use crate::testing::deps_with_balance;
use cosmwasm_std::{coin, Order, Storage};
use nym_contracts_common_testing::deps_with_balance;
use nym_pool_contract_common::{Allowance, BasicAllowance, Grant, GranteeAddress};
use std::collections::HashMap;
+3 -2
View File
@@ -26,12 +26,13 @@ pub fn validate_usage_coin(storage: &dyn Storage, coin: &Coin) -> Result<(), Nym
mod tests {
use super::*;
use crate::storage::NymPoolStorage;
use crate::testing::TestSetup;
use crate::testing::init_contract_tester;
use cosmwasm_std::coin;
use nym_contracts_common_testing::ContractOpts;
#[test]
fn validating_coin_usage() -> anyhow::Result<()> {
let test = TestSetup::init();
let test = init_contract_tester();
let storage = NymPoolStorage::new();
let denom = storage.pool_denomination.load(test.storage())?;
+39 -22
View File
@@ -182,20 +182,22 @@ pub fn query_granters_paged(
mod tests {
use super::*;
use crate::contract::instantiate;
use crate::testing::{TestSetup, TEST_DENOM};
use crate::testing::{init_contract_tester, NymPoolContractTesterExt, TEST_DENOM};
use cosmwasm_std::testing::{message_info, mock_dependencies_with_balance, mock_env};
use cosmwasm_std::{coin, Uint128};
use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, DenomExt, RandExt};
use nym_pool_contract_common::{Allowance, BasicAllowance, GranterInformation, InstantiateMsg};
#[cfg(test)]
mod admin_query {
use super::*;
use crate::testing::TestSetup;
use crate::testing::init_contract_tester;
use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, RandExt};
use nym_pool_contract_common::ExecuteMsg;
#[test]
fn returns_current_admin() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let initial_admin = test.admin_unchecked();
@@ -255,7 +257,7 @@ mod tests {
#[test]
fn total_locked_tokens_query() {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let locked = query_total_locked_tokens(test.deps()).unwrap().locked;
assert!(locked.amount.is_zero());
@@ -271,7 +273,7 @@ mod tests {
#[test]
fn locked_tokens_query() {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee1 = test.add_dummy_grant().grantee;
test.lock_allowance(grantee1.as_str(), Uint128::new(1234));
@@ -295,8 +297,13 @@ mod tests {
#[cfg(test)]
mod locked_tokens_paged_query {
use super::*;
use crate::testing::NymPoolContract;
use nym_contracts_common_testing::ContractTester;
fn lock_sorted(test: &mut TestSetup, count: usize) -> Vec<LockedTokens> {
fn lock_sorted(
test: &mut ContractTester<NymPoolContract>,
count: usize,
) -> Vec<LockedTokens> {
let mut grantees = Vec::new();
for _ in 0..count {
@@ -314,7 +321,7 @@ mod tests {
#[test]
fn obeys_limits() {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let _locked = lock_sorted(&mut test, 1000);
let limit = 42;
@@ -324,7 +331,7 @@ mod tests {
#[test]
fn has_default_limit() {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let _locked = lock_sorted(&mut test, 1000);
// query without explicitly setting a limit
@@ -337,7 +344,7 @@ mod tests {
#[test]
fn has_max_limit() {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let _locked = lock_sorted(&mut test, 1000);
// query with a crazily high limit in an attempt to use too many resources
@@ -352,7 +359,7 @@ mod tests {
#[test]
fn pagination_works() {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let locked = lock_sorted(&mut test, 1000);
// first page should return 2 results...
@@ -371,7 +378,7 @@ mod tests {
#[test]
fn grant_query() {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let env = test.env();
// bad address
@@ -433,7 +440,7 @@ mod tests {
#[test]
fn granter_query() {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let admin = test.admin_unchecked();
let env = test.env();
@@ -482,8 +489,13 @@ mod tests {
#[cfg(test)]
mod granters_paged_query {
use super::*;
use crate::testing::NymPoolContract;
use nym_contracts_common_testing::ContractTester;
fn granters_sorted(test: &mut TestSetup, count: usize) -> Vec<GranterDetails> {
fn granters_sorted(
test: &mut ContractTester<NymPoolContract>,
count: usize,
) -> Vec<GranterDetails> {
let mut granters = Vec::new();
for _ in 0..count {
@@ -504,7 +516,7 @@ mod tests {
#[test]
fn obeys_limits() {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let _granters = granters_sorted(&mut test, 1000);
let limit = 42;
@@ -514,7 +526,7 @@ mod tests {
#[test]
fn has_default_limit() {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let _granters = granters_sorted(&mut test, 1000);
// query without explicitly setting a limit
@@ -527,7 +539,7 @@ mod tests {
#[test]
fn has_max_limit() {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let _granters = granters_sorted(&mut test, 1000);
// query with a crazily high limit in an attempt to use too many resources
@@ -542,7 +554,7 @@ mod tests {
#[test]
fn pagination_works() {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let locked = granters_sorted(&mut test, 1000);
// first page should return 2 results...
@@ -562,8 +574,13 @@ mod tests {
#[cfg(test)]
mod grants_paged_query {
use super::*;
use crate::testing::{init_contract_tester, NymPoolContract};
use nym_contracts_common_testing::{ContractOpts, ContractTester};
fn grants_sorted(test: &mut TestSetup, count: usize) -> Vec<GrantInformation> {
fn grants_sorted(
test: &mut ContractTester<NymPoolContract>,
count: usize,
) -> Vec<GrantInformation> {
let mut grantees = Vec::new();
for _ in 0..count {
@@ -580,7 +597,7 @@ mod tests {
#[test]
fn obeys_limits() {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let _grantees = grants_sorted(&mut test, 1000);
let limit = 42;
@@ -590,7 +607,7 @@ mod tests {
#[test]
fn has_default_limit() {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let _grantees = grants_sorted(&mut test, 1000);
// query without explicitly setting a limit
@@ -603,7 +620,7 @@ mod tests {
#[test]
fn has_max_limit() {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let _grantees = grants_sorted(&mut test, 1000);
// query with a crazily high limit in an attempt to use too many resources
@@ -619,7 +636,7 @@ mod tests {
#[test]
fn pagination_works() {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grants = grants_sorted(&mut test, 1000);
// first page should return 2 results...
+58 -46
View File
@@ -489,19 +489,21 @@ mod tests {
#[cfg(test)]
mod nympool_storage {
use super::*;
use crate::testing::{TestSetup, TEST_DENOM};
use crate::testing::{init_contract_tester, NymPoolContractTesterExt, TEST_DENOM};
use cosmwasm_std::testing::{
mock_dependencies, mock_env, MockApi, MockQuerier, MockStorage,
};
use cosmwasm_std::{coin, coins, Empty, OwnedDeps};
use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt};
use nym_pool_contract_common::BasicAllowance;
#[cfg(test)]
mod initialisation {
use super::*;
use crate::testing::{deps_with_balance, TEST_DENOM};
use crate::testing::TEST_DENOM;
use cosmwasm_std::testing::{mock_dependencies, mock_env};
use cosmwasm_std::{coin, Order};
use nym_contracts_common_testing::deps_with_balance;
use nym_pool_contract_common::BasicAllowance;
fn all_grants(storage: &dyn Storage) -> HashMap<GranteeAddress, Grant> {
@@ -914,7 +916,7 @@ mod tests {
#[test]
fn loading_granter_information() -> anyhow::Result<()> {
let storage = NymPoolStorage::new();
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let granter = test.generate_account();
@@ -941,7 +943,7 @@ mod tests {
#[test]
fn checking_granter_permission() -> anyhow::Result<()> {
let storage = NymPoolStorage::new();
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let granter = test.generate_account();
test.add_granter(&granter);
@@ -957,7 +959,7 @@ mod tests {
#[test]
fn ensuring_granter_permission() -> anyhow::Result<()> {
let storage = NymPoolStorage::new();
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let granter = test.generate_account();
test.add_granter(&granter);
@@ -1047,7 +1049,7 @@ mod tests {
#[test]
fn attempting_to_load_grant() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
// doesn't exist...
@@ -1070,7 +1072,7 @@ mod tests {
#[test]
fn loading_grant() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
// doesn't exist...
@@ -1094,11 +1096,12 @@ mod tests {
#[cfg(test)]
mod adding_new_granter {
use super::*;
use crate::testing::init_contract_tester;
use cw_controllers::AdminError;
#[test]
fn can_only_be_performed_by_contract_admin() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let admin = test.admin_unchecked();
@@ -1121,7 +1124,7 @@ mod tests {
#[test]
fn can_only_be_performed_if_account_is_not_already_a_granter() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let admin = test.admin_unchecked();
@@ -1143,7 +1146,7 @@ mod tests {
#[test]
fn saves_basic_metadata() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let admin = test.admin_unchecked();
@@ -1194,10 +1197,11 @@ mod tests {
#[cfg(test)]
mod removing_granter {
use super::*;
use crate::testing::init_contract_tester;
#[test]
fn requires_granter_to_exist() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let admin = test.admin_unchecked();
@@ -1217,7 +1221,7 @@ mod tests {
#[test]
fn can_only_be_performed_by_admin() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let random_address = test.generate_account();
@@ -1259,7 +1263,7 @@ mod tests {
#[test]
fn removes_it_from_granter_list() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let admin = test.admin_unchecked();
@@ -1284,11 +1288,12 @@ mod tests {
#[cfg(test)]
mod adding_new_grant {
use super::*;
use crate::testing::init_contract_tester;
use nym_pool_contract_common::ClassicPeriodicAllowance;
#[test]
fn can_only_be_done_by_whitelisted_granter() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let not_valid_granter = test.generate_account();
@@ -1319,7 +1324,7 @@ mod tests {
#[test]
fn cant_be_done_if_grant_already_existed() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let admin = test.admin_unchecked();
@@ -1340,7 +1345,7 @@ mod tests {
#[test]
fn only_accepts_valid_allowances() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
// allowance with 0 limit and wrong denom
@@ -1364,7 +1369,7 @@ mod tests {
#[test]
fn explicit_limit_cant_be_larger_than_available_tokens() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let admin = test.admin_unchecked();
@@ -1401,7 +1406,7 @@ mod tests {
assert!(res.is_ok());
// and below the available
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let mut limit = available.clone();
limit.amount -= Uint128::new(1);
let allowance = Allowance::Basic(BasicAllowance {
@@ -1418,7 +1423,7 @@ mod tests {
#[test]
fn updates_allowances_initial_state_and_saves_it_to_storage() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let admin = test.admin_unchecked();
@@ -1456,10 +1461,11 @@ mod tests {
#[cfg(test)]
mod spending_part_of_grant {
use super::*;
use crate::testing::init_contract_tester;
#[test]
fn requires_grant_to_exist() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let grantee = test.generate_account();
@@ -1485,7 +1491,7 @@ mod tests {
#[test]
fn requires_grant_to_be_spendable() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let admin = test.admin_unchecked();
@@ -1514,7 +1520,7 @@ mod tests {
#[test]
fn updates_stored_grant() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let admin = test.admin_unchecked();
@@ -1548,7 +1554,7 @@ mod tests {
#[test]
fn removes_grant_from_storage_if_its_used_up() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let admin = test.admin_unchecked();
@@ -1612,7 +1618,7 @@ mod tests {
#[test]
fn removing_grant() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let grantee = test.generate_account();
@@ -1656,10 +1662,11 @@ mod tests {
#[cfg(test)]
mod revoking_grant {
use super::*;
use crate::testing::init_contract_tester;
#[test]
fn requires_grant_to_exist() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let admin = test.admin_unchecked();
@@ -1684,7 +1691,7 @@ mod tests {
#[test]
fn can_always_be_called_by_current_admin() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let grantee = test.add_dummy_grant().grantee;
@@ -1717,7 +1724,7 @@ mod tests {
#[test]
fn can_be_called_by_original_granter_if_its_still_whitelisted() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let admin = test.admin_unchecked();
@@ -1761,7 +1768,7 @@ mod tests {
#[test]
fn removes_the_underlying_grant() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let admin = test.admin_unchecked();
@@ -1780,10 +1787,12 @@ mod tests {
#[cfg(test)]
mod locking_part_of_allowance {
use super::*;
use crate::testing::init_contract_tester;
use nym_contracts_common_testing::DenomExt;
#[test]
fn requires_providing_valid_coin() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let grantee = test.add_dummy_grant().grantee;
@@ -1804,7 +1813,7 @@ mod tests {
#[test]
fn requires_grant_to_exist() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let grantee = test.generate_account();
let env = test.env();
@@ -1826,7 +1835,7 @@ mod tests {
#[test]
fn does_not_allow_locking_more_than_spend_limit() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let admin = test.admin_unchecked();
let env = test.env();
@@ -1853,7 +1862,7 @@ mod tests {
#[test]
fn deducts_locked_amount_from_the_allowance() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let admin = test.admin_unchecked();
let env = test.env();
@@ -1892,7 +1901,7 @@ mod tests {
#[test]
fn preserves_grant_even_if_resultant_allowance_is_zero() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let admin = test.admin_unchecked();
let env = test.env();
@@ -1918,7 +1927,7 @@ mod tests {
#[test]
fn updates_internal_locked_counter() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let env = test.env();
let grantee = test.add_dummy_grant().grantee;
@@ -1953,8 +1962,10 @@ mod tests {
#[cfg(test)]
mod unlocking_part_of_allowance {
use super::*;
use crate::testing::{init_contract_tester, NymPoolContract};
use nym_contracts_common_testing::{ContractTester, DenomExt};
fn setup_locked_grant(test: &mut TestSetup) -> Addr {
fn setup_locked_grant(test: &mut ContractTester<NymPoolContract>) -> Addr {
let grantee = test.add_dummy_grant().grantee;
test.lock_allowance(&grantee, Uint128::new(100));
grantee
@@ -1962,7 +1973,7 @@ mod tests {
#[test]
fn requires_providing_valid_coin() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let grantee = setup_locked_grant(&mut test);
@@ -1981,7 +1992,7 @@ mod tests {
#[test]
fn does_not_allow_unlocking_more_than_currently_locked() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let grantee = setup_locked_grant(&mut test);
@@ -1999,7 +2010,7 @@ mod tests {
#[test]
fn requires_grant_to_exist() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let grantee = test.generate_account();
@@ -2018,7 +2029,7 @@ mod tests {
#[test]
fn requires_having_locked_coins() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let grantee = test.add_dummy_grant().grantee;
@@ -2036,7 +2047,7 @@ mod tests {
#[test]
fn increases_internal_grant_spend_limit() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
let admin = test.admin_unchecked();
let env = test.env();
@@ -2082,7 +2093,7 @@ mod tests {
#[test]
fn updates_internal_locked_counter() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = NymPoolStorage::new();
// 100tokens locked
@@ -2116,8 +2127,9 @@ mod tests {
#[cfg(test)]
mod locked_storage {
use super::*;
use crate::testing::TestSetup;
use crate::testing::{init_contract_tester, NymPoolContractTesterExt};
use cosmwasm_std::testing::mock_dependencies;
use nym_contracts_common_testing::{ContractOpts, RandExt};
#[test]
fn is_initialised_with_zero_total_locked() -> anyhow::Result<()> {
@@ -2136,7 +2148,7 @@ mod tests {
#[test]
fn getting_grantee_locked() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.generate_account();
let storage = LockedStorage::new();
@@ -2167,7 +2179,7 @@ mod tests {
#[test]
fn getting_maybe_grantee_locked() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.generate_account();
let storage = LockedStorage::new();
@@ -2198,7 +2210,7 @@ mod tests {
#[test]
fn locking_tokens() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = LockedStorage::new();
let grantee1 = test.generate_account();
@@ -2259,7 +2271,7 @@ mod tests {
#[test]
fn unlocking_tokens() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let storage = LockedStorage::new();
let grantee1 = test.generate_account();
+58 -236
View File
@@ -1,226 +1,70 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::contract;
use crate::contract::{execute, instantiate, migrate, query};
use crate::storage::NYM_POOL_STORAGE;
use crate::testing::storage::{ContractStorageWrapper, StorageWrapper};
use cosmwasm_std::testing::{message_info, mock_env, MockApi, MockQuerier, MockStorage};
use cosmwasm_std::{
coin, coins, Addr, Coin, ContractInfo, Deps, DepsMut, Empty, Env, MemoryStorage, MessageInfo,
Order, OwnedDeps, Response, StdResult, Storage, Uint128,
};
use cw_multi_test::{
next_block, App, AppBuilder, AppResponse, BankKeeper, Contract, ContractWrapper, Executor,
use cosmwasm_std::{Addr, Order, Uint128};
use nym_contracts_common_testing::{
AdminExt, ChainOpts, CommonStorageKeys, ContractFn, ContractOpts, ContractTester, DenomExt,
PermissionedFn, QueryFn, RandExt, TestableNymContract,
};
use nym_pool_contract_common::constants::storage_keys;
use nym_pool_contract_common::{
Allowance, BasicAllowance, ExecuteMsg, Grant, InstantiateMsg, NymPoolContractError, QueryMsg,
Allowance, BasicAllowance, ExecuteMsg, Grant, InstantiateMsg, MigrateMsg, NymPoolContractError,
QueryMsg,
};
use rand::{RngCore, SeedableRng};
use rand_chacha::ChaCha20Rng;
use serde::de::DeserializeOwned;
use std::collections::HashMap;
mod storage;
pub use nym_contracts_common_testing::TEST_DENOM;
pub fn test_rng() -> ChaCha20Rng {
let dummy_seed = [42u8; 32];
ChaCha20Rng::from_seed(dummy_seed)
}
pub struct NymPoolContract;
pub fn deps_with_balance(env: &Env) -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
OwnedDeps {
storage: MockStorage::default(),
api: MockApi::default(),
querier: MockQuerier::<Empty>::new(&[(
env.contract.address.as_str(),
coins(100000000000, TEST_DENOM).as_slice(),
)]),
custom_query_type: Default::default(),
impl TestableNymContract for NymPoolContract {
const NAME: &'static str = "nym-pool-contract";
type InitMsg = InstantiateMsg;
type ExecuteMsg = ExecuteMsg;
type QueryMsg = QueryMsg;
type MigrateMsg = MigrateMsg;
type ContractError = NymPoolContractError;
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
instantiate
}
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
execute
}
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
query
}
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
migrate
}
fn base_init_msg() -> Self::InitMsg {
InstantiateMsg {
pool_denomination: TEST_DENOM.to_string(),
grants: Default::default(),
}
}
}
pub const TEST_DENOM: &str = "unym";
pub struct TestSetup {
pub app: App<BankKeeper, MockApi, StorageWrapper>,
pub rng: ChaCha20Rng,
pub contract_address: Addr,
pub master_address: Addr,
pub(crate) storage: ContractStorageWrapper,
pub fn init_contract_tester() -> ContractTester<NymPoolContract> {
NymPoolContract::init()
.with_common_storage_key(CommonStorageKeys::Admin, storage_keys::CONTRACT_ADMIN)
.with_common_storage_key(CommonStorageKeys::Denom, storage_keys::POOL_DENOMINATION)
}
pub fn contract() -> Box<dyn Contract<Empty>> {
let contract = ContractWrapper::new(execute, instantiate, query).with_migrate(migrate);
Box::new(contract)
}
impl TestSetup {
pub fn init() -> TestSetup {
let storage = StorageWrapper::new();
let api = MockApi::default().with_prefix("n");
let master_address = api.addr_make("master-owner");
let mut 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()
});
let code_id = app.store_code(contract());
let contract_address = app
.instantiate_contract(
code_id,
master_address.clone(),
&InstantiateMsg {
pool_denomination: TEST_DENOM.to_string(),
grants: Default::default(),
},
&[],
"nym-pool-contract",
Some(master_address.to_string()),
)
.unwrap();
// send some tokens to the contract
app.send_tokens(
master_address.clone(),
contract_address.clone(),
&[coin(100000000, TEST_DENOM)],
)
.unwrap();
TestSetup {
app,
rng: test_rng(),
storage: storage.contract_storage_wrapper(&contract_address),
contract_address,
master_address,
}
}
pub 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();
}
pub fn deps(&self) -> Deps<'_> {
Deps {
storage: &self.storage,
api: self.app.api(),
querier: self.app.wrap(),
}
}
pub fn deps_mut(&mut self) -> DepsMut<'_> {
DepsMut {
storage: &mut self.storage,
api: self.app.api(),
querier: self.app.wrap(),
}
}
pub fn deps_mut_env(&mut self) -> (DepsMut<'_>, Env) {
let env = self.env().clone();
(self.deps_mut(), env)
}
pub fn storage(&self) -> &dyn Storage {
&self.storage
}
pub fn storage_mut(&mut self) -> &mut dyn Storage {
&mut self.storage
}
pub fn env(&self) -> Env {
Env {
block: self.app.block_info(),
contract: ContractInfo {
address: self.contract_address.clone(),
},
..mock_env()
}
}
pub fn next_block(&mut self) {
self.app.update_block(next_block)
}
pub fn execute_raw(
&mut self,
sender: Addr,
message: ExecuteMsg,
) -> Result<Response, NymPoolContractError> {
self.execute_raw_with_balance(sender, &[], message)
}
pub fn execute_raw_with_balance(
&mut self,
sender: Addr,
coins: &[Coin],
message: ExecuteMsg,
) -> Result<Response, NymPoolContractError> {
let env = self.env();
let info = message_info(&sender, coins);
contract::execute(self.deps_mut(), env, info, message)
}
pub fn execute_msg(
&mut self,
sender: Addr,
message: &ExecuteMsg,
) -> anyhow::Result<AppResponse> {
self.execute_msg_with_balance(sender, &[], message)
}
pub fn execute_msg_with_balance(
&mut self,
sender: Addr,
coins: &[Coin],
message: &ExecuteMsg,
) -> anyhow::Result<AppResponse> {
self.app
.execute_contract(sender, self.contract_address.clone(), message, coins)
}
pub fn query<T: DeserializeOwned>(&self, message: &QueryMsg) -> StdResult<T> {
self.app
.wrap()
.query_wasm_smart(self.contract_address.as_str(), message)
}
pub fn generate_account(&mut self) -> Addr {
self.app
.api()
.addr_make(&format!("foomp{}", self.rng.next_u64()))
}
pub fn admin_unchecked(&self) -> Addr {
NYM_POOL_STORAGE
.contract_admin
.get(self.deps())
.unwrap()
.unwrap()
}
pub fn change_admin(&mut self, new_admin: &Addr) {
pub trait NymPoolContractTesterExt:
ContractOpts<ExecuteMsg = ExecuteMsg, QueryMsg = QueryMsg, ContractError = NymPoolContractError>
+ ChainOpts
+ AdminExt
+ DenomExt
+ RandExt
{
fn change_admin(&mut self, new_admin: &Addr) {
self.execute_msg(
self.admin_unchecked(),
&ExecuteMsg::UpdateAdmin {
@@ -231,33 +75,14 @@ impl TestSetup {
.unwrap();
}
pub fn admin_msg(&self) -> MessageInfo {
message_info(&self.admin_unchecked(), &[])
}
pub fn denom(&self) -> String {
NYM_POOL_STORAGE
.pool_denomination
.load(self.storage())
.unwrap()
}
pub fn coin(&self, amount: u128) -> Coin {
coin(amount, self.denom())
}
pub fn coins(&self, amount: u128) -> Vec<Coin> {
coins(amount, self.denom())
}
#[track_caller]
pub fn add_dummy_grant(&mut self) -> Grant {
fn add_dummy_grant(&mut self) -> Grant {
let grantee = self.generate_account();
self.add_dummy_grant_for(&grantee)
}
#[track_caller]
pub fn add_dummy_grant_for(&mut self, grantee: impl Into<String>) -> Grant {
fn add_dummy_grant_for(&mut self, grantee: impl Into<String>) -> Grant {
let grantee = Addr::unchecked(grantee);
let granter = self.admin_unchecked();
let env = self.env();
@@ -275,23 +100,18 @@ impl TestSetup {
}
#[track_caller]
pub fn lock_allowance(&mut self, grantee: impl Into<String>, amount: impl Into<Uint128>) {
let denom = NYM_POOL_STORAGE
.pool_denomination
.load(self.deps().storage)
.unwrap();
fn lock_allowance(&mut self, grantee: impl Into<String>, amount: impl Into<Uint128>) {
self.execute_msg(
Addr::unchecked(grantee),
&ExecuteMsg::LockAllowance {
amount: coin(amount.into().u128(), denom),
amount: self.coin(amount.into().u128()),
},
)
.unwrap();
}
#[track_caller]
pub fn full_locked_map(&self) -> HashMap<Addr, Uint128> {
fn full_locked_map(&self) -> HashMap<Addr, Uint128> {
NYM_POOL_STORAGE
.locked
.grantees
@@ -301,7 +121,7 @@ impl TestSetup {
}
#[track_caller]
pub fn add_granter(&mut self, granter: &Addr) {
fn add_granter(&mut self, granter: &Addr) {
let env = self.env();
let admin = self.admin_unchecked();
NYM_POOL_STORAGE
@@ -309,3 +129,5 @@ impl TestSetup {
.unwrap();
}
}
impl NymPoolContractTesterExt for ContractTester<NymPoolContract> {}
+49 -43
View File
@@ -262,21 +262,22 @@ pub fn try_remove_expired(
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::TestSetup;
use crate::testing::{init_contract_tester, NymPoolContractTesterExt};
use nym_contracts_common_testing::{AdminExt, ContractOpts, DenomExt, RandExt};
use nym_pool_contract_common::ExecuteMsg;
#[cfg(test)]
mod updating_contract_admin {
use super::*;
use crate::testing::TestSetup;
use cosmwasm_std::{Deps, Order};
use cw_controllers::AdminError;
use nym_contracts_common_testing::{AdminExt, RandExt};
use nym_pool_contract_common::{ExecuteMsg, GranterAddress, GranterInformation};
use std::collections::HashMap;
#[test]
fn can_only_be_performed_by_current_admin() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let random_acc = test.generate_account();
let new_admin = test.generate_account();
@@ -310,7 +311,7 @@ mod tests {
#[test]
fn requires_providing_valid_address() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let bad_account = "definitely-not-valid-account";
let res = test.execute_raw(
@@ -347,7 +348,7 @@ mod tests {
.collect()
}
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let current_admin = test.admin_unchecked();
let new_admin = test.generate_account();
@@ -369,7 +370,7 @@ mod tests {
//
//
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let current_admin = test.admin_unchecked();
let new_admin = test.generate_account();
let old_granters = granters(test.deps());
@@ -392,13 +393,13 @@ mod tests {
#[cfg(test)]
mod granting_allowance {
use super::*;
use crate::testing::TestSetup;
use cosmwasm_std::StdError;
use nym_contracts_common_testing::{AdminExt, RandExt};
use nym_pool_contract_common::BasicAllowance;
#[test]
fn requires_providing_valid_grantee_address() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let env = test.env();
let admin = test.admin_msg();
@@ -433,12 +434,12 @@ mod tests {
#[cfg(test)]
mod revoking_allowance {
use super::*;
use crate::testing::TestSetup;
use cosmwasm_std::StdError;
use nym_contracts_common_testing::{AdminExt, RandExt};
#[test]
fn requires_providing_valid_grantee_address() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let env = test.env();
let admin = test.admin_msg();
@@ -487,12 +488,12 @@ mod tests {
#[cfg(test)]
mod using_allowance {
use super::*;
use crate::testing::TestSetup;
use nym_contracts_common_testing::{AdminExt, ChainOpts, RandExt};
use nym_pool_contract_common::{BasicAllowance, ExecuteMsg};
#[test]
fn requires_at_least_a_single_coin_receiver() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.add_dummy_grant().grantee;
let res = test.execute_raw(grantee, ExecuteMsg::UseAllowance { recipients: vec![] });
@@ -504,7 +505,7 @@ mod tests {
#[test]
fn requires_valid_coin_for_each_receiver() -> anyhow::Result<()> {
// 1 bad receiver
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.add_dummy_grant().grantee;
let res = test.execute_raw(
@@ -519,7 +520,7 @@ mod tests {
assert!(res.is_err());
// 3 receivers, one invalid
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.add_dummy_grant().grantee;
let addr1 = test.generate_account();
@@ -547,7 +548,7 @@ mod tests {
assert!(res.is_err());
// all fine
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.add_dummy_grant().grantee;
let res = test.execute_raw(
@@ -576,7 +577,7 @@ mod tests {
#[test]
fn requires_the_total_to_be_available_for_spending() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let recipient = test.generate_account();
// contract balance < required
@@ -665,7 +666,7 @@ mod tests {
#[test]
fn requires_the_total_to_be_within_spend_limit() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let allowance = Allowance::Basic(BasicAllowance {
spend_limit: Some(test.coin(100)),
expiration_unix_timestamp: None,
@@ -712,7 +713,7 @@ mod tests {
#[test]
fn attaches_appropriate_bank_message_for_each_receiver() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.add_dummy_grant().grantee;
@@ -774,7 +775,7 @@ mod tests {
#[test]
fn requires_grant_to_not_be_expired() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let env = test.env();
let allowance = Allowance::Basic(BasicAllowance {
spend_limit: None,
@@ -810,13 +811,14 @@ mod tests {
#[cfg(test)]
mod withdrawing_from_allowance {
use super::*;
use crate::testing::TestSetup;
use crate::testing::{init_contract_tester, NymPoolContractTesterExt};
use cosmwasm_std::coin;
use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, DenomExt, RandExt};
use nym_pool_contract_common::{BasicAllowance, ExecuteMsg};
#[test]
fn requires_valid_coin() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.add_dummy_grant().grantee;
let res = test.execute_raw(
@@ -848,7 +850,7 @@ mod tests {
#[test]
fn requires_the_amount_to_be_available_for_spending() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
// contract balance < required
let grantee = test.add_dummy_grant().grantee;
@@ -912,7 +914,7 @@ mod tests {
#[test]
fn requires_the_amount_to_be_within_spend_limit() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let allowance = Allowance::Basic(BasicAllowance {
spend_limit: Some(test.coin(100)),
expiration_unix_timestamp: None,
@@ -952,7 +954,7 @@ mod tests {
#[test]
fn attaches_appropriate_bank_message() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.add_dummy_grant().grantee;
@@ -978,7 +980,7 @@ mod tests {
#[test]
fn requires_grant_to_not_be_expired() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let env = test.env();
let allowance = Allowance::Basic(BasicAllowance {
spend_limit: None,
@@ -1011,7 +1013,7 @@ mod tests {
#[test]
fn locking_allowance() -> anyhow::Result<()> {
// internals got tested in storage tests, so this is mostly about checking events (TODO)
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.add_dummy_grant().grantee;
let res = test.execute_raw(
@@ -1035,7 +1037,7 @@ mod tests {
#[test]
fn unlocking_allowance() -> anyhow::Result<()> {
// internals got tested in storage tests, so this is mostly about checking events (TODO)
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.add_dummy_grant().grantee;
test.lock_allowance(&grantee, Uint128::new(100));
@@ -1060,11 +1062,12 @@ mod tests {
#[cfg(test)]
mod using_locked_allowance {
use super::*;
use nym_contracts_common_testing::{AdminExt, ChainOpts, RandExt};
use nym_pool_contract_common::BasicAllowance;
#[test]
fn requires_at_least_a_single_coin_receiver() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.add_dummy_grant().grantee;
let res = test.execute_raw(
@@ -1079,7 +1082,7 @@ mod tests {
#[test]
fn requires_valid_coin_for_each_receiver() -> anyhow::Result<()> {
// 1 bad receiver
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.add_dummy_grant().grantee;
test.lock_allowance(&grantee, Uint128::new(10000));
@@ -1095,7 +1098,7 @@ mod tests {
assert!(res.is_err());
// 3 receivers, one invalid
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.add_dummy_grant().grantee;
test.lock_allowance(&grantee, Uint128::new(10000));
@@ -1124,7 +1127,7 @@ mod tests {
assert!(res.is_err());
// all fine
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.add_dummy_grant().grantee;
test.lock_allowance(&grantee, Uint128::new(10000));
@@ -1154,7 +1157,7 @@ mod tests {
#[test]
fn requires_the_total_to_be_locked_by_grantee() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.add_dummy_grant().grantee;
test.lock_allowance(&grantee, Uint128::new(100));
@@ -1193,7 +1196,7 @@ mod tests {
#[test]
fn attaches_appropriate_bank_message_for_each_receiver() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.add_dummy_grant().grantee;
test.lock_allowance(&grantee, Uint128::new(10000));
@@ -1256,7 +1259,7 @@ mod tests {
#[test]
fn requires_grant_to_not_be_expired() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let env = test.env();
let allowance = Allowance::Basic(BasicAllowance {
spend_limit: None,
@@ -1294,11 +1297,12 @@ mod tests {
mod withdrawing_from_locked_allowance {
use super::*;
use cosmwasm_std::coin;
use nym_contracts_common_testing::{AdminExt, ChainOpts, RandExt};
use nym_pool_contract_common::BasicAllowance;
#[test]
fn requires_valid_coin() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.add_dummy_grant().grantee;
test.lock_allowance(&grantee, Uint128::new(10000));
@@ -1331,7 +1335,7 @@ mod tests {
#[test]
fn attaches_appropriate_bank_message() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.add_dummy_grant().grantee;
test.lock_allowance(&grantee, Uint128::new(10000));
@@ -1358,7 +1362,7 @@ mod tests {
#[test]
fn requires_grant_to_not_be_expired() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let env = test.env();
let allowance = Allowance::Basic(BasicAllowance {
spend_limit: None,
@@ -1391,7 +1395,7 @@ mod tests {
#[test]
fn requires_the_amount_to_be_locked_by_grantee() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let grantee = test.add_dummy_grant().grantee;
test.lock_allowance(&grantee, Uint128::new(100));
@@ -1424,7 +1428,7 @@ mod tests {
#[test]
fn adding_new_granter() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let bad_address = "foomp";
let good_address = test.generate_account();
@@ -1456,7 +1460,7 @@ mod tests {
#[test]
fn revoking_granter() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let bad_address = "foomp";
let good_address = test.generate_account();
let granter_address = test.generate_account();
@@ -1500,10 +1504,12 @@ mod tests {
#[cfg(test)]
mod removing_expired {
use super::*;
use crate::testing::{init_contract_tester, NymPoolContract, NymPoolContractTesterExt};
use nym_contracts_common_testing::{ChainOpts, ContractOpts, ContractTester, RandExt};
use nym_pool_contract_common::{BasicAllowance, GranteeAddress};
fn setup_with_expired_grant() -> (TestSetup, GranteeAddress) {
let mut test = TestSetup::init();
fn setup_with_expired_grant() -> (ContractTester<NymPoolContract>, GranteeAddress) {
let mut test = init_contract_tester();
let env = test.env();
let allowance = Allowance::Basic(BasicAllowance {
spend_limit: None,
@@ -1543,7 +1549,7 @@ mod tests {
#[test]
fn requires_grant_to_actually_exist_and_be_expired() -> anyhow::Result<()> {
let mut test = TestSetup::init();
let mut test = init_contract_tester();
let sender = test.generate_account();
let grantee = test.add_dummy_grant().grantee;
let not_grantee = test.generate_account();
+4
View File
@@ -0,0 +1,4 @@
[alias]
wasm = "build --release --lib --target wasm32-unknown-unknown"
unit-test = "test --lib"
schema = "run --bin schema --features=schema-gen"
+42
View File
@@ -0,0 +1,42 @@
[package]
name = "nym-performance-contract"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
[[bin]]
name = "schema"
required-features = ["schema-gen"]
[lib]
name = "nym_performance_contract"
crate-type = ["cdylib", "rlib"]
[dependencies]
cosmwasm-std = { workspace = true }
cw2 = { workspace = true }
cw-storage-plus = { workspace = true }
cw-controllers = { workspace = true }
serde = { workspace = true }
cosmwasm-schema = { workspace = true, optional = true }
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
nym-performance-contract-common = { path = "../../common/cosmwasm-smart-contracts/nym-performance-contract" }
nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" }
[dev-dependencies]
anyhow = { workspace = true }
nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing" }
nym-mixnet-contract = { path = "../mixnet", features = ["testable-mixnet-contract"] }
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
[features]
schema-gen = ["nym-performance-contract-common/schema", "cosmwasm-schema"]
[lints]
workspace = true
+5
View File
@@ -0,0 +1,5 @@
wasm:
RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown
generate-schema:
cargo schema
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::write_api;
use nym_performance_contract_common::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
fn main() {
write_api! {
instantiate: InstantiateMsg,
query: QueryMsg,
execute: ExecuteMsg,
migrate: MigrateMsg,
}
}
+187
View File
@@ -0,0 +1,187 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::queries::{
query_admin, query_epoch_measurements_paged, query_epoch_performance_paged,
query_full_historical_performance_paged, query_network_monitor_details,
query_network_monitors_paged, query_node_measurements, query_node_performance,
query_node_performance_paged, query_retired_network_monitors_paged,
};
use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE;
use crate::transactions::{
try_authorise_network_monitor, try_batch_submit_performance_results,
try_remove_epoch_measurements, try_remove_node_measurements, try_retire_network_monitor,
try_submit_performance_results, try_update_contract_admin,
};
use cosmwasm_std::{
entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response,
};
use nym_contracts_common::set_build_information;
use nym_performance_contract_common::{
ExecuteMsg, InstantiateMsg, MigrateMsg, NymPerformanceContractError, QueryMsg,
};
const CONTRACT_NAME: &str = "crate:nym-performance-contract";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
#[entry_point]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, NymPerformanceContractError> {
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
set_build_information!(deps.storage)?;
let mixnet_contract_address = deps.api.addr_validate(&msg.mixnet_contract_address)?;
NYM_PERFORMANCE_CONTRACT_STORAGE.initialise(
deps,
env,
info.sender,
mixnet_contract_address.clone(),
msg.authorised_network_monitors,
)?;
Ok(Response::default())
}
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, NymPerformanceContractError> {
match msg {
ExecuteMsg::UpdateAdmin { admin } => try_update_contract_admin(deps, info, admin),
ExecuteMsg::Submit { epoch, data } => {
try_submit_performance_results(deps, info, epoch, data)
}
ExecuteMsg::BatchSubmit { epoch, data } => {
try_batch_submit_performance_results(deps, info, epoch, data)
}
ExecuteMsg::AuthoriseNetworkMonitor { address } => {
try_authorise_network_monitor(deps, env, info, address)
}
ExecuteMsg::RetireNetworkMonitor { address } => {
try_retire_network_monitor(deps, env, info, address)
}
ExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id } => {
try_remove_node_measurements(deps, info, epoch_id, node_id)
}
ExecuteMsg::RemoveEpochMeasurements { epoch_id } => {
try_remove_epoch_measurements(deps, info, epoch_id)
}
}
}
#[entry_point]
pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> Result<Binary, NymPerformanceContractError> {
match msg {
QueryMsg::Admin {} => Ok(to_json_binary(&query_admin(deps)?)?),
QueryMsg::NodePerformance { epoch_id, node_id } => Ok(to_json_binary(
&query_node_performance(deps, epoch_id, node_id)?,
)?),
QueryMsg::NodePerformancePaged {
node_id,
start_after,
limit,
} => Ok(to_json_binary(&query_node_performance_paged(
deps,
node_id,
start_after,
limit,
)?)?),
QueryMsg::EpochPerformancePaged {
epoch_id,
start_after,
limit,
} => Ok(to_json_binary(&query_epoch_performance_paged(
deps,
epoch_id,
start_after,
limit,
)?)?),
QueryMsg::FullHistoricalPerformancePaged { start_after, limit } => Ok(to_json_binary(
&query_full_historical_performance_paged(deps, start_after, limit)?,
)?),
QueryMsg::NetworkMonitor { address } => Ok(to_json_binary(
&query_network_monitor_details(deps, address)?,
)?),
QueryMsg::NetworkMonitorsPaged { start_after, limit } => Ok(to_json_binary(
&query_network_monitors_paged(deps, start_after, limit)?,
)?),
QueryMsg::RetiredNetworkMonitorsPaged { start_after, limit } => Ok(to_json_binary(
&query_retired_network_monitors_paged(deps, start_after, limit)?,
)?),
QueryMsg::NodeMeasurements { epoch_id, node_id } => Ok(to_json_binary(
&query_node_measurements(deps, epoch_id, node_id)?,
)?),
QueryMsg::EpochMeasurementsPaged {
epoch_id,
start_after,
limit,
} => Ok(to_json_binary(&query_epoch_measurements_paged(
deps,
epoch_id,
start_after,
limit,
)?)?),
}
}
#[entry_point]
pub fn migrate(
deps: DepsMut,
_: Env,
_msg: MigrateMsg,
) -> Result<Response, NymPerformanceContractError> {
set_build_information!(deps.storage)?;
cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
Ok(Default::default())
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(test)]
mod contract_instantiation {
use super::*;
use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE;
use crate::testing::PreInitContract;
use cosmwasm_std::testing::message_info;
#[test]
fn sets_contract_admin_to_the_message_sender() -> anyhow::Result<()> {
// we need to mock dependencies in a state where mixnet contract has already been instantiated
// (we query it at init)
let mut pre_init = PreInitContract::new();
let env = pre_init.env();
let mixnet_contract_address = pre_init.mixnet_contract_address.to_string();
let some_sender = pre_init.addr_make("some_sender");
let deps = pre_init.deps_mut();
instantiate(
deps,
env,
message_info(&some_sender, &[]),
InstantiateMsg {
mixnet_contract_address,
authorised_network_monitors: vec![],
},
)?;
let deps = pre_init.deps();
NYM_PERFORMANCE_CONTRACT_STORAGE
.contract_admin
.assert_admin(deps, &some_sender)?;
Ok(())
}
}
}
+118
View File
@@ -0,0 +1,118 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{from_json, Binary, CustomQuery, QuerierWrapper, StdError, StdResult};
use cw_storage_plus::{Key, Namespace, Path, PrimaryKey};
use nym_mixnet_contract_common::{Interval, MixNodeBond, NymNodeBond};
use nym_performance_contract_common::{EpochId, NodeId};
use serde::de::DeserializeOwned;
use std::ops::Deref;
pub(crate) trait MixnetContractQuerier {
#[allow(dead_code)]
fn query_mixnet_contract<T: DeserializeOwned>(
&self,
address: impl Into<String>,
msg: &nym_mixnet_contract_common::QueryMsg,
) -> StdResult<T>;
fn query_mixnet_contract_storage(
&self,
address: impl Into<String>,
key: impl Into<Binary>,
) -> StdResult<Option<Vec<u8>>>;
fn query_mixnet_contract_storage_value<T: DeserializeOwned>(
&self,
address: impl Into<String>,
key: impl Into<Binary>,
) -> StdResult<Option<T>> {
match self.query_mixnet_contract_storage(address, key)? {
None => Ok(None),
Some(value) => Ok(Some(from_json(&value)?)),
}
}
fn query_current_mixnet_interval(&self, address: impl Into<String>) -> StdResult<Interval> {
self.query_mixnet_contract_storage_value(address, b"ci")?
.ok_or(StdError::not_found(
"unable to retrieve interval information from the mixnet contract storage",
))
}
fn query_current_absolute_mixnet_epoch_id(
&self,
address: impl Into<String>,
) -> StdResult<EpochId> {
self.query_current_mixnet_interval(address)
.map(|interval| interval.current_epoch_absolute_id())
}
fn check_node_existence(&self, address: impl Into<String>, node_id: NodeId) -> StdResult<bool> {
let mixnet_contract_address = address.into();
// 1. check if it's a nym-node
if let Some(nym_node) = self.query_nymnode_bond(mixnet_contract_address.clone(), node_id)? {
return Ok(!nym_node.is_unbonding);
}
// 2. try a legacy mixnode
if let Some(nym_node) = self.query_mixnode_bond(mixnet_contract_address, node_id)? {
return Ok(!nym_node.is_unbonding);
}
Ok(false)
}
fn query_nymnode_bond(
&self,
address: impl Into<String>,
node_id: NodeId,
) -> StdResult<Option<NymNodeBond>> {
// construct proper map key
let pk_namespace = "nn";
let path: Path<NymNodeBond> = Path::new(
Namespace::from_static_str(pk_namespace).as_slice(),
&node_id.key().iter().map(Key::as_ref).collect::<Vec<_>>(),
);
let storage_key = path.deref();
self.query_mixnet_contract_storage_value(address, storage_key)
}
fn query_mixnode_bond(
&self,
address: impl Into<String>,
node_id: NodeId,
) -> StdResult<Option<MixNodeBond>> {
// construct proper map key
let pk_namespace = "mnn";
let path: Path<MixNodeBond> = Path::new(
Namespace::from_static_str(pk_namespace).as_slice(),
&node_id.key().iter().map(Key::as_ref).collect::<Vec<_>>(),
);
let storage_key = path.deref();
self.query_mixnet_contract_storage_value(address, storage_key)
}
}
impl<C> MixnetContractQuerier for QuerierWrapper<'_, C>
where
C: CustomQuery,
{
fn query_mixnet_contract<T: DeserializeOwned>(
&self,
address: impl Into<String>,
msg: &nym_mixnet_contract_common::QueryMsg,
) -> StdResult<T> {
self.query_wasm_smart(address, msg)
}
fn query_mixnet_contract_storage(
&self,
address: impl Into<String>,
key: impl Into<Binary>,
) -> StdResult<Option<Vec<u8>>> {
self.query_wasm_raw(address, key)
}
}
+13
View File
@@ -0,0 +1,13 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod contract;
pub mod queued_migrations;
pub mod storage;
mod helpers;
mod queries;
mod transactions;
#[cfg(test)]
pub mod testing;
+588
View File
@@ -0,0 +1,588 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::storage::{retrieval_limits, NYM_PERFORMANCE_CONTRACT_STORAGE};
use cosmwasm_std::{Addr, Deps, Order, StdResult};
use cw_controllers::AdminResponse;
use cw_storage_plus::Bound;
use nym_performance_contract_common::{
EpochId, EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse,
FullHistoricalPerformancePagedResponse, HistoricalPerformance, NetworkMonitorInformation,
NetworkMonitorResponse, NetworkMonitorsPagedResponse, NodeId, NodeMeasurement,
NodeMeasurementsResponse, NodePerformance, NodePerformancePagedResponse,
NodePerformanceResponse, NymPerformanceContractError, RetiredNetworkMonitorsPagedResponse,
};
pub fn query_admin(deps: Deps) -> Result<AdminResponse, NymPerformanceContractError> {
NYM_PERFORMANCE_CONTRACT_STORAGE
.contract_admin
.query_admin(deps)
.map_err(Into::into)
}
pub fn query_node_performance(
deps: Deps,
epoch_id: EpochId,
node_id: NodeId,
) -> Result<NodePerformanceResponse, NymPerformanceContractError> {
let performance =
NYM_PERFORMANCE_CONTRACT_STORAGE.try_load_performance(deps.storage, epoch_id, node_id)?;
Ok(NodePerformanceResponse { performance })
}
pub fn query_node_measurements(
deps: Deps,
epoch_id: EpochId,
node_id: NodeId,
) -> Result<NodeMeasurementsResponse, NymPerformanceContractError> {
let measurements = NYM_PERFORMANCE_CONTRACT_STORAGE
.performance_results
.results
.may_load(deps.storage, (epoch_id, node_id))?;
Ok(NodeMeasurementsResponse { measurements })
}
pub fn query_node_performance_paged(
deps: Deps,
node_id: NodeId,
start_after: Option<EpochId>,
limit: Option<u32>,
) -> Result<NodePerformancePagedResponse, NymPerformanceContractError> {
let current_epoch_id = NYM_PERFORMANCE_CONTRACT_STORAGE.current_mixnet_epoch_id(deps)?;
let start = match start_after {
None => NYM_PERFORMANCE_CONTRACT_STORAGE
.mixnet_epoch_id_at_creation
.load(deps.storage)?,
Some(start_after) => start_after + 1,
};
let mut performance = Vec::new();
if current_epoch_id < start {
return Ok(NodePerformancePagedResponse {
node_id,
performance,
start_next_after: None,
});
}
let limit = limit
.unwrap_or(retrieval_limits::NODE_PERFORMANCE_DEFAULT_LIMIT)
.min(retrieval_limits::NODE_PERFORMANCE_MAX_LIMIT) as usize;
for epoch_id in (start..=current_epoch_id).take(limit) {
performance.push(EpochNodePerformance {
epoch: epoch_id,
performance: NYM_PERFORMANCE_CONTRACT_STORAGE.try_load_performance(
deps.storage,
epoch_id,
node_id,
)?,
})
}
let start_next_after = performance.last().and_then(|last| {
if last.epoch != current_epoch_id {
Some(last.epoch)
} else {
None
}
});
Ok(NodePerformancePagedResponse {
node_id,
performance,
start_next_after,
})
}
pub fn query_epoch_performance_paged(
deps: Deps,
epoch_id: EpochId,
start_after: Option<NodeId>,
limit: Option<u32>,
) -> Result<EpochPerformancePagedResponse, NymPerformanceContractError> {
let limit = limit
.unwrap_or(retrieval_limits::NODE_EPOCH_PERFORMANCE_DEFAULT_LIMIT)
.min(retrieval_limits::NODE_EPOCH_PERFORMANCE_MAX_LIMIT) as usize;
let start = start_after.map(Bound::exclusive);
let performance = NYM_PERFORMANCE_CONTRACT_STORAGE
.performance_results
.results
.prefix(epoch_id)
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|record| {
record.map(|(node_id, results)| NodePerformance {
node_id,
performance: results.median(),
})
})
.collect::<StdResult<Vec<_>>>()?;
let start_next_after = performance.last().map(|last| last.node_id);
Ok(EpochPerformancePagedResponse {
epoch_id,
performance,
start_next_after,
})
}
pub fn query_epoch_measurements_paged(
deps: Deps,
epoch_id: EpochId,
start_after: Option<NodeId>,
limit: Option<u32>,
) -> Result<EpochMeasurementsPagedResponse, NymPerformanceContractError> {
let limit = limit
.unwrap_or(retrieval_limits::NODE_EPOCH_MEASUREMENTS_DEFAULT_LIMIT)
.min(retrieval_limits::NODE_EPOCH_MEASUREMENTS_MAX_LIMIT) as usize;
let start = start_after.map(Bound::exclusive);
let measurements = NYM_PERFORMANCE_CONTRACT_STORAGE
.performance_results
.results
.prefix(epoch_id)
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|record| {
record.map(|(node_id, measurements)| NodeMeasurement {
node_id,
measurements,
})
})
.collect::<StdResult<Vec<_>>>()?;
let start_next_after = measurements.last().map(|last| last.node_id);
Ok(EpochMeasurementsPagedResponse {
epoch_id,
measurements,
start_next_after,
})
}
pub fn query_full_historical_performance_paged(
deps: Deps,
start_after: Option<(EpochId, NodeId)>,
limit: Option<u32>,
) -> Result<FullHistoricalPerformancePagedResponse, NymPerformanceContractError> {
let limit = limit
.unwrap_or(retrieval_limits::NODE_HISTORICAL_PERFORMANCE_DEFAULT_LIMIT)
.min(retrieval_limits::NODE_HISTORICAL_PERFORMANCE_MAX_LIMIT) as usize;
let start = start_after.map(Bound::exclusive);
let performance = NYM_PERFORMANCE_CONTRACT_STORAGE
.performance_results
.results
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|record| {
record.map(|((epoch_id, node_id), results)| HistoricalPerformance {
epoch_id,
node_id,
performance: results.median(),
})
})
.collect::<StdResult<Vec<_>>>()?;
let start_next_after = performance.last().map(|last| (last.epoch_id, last.node_id));
Ok(FullHistoricalPerformancePagedResponse {
performance,
start_next_after,
})
}
fn get_network_monitor_information(
deps: Deps,
address: &Addr,
) -> Result<Option<NetworkMonitorInformation>, NymPerformanceContractError> {
let Some(details) = NYM_PERFORMANCE_CONTRACT_STORAGE
.network_monitors
.authorised
.may_load(deps.storage, address)?
else {
return Ok(None);
};
let current_submission_metadata = NYM_PERFORMANCE_CONTRACT_STORAGE
.performance_results
.submission_metadata
.load(deps.storage, address)?;
Ok(Some(NetworkMonitorInformation {
details,
current_submission_metadata,
}))
}
pub fn query_network_monitor_details(
deps: Deps,
address: String,
) -> Result<NetworkMonitorResponse, NymPerformanceContractError> {
let address = deps.api.addr_validate(&address)?;
Ok(NetworkMonitorResponse {
info: get_network_monitor_information(deps, &address)?,
})
}
pub fn query_network_monitors_paged(
deps: Deps,
start_after: Option<String>,
limit: Option<u32>,
) -> Result<NetworkMonitorsPagedResponse, NymPerformanceContractError> {
let limit = limit
.unwrap_or(retrieval_limits::NETWORK_MONITORS_DEFAULT_LIMIT)
.min(retrieval_limits::NETWORK_MONITORS_MAX_LIMIT) as usize;
let addr = start_after
.map(|addr| deps.api.addr_validate(&addr))
.transpose()?;
let start = addr.as_ref().map(Bound::exclusive);
let info = NYM_PERFORMANCE_CONTRACT_STORAGE
.network_monitors
.authorised
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|record| {
record.and_then(|(address, details)| {
NYM_PERFORMANCE_CONTRACT_STORAGE
.performance_results
.submission_metadata
.load(deps.storage, &address)
.map(|current_submission_metadata| NetworkMonitorInformation {
details,
current_submission_metadata,
})
})
})
.collect::<StdResult<Vec<_>>>()?;
let start_next_after = info.last().map(|last| last.details.address.to_string());
Ok(NetworkMonitorsPagedResponse {
info,
start_next_after,
})
}
pub fn query_retired_network_monitors_paged(
deps: Deps,
start_after: Option<String>,
limit: Option<u32>,
) -> Result<RetiredNetworkMonitorsPagedResponse, NymPerformanceContractError> {
let limit = limit
.unwrap_or(retrieval_limits::RETIRED_NETWORK_MONITORS_DEFAULT_LIMIT)
.min(retrieval_limits::RETIRED_NETWORK_MONITORS_MAX_LIMIT) as usize;
let addr = start_after
.map(|addr| deps.api.addr_validate(&addr))
.transpose()?;
let start = addr.as_ref().map(Bound::exclusive);
let info = NYM_PERFORMANCE_CONTRACT_STORAGE
.network_monitors
.retired
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|record| record.map(|(_, details)| details))
.collect::<StdResult<Vec<_>>>()?;
let start_next_after = info.last().map(|last| last.details.address.to_string());
Ok(RetiredNetworkMonitorsPagedResponse {
info,
start_next_after,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::{init_contract_tester, PerformanceContractTesterExt};
use nym_contracts_common_testing::{ContractOpts, RandExt};
#[cfg(test)]
mod admin_query {
use super::*;
use crate::testing::init_contract_tester;
use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, RandExt};
use nym_performance_contract_common::ExecuteMsg;
#[test]
fn returns_current_admin() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let initial_admin = test.admin_unchecked();
// initial
let res = query_admin(test.deps())?;
assert_eq!(res.admin, Some(initial_admin.to_string()));
let new_admin = test.generate_account();
// sanity check
assert_ne!(initial_admin, new_admin);
// after update
test.execute_msg(
initial_admin.clone(),
&ExecuteMsg::UpdateAdmin {
admin: new_admin.to_string(),
},
)?;
let updated_admin = query_admin(test.deps())?;
assert_eq!(updated_admin.admin, Some(new_admin.to_string()));
Ok(())
}
}
#[test]
fn querying_node_performance_paged() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let node_id = test.bond_dummy_nymnode()?;
let nm = test.generate_account();
test.authorise_network_monitor(&nm)?;
// epoch 0
test.insert_raw_performance(&nm, node_id, "0")?;
// epoch 1
test.advance_mixnet_epoch()?;
test.insert_raw_performance(&nm, node_id, "0.1")?;
// epoch 2
test.advance_mixnet_epoch()?;
test.insert_raw_performance(&nm, node_id, "0.2")?;
// epoch 3
test.advance_mixnet_epoch()?;
test.insert_raw_performance(&nm, node_id, "0.3")?;
// epoch 4
test.advance_mixnet_epoch()?;
test.insert_raw_performance(&nm, node_id, "0.4")?;
// epoch 5
test.advance_mixnet_epoch()?;
test.insert_raw_performance(&nm, node_id, "0.5")?;
let deps = test.deps();
let res = query_node_performance_paged(deps, node_id, Some(5), None)?;
assert!(res.start_next_after.is_none());
assert!(res.performance.is_empty());
let res = query_node_performance_paged(deps, node_id, Some(42), None)?;
assert!(res.start_next_after.is_none());
assert!(res.performance.is_empty());
let res = query_node_performance_paged(deps, node_id, Some(4), None)?;
assert!(res.start_next_after.is_none());
assert_eq!(
res.performance,
vec![EpochNodePerformance {
epoch: 5,
performance: Some("0.5".parse()?),
}]
);
let res = query_node_performance_paged(deps, node_id, Some(2), None)?;
assert!(res.start_next_after.is_none());
assert_eq!(
res.performance,
vec![
EpochNodePerformance {
epoch: 3,
performance: Some("0.3".parse()?),
},
EpochNodePerformance {
epoch: 4,
performance: Some("0.4".parse()?),
},
EpochNodePerformance {
epoch: 5,
performance: Some("0.5".parse()?),
}
]
);
let res = query_node_performance_paged(deps, node_id, None, None)?;
assert!(res.start_next_after.is_none());
assert_eq!(
res.performance,
vec![
EpochNodePerformance {
epoch: 0,
performance: Some("0".parse()?),
},
EpochNodePerformance {
epoch: 1,
performance: Some("0.1".parse()?),
},
EpochNodePerformance {
epoch: 2,
performance: Some("0.2".parse()?),
},
EpochNodePerformance {
epoch: 3,
performance: Some("0.3".parse()?),
},
EpochNodePerformance {
epoch: 4,
performance: Some("0.4".parse()?),
},
EpochNodePerformance {
epoch: 5,
performance: Some("0.5".parse()?),
}
]
);
let res = query_node_performance_paged(deps, node_id, Some(2), Some(1))?;
assert_eq!(res.start_next_after, Some(3));
assert_eq!(
res.performance,
vec![EpochNodePerformance {
epoch: 3,
performance: Some("0.3".parse()?),
}]
);
Ok(())
}
#[test]
fn querying_epoch_performance_paged() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let nm = test.generate_account();
test.authorise_network_monitor(&nm)?;
let mut nodes = Vec::new();
for _ in 0..10 {
nodes.push(test.bond_dummy_nymnode()?);
}
let epoch_id = 5;
test.set_mixnet_epoch(epoch_id)?;
test.insert_raw_performance(&nm, nodes[1], "0.1")?;
test.insert_raw_performance(&nm, nodes[2], "0.2")?;
test.insert_raw_performance(&nm, nodes[3], "0.3")?;
// 4 is missing
test.insert_raw_performance(&nm, nodes[5], "0.5")?;
test.insert_raw_performance(&nm, nodes[6], "0.6")?;
let deps = test.deps();
let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[6]), None)?;
assert!(res.start_next_after.is_none());
assert!(res.performance.is_empty());
let res = query_epoch_performance_paged(deps, epoch_id, Some(42), None)?;
assert!(res.start_next_after.is_none());
assert!(res.performance.is_empty());
let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[4]), None)?;
assert_eq!(res.start_next_after, Some(nodes[6]));
assert_eq!(
res.performance,
vec![
NodePerformance {
node_id: nodes[5],
performance: "0.5".parse()?,
},
NodePerformance {
node_id: nodes[6],
performance: "0.6".parse()?,
}
]
);
let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[3]), None)?;
assert_eq!(res.start_next_after, Some(nodes[6]));
assert_eq!(
res.performance,
vec![
NodePerformance {
node_id: nodes[5],
performance: "0.5".parse()?,
},
NodePerformance {
node_id: nodes[6],
performance: "0.6".parse()?,
}
]
);
let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[2]), None)?;
assert_eq!(res.start_next_after, Some(nodes[6]));
assert_eq!(
res.performance,
vec![
NodePerformance {
node_id: nodes[3],
performance: "0.3".parse()?,
},
NodePerformance {
node_id: nodes[5],
performance: "0.5".parse()?,
},
NodePerformance {
node_id: nodes[6],
performance: "0.6".parse()?,
}
]
);
let res = query_epoch_performance_paged(deps, epoch_id, None, None)?;
assert_eq!(res.start_next_after, Some(nodes[6]));
assert_eq!(
res.performance,
vec![
NodePerformance {
node_id: nodes[1],
performance: "0.1".parse()?,
},
NodePerformance {
node_id: nodes[2],
performance: "0.2".parse()?,
},
NodePerformance {
node_id: nodes[3],
performance: "0.3".parse()?,
},
NodePerformance {
node_id: nodes[5],
performance: "0.5".parse()?,
},
NodePerformance {
node_id: nodes[6],
performance: "0.6".parse()?,
}
]
);
let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[2]), Some(1))?;
assert_eq!(res.start_next_after, Some(nodes[3]));
assert_eq!(
res.performance,
vec![NodePerformance {
node_id: nodes[3],
performance: "0.3".parse()?,
}]
);
Ok(())
}
}
@@ -0,0 +1,2 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
File diff suppressed because it is too large Load Diff
+606
View File
@@ -0,0 +1,606 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::contract::{execute, instantiate, migrate, query};
use crate::helpers::MixnetContractQuerier;
use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE;
use cosmwasm_std::testing::{message_info, mock_env, MockApi};
use cosmwasm_std::{
coin, coins, Addr, Binary, ContractInfo, Deps, DepsMut, Env, MessageInfo, QuerierWrapper,
StdError, StdResult,
};
use cw_storage_plus::PrimaryKey;
use mixnet_contract::testable_mixnet_contract::MixnetContract;
use nym_contracts_common::signing::{ContractMessageContent, MessageSignature};
use nym_contracts_common::Percent;
use nym_contracts_common_testing::{
addr, AdminExt, ArbitraryContractStorageReader, ArbitraryContractStorageWriter, BankExt,
ChainOpts, CommonStorageKeys, ContractFn, ContractOpts, ContractStorageWrapper, ContractTester,
ContractTesterBuilder, DenomExt, PermissionedFn, QueryFn, RandExt, TestableNymContract,
TEST_DENOM,
};
use nym_crypto::asymmetric::ed25519;
use nym_mixnet_contract_common::nym_node::{NodeDetailsResponse, NodeOwnershipResponse, Role};
use nym_mixnet_contract_common::{
CurrentIntervalResponse, EpochId, Interval, MixNode, MixNodeBond, MixnodeDetailsResponse,
NodeCostParams, NodeRewarding, NymNode, NymNodeBondingPayload, RoleAssignment,
SignableNymNodeBondingMsg, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT,
DEFAULT_PROFIT_MARGIN_PERCENT,
};
use nym_performance_contract_common::constants::storage_keys;
use nym_performance_contract_common::{
ExecuteMsg, InstantiateMsg, MigrateMsg, NodeId, NodePerformance, NodeResults,
NymPerformanceContractError, QueryMsg,
};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
pub struct PerformanceContract;
impl TestableNymContract for PerformanceContract {
const NAME: &'static str = "performance-contract";
type InitMsg = InstantiateMsg;
type ExecuteMsg = ExecuteMsg;
type QueryMsg = QueryMsg;
type MigrateMsg = MigrateMsg;
type ContractError = NymPerformanceContractError;
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
instantiate
}
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
execute
}
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
query
}
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
migrate
}
fn base_init_msg() -> Self::InitMsg {
InstantiateMsg {
mixnet_contract_address: addr("mixnet-contract").to_string(),
authorised_network_monitors: vec![],
}
}
fn init() -> ContractTester<Self>
where
Self: Sized,
{
let builder = ContractTesterBuilder::new().instantiate::<MixnetContract>(None);
// we just instantiated it
let mixnet_address = builder
.well_known_contracts
.get(MixnetContract::NAME)
.unwrap()
.clone();
builder
.instantiate::<Self>(Some(InstantiateMsg {
mixnet_contract_address: mixnet_address.to_string(),
authorised_network_monitors: vec![],
}))
.build()
}
}
pub fn init_contract_tester() -> ContractTester<PerformanceContract> {
PerformanceContract::init()
.with_common_storage_key(CommonStorageKeys::Admin, storage_keys::CONTRACT_ADMIN)
}
// we need to be able to test instantiation, but for that we require
// deps in a state that already includes instantiated mixnet contract
pub(crate) struct PreInitContract {
tester_builder: ContractTesterBuilder<PerformanceContract>,
pub(crate) mixnet_contract_address: Addr,
pub(crate) api: MockApi,
storage: ContractStorageWrapper,
placeholder_address: Addr,
}
#[allow(dead_code)]
impl PreInitContract {
pub(crate) fn new() -> PreInitContract {
let tester_builder =
ContractTesterBuilder::<PerformanceContract>::new().instantiate::<MixnetContract>(None);
let mixnet_contract = tester_builder
.well_known_contracts
.get(&MixnetContract::NAME)
.unwrap();
let api = tester_builder.api();
let placeholder_address = api.addr_make("to-be-performance-contract");
let storage = tester_builder.contract_storage_wrapper(&placeholder_address);
PreInitContract {
mixnet_contract_address: mixnet_contract.clone(),
tester_builder,
api,
storage,
placeholder_address,
}
}
pub(crate) fn deps(&self) -> Deps {
Deps {
storage: &self.storage,
api: &self.api,
querier: self.tester_builder.querier(),
}
}
pub(crate) fn deps_mut(&mut self) -> DepsMut {
DepsMut {
storage: &mut self.storage,
api: &self.api,
querier: self.tester_builder.querier(),
}
}
pub(crate) fn querier(&self) -> QuerierWrapper {
self.tester_builder.querier()
}
pub(crate) fn env(&self) -> Env {
Env {
contract: ContractInfo {
address: self.placeholder_address.clone(),
},
..mock_env()
}
}
pub(crate) fn addr_make(&self, input: &str) -> Addr {
self.api.addr_make(input)
}
pub(crate) fn write_to_mixnet_contract_storage(
&mut self,
key: impl AsRef<[u8]>,
value: impl AsRef<[u8]>,
) -> StdResult<()> {
let address = NYM_PERFORMANCE_CONTRACT_STORAGE
.mixnet_contract_address
.load(self.deps().storage)?;
self.set_contract_storage(address, key, value);
Ok(())
}
pub(crate) fn write_to_mixnet_contract_storage_value<T: Serialize>(
&mut self,
key: impl AsRef<[u8]>,
value: &T,
) -> StdResult<()> {
let address = NYM_PERFORMANCE_CONTRACT_STORAGE
.mixnet_contract_address
.load(self.deps().storage)?;
self.set_contract_storage_value(address, key, value)
}
}
impl ArbitraryContractStorageWriter for PreInitContract {
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);
}
}
#[allow(dead_code)]
pub(crate) trait PerformanceContractTesterExt:
ContractOpts<
ExecuteMsg = ExecuteMsg,
QueryMsg = QueryMsg,
ContractError = NymPerformanceContractError,
> + ChainOpts
+ AdminExt
+ DenomExt
+ RandExt
+ BankExt
+ ArbitraryContractStorageReader
+ ArbitraryContractStorageWriter
{
fn mixnet_contract_address(&self) -> StdResult<Addr> {
NYM_PERFORMANCE_CONTRACT_STORAGE
.mixnet_contract_address
.load(self.deps().storage)
}
fn execute_mixnet_contract(
&mut self,
sender: MessageInfo,
msg: &nym_mixnet_contract_common::ExecuteMsg,
) -> StdResult<()> {
let address = self.mixnet_contract_address()?;
self.execute_arbitrary_contract(address, sender, msg)
.map_err(|err| {
StdError::generic_err(format!("mixnet contract execution failure: {err}"))
})?;
Ok(())
}
fn read_from_mixnet_contract_storage<T: DeserializeOwned>(
&self,
key: impl AsRef<[u8]>,
) -> StdResult<T> {
let address = self.mixnet_contract_address()?;
self.must_read_value_from_contract_storage(address, key)
}
fn write_to_mixnet_contract_storage(
&mut self,
key: impl AsRef<[u8]>,
value: impl AsRef<[u8]>,
) -> StdResult<()> {
let address = self.mixnet_contract_address()?;
<Self as ArbitraryContractStorageWriter>::set_contract_storage(self, address, key, value);
Ok(())
}
fn write_to_mixnet_contract_storage_value<T: Serialize>(
&mut self,
key: impl AsRef<[u8]>,
value: &T,
) -> StdResult<()> {
let address = self.mixnet_contract_address()?;
self.set_contract_storage_value(address, key, value)
}
fn current_mixnet_epoch(&self) -> StdResult<EpochId> {
let address = self.mixnet_contract_address()?;
Ok(self
.deps()
.querier
.query_current_mixnet_interval(address.clone())?
.current_epoch_absolute_id())
}
fn advance_mixnet_epoch(&mut self) -> StdResult<()> {
let interval_details: CurrentIntervalResponse = self.query_arbitrary_contract(
self.mixnet_contract_address()?,
&nym_mixnet_contract_common::QueryMsg::GetCurrentIntervalDetails {},
)?;
let until_end = interval_details.time_until_current_epoch_end().as_secs();
let timestamp = self.env().block.time.plus_seconds(until_end + 1);
self.set_block_time(timestamp);
self.next_block();
// this was hardcoded in mixnet init
let mixnet_rewarder = self.addr_make("rewarder");
let rewarder = message_info(&mixnet_rewarder, &[]);
self.execute_mixnet_contract(
rewarder.clone(),
&nym_mixnet_contract_common::ExecuteMsg::BeginEpochTransition {},
)?;
self.execute_mixnet_contract(
rewarder.clone(),
&nym_mixnet_contract_common::ExecuteMsg::ReconcileEpochEvents { limit: None },
)?;
for role in [
Role::ExitGateway,
Role::EntryGateway,
Role::Layer1,
Role::Layer2,
Role::Layer3,
Role::Standby,
] {
self.execute_mixnet_contract(
rewarder.clone(),
&nym_mixnet_contract_common::ExecuteMsg::AssignRoles {
assignment: RoleAssignment {
role,
nodes: vec![],
},
},
)?;
}
Ok(())
}
fn set_mixnet_epoch(&mut self, epoch_id: EpochId) -> StdResult<()> {
let address = self.mixnet_contract_address()?;
let interval = self
.deps()
.querier
.query_current_mixnet_interval(address.clone())?;
let mut to_update = if interval.current_epoch_absolute_id() <= epoch_id {
interval
} else {
Interval::init_interval(
interval.epochs_in_interval(),
interval.epoch_length(),
&mock_env(),
)
};
let current = to_update.current_epoch_absolute_id();
let diff = epoch_id - current;
for _ in 0..diff {
to_update = to_update.advance_epoch();
}
self.set_contract_storage_value(&address, b"ci", &to_update)
}
fn authorise_network_monitor(
&mut self,
addr: &Addr,
) -> Result<(), NymPerformanceContractError> {
let admin = self.admin_unchecked();
self.execute_raw(
admin,
ExecuteMsg::AuthoriseNetworkMonitor {
address: addr.to_string(),
},
)?;
Ok(())
}
fn dummy_node_performance(&mut self) -> NodePerformance {
let node_id = self.bond_dummy_nymnode().unwrap();
NodePerformance {
node_id,
performance: Percent::from_percentage_value(69).unwrap(),
}
}
fn retire_network_monitor(&mut self, addr: &Addr) -> Result<(), NymPerformanceContractError> {
let admin = self.admin_unchecked();
self.execute_raw(
admin,
ExecuteMsg::RetireNetworkMonitor {
address: addr.to_string(),
},
)?;
Ok(())
}
fn insert_epoch_performance(
&mut self,
addr: &Addr,
epoch_id: EpochId,
node_id: NodeId,
performance: Percent,
) -> Result<(), NymPerformanceContractError> {
NYM_PERFORMANCE_CONTRACT_STORAGE.submit_performance_data(
self.deps_mut(),
addr,
epoch_id,
NodePerformance {
node_id,
performance,
},
)
}
fn insert_performance(
&mut self,
addr: &Addr,
node_id: NodeId,
performance: Percent,
) -> Result<(), NymPerformanceContractError> {
let epoch_id = self.current_mixnet_epoch()?;
self.insert_epoch_performance(addr, epoch_id, node_id, performance)
}
// makes testing easier
fn insert_raw_performance(
&mut self,
addr: &Addr,
node_id: NodeId,
raw: &str,
) -> Result<(), NymPerformanceContractError> {
self.insert_performance(
addr,
node_id,
Percent::from_str(raw).map_err(|err| {
NymPerformanceContractError::StdErr(StdError::parse_err("Percent", err.to_string()))
})?,
)
}
fn read_raw_scores(
&self,
epoch_id: EpochId,
node_id: NodeId,
) -> Result<NodeResults, NymPerformanceContractError> {
let scores = NYM_PERFORMANCE_CONTRACT_STORAGE
.performance_results
.results
.load(self.deps().storage, (epoch_id, node_id))?;
Ok(scores)
}
fn bond_dummy_nymnode(&mut self) -> Result<NodeId, NymPerformanceContractError> {
let node_owner = self.generate_account_with_balance();
let pledge = coins(100_000000, TEST_DENOM);
let keypair = ed25519::KeyPair::new(self.raw_rng());
let identity_key = keypair.public_key().to_base58_string();
let node = NymNode {
host: "1.2.3.4".to_string(),
custom_http_port: None,
identity_key,
};
let cost_params = NodeCostParams {
profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT)
.unwrap(),
interval_operating_cost: coin(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, TEST_DENOM),
};
// initial signing nonce is 0 for a new address
let signing_nonce = 0;
let payload = NymNodeBondingPayload::new(node.clone(), cost_params.clone());
let content = ContractMessageContent::new(node_owner.clone(), pledge.clone(), payload);
let msg = SignableNymNodeBondingMsg::new(signing_nonce, content);
let owner_signature = keypair.private_key().sign(msg.to_plaintext()?);
let owner_signature = MessageSignature::from(owner_signature.to_bytes().as_ref());
self.execute_mixnet_contract(
message_info(&node_owner, &pledge),
&nym_mixnet_contract_common::ExecuteMsg::BondNymNode {
node,
cost_params,
owner_signature,
},
)?;
let bond: NodeOwnershipResponse = self.query_arbitrary_contract(
self.mixnet_contract_address()?,
&nym_mixnet_contract_common::QueryMsg::GetOwnedNymNode {
address: node_owner.to_string(),
},
)?;
Ok(bond.details.unwrap().bond_information.node_id)
}
fn unbond_nymnode(&mut self, node_id: NodeId) -> Result<(), NymPerformanceContractError> {
let bond: NodeDetailsResponse = self.query_arbitrary_contract(
self.mixnet_contract_address()?,
&nym_mixnet_contract_common::QueryMsg::GetNymNodeDetails { node_id },
)?;
let node_owner = bond.details.unwrap().bond_information.owner;
self.execute_mixnet_contract(
message_info(&node_owner, &[]),
&nym_mixnet_contract_common::ExecuteMsg::UnbondNymNode {},
)?;
self.advance_mixnet_epoch()?;
Ok(())
}
fn bond_dummy_legacy_mixnode(&mut self) -> Result<NodeId, NymPerformanceContractError> {
#[derive(Deserialize, Serialize)]
pub(crate) struct UniqueRef<T> {
// note, we collapse the pk - combining everything under the namespace - even if it is composite
pk: Binary,
value: T,
}
// there's no proper Execute flow for this anymore, so we have to "hack" the storage a bit,
// ensuring all invariants still hold
let owner = self.generate_account_with_balance();
let mixnode = MixNode {
host: "1.2.3.4".to_string(),
mix_port: 123,
verloc_port: 123,
http_api_port: 123,
sphinx_key: "aaaa".to_string(),
identity_key: "bbbbb".to_string(),
version: "ccc".to_string(),
};
let cost_params = NodeCostParams {
profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT)
.unwrap(),
interval_operating_cost: coin(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, TEST_DENOM),
};
// adjust node counter
let node_id_counter: u32 = self.read_from_mixnet_contract_storage("nic")?;
let node_id = node_id_counter + 1;
self.write_to_mixnet_contract_storage_value("nic", &node_id)?;
let current_epoch = self.current_mixnet_epoch()?;
let pledge = coin(100_000000, TEST_DENOM);
let mixnode_rewarding =
NodeRewarding::initialise_new(cost_params, &pledge, current_epoch).unwrap();
let env = self.env();
let mixnode_bond = MixNodeBond {
mix_id: node_id,
owner,
original_pledge: pledge,
mix_node: mixnode,
proxy: None,
bonding_height: env.block.height,
is_unbonding: false,
};
// save to the main mixnode storage
self.set_contract_map_value(
self.mixnet_contract_address()?,
"mnn",
node_id,
&mixnode_bond,
)?;
// update indices
let pk = node_id.joined_key();
let unique_ref = UniqueRef {
pk: pk.into(),
value: mixnode_bond.clone(),
};
// owner index
let idx = mixnode_bond.owner.clone();
self.set_contract_map_value(self.mixnet_contract_address()?, "mno", idx, &unique_ref)?;
// identity key index
let idx = mixnode_bond.mix_node.identity_key.clone();
self.set_contract_map_value(self.mixnet_contract_address()?, "mni", idx, &unique_ref)?;
// sphinx key index
let idx = mixnode_bond.mix_node.sphinx_key.clone();
self.set_contract_map_value(self.mixnet_contract_address()?, "mns", idx, &unique_ref)?;
// update rewarding data
self.set_contract_map_value(
self.mixnet_contract_address()?,
"mnr",
node_id,
&mixnode_rewarding,
)?;
Ok(node_id)
}
fn unbond_legacy_mixnode(
&mut self,
node_id: NodeId,
) -> Result<(), NymPerformanceContractError> {
let bond: MixnodeDetailsResponse = self.query_arbitrary_contract(
self.mixnet_contract_address()?,
&nym_mixnet_contract_common::QueryMsg::GetMixnodeDetails { mix_id: node_id },
)?;
let node_owner = bond.mixnode_details.unwrap().bond_information.owner;
self.execute_mixnet_contract(
message_info(&node_owner, &[]),
&nym_mixnet_contract_common::ExecuteMsg::UnbondMixnode {},
)?;
self.advance_mixnet_epoch()?;
Ok(())
}
}
impl PerformanceContractTesterExt for ContractTester<PerformanceContract> {}
+305
View File
@@ -0,0 +1,305 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE;
use cosmwasm_std::{to_json_binary, DepsMut, Env, Event, MessageInfo, Response};
use nym_performance_contract_common::{
EpochId, NodeId, NodePerformance, NymPerformanceContractError,
};
pub fn try_update_contract_admin(
deps: DepsMut<'_>,
info: MessageInfo,
new_admin: String,
) -> Result<Response, NymPerformanceContractError> {
let new_admin = deps.api.addr_validate(&new_admin)?;
let res = NYM_PERFORMANCE_CONTRACT_STORAGE
.contract_admin
.execute_update_admin(deps, info, Some(new_admin))?;
Ok(res)
}
pub fn try_submit_performance_results(
deps: DepsMut<'_>,
info: MessageInfo,
epoch_id: EpochId,
data: NodePerformance,
) -> Result<Response, NymPerformanceContractError> {
NYM_PERFORMANCE_CONTRACT_STORAGE.submit_performance_data(deps, &info.sender, epoch_id, data)?;
// TODO: emit events
Ok(Response::new())
}
pub fn try_batch_submit_performance_results(
deps: DepsMut<'_>,
info: MessageInfo,
epoch_id: EpochId,
data: Vec<NodePerformance>,
) -> Result<Response, NymPerformanceContractError> {
let res = NYM_PERFORMANCE_CONTRACT_STORAGE.batch_submit_performance_results(
deps,
&info.sender,
epoch_id,
data,
)?;
let response = Response::new().set_data(to_json_binary(&res)?).add_event(
Event::new("batch_performance_submission")
.add_attribute("accepted_scores", res.accepted_scores.to_string())
.add_attribute(
"non_existent_nodes",
format!("{:?}", res.non_existent_nodes),
),
);
Ok(response)
}
pub fn try_authorise_network_monitor(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
address: String,
) -> Result<Response, NymPerformanceContractError> {
let address = deps.api.addr_validate(&address)?;
NYM_PERFORMANCE_CONTRACT_STORAGE.authorise_network_monitor(
deps,
&env,
&info.sender,
address,
)?;
// TODO: emit events
Ok(Response::new())
}
pub fn try_retire_network_monitor(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
address: String,
) -> Result<Response, NymPerformanceContractError> {
let address = deps.api.addr_validate(&address)?;
NYM_PERFORMANCE_CONTRACT_STORAGE.retire_network_monitor(deps, env, &info.sender, address)?;
// TODO: emit events
Ok(Response::new())
}
pub fn try_remove_node_measurements(
deps: DepsMut<'_>,
info: MessageInfo,
epoch_id: EpochId,
node_id: NodeId,
) -> Result<Response, NymPerformanceContractError> {
NYM_PERFORMANCE_CONTRACT_STORAGE.remove_node_measurements(
deps,
&info.sender,
epoch_id,
node_id,
)?;
Ok(Response::new())
}
pub fn try_remove_epoch_measurements(
deps: DepsMut<'_>,
info: MessageInfo,
epoch_id: EpochId,
) -> Result<Response, NymPerformanceContractError> {
let res =
NYM_PERFORMANCE_CONTRACT_STORAGE.remove_epoch_measurements(deps, &info.sender, epoch_id)?;
Ok(Response::new().set_data(to_json_binary(&res)?))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::retrieval_limits;
use crate::testing::{init_contract_tester, PerformanceContractTesterExt};
use cosmwasm_std::from_json;
use nym_contracts_common_testing::{AdminExt, ContractOpts};
use nym_performance_contract_common::RemoveEpochMeasurementsResponse;
#[cfg(test)]
mod updating_contract_admin {
use super::*;
use crate::testing::init_contract_tester;
use cw_controllers::AdminError;
use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt};
use nym_performance_contract_common::ExecuteMsg;
#[test]
fn can_only_be_performed_by_current_admin() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let random_acc = test.generate_account();
let new_admin = test.generate_account();
let res = test
.execute_raw(
random_acc,
ExecuteMsg::UpdateAdmin {
admin: new_admin.to_string(),
},
)
.unwrap_err();
assert_eq!(
res,
NymPerformanceContractError::Admin(AdminError::NotAdmin {})
);
let actual_admin = test.admin_unchecked();
let res = test.execute_raw(
actual_admin.clone(),
ExecuteMsg::UpdateAdmin {
admin: new_admin.to_string(),
},
);
assert!(res.is_ok());
let updated_admin = test.admin_unchecked();
assert_eq!(new_admin, updated_admin);
Ok(())
}
#[test]
fn requires_providing_valid_address() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let bad_account = "definitely-not-valid-account";
let res = test.execute_raw(
test.admin_unchecked(),
ExecuteMsg::UpdateAdmin {
admin: bad_account.to_string(),
},
);
assert!(res.is_err());
let empty_account = "";
let res = test.execute_raw(
test.admin_unchecked(),
ExecuteMsg::UpdateAdmin {
admin: empty_account.to_string(),
},
);
assert!(res.is_err());
Ok(())
}
}
#[cfg(test)]
mod authorising_network_monitor {
use super::*;
use crate::testing::init_contract_tester;
use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt};
#[test]
fn requires_valid_address() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let bad_address = "foomp".to_string();
let good_address = test.generate_account();
let env = test.env();
let admin = test.admin_msg();
assert!(try_authorise_network_monitor(
test.deps_mut(),
env.clone(),
admin.clone(),
bad_address
)
.is_err());
assert!(try_authorise_network_monitor(
test.deps_mut(),
env,
admin,
good_address.to_string()
)
.is_ok());
Ok(())
}
}
#[cfg(test)]
mod retiring_network_monitor {
use super::*;
use crate::testing::{init_contract_tester, PerformanceContractTesterExt};
use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt};
#[test]
fn requires_valid_address() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let bad_address = "foomp".to_string();
let good_address = test.generate_account();
test.authorise_network_monitor(&good_address)?;
let env = test.env();
let admin = test.admin_msg();
assert!(try_retire_network_monitor(
test.deps_mut(),
env.clone(),
admin.clone(),
bad_address
)
.is_err());
assert!(try_retire_network_monitor(
test.deps_mut(),
env,
admin,
good_address.to_string()
)
.is_ok());
Ok(())
}
}
// panics in tests are fine...
#[allow(clippy::panic)]
#[test]
fn removing_epoch_measurements_returns_binary_data() -> anyhow::Result<()> {
let mut tester = init_contract_tester();
let nm = tester.addr_make("network-monitor");
tester.authorise_network_monitor(&nm)?;
tester.advance_mixnet_epoch()?;
for _ in 0..2 * retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT {
let node_id = tester.bond_dummy_nymnode()?;
tester.insert_raw_performance(&nm, node_id, "0.42")?;
}
let admin = tester.admin_msg();
let res = try_remove_epoch_measurements(tester.deps_mut(), admin.clone(), 0)?;
let Some(data) = res.data else {
panic!("missing binary response");
};
let deserialised: RemoveEpochMeasurementsResponse = from_json(&data)?;
assert!(!deserialised.additional_entries_to_remove_remaining);
let res = try_remove_epoch_measurements(tester.deps_mut(), admin, 1)?;
let Some(data) = res.data else {
panic!("missing binary response");
};
let deserialised: RemoveEpochMeasurementsResponse = from_json(&data)?;
assert!(deserialised.additional_entries_to_remove_remaining);
Ok(())
}
}
-1
View File
@@ -18,7 +18,6 @@ GROUP_CONTRACT_ADDRESS=n1qg5ega6dykkxc307y25pecuufrjkxkaggkkxh7nad0vhyhtuhw3sa07
MULTISIG_CONTRACT_ADDRESS=n1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqx5a364
COCONUT_DKG_CONTRACT_ADDRESS=n1aakfpghcanxtc45gpqlx8j3rq0zcpyf49qmhm9mdjrfx036h4z5sy2vfh9
EXPLORER_API=https://canary-explorer.performance.nymte.ch/api/
NYXD=https://rpc.canary-validator.performance.nymte.ch
NYM_API=https://canary-api.performance.nymte.ch/api/
NYXD_WS=wss://rpc.canary-validator.performance.nymte.ch/websocket
-1
View File
@@ -23,5 +23,4 @@ STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090"
NYXD="https://rpc.nymtech.net"
NYM_API="http://127.0.0.1:8000"
NYXD_WS="wss://rpc.nymtech.net/websocket"
EXPLORER_API="https://explorer.nymtech.net/api/"
NYM_VPN_API="https://nymvpn.com/api"
-1
View File
@@ -24,5 +24,4 @@ STATISTICS_SERVICE_DOMAIN_ADDRESS=https://mainnet-stats.nymte.ch:8090
NYXD=https://rpc.nymtech.net
NYM_API=https://validator.nymtech.net/api/
NYXD_WS=wss://rpc.nymtech.net/websocket
EXPLORER_API=https://explorer.nymtech.net/api/
NYM_VPN_API=https://nymvpn.com/api/
-1
View File
@@ -19,7 +19,6 @@ COCONUT_DKG_CONTRACT_ADDRESS=n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865
ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jlwz9
STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0"
EXPLORER_API=https://sandbox-explorer.nymtech.net/api/
NYXD=https://rpc.sandbox.nymtech.net
NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket
NYM_API=https://sandbox-nym-api1.nymtech.net/api/
-3
View File
@@ -1,3 +0,0 @@
{
"extends": ["next/core-web-vitals"]
}
-36
View File
@@ -1,36 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
-36
View File
@@ -1,36 +0,0 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
-11
View File
@@ -1,11 +0,0 @@
import React from 'react'
import { Navbar } from './components/Nav/Navbar'
import { Providers } from './providers'
const App = ({ children }: { children: React.ReactNode }) => (
<Providers>
<Navbar>{children}</Navbar>
</Providers>
)
export { App }
-407
View File
@@ -1,407 +0,0 @@
'use client'
import * as React from 'react'
import {Alert, AlertTitle, Box, Button, Chip, CircularProgress, Grid, Tooltip, Typography} from '@mui/material'
import { useParams } from 'next/navigation'
import { useMainContext } from '@/app/context/main'
import { Title } from '@/app/components/Title'
import { MaterialReactTable, MRT_ColumnDef, useMaterialReactTable } from "material-react-table";
import { useMemo } from "react";
import { humanReadableCurrencyToString } from "@/app/utils/currency";
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
import { PieChart } from '@mui/x-charts/PieChart';
import { useTheme } from "@mui/material/styles";
import { useIsMobile } from "@/app/hooks";
import { StyledLink } from "@/app/components";
const AccumulatedRewards = ({account}: { account?: any}) => {
const columns = useMemo<
MRT_ColumnDef<any>[]
>(() => {
return [
{
id: 'accumulated-rewards-data',
header: 'Accumulated Rewards Data',
columns: [
{
id: 'node_id',
accessorKey: 'node_id',
header: 'Node ID',
size: 150,
Cell: ({ row }) => (<StyledLink
color="text.primary"
to={`/network-components/nodes/${row.original.node_id}`}>
{row.original.node_id}
</StyledLink>),
},
{
id: 'node_still_fully_bonded',
accessorKey: 'node_still_fully_bonded',
header: 'Node still bonded?',
width: 150,
Cell: ({ row }) => (
<>{row.original.node_still_fully_bonded ? <CheckCircleOutlineIcon/> :
<Typography fontSize="inherit" alignItems="center" display="flex" sx={{ color: theme => theme.palette.warning.main }}>
<WarningAmberIcon sx={{ mr: 1 }}/>
Unbonded
</Typography>}</>
)
},
{
id: 'amount_staked',
accessorKey: 'amount_staked',
header: 'Amount',
width: 150,
Cell: ({ row }) => (
<>{humanReadableCurrencyToString(row.original.amount_staked)}</>
)
},
{
id: 'rewards',
accessorKey: 'rewards',
header: 'Rewards',
width: 150,
Cell: ({ row }) => (
<Typography fontSize="inherit" color="success.main">{humanReadableCurrencyToString(row.original.rewards)}</Typography>
)
},
],
},
]
}, [])
const table = useMaterialReactTable({
columns,
data: account?.accumulated_rewards || [],
enableFullScreenToggle: false,
})
return (<MaterialReactTable table={table} />);
}
const DelegationHistory = ({account}: { account?: any}) => {
const columns = useMemo<
MRT_ColumnDef<any>[]
>(() => {
return [
{
id: 'delegation-history-data',
header: 'Delegation History',
columns: [
{
id: 'node_id',
accessorKey: 'node_id',
header: 'Node ID',
size: 150,
},
{
id: 'delegated',
accessorKey: 'delegated',
header: 'Amount',
width: 150,
Cell: ({ row }) => (
<>{humanReadableCurrencyToString(row.original.delegated)}</>
)
},
{
id: 'height',
accessorKey: 'height',
header: 'Delegated at height',
width: 150,
Cell: ({ row }) => (
<>{row.original.height}</>
)
},
],
},
]
}, [])
const table = useMaterialReactTable({
columns,
data: account?.delegations || [],
enableFullScreenToggle: false,
})
return (<MaterialReactTable table={table} />);
}
/**
* Shows account details
*/
const PageAccountWithState = ({ account }: {
account?: any;
}) => {
const theme = useTheme();
const isMobile = useIsMobile();
const pieChartData = React.useMemo(() => {
if(!account) {
return [];
}
const parts = [];
const nymBalance = Number.parseFloat(account.balances.find((b: any) => b.denom === "unym")?.amount || "0") / 1e6;
if(nymBalance > 0) {
parts.push({label: "Spendable", value: nymBalance, color: theme.palette.primary.main});
}
if(account.vesting_account) {
if (`${account.vesting_account.locked?.amount}` !== "0") {
const value = Number.parseFloat(account.vesting_account.locked.amount) / 1e6;
if(value > 0) {
parts.push({
label: "Vesting locked",
value,
color: 'red'
});
}
}
if (`${account.vesting_account.spendable?.amount}` !== "0") {
const value = Number.parseFloat(account.vesting_account.spendable.amount) / 1e6;
if(value > 0) {
parts.push({
label: "Vesting spendable",
value,
color: theme.palette.primary.light
});
}
}
}
if (account.claimable_rewards &&`${account.claimable_rewards.amount}` !== "0") {
const value = Number.parseFloat(account.claimable_rewards.amount) / 1e6;
if(value > 0) {
parts.push({
label: "Claimable delegation rewards",
value,
color: theme.palette.success.light
});
}
}
if (account.operator_rewards && `${account.operator_rewards.amount}` !== "0") {
const value = Number.parseFloat(account.operator_rewards.amount) / 1e6;
if(value > 0) {
parts.push({
label: "Claimable operator rewards",
value,
color: theme.palette.success.dark
});
}
}
if (account.total_delegations && `${account.total_delegations.amount}` !== "0") {
const value = Number.parseFloat(account.total_delegations.amount) / 1e6;
if(value > 0) {
parts.push({
label: "Total delegations",
value,
color: '#888'
});
}
}
return parts;
}, [account]);
return (
<Box component="main">
<Box overflow="scroll">
<Title text={`Account ${account.address}`} />
</Box>
<Box mt={4} sx={{ maxWidth: "600px" }}>
<PieChart
series={[
{
data: pieChartData,
innerRadius: 40,
outerRadius: 80,
cy: isMobile ? 200 : undefined,
},
]}
height={300}
slotProps={isMobile ? {
legend: { position: { vertical: "top", horizontal: "right" } }
} : undefined}
/>
</Box>
<Box mt={4}>
<TableContainer component={Paper} sx={{ maxWidth: "400px" }}>
<Table>
<TableBody>
<TableRow sx={{ color: theme => theme.palette.primary.main }}>
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
<strong>Spendable Balance</strong>
</TableCell>
<TableCell align="right" sx={{ color: "inherit" }}>
{account.balances.map((b: any) => (<strong key={`balance-${b.denom}`}>{humanReadableCurrencyToString(b)}<br/></strong>))}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
Total delegations
</TableCell>
<TableCell align="right">
{humanReadableCurrencyToString(account.total_delegations)}
</TableCell>
</TableRow>
{account.claimable_rewards && <TableRow sx={{ color: theme => theme.palette.success.light }}>
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
Claimable delegation rewards
</TableCell>
<TableCell align="right" sx={{ color: "inherit" }}>
{humanReadableCurrencyToString(account.claimable_rewards)}
</TableCell>
</TableRow>}
{account.operator_rewards && `${account.operator_rewards.amount}` !== "0" && <TableRow sx={{ color: theme => theme.palette.success.light }}>
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
Claimable operator rewards
</TableCell>
<TableCell align="right" sx={{ color: "inherit" }}>
{humanReadableCurrencyToString(account.operator_rewards)}
</TableCell>
</TableRow>}
{account.vesting_account && (
<>
<TableRow>
<TableCell component="th" scope="row" colSpan={2}>
Vesting account
</TableCell>
</TableRow>
{`${account.vesting_account.locked.amount}` !== "0" &&
<TableRow>
<TableCell component="th" scope="row" sx={{ pl: 4 }}>
Locked
</TableCell>
<TableCell align="right" sx={{ color: "inherit" }}>
{humanReadableCurrencyToString(account.vesting_account.locked)}
</TableCell>
</TableRow>
}
{`${account.vesting_account.vested.amount}` !== "0" &&
<TableRow>
<TableCell component="th" scope="row" sx={{ pl: 4 }}>
Vested
</TableCell>
<TableCell align="right" sx={{ color: "inherit" }}>
{humanReadableCurrencyToString(account.vesting_account.vested)}
</TableCell>
</TableRow>
}
{`${account.vesting_account.vesting.amount}` !== "0" &&
<TableRow>
<TableCell component="th" scope="row" sx={{ pl: 4 }}>
Vesting
</TableCell>
<TableCell align="right" sx={{ color: "inherit" }}>
{humanReadableCurrencyToString(account.vesting_account.vesting)}
</TableCell>
</TableRow>
}
{`${account.vesting_account.spendable.amount}` !== "0" &&
<TableRow>
<TableCell component="th" scope="row" sx={{ pl: 4 }}>
Spendable
</TableCell>
<TableCell align="right" sx={{ color: "inherit" }}>
{humanReadableCurrencyToString(account.vesting_account.spendable)}
</TableCell>
</TableRow>
}
</>
)}
<TableRow>
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
<h3>Total value</h3>
</TableCell>
<TableCell align="right" sx={{ color: "inherit" }}>
<h3>{humanReadableCurrencyToString(account.total_value)}</h3>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Box>
<Box mt={4}>
<AccumulatedRewards account={account}/>
</Box>
<Box mt={4}>
<DelegationHistory account={account}/>
</Box>
</Box>
)
}
/**
* Guard component to handle loading and not found states
*/
const PageAccountDetailGuard = ({ account } : { account: string }) => {
const [accountDetails, setAccountDetails] = React.useState<any>();
const [isLoading, setLoading] = React.useState<boolean>(true);
const [error, setError] = React.useState<string>();
const { fetchAccountById } = useMainContext()
const { id } = useParams()
React.useEffect(() => {
setLoading(true);
(async () => {
if(typeof(id) === "string") {
try {
const res = await fetchAccountById(account);
setAccountDetails(res);
} catch(e: any) {
setError(e.message);
}
finally {
setLoading(false);
}
}
})();
}, [id])
if (isLoading) {
return <CircularProgress />
}
// loaded, but not found
if (error) {
return (
<Alert severity="warning">
<AlertTitle>Account not found</AlertTitle>
Sorry, we could not find the account <code>{id || ''}</code>
</Alert>
)
}
return <PageAccountWithState account={accountDetails} />
}
/**
* Wrapper component that adds the account details based on the `id` in the address URL
*/
const PageAccountDetail = () => {
const { id } = useParams()
if (!id || typeof id !== 'string') {
return (
<Alert severity="error">Oh no! Could not find that account</Alert>
)
}
return (
<PageAccountDetailGuard account={id} />
)
}
export default PageAccountDetail
-39
View File
@@ -1,39 +0,0 @@
// master APIs
export const API_BASE_URL = process.env.NEXT_PUBLIC_EXPLORER_API_URL || 'https://explorer.nymtech.net/api/v1';
export const NYM_API_BASE_URL = process.env.NEXT_PUBLIC_NYM_API_URL || 'https://validator.nymtech.net';
export const NYX_RPC_BASE_URL = process.env.NEXT_PUBLIC_NYX_RPC_BASE_URL || 'https://rpc.nymtech.net';
export const VALIDATOR_BASE_URL = process.env.NEXT_PUBLIC_VALIDATOR_URL || 'https://rpc.nymtech.net';
export const BLOCK_EXPLORER_BASE_URL = process.env.NEXT_PUBLIC_BIG_DIPPER_URL || 'https://nym.explorers.guru';
// specific API routes
export const OVERVIEW_API = `${API_BASE_URL}/overview`;
export const MIXNODE_PING = `${API_BASE_URL}/ping`;
export const MIXNODES_API = `${API_BASE_URL}/mix-nodes`;
export const MIXNODE_API = `${API_BASE_URL}/mix-node`;
export const VALIDATORS_API = `${NYX_RPC_BASE_URL}/validators`;
export const BLOCK_API = `${NYX_RPC_BASE_URL}/block`;
export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`;
export const UPTIME_STORY_API = `${NYM_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this.
export const UPTIME_STORY_API_GATEWAY = `${NYM_API_BASE_URL}/api/v1/status/gateway`; // add ID then '/history' or '/report' to this
export const SERVICE_PROVIDERS = `${API_BASE_URL}/service-providers`;
export const TEMP_UNSTABLE_NYM_NODES = `${API_BASE_URL}/tmp/unstable/nym-nodes`;
export const TEMP_UNSTABLE_ACCOUNT = `${API_BASE_URL}/tmp/unstable/account`;
export const NYM_API_NODE_UPTIME = `${NYM_API_BASE_URL}/api/v1/nym-nodes/uptime-history`;
export const NYM_API_NODE_PERFORMANCE = `${NYM_API_BASE_URL}/api/v1/nym-nodes/performance-history`;
export const LEGACY_MIXNODES_API = `${API_BASE_URL}/tmp/unstable/legacy-mixnode-bonds`;
export const LEGACY_GATEWAYS_API = `${API_BASE_URL}/tmp/unstable/legacy-gateway-bonds`;
// errors
export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us.";
export const NYM_WEBSITE = 'https://nymtech.net';
export const EXPLORER_FOR_ACCOUNTS = ''; // set to empty to use this Nym Explorer and NOT an external one
export const NYM_MIXNET_CONTRACT =
process.env.NYM_MIXNET_CONTRACT || 'n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr';
export const COSMOS_KIT_USE_CHAIN = process.env.NEXT_PUBLIC_COSMOS_KIT_USE_CHAIN || 'sandbox';
export const WALLET_CONNECT_PROJECT_ID = process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID || '';
-217
View File
@@ -1,217 +0,0 @@
import keyBy from 'lodash/keyBy';
import {
API_BASE_URL,
BLOCK_API,
COUNTRY_DATA_API,
UPTIME_STORY_API_GATEWAY,
MIXNODE_API,
MIXNODE_PING,
MIXNODES_API,
OVERVIEW_API,
UPTIME_STORY_API,
VALIDATORS_API,
SERVICE_PROVIDERS,
TEMP_UNSTABLE_NYM_NODES,
NYM_API_NODE_UPTIME,
NYM_API_NODE_PERFORMANCE,
TEMP_UNSTABLE_ACCOUNT,
LEGACY_MIXNODES_API, LEGACY_GATEWAYS_API,
} from './constants';
import {
CountryDataResponse,
DelegationsResponse,
UniqDelegationsResponse,
GatewayReportResponse,
UptimeStoryResponse,
MixNodeDescriptionResponse,
MixNodeResponse,
MixNodeResponseItem,
MixnodeStatus,
MixNodeEconomicDynamicsStatsResponse,
StatsResponse,
StatusResponse,
SummaryOverviewResponse,
ValidatorsResponse,
Environment,
GatewayBondAnnotated,
GatewayBond,
DirectoryServiceProvider,
LocatedGateway,
} from '../typeDefs/explorer-api';
function getFromCache(key: string) {
const ts = Number(localStorage.getItem('ts'));
const hasExpired = Date.now() - ts > 5000;
const curr = localStorage.getItem(key);
if (curr && !hasExpired) {
return JSON.parse(curr);
}
return undefined;
}
function storeInCache(key: string, data: any) {
localStorage.setItem(key, data);
localStorage.setItem('ts', Date.now().toString());
}
export class Api {
static fetchOverviewSummary = async (): Promise<SummaryOverviewResponse> => {
const cache = getFromCache('overview-summary');
if (cache) {
return cache;
}
const res = await fetch(`${OVERVIEW_API}/summary`);
const json: SummaryOverviewResponse = await res.json();
if (json.nymnodes?.roles) {
json.mixnodes.count += json.nymnodes.roles.mixnode;
json.gateways.count += json.nymnodes.roles.entry;
json.gateways.count += Math.max(json.nymnodes.roles.exit_ipr, json.nymnodes.roles.exit_nr);
}
storeInCache('overview-summary', JSON.stringify(json));
return json;
};
static fetchMixnodes = async (): Promise<MixNodeResponse> => {
const cachedMixnodes = getFromCache('mixnodes');
if (cachedMixnodes) {
return cachedMixnodes;
}
const res = await fetch(LEGACY_MIXNODES_API);
const json = await res.json();
storeInCache('mixnodes', JSON.stringify(json));
return json;
};
static fetchMixnodesActiveSetByStatus = async (status: MixnodeStatus): Promise<MixNodeResponse> => {
const cachedMixnodes = getFromCache(`mixnodes-${status}`);
if (cachedMixnodes) {
return cachedMixnodes;
}
const res = await fetch(`${MIXNODES_API}/active-set/${status}`);
const json = await res.json();
storeInCache(`mixnodes-${status}`, JSON.stringify(json));
return json;
};
static fetchMixnodeByID = async (id: string): Promise<MixNodeResponseItem | undefined> => {
const response = await fetch(`${MIXNODE_API}/${id}`);
// when the mixnode is not found, returned undefined
if (response.status === 404) {
return undefined;
}
return response.json();
};
static fetchGateways = async (): Promise<LocatedGateway[]> => {
// const res = await fetch(GATEWAYS_API);
// const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json();
// const res2 = await fetch(GATEWAYS_EXPLORER_API);
// const locatedGateways: LocatedGateway[] = await res2.json();
// const locatedGatewaysByOwner = keyBy(locatedGateways, 'owner');
// return gatewaysAnnotated.map(({ gateway_bond, node_performance }) => ({
// ...gateway_bond,
// node_performance,
// location: locatedGatewaysByOwner[gateway_bond.owner]?.location,
// }));
const res = await fetch(LEGACY_GATEWAYS_API);
const locatedGateways: LocatedGateway[] = await res.json();
return locatedGateways;
};
static fetchGatewayUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
(await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/history`)).json();
static fetchGatewayReportById = async (id: string): Promise<GatewayReportResponse> =>
(await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/report`)).json();
static fetchValidators = async (): Promise<ValidatorsResponse> => {
const res = await fetch(VALIDATORS_API);
const json = await res.json();
return json.result;
};
static fetchBlock = async (): Promise<number> => {
const res = await fetch(BLOCK_API);
const json = await res.json();
const { height } = json.result.block.header;
return height;
};
static fetchCountryData = async (): Promise<CountryDataResponse> => {
const result: CountryDataResponse = {};
const res = await fetch(COUNTRY_DATA_API);
const json = await res.json();
Object.keys(json).forEach((ISO3) => {
result[ISO3] = { ISO3, nodes: json[ISO3] };
});
return result;
};
static fetchDelegationsById = async (id: string): Promise<DelegationsResponse> =>
(await fetch(`${MIXNODE_API}/${id}/delegations`)).json();
static fetchUniqDelegationsById = async (id: string): Promise<UniqDelegationsResponse> =>
(await fetch(`${MIXNODE_API}/${id}/delegations/summed`)).json();
static fetchStatsById = async (id: string): Promise<StatsResponse> =>
(await fetch(`${MIXNODE_API}/${id}/stats`)).json();
static fetchMixnodeDescriptionById = async (id: string): Promise<MixNodeDescriptionResponse> =>
(await fetch(`${MIXNODE_API}/${id}/description`)).json();
static fetchMixnodeEconomicDynamicsStatsById = async (id: string): Promise<MixNodeEconomicDynamicsStatsResponse> =>
(await fetch(`${MIXNODE_API}/${id}/economic-dynamics-stats`)).json();
static fetchStatusById = async (id: string): Promise<StatusResponse> => (await fetch(`${MIXNODE_PING}/${id}`)).json();
static fetchUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
(await fetch(`${UPTIME_STORY_API}/${id}/history`)).json();
static fetchServiceProviders = async (): Promise<DirectoryServiceProvider[]> => {
const res = await fetch(SERVICE_PROVIDERS);
const json = await res.json();
return json;
};
static fetchNodes = async () => {
const res = await fetch(TEMP_UNSTABLE_NYM_NODES);
const json = await res.json();
return json;
}
static fetchNodeById = async (id: number) => {
const res = await fetch(`${TEMP_UNSTABLE_NYM_NODES}/${id}`);
const json = await res.json();
return json;
}
static fetchNymNodeUptimeHistoryById = async (id: number | string) => {
const res = await fetch(`${NYM_API_NODE_UPTIME}/${id}`)
const json = await res.json();
return json;
}
static fetchNymNodePerformanceById = async (id: number | string) => {
const res = await fetch(`${NYM_API_NODE_PERFORMANCE}/${id}`)
const json = await res.json();
return json;
}
static fetchAccountById = async (id: string) => {
const res = await fetch(`${TEMP_UNSTABLE_ACCOUNT}/${id}`);
const json = await res.json();
return json;
}
}
export const getEnvironment = (): Environment => {
const matchEnv = (env: Environment) => API_BASE_URL?.toLocaleLowerCase().includes(env) && env;
return matchEnv('sandbox') || matchEnv('qa') || 'mainnet';
};
File diff suppressed because it is too large Load Diff

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