Compare commits

...

49 Commits

Author SHA1 Message Date
Tommy Verrall 2fa21dc4e9 amend wallet for sandbox link 2025-06-26 17:20:29 +02:00
benedetta davico ef5990658a Merge pull request #5873 from nymtech/wallet/fix-link 2025-06-26 13:26:36 +02:00
benedettadavico 658dec8299 fix the broken link 2025-06-26 12:44:47 +02:00
dynco-nym 447352b8d6 Set busy_timeout in sqlx (#5872)
* Set busy_timeout

* Bump version
2025-06-26 10:44:06 +02:00
Simon Wicky eb59615c56 StatsAPI qol : disable swagger try it out and remove debug level from nym_http_api_client (#5868) 2025-06-23 14:58:29 +02:00
Bogdan-Ștefan Neacşu 07c908c497 Return true remaining (#5866) 2025-06-23 11:53:39 +03:00
Jędrzej Stuczyński 6de0c4ce92 feat: initial performance contract (#5833)
* initialised basic structure for the performance contract

* shared code for contract testing

* unified common testing methods between performance and nym pool contracts

* impl of ExecuteMsg for the contract

* impl of QueryMsg for the contract

* setting initial authorised NMs during instantiation

* additional tests and fixes

* ibid

* scaffolding for client traits

* completed client traits

* clippy

* naive add performance contract to testnet manager

* placeholder values for the performance contract address

* introduced admin messages to purge old measurements from the storage

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

* Bump package version

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

* add dev features

* add operator updates

* add updated stats

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

* Test .rc version

* Undo cargo.toml version

* Remove comment

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

* Endpoint & cache

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

* Rest of the data for /gateways

* Revert before merge: pin deps to cheddar release

* Filter gw by country

* Return percent string instead of u8

* Filter by semver

* Bump package version

* Fix probe types

* Reorg

* Add exit, entry endpoints

* Different entry/exit selection criteria

* Date fix migration

* Unpin from cheddar

* Revert "Unpin from cheddar"

This reverts commit f17239075b.

* Validation with celes

* PR feedback

* Fix path

* Bump version

---------

Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
2025-06-12 09:56:31 +02:00
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
421 changed files with 9903 additions and 98755 deletions
@@ -44,8 +44,10 @@ jobs:
echo "Tag is empty"
exit 1
fi
# first, list all tags for logging purposes
curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' )
# check if there's a matching tag
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' )
if [[ $exists = "true" ]]; then
echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo"
exit 1
@@ -53,5 +55,5 @@ jobs:
echo "Version '$TAG' doesn't exist on the remote"
else
echo "Unknown output '$exists'"
exit 1
exit 2
fi
@@ -44,8 +44,10 @@ jobs:
echo "Tag is empty"
exit 1
fi
# first, list all tags for logging purposes
curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' )
# check if there's a matching tag
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' )
if [[ $exists = "true" ]]; then
echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo"
exit 1
@@ -53,5 +55,5 @@ jobs:
echo "Version '$TAG' doesn't exist on the remote"
else
echo "Unknown output '$exists'"
exit 1
exit 2
fi
@@ -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
+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 }}
Generated
+694 -237
View File
File diff suppressed because it is too large Load Diff
+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 +1 @@
Friday, June 6th 2025, 09:33:10 UTC
Thursday, June 12th 2025, 11:03:30 UTC
@@ -47,6 +47,53 @@ This page displays a full list of all the changes during our release cycle from
<VarInfo />
## `v2025.11-cheddar`
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.11-cheddar)
- [`nym-node`](nodes/nym-node.mdx) version `1.13.0`
```sh
nym-node
Binary Name: nym-node
Build Timestamp: 2025-06-10T09:41:31.291089877Z
Build Version: 1.13.0
Commit SHA: ef220882d4bcf0a8a703ea62f9273e017c9ebb51
Commit Date: 2025-06-10T11:39:20.000000000+02:00
Commit Branch: HEAD
rustc Version: 1.86.0
rustc Channel: stable
cargo Profile: release
```
### Operators Updates & Tools
- **Nym Improvement Proposals**: NIPs are being introduced, allowing us to propose changes to the Nym network and ecosystem, and decentralise governance
- **NIP1** will outline the governance process: Its draft will be published soon for discussion
- **NIP2** will be about reducing the stake saturation point variable, which would make reward distribution across the network more equitable
- **Join the Operator AMA next Tuesday** for an in-depth overview of what this all means (feat Nym Chief Scientist Claudia Diaz)
- Keep an eye on our channels for information about how to participate in the upcoming voting (no need for forum registration, we are building something new!)
### Features
- [Track wireguard credential retries](https://github.com/nymtech/nym/pull/5783)
- [Swap a decode into a `fromrow` to please future `postgres` feature](https://github.com/nymtech/nym/pull/5785): This PR change a sqlx derive from `Decode` to `FromRow` because a future PR will bring the `postgres` feature and it doesn't like that `Decode` there
- [Fix contains ticketbook function that always returned true](https://github.com/nymtech/nym/pull/5787)
- [QoL: `RequestPath` trait for `http-api-client`](https://github.com/nymtech/nym/pull/5788): Those `PathSegments` in `ApiClient` weren't very user-friendly. Instead we've defined a `RequestPath` trait that allows you to also pass a good old `&str` or `String` and internally it does exactly the same sanitization as before. `PathSegments` are also fully supported to not break any existing compatibility.
- [Nym Statistics API](https://github.com/nymtech/nym/pull/5800):
- Types for `nym-vpn-client`: It introduces `VpnClientStatsReport` which will be used by nym vpn clients to report statistics. Those types are in the monorepo because they're used by the `num-statistics-API`
- Nym Statistics API: Basically a `REST API` with a `postgres` backend. In this first iteration, it only accepts `VpnClientStatsReport` and stores them in its database. It optionally connects to the Nym API to confirm reports are coming from the network and attach country code to them if it's the case (exit node country code).
- [Resolve 1.87 clippy warnings](https://github.com/nymtech/nym/pull/5802)
- [Adjusted wallet storybook mocks to fix the build](https://github.com/nymtech/nym/pull/5804)
- [Set cached storage counters to 0](https://github.com/nymtech/nym/pull/5812)
- [No autoremoval of peers](https://github.com/nymtech/nym/pull/5831)
## `v2025.10-brie`
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.10-brie)
@@ -18,12 +18,12 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](.
## Current version
```sh
nym-node
ym-node
Binary Name: nym-node
Build Timestamp: 2025-05-27T10:13:18.511075453Z
Build Version: 1.12.0
Commit SHA: 1c6db86259d08d80e8bcfbc4fcc71ccb147fcfd0
Commit Date: 2025-05-27T12:11:13.000000000+02:00
Build Timestamp: 2025-06-10T09:41:31.291089877Z
Build Version: 1.13.0
Commit SHA: ef220882d4bcf0a8a703ea62f9273e017c9ebb51
Commit Date: 2025-06-10T11:39:20.000000000+02:00
Commit Branch: HEAD
rustc Version: 1.86.0
rustc Channel: stable
-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

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