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
This commit is contained in:
committed by
GitHub
parent
05d8b31e51
commit
6de0c4ce92
@@ -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
|
||||
|
||||
Generated
+49
-1
@@ -1923,6 +1923,26 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cw-multi-test"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "533b31c94b9e10e77e2468a2b1559aa506505d18c4e52eb64cbfc624ca876ad2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bech32",
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-storage-plus",
|
||||
"cw-utils",
|
||||
"itertools 0.14.0",
|
||||
"prost 0.13.5",
|
||||
"schemars",
|
||||
"serde",
|
||||
"sha2 0.10.9",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cw-storage-plus"
|
||||
version = "2.0.0"
|
||||
@@ -5451,6 +5471,7 @@ dependencies = [
|
||||
name = "nym-contracts-common"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -5478,6 +5499,19 @@ dependencies = [
|
||||
"vergen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-contracts-common-testing"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cosmwasm-std",
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-cpp-ffi"
|
||||
version = "0.1.2"
|
||||
@@ -6288,7 +6322,6 @@ dependencies = [
|
||||
"schemars",
|
||||
"semver 1.0.26",
|
||||
"serde",
|
||||
"serde-json-wasm",
|
||||
"serde_repr",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
@@ -6893,6 +6926,19 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-performance-contract-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"nym-contracts-common 0.5.0",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-pool-contract-common"
|
||||
version = "0.1.0"
|
||||
@@ -7712,6 +7758,7 @@ dependencies = [
|
||||
"nym-mixnet-contract-common 0.6.0",
|
||||
"nym-multisig-contract-common 0.1.0",
|
||||
"nym-network-defaults 0.1.0",
|
||||
"nym-performance-contract-common",
|
||||
"nym-serde-helpers 0.1.0",
|
||||
"nym-vesting-contract-common 0.7.0",
|
||||
"prost 0.13.5",
|
||||
@@ -10757,6 +10804,7 @@ dependencies = [
|
||||
"nym-mixnet-contract-common 0.6.0",
|
||||
"nym-multisig-contract-common 0.1.0",
|
||||
"nym-pemstore 0.3.0",
|
||||
"nym-performance-contract-common",
|
||||
"nym-validator-client 0.1.0",
|
||||
"nym-vesting-contract-common 0.7.0",
|
||||
"rand 0.8.5",
|
||||
|
||||
+3
-5
@@ -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",
|
||||
@@ -136,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",
|
||||
@@ -370,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"
|
||||
@@ -380,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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
+265
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
+217
@@ -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)
|
||||
}
|
||||
}
|
||||
+18
-2
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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 =
|
||||
|
||||
@@ -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>,
|
||||
@@ -175,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),
|
||||
|
||||
Generated
+50
-7
@@ -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]]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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())?;
|
||||
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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> {}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
[alias]
|
||||
wasm = "build --release --lib --target wasm32-unknown-unknown"
|
||||
unit-test = "test --lib"
|
||||
schema = "run --bin schema --features=schema-gen"
|
||||
@@ -0,0 +1,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
|
||||
@@ -0,0 +1,5 @@
|
||||
wasm:
|
||||
RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown
|
||||
|
||||
generate-schema:
|
||||
cargo schema
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
@@ -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> {}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
Generated
+14
-1
@@ -4246,7 +4246,6 @@ dependencies = [
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde-json-wasm",
|
||||
"serde_repr",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
@@ -4325,6 +4324,19 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[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-serde-helpers"
|
||||
version = "0.1.0"
|
||||
@@ -4432,6 +4444,7 @@ dependencies = [
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-multisig-contract-common",
|
||||
"nym-network-defaults",
|
||||
"nym-performance-contract-common",
|
||||
"nym-serde-helpers",
|
||||
"nym-vesting-contract-common",
|
||||
"prost",
|
||||
|
||||
@@ -24,6 +24,10 @@ pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str =
|
||||
pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str =
|
||||
"n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865utqj5elvh";
|
||||
|
||||
// \/ TODO: this has to be updated once the contract is deployed
|
||||
pub(crate) const PERFORMANCE_CONTRACT_ADDRESS: &str = "";
|
||||
// /\ TODO: this has to be updated once the contract is deployed
|
||||
|
||||
// -- Constructor functions --
|
||||
|
||||
pub(crate) fn validators() -> Vec<ValidatorDetails> {
|
||||
@@ -46,6 +50,7 @@ pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails {
|
||||
contracts: NymContracts {
|
||||
mixnet_contract_address: parse_optional_str(MIXNET_CONTRACT_ADDRESS),
|
||||
vesting_contract_address: parse_optional_str(VESTING_CONTRACT_ADDRESS),
|
||||
performance_contract_address: parse_optional_str(PERFORMANCE_CONTRACT_ADDRESS),
|
||||
ecash_contract_address: parse_optional_str(ECASH_CONTRACT_ADDRESS),
|
||||
group_contract_address: parse_optional_str(GROUP_CONTRACT_ADDRESS),
|
||||
multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS),
|
||||
|
||||
@@ -45,6 +45,7 @@ nym-group-contract-common = { path = "../../../common/cosmwasm-smart-contracts/g
|
||||
nym-ecash-contract-common = { path = "../../../common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-coconut-dkg-common = { path = "../../../common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-multisig-contract-common = { path = "../../../common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-performance-contract-common = { path = "../../../common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-pemstore = { path = "../../../common/pemstore" }
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
|
||||
CREATE TABLE network_old
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL,
|
||||
|
||||
mixnet_contract_id INTEGER NOT NULL REFERENCES contract (id),
|
||||
vesting_contract_id INTEGER NOT NULL REFERENCES contract (id),
|
||||
ecash_contract_id INTEGER NOT NULL REFERENCES contract (id),
|
||||
cw3_multisig_contract_id INTEGER NOT NULL REFERENCES contract (id),
|
||||
cw4_group_contract_id INTEGER NOT NULL REFERENCES contract (id),
|
||||
dkg_contract_id INTEGER NOT NULL REFERENCES contract (id),
|
||||
|
||||
rewarder_address TEXT NOT NULL REFERENCES account (address),
|
||||
ecash_holding_account_address TEXT NOT NULL REFERENCES account (address)
|
||||
);
|
||||
|
||||
INSERT INTO network_old
|
||||
SELECT *
|
||||
from network;
|
||||
|
||||
DROP TABLE network;
|
||||
|
||||
CREATE TABLE network
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL,
|
||||
|
||||
mixnet_contract_id INTEGER NOT NULL REFERENCES contract (id),
|
||||
vesting_contract_id INTEGER NOT NULL REFERENCES contract (id),
|
||||
ecash_contract_id INTEGER NOT NULL REFERENCES contract (id),
|
||||
cw3_multisig_contract_id INTEGER NOT NULL REFERENCES contract (id),
|
||||
cw4_group_contract_id INTEGER NOT NULL REFERENCES contract (id),
|
||||
dkg_contract_id INTEGER NOT NULL REFERENCES contract (id),
|
||||
performance_contract_id INTEGER NOT NULL REFERENCES contract (id),
|
||||
|
||||
rewarder_address TEXT NOT NULL REFERENCES account (address),
|
||||
ecash_holding_account_address TEXT NOT NULL REFERENCES account (address)
|
||||
);
|
||||
|
||||
CREATE TABLE authorised_network_monitor
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
network_id INTEGER NOT NULL REFERENCES network (id),
|
||||
address TEXT NOT NULL REFERENCES account (address)
|
||||
);
|
||||
@@ -18,6 +18,7 @@ pub(crate) struct LoadedNymContracts {
|
||||
pub(crate) cw3_multisig: LoadedContract,
|
||||
pub(crate) cw4_group: LoadedContract,
|
||||
pub(crate) dkg: LoadedContract,
|
||||
pub(crate) performance: LoadedContract,
|
||||
}
|
||||
|
||||
impl From<NymContracts> for LoadedNymContracts {
|
||||
@@ -29,6 +30,7 @@ impl From<NymContracts> for LoadedNymContracts {
|
||||
cw3_multisig: value.cw3_multisig.into(),
|
||||
cw4_group: value.cw4_group.into(),
|
||||
dkg: value.dkg.into(),
|
||||
performance: value.performance.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +43,7 @@ pub(crate) struct NymContracts {
|
||||
pub(crate) cw3_multisig: Contract,
|
||||
pub(crate) cw4_group: Contract,
|
||||
pub(crate) dkg: Contract,
|
||||
pub(crate) performance: Contract,
|
||||
}
|
||||
|
||||
impl NymContracts {
|
||||
@@ -52,6 +55,7 @@ impl NymContracts {
|
||||
&self.cw3_multisig,
|
||||
&self.cw4_group,
|
||||
&self.dkg,
|
||||
&self.performance,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -63,11 +67,12 @@ impl NymContracts {
|
||||
&mut self.cw3_multisig,
|
||||
&mut self.cw4_group,
|
||||
&mut self.dkg,
|
||||
&mut self.performance,
|
||||
]
|
||||
}
|
||||
|
||||
pub(crate) fn count(&self) -> usize {
|
||||
6
|
||||
7
|
||||
}
|
||||
|
||||
pub(crate) fn discover_paths<P: AsRef<Path>>(
|
||||
@@ -100,6 +105,9 @@ impl NymContracts {
|
||||
if name.contains("dkg") {
|
||||
self.dkg.wasm_path = Some(entry.path())
|
||||
}
|
||||
if name.contains("performance") {
|
||||
self.performance.wasm_path = Some(entry.path())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +130,7 @@ impl Default for NymContracts {
|
||||
cw4_group: Contract::new("cw4_group"),
|
||||
cw3_multisig: Contract::new("cw3_multisig"),
|
||||
dkg: Contract::new("dkg"),
|
||||
performance: Contract::new("performance"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ impl<'a> From<&'a LoadedNetwork> for nym_config::defaults::NymNetworkDetails {
|
||||
let contracts = nym_config::defaults::NymContracts {
|
||||
mixnet_contract_address: Some(value.contracts.mixnet.address.to_string()),
|
||||
vesting_contract_address: Some(value.contracts.vesting.address.to_string()),
|
||||
performance_contract_address: Some(value.contracts.performance.address.to_string()),
|
||||
ecash_contract_address: Some(value.contracts.ecash.address.to_string()),
|
||||
group_contract_address: Some(value.contracts.cw4_group.address.to_string()),
|
||||
multisig_contract_address: Some(value.contracts.cw3_multisig.address.to_string()),
|
||||
@@ -134,6 +135,7 @@ impl LoadedNetwork {
|
||||
pub struct SpecialAddresses {
|
||||
pub ecash_holding_account: Account,
|
||||
pub mixnet_rewarder: Account,
|
||||
pub network_monitors: Vec<Account>,
|
||||
}
|
||||
|
||||
impl Default for SpecialAddresses {
|
||||
@@ -141,6 +143,8 @@ impl Default for SpecialAddresses {
|
||||
SpecialAddresses {
|
||||
ecash_holding_account: Account::new(),
|
||||
mixnet_rewarder: Account::new(),
|
||||
// by default use one address; to be adjusted in the future
|
||||
network_monitors: vec![Account::new()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +261,22 @@ impl NetworkManager {
|
||||
})
|
||||
}
|
||||
|
||||
fn performance_init_message(
|
||||
&self,
|
||||
ctx: &InitCtx,
|
||||
) -> Result<nym_performance_contract_common::msg::InstantiateMsg, NetworkManagerError> {
|
||||
Ok(nym_performance_contract_common::msg::InstantiateMsg {
|
||||
mixnet_contract_address: ctx.network.contracts.mixnet.address()?.to_string(),
|
||||
authorised_network_monitors: ctx
|
||||
.network
|
||||
.auxiliary_addresses
|
||||
.network_monitors
|
||||
.iter()
|
||||
.map(|acc| acc.address.to_string())
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn find_contracts<P: AsRef<Path>>(
|
||||
&self,
|
||||
ctx: &mut InitCtx,
|
||||
@@ -296,6 +312,10 @@ impl NetworkManager {
|
||||
"\tdiscovered dkg contract at '{}'",
|
||||
ctx.network.contracts.dkg.wasm_path()?.display()
|
||||
));
|
||||
ctx.println(format!(
|
||||
"\tdiscovered performance contract at '{}'",
|
||||
ctx.network.contracts.performance.wasm_path()?.display()
|
||||
));
|
||||
|
||||
ctx.println("\t✅ found all the contracts!");
|
||||
|
||||
@@ -392,6 +412,11 @@ impl NetworkManager {
|
||||
ctx.admin.mix_coins(10_000000),
|
||||
));
|
||||
|
||||
// and to any network monitors
|
||||
for network_monitor in &ctx.network.auxiliary_addresses.network_monitors {
|
||||
receivers.push((network_monitor.address(), ctx.admin.mix_coins(10_000000)))
|
||||
}
|
||||
|
||||
ctx.set_pb_message("attempting to send admin tokens...");
|
||||
|
||||
let send_future =
|
||||
@@ -553,6 +578,28 @@ impl NetworkManager {
|
||||
));
|
||||
ctx.network.contracts.ecash.init_info = Some(res.into());
|
||||
|
||||
// performance (semi-temp)
|
||||
ctx.set_pb_prefix(format!("[7/{total}]"));
|
||||
let name = &ctx.network.contracts.performance.name;
|
||||
let code_id = ctx.network.contracts.performance.upload_info()?.code_id;
|
||||
let admin = ctx.network.contracts.performance.admin()?.address.clone();
|
||||
ctx.set_pb_message(format!("attempting to instantiate {name} contract..."));
|
||||
let init_msg = self.performance_init_message(ctx)?;
|
||||
let init_fut = ctx.admin.instantiate(
|
||||
code_id,
|
||||
&init_msg,
|
||||
format!("{name} contract"),
|
||||
"contract instantiation from testnet-manager",
|
||||
Some(InstantiateOptions::default().with_admin(admin)),
|
||||
None,
|
||||
);
|
||||
let res = ctx.async_with_progress(init_fut).await?;
|
||||
let address = &res.contract_address;
|
||||
ctx.println(format!(
|
||||
"\t{name} contract instantiated with address: {address}",
|
||||
));
|
||||
ctx.network.contracts.performance.init_info = Some(res.into());
|
||||
|
||||
ctx.println("\t✅ instantiated all the contracts!");
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
use crate::manager::storage::models::{RawAccount, RawContract, RawNetwork};
|
||||
use crate::manager::storage::models::{
|
||||
RawAccount, RawAuthorisedNetworkMonitor, RawContract, RawNetwork,
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -85,6 +87,7 @@ impl StorageManager {
|
||||
cw3_id: i64,
|
||||
cw4_id: i64,
|
||||
dkg_id: i64,
|
||||
performance_id: i64,
|
||||
rewarder_address: &str,
|
||||
ecash_holding_address: &str,
|
||||
) -> Result<i64, sqlx::Error> {
|
||||
@@ -99,10 +102,11 @@ impl StorageManager {
|
||||
cw3_multisig_contract_id,
|
||||
cw4_group_contract_id,
|
||||
dkg_contract_id,
|
||||
performance_contract_id,
|
||||
rewarder_address,
|
||||
ecash_holding_account_address
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
name,
|
||||
created_at,
|
||||
@@ -112,6 +116,7 @@ impl StorageManager {
|
||||
cw3_id,
|
||||
cw4_id,
|
||||
dkg_id,
|
||||
performance_id,
|
||||
rewarder_address,
|
||||
ecash_holding_address,
|
||||
)
|
||||
@@ -159,19 +164,46 @@ impl StorageManager {
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn save_authorised_network_monitor(
|
||||
&self,
|
||||
network_id: i64,
|
||||
address: &str,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO authorised_network_monitor (network_id, address) VALUES (?, ?)",
|
||||
network_id,
|
||||
address,
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn load_authorised_network_monitors(
|
||||
&self,
|
||||
network_id: i64,
|
||||
) -> Result<Vec<RawAuthorisedNetworkMonitor>, sqlx::Error> {
|
||||
sqlx::query_as("SELECT * FROM authorised_network_monitor WHERE network_id = ?")
|
||||
.bind(network_id)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn save_account(
|
||||
&self,
|
||||
address: &str,
|
||||
mnemonic: &str,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
) -> Result<i64, sqlx::Error> {
|
||||
let account_id = sqlx::query!(
|
||||
"INSERT INTO account (address, mnemonic) VALUES (?, ?)",
|
||||
address,
|
||||
mnemonic
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
.await?
|
||||
.last_insert_rowid();
|
||||
Ok(account_id)
|
||||
}
|
||||
|
||||
pub(crate) async fn load_account(&self, address: &str) -> Result<RawAccount, sqlx::Error> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::fs;
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::{
|
||||
error::NetworkManagerError,
|
||||
manager::{
|
||||
@@ -14,6 +14,7 @@ use sqlx::{
|
||||
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
|
||||
ConnectOptions,
|
||||
};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use tracing::{error, info};
|
||||
use url::Url;
|
||||
@@ -159,7 +160,7 @@ impl NetworkManagerStorage {
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn persist_account(&self, account: &Account) -> Result<(), NetworkManagerError> {
|
||||
async fn persist_account(&self, account: &Account) -> Result<i64, NetworkManagerError> {
|
||||
let as_str = Zeroizing::new(account.mnemonic.to_string());
|
||||
Ok(self
|
||||
.manager
|
||||
@@ -191,6 +192,18 @@ impl NetworkManagerStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn persist_authorised_network_monitor(
|
||||
&self,
|
||||
network_id: i64,
|
||||
account: &Account,
|
||||
) -> Result<(), NetworkManagerError> {
|
||||
self.persist_account(account).await?;
|
||||
self.manager
|
||||
.save_authorised_network_monitor(network_id, account.address.as_ref())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn persist_network(
|
||||
&self,
|
||||
network: &Network,
|
||||
@@ -206,6 +219,8 @@ impl NetworkManagerStorage {
|
||||
self.persist_account(network.contracts.cw4_group.admin()?)
|
||||
.await?;
|
||||
self.persist_account(network.contracts.dkg.admin()?).await?;
|
||||
self.persist_account(network.contracts.performance.admin()?)
|
||||
.await?;
|
||||
|
||||
self.persist_account(&network.auxiliary_addresses.mixnet_rewarder)
|
||||
.await?;
|
||||
@@ -220,6 +235,9 @@ impl NetworkManagerStorage {
|
||||
.await?;
|
||||
let cw4_group_id = self.persist_contract(&network.contracts.cw4_group).await?;
|
||||
let dkg_id = self.persist_contract(&network.contracts.dkg).await?;
|
||||
let performance_id = self
|
||||
.persist_contract(&network.contracts.performance)
|
||||
.await?;
|
||||
|
||||
let network_id = self
|
||||
.manager
|
||||
@@ -232,6 +250,7 @@ impl NetworkManagerStorage {
|
||||
cw3_multisig_id,
|
||||
cw4_group_id,
|
||||
dkg_id,
|
||||
performance_id,
|
||||
network.auxiliary_addresses.mixnet_rewarder.address.as_ref(),
|
||||
network
|
||||
.auxiliary_addresses
|
||||
@@ -242,6 +261,10 @@ impl NetworkManagerStorage {
|
||||
.await?;
|
||||
|
||||
self.manager.save_latest_network_id(network_id).await?;
|
||||
for nm in &network.auxiliary_addresses.network_monitors {
|
||||
self.persist_authorised_network_monitor(network_id, nm)
|
||||
.await?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -256,6 +279,20 @@ impl NetworkManagerStorage {
|
||||
.await?
|
||||
.ok_or_else(|| NetworkManagerError::RpcEndpointNotSet)?;
|
||||
|
||||
let authorised = self
|
||||
.manager
|
||||
.load_authorised_network_monitors(base_network.id)
|
||||
.await?;
|
||||
let mut network_monitors = Vec::with_capacity(authorised.len());
|
||||
for authorised in authorised {
|
||||
network_monitors.push(
|
||||
self.manager
|
||||
.load_account(&authorised.address)
|
||||
.await?
|
||||
.try_into()?,
|
||||
)
|
||||
}
|
||||
|
||||
Ok(LoadedNetwork {
|
||||
id: base_network.id,
|
||||
name: base_network.name,
|
||||
@@ -292,6 +329,11 @@ impl NetworkManagerStorage {
|
||||
.load_contract(base_network.dkg_contract_id)
|
||||
.await?
|
||||
.try_into()?,
|
||||
performance: self
|
||||
.manager
|
||||
.load_contract(base_network.performance_contract_id)
|
||||
.await?
|
||||
.try_into()?,
|
||||
},
|
||||
auxiliary_addresses: SpecialAddresses {
|
||||
ecash_holding_account: self
|
||||
@@ -304,6 +346,7 @@ impl NetworkManagerStorage {
|
||||
.load_account(&base_network.rewarder_address)
|
||||
.await?
|
||||
.try_into()?,
|
||||
network_monitors,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,6 +6,14 @@ use crate::manager::contract::{Account, LoadedContract};
|
||||
use sqlx::FromRow;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(FromRow)]
|
||||
pub(crate) struct RawAuthorisedNetworkMonitor {
|
||||
pub(crate) id: i64,
|
||||
pub(crate) network_id: i64,
|
||||
pub(crate) address: String,
|
||||
}
|
||||
|
||||
#[derive(FromRow)]
|
||||
pub(crate) struct RawAccount {
|
||||
pub(crate) address: String,
|
||||
@@ -70,6 +78,7 @@ pub(crate) struct RawNetwork {
|
||||
pub(crate) cw3_multisig_contract_id: i64,
|
||||
pub(crate) cw4_group_contract_id: i64,
|
||||
pub(crate) dkg_contract_id: i64,
|
||||
pub(crate) performance_contract_id: i64,
|
||||
|
||||
pub(crate) rewarder_address: String,
|
||||
pub(crate) ecash_holding_account_address: String,
|
||||
|
||||
Reference in New Issue
Block a user