node families (#6715)
* start node families topic branch * start node families topic branch * initialise node families contract * define contract storage * registering new family in storage * accepting family invitation * add_pending_invitation * revoke_pending_invitation * remove_family_member * reject_pending_invitation * disband_family * added unit tests for the storage methods * added restriction on uniquness of family names * update rustc version for node families contract common * clippy * basic queries by id * query_families_paged * change family membership storage and expose query for all members of a family * queries for pending invitations * queries for past invitations * queries for past data per node * queries for past family members * query_past_members_for_node_paged * queries for family by name and by owner * fixup family name normalisation * fixed incorrect lower bound for queries for past data * implement contract and storage initialisation * stubbing tx messages that are to be exposed by the contract * handler for updating config * removed partial fee return * wip: create family * move mixnet contract interaction traits to shared location * store original family name alongside the normalised variant * prevent family creation if owner has a node in another family * try_disband_family * try_invite_to_family + shared helpers * try_revoke_family_invitation * accept_family_invitation * stub method for node unbonding * try_reject_family_invitation * unit tests for family name normalisation * try_leave_family * try_kick_from_family * fix outdated comments and add paid fee event attribute * feat: NMv3: leave family upon node unbonding * NF contract handling of unbonding * lints * init node families contract when creating performance contract tester * clippy * avoid self-dep in the contract dev deps * introduced client traits for interacting with the node families contract * add node families contract to cache refresher * added query for all node family members (globally) and started scaffolding nym-api caches * docs and cache -> api conversion * calculating average node age based on individual timestamps * wire up node families cache * http stubs * filled in the implementation * route tests + extracting shared code * review fixes * feat: expose family information for all dvpn gateway endpoints within NS API * expose family information for explorer v3 route * clippy * review comments and optimise db family update * feat: Node Families: expose stake information inside DVpnGateway * chore: update lock files after rebase * chore: sort workspace members * explicitly require providing node families contract address for mixnet contract migration * fix missing node families contract address env export * dont swallow cache overwrite failures in fixture * pin network-defaults rustc version due to contracts dep * further version pinning * chore: update mixnet contract schema
This commit is contained in:
committed by
GitHub
parent
362f84b5f6
commit
a21a01cf1a
Generated
+18
@@ -5768,6 +5768,7 @@ dependencies = [
|
||||
"nym-http-api-client",
|
||||
"nym-http-api-common",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-node-families-contract-common",
|
||||
"nym-node-requests",
|
||||
"nym-node-tester-utils",
|
||||
"nym-pemstore",
|
||||
@@ -7608,6 +7609,21 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-families-contract-common"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"cw-utils",
|
||||
"nym-contracts-common",
|
||||
"nym-mixnet-contract-common",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-metrics"
|
||||
version = "1.21.0"
|
||||
@@ -7688,6 +7704,7 @@ dependencies = [
|
||||
"humantime",
|
||||
"itertools 0.14.0",
|
||||
"moka",
|
||||
"nym-api-requests",
|
||||
"nym-bin-common",
|
||||
"nym-contracts-common",
|
||||
"nym-credential-proxy-lib",
|
||||
@@ -8572,6 +8589,7 @@ dependencies = [
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-multisig-contract-common",
|
||||
"nym-network-defaults",
|
||||
"nym-node-families-contract-common",
|
||||
"nym-performance-contract-common",
|
||||
"nym-serde-helpers",
|
||||
"nym-vesting-contract-common",
|
||||
|
||||
+17
-14
@@ -31,7 +31,6 @@ members = [
|
||||
"common/client-libs/mixnet-client",
|
||||
"common/client-libs/validator-client",
|
||||
"common/commands",
|
||||
"common/nym-common",
|
||||
"common/config",
|
||||
"common/cosmwasm-smart-contracts/coconut-dkg",
|
||||
"common/cosmwasm-smart-contracts/contracts-common",
|
||||
@@ -41,6 +40,7 @@ members = [
|
||||
"common/cosmwasm-smart-contracts/group-contract",
|
||||
"common/cosmwasm-smart-contracts/mixnet-contract",
|
||||
"common/cosmwasm-smart-contracts/multisig-contract",
|
||||
"common/cosmwasm-smart-contracts/node-families-contract",
|
||||
"common/cosmwasm-smart-contracts/nym-performance-contract",
|
||||
"common/cosmwasm-smart-contracts/nym-pool-contract",
|
||||
"common/cosmwasm-smart-contracts/vesting-contract",
|
||||
@@ -70,11 +70,14 @@ members = [
|
||||
"common/node-tester-utils",
|
||||
"common/nonexhaustive-delayqueue",
|
||||
"common/nym-cache",
|
||||
"common/nym-common",
|
||||
"common/nym-connection-monitor",
|
||||
"common/nym-id",
|
||||
"common/nym-kcp",
|
||||
"common/nym-lp",
|
||||
"common/nym-kkt",
|
||||
"common/nym-kkt-ciphersuite",
|
||||
"common/nym-kkt-context",
|
||||
"common/nym-lp",
|
||||
"common/nym-metrics",
|
||||
"common/nym_offline_compact_ecash",
|
||||
"common/nymnoise",
|
||||
@@ -90,9 +93,9 @@ members = [
|
||||
"common/nymsphinx/params",
|
||||
"common/nymsphinx/routing",
|
||||
"common/nymsphinx/types",
|
||||
"common/nyxd-scraper-sqlite",
|
||||
"common/nyxd-scraper-psql",
|
||||
"common/nyxd-scraper-shared",
|
||||
"common/nyxd-scraper-sqlite",
|
||||
"common/pemstore",
|
||||
"common/registration",
|
||||
"common/serde-helpers",
|
||||
@@ -122,6 +125,7 @@ members = [
|
||||
"common/zulip-client",
|
||||
"documentation/autodoc",
|
||||
"gateway",
|
||||
"integration-tests",
|
||||
"nym-api",
|
||||
"nym-api/nym-api-requests",
|
||||
"nym-authenticator-client",
|
||||
@@ -129,6 +133,7 @@ members = [
|
||||
"nym-credential-proxy/nym-credential-proxy",
|
||||
"nym-credential-proxy/nym-credential-proxy-requests",
|
||||
"nym-data-observatory",
|
||||
"nym-gateway-probe",
|
||||
"nym-ip-packet-client",
|
||||
"nym-network-monitor",
|
||||
"nym-node",
|
||||
@@ -140,6 +145,7 @@ members = [
|
||||
"nym-outfox",
|
||||
"nym-registration-client",
|
||||
"nym-signers-monitor",
|
||||
"nym-sqlx-pool-guard",
|
||||
"nym-statistics-api",
|
||||
"nym-validator-rewarder",
|
||||
"nyx-chain-watcher",
|
||||
@@ -147,19 +153,18 @@ members = [
|
||||
"sdk/ffi/go",
|
||||
"sdk/ffi/shared",
|
||||
"sdk/rust/nym-sdk",
|
||||
"smolmix/core",
|
||||
"service-providers/common",
|
||||
"service-providers/ip-packet-router",
|
||||
"service-providers/network-requester",
|
||||
"nym-sqlx-pool-guard",
|
||||
"smolmix/core",
|
||||
"tools/echo-server",
|
||||
"tools/internal/contract-state-importer/importer-cli",
|
||||
"tools/internal/contract-state-importer/importer-contract",
|
||||
"tools/internal/localnet-orchestrator",
|
||||
"tools/internal/localnet-orchestrator/dkg-bypass-contract",
|
||||
"tools/internal/mixnet-connectivity-check",
|
||||
# "tools/internal/sdk-version-bump",
|
||||
"tools/internal/ssl-inject",
|
||||
"tools/internal/localnet-orchestrator",
|
||||
"tools/internal/localnet-orchestrator/dkg-bypass-contract",
|
||||
"tools/internal/validator-status-check",
|
||||
"tools/nym-cli",
|
||||
"tools/nym-id-cli",
|
||||
@@ -172,27 +177,23 @@ members = [
|
||||
"wasm/mix-fetch",
|
||||
"wasm/node-tester",
|
||||
"wasm/zknym-lib",
|
||||
"nym-gateway-probe",
|
||||
"integration-tests",
|
||||
"common/nym-kkt-ciphersuite",
|
||||
"common/nym-kkt-context",
|
||||
]
|
||||
|
||||
default-members = [
|
||||
"clients/native",
|
||||
"clients/socks5",
|
||||
"nym-authenticator-client",
|
||||
"nym-api",
|
||||
"nym-authenticator-client",
|
||||
"nym-credential-proxy/nym-credential-proxy",
|
||||
"nym-node",
|
||||
"nym-registration-client",
|
||||
"nym-statistics-api",
|
||||
"nym-validator-rewarder",
|
||||
"nyx-chain-watcher",
|
||||
"service-providers/ip-packet-router",
|
||||
"service-providers/network-requester",
|
||||
"tools/internal/localnet-orchestrator",
|
||||
"tools/nymvisor",
|
||||
"nym-registration-client",
|
||||
"tools/internal/localnet-orchestrator"
|
||||
]
|
||||
|
||||
exclude = ["contracts", "nym-wallet", "cpu-cycles"]
|
||||
@@ -472,6 +473,7 @@ nym-noise-keys = { version = "1.21.0", path = "common/nymnoise/keys" }
|
||||
nym-nonexhaustive-delayqueue = { version = "1.21.0", path = "common/nonexhaustive-delayqueue" }
|
||||
nym-node-requests = { version = "1.21.0", path = "nym-node/nym-node-requests", default-features = false }
|
||||
nym-node-metrics = { version = "1.21.0", path = "nym-node/nym-node-metrics" }
|
||||
nym-node-families-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/node-families-contract" }
|
||||
nym-ordered-buffer = { version = "1.21.0", path = "common/socks5/ordered-buffer" }
|
||||
nym-outfox = { version = "1.21.0", path = "nym-outfox" }
|
||||
nym-registration-common = { version = "1.21.0", path = "common/registration" }
|
||||
@@ -617,3 +619,4 @@ exit = "deny"
|
||||
panic = "deny"
|
||||
unimplemented = "deny"
|
||||
unreachable = "deny"
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ nym-ecash-contract-common = { workspace = true }
|
||||
nym-multisig-contract-common = { workspace = true }
|
||||
nym-group-contract-common = { workspace = true }
|
||||
nym-performance-contract-common = { workspace = true }
|
||||
nym-node-families-contract-common = { workspace = true }
|
||||
nym-serde-helpers = { workspace = true, features = ["hex", "base64"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -15,6 +15,7 @@ use nym_api_requests::ecash::models::{
|
||||
VerifyEcashTicketBody,
|
||||
};
|
||||
use nym_api_requests::ecash::VerificationKeyResponse;
|
||||
use nym_api_requests::models::node_families::NodeFamily;
|
||||
use nym_api_requests::models::{
|
||||
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
|
||||
ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
|
||||
@@ -389,6 +390,45 @@ pub trait NymApiClientExt: ApiClient {
|
||||
Ok(bonds)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn get_node_families(
|
||||
&self,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
) -> Result<PaginatedResponse<NodeFamily>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
if let Some(page) = page {
|
||||
params.push(("page", page.to_string()))
|
||||
}
|
||||
if let Some(per_page) = per_page {
|
||||
params.push(("per_page", per_page.to_string()))
|
||||
}
|
||||
self.get_json(
|
||||
&[routes::V1_API_VERSION, routes::NODE_FAMILIES_ROUTES],
|
||||
¶ms,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_node_families(&self) -> Result<Vec<NodeFamily>, NymAPIError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut families = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut res = self.get_node_families(Some(page), None).await?;
|
||||
|
||||
families.append(&mut res.data);
|
||||
if families.len() < res.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(families)
|
||||
}
|
||||
|
||||
#[deprecated]
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNodeV1>, NymAPIError> {
|
||||
|
||||
@@ -38,6 +38,7 @@ pub mod ecash {
|
||||
}
|
||||
|
||||
pub const NYM_NODES_ROUTES: &str = "nym-nodes";
|
||||
pub const NODE_FAMILIES_ROUTES: &str = "node-families";
|
||||
|
||||
pub use nym_nodes::*;
|
||||
pub mod nym_nodes {
|
||||
|
||||
@@ -13,6 +13,7 @@ pub mod ecash_query_client;
|
||||
pub mod group_query_client;
|
||||
pub mod mixnet_query_client;
|
||||
pub mod multisig_query_client;
|
||||
pub mod node_families_query_client;
|
||||
pub mod performance_query_client;
|
||||
pub mod vesting_query_client;
|
||||
|
||||
@@ -22,6 +23,7 @@ pub mod ecash_signing_client;
|
||||
pub mod group_signing_client;
|
||||
pub mod mixnet_signing_client;
|
||||
pub mod multisig_signing_client;
|
||||
pub mod node_families_signing_client;
|
||||
pub mod performance_signing_client;
|
||||
pub mod vesting_signing_client;
|
||||
|
||||
@@ -31,6 +33,7 @@ pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient};
|
||||
pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient};
|
||||
pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient};
|
||||
pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient};
|
||||
pub use node_families_query_client::{NodeFamiliesQueryClient, PagedNodeFamiliesQueryClient};
|
||||
pub use performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient};
|
||||
pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient};
|
||||
|
||||
@@ -40,6 +43,7 @@ pub use ecash_signing_client::EcashSigningClient;
|
||||
pub use group_signing_client::GroupSigningClient;
|
||||
pub use mixnet_signing_client::MixnetSigningClient;
|
||||
pub use multisig_signing_client::MultisigSigningClient;
|
||||
pub use node_families_signing_client::NodeFamiliesSigningClient;
|
||||
pub use performance_signing_client::PerformanceSigningClient;
|
||||
pub use vesting_signing_client::VestingSigningClient;
|
||||
|
||||
@@ -49,6 +53,7 @@ pub trait NymContractsProvider {
|
||||
fn mixnet_contract_address(&self) -> Option<&AccountId>;
|
||||
fn vesting_contract_address(&self) -> Option<&AccountId>;
|
||||
fn performance_contract_address(&self) -> Option<&AccountId>;
|
||||
fn node_families_contract_address(&self) -> Option<&AccountId>;
|
||||
|
||||
// coconut-related
|
||||
fn ecash_contract_address(&self) -> Option<&AccountId>;
|
||||
@@ -62,6 +67,7 @@ pub struct TypedNymContracts {
|
||||
pub mixnet_contract_address: Option<AccountId>,
|
||||
pub vesting_contract_address: Option<AccountId>,
|
||||
pub performance_contract_address: Option<AccountId>,
|
||||
pub node_families_contract_address: Option<AccountId>,
|
||||
|
||||
pub ecash_contract_address: Option<AccountId>,
|
||||
pub group_contract_address: Option<AccountId>,
|
||||
@@ -86,6 +92,10 @@ impl TryFrom<NymContracts> for TypedNymContracts {
|
||||
.performance_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
.transpose()?,
|
||||
node_families_contract_address: value
|
||||
.node_families_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
.transpose()?,
|
||||
ecash_contract_address: value
|
||||
.ecash_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
|
||||
+441
@@ -0,0 +1,441 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::collect_paged;
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::CosmWasmClient;
|
||||
use async_trait::async_trait;
|
||||
use cosmrs::AccountId;
|
||||
use serde::Deserialize;
|
||||
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
pub use nym_node_families_contract_common::{
|
||||
msg::QueryMsg as NodeFamiliesQueryMsg, AllFamilyMembersPagedResponse,
|
||||
AllPastFamilyInvitationsPagedResponse, FamiliesPagedResponse, FamilyMemberRecord,
|
||||
FamilyMembersPagedResponse, GlobalPastFamilyInvitationCursor, NodeFamily,
|
||||
NodeFamilyByNameResponse, NodeFamilyByOwnerResponse, NodeFamilyId,
|
||||
NodeFamilyMembershipResponse, NodeFamilyResponse, PastFamilyInvitation,
|
||||
PastFamilyInvitationCursor, PastFamilyInvitationForNodeCursor,
|
||||
PastFamilyInvitationsForNodePagedResponse, PastFamilyInvitationsPagedResponse,
|
||||
PastFamilyMember, PastFamilyMemberCursor, PastFamilyMemberForNodeCursor,
|
||||
PastFamilyMembersForNodePagedResponse, PastFamilyMembersPagedResponse,
|
||||
PendingFamilyInvitationDetails, PendingFamilyInvitationResponse,
|
||||
PendingFamilyInvitationsPagedResponse, PendingInvitationsForNodePagedResponse,
|
||||
PendingInvitationsPagedResponse,
|
||||
};
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait NodeFamiliesQueryClient {
|
||||
async fn query_node_families_contract<T>(
|
||||
&self,
|
||||
query: NodeFamiliesQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>;
|
||||
|
||||
async fn get_family_by_id(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<NodeFamilyResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyById { family_id })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_family_by_owner(
|
||||
&self,
|
||||
owner: &AccountId,
|
||||
) -> Result<NodeFamilyByOwnerResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyByOwner {
|
||||
owner: owner.to_string(),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_family_by_name(
|
||||
&self,
|
||||
name: String,
|
||||
) -> Result<NodeFamilyByNameResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyByName { name })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_families_paged(
|
||||
&self,
|
||||
start_after: Option<NodeFamilyId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<FamiliesPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamiliesPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_family_membership(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<NodeFamilyMembershipResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyMembership { node_id })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_family_members_paged(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<FamilyMembersPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyMembersPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_family_members_paged(
|
||||
&self,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<AllFamilyMembersPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetAllFamilyMembersPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_pending_invitation(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
) -> Result<PendingFamilyInvitationResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPendingInvitation {
|
||||
family_id,
|
||||
node_id,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_pending_invitations_for_family_paged(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PendingFamilyInvitationsPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(
|
||||
NodeFamiliesQueryMsg::GetPendingInvitationsForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_pending_invitations_for_node_paged(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
start_after: Option<NodeFamilyId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PendingInvitationsForNodePagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPendingInvitationsForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_pending_invitations_paged(
|
||||
&self,
|
||||
start_after: Option<(NodeFamilyId, NodeId)>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PendingInvitationsPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetAllPendingInvitationsPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_past_invitations_for_family_paged(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<PastFamilyInvitationCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PastFamilyInvitationsPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastInvitationsForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_past_invitations_for_node_paged(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
start_after: Option<PastFamilyInvitationForNodeCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PastFamilyInvitationsForNodePagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastInvitationsForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_past_invitations_paged(
|
||||
&self,
|
||||
start_after: Option<GlobalPastFamilyInvitationCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<AllPastFamilyInvitationsPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetAllPastInvitationsPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_past_members_for_family_paged(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<PastFamilyMemberCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PastFamilyMembersPagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastMembersForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_past_members_for_node_paged(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
start_after: Option<PastFamilyMemberForNodeCursor>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PastFamilyMembersForNodePagedResponse, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastMembersForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// extension trait to the query client to deal with the paged queries
|
||||
// (it didn't feel appropriate to combine it with the existing trait)
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait PagedNodeFamiliesQueryClient: NodeFamiliesQueryClient {
|
||||
async fn get_all_families(&self) -> Result<Vec<NodeFamily>, NyxdError> {
|
||||
collect_paged!(self, get_families_paged, families)
|
||||
}
|
||||
|
||||
async fn get_all_family_members_for_family(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<Vec<FamilyMemberRecord>, NyxdError> {
|
||||
collect_paged!(self, get_family_members_paged, members, family_id)
|
||||
}
|
||||
|
||||
async fn get_all_family_members(&self) -> Result<Vec<FamilyMemberRecord>, NyxdError> {
|
||||
collect_paged!(self, get_all_family_members_paged, members)
|
||||
}
|
||||
|
||||
async fn get_all_pending_invitations_for_family(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<Vec<PendingFamilyInvitationDetails>, NyxdError> {
|
||||
collect_paged!(
|
||||
self,
|
||||
get_pending_invitations_for_family_paged,
|
||||
invitations,
|
||||
family_id
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_all_pending_invitations_for_node(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<Vec<PendingFamilyInvitationDetails>, NyxdError> {
|
||||
collect_paged!(
|
||||
self,
|
||||
get_pending_invitations_for_node_paged,
|
||||
invitations,
|
||||
node_id
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_all_pending_invitations(
|
||||
&self,
|
||||
) -> Result<Vec<PendingFamilyInvitationDetails>, NyxdError> {
|
||||
collect_paged!(self, get_all_pending_invitations_paged, invitations)
|
||||
}
|
||||
|
||||
async fn get_all_past_invitations_for_family(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<Vec<PastFamilyInvitation>, NyxdError> {
|
||||
collect_paged!(
|
||||
self,
|
||||
get_past_invitations_for_family_paged,
|
||||
invitations,
|
||||
family_id
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_all_past_invitations_for_node(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<Vec<PastFamilyInvitation>, NyxdError> {
|
||||
collect_paged!(
|
||||
self,
|
||||
get_past_invitations_for_node_paged,
|
||||
invitations,
|
||||
node_id
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_all_past_invitations(&self) -> Result<Vec<PastFamilyInvitation>, NyxdError> {
|
||||
collect_paged!(self, get_all_past_invitations_paged, invitations)
|
||||
}
|
||||
|
||||
async fn get_all_past_members_for_family(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
) -> Result<Vec<PastFamilyMember>, NyxdError> {
|
||||
collect_paged!(self, get_past_members_for_family_paged, members, family_id)
|
||||
}
|
||||
|
||||
async fn get_all_past_members_for_node(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<Vec<PastFamilyMember>, NyxdError> {
|
||||
collect_paged!(self, get_past_members_for_node_paged, members, node_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> PagedNodeFamiliesQueryClient for T where T: NodeFamiliesQueryClient {}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> NodeFamiliesQueryClient for C
|
||||
where
|
||||
C: CosmWasmClient + NymContractsProvider + Send + Sync,
|
||||
{
|
||||
async fn query_node_families_contract<T>(
|
||||
&self,
|
||||
query: NodeFamiliesQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
let node_families_contract_address = &self
|
||||
.node_families_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("node families contract"))?;
|
||||
self.query_contract_smart(node_families_contract_address, &query)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
use nym_node_families_contract_common::QueryMsg;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_query_variants_are_covered<C: NodeFamiliesQueryClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: NodeFamiliesQueryMsg,
|
||||
) {
|
||||
match msg {
|
||||
NodeFamiliesQueryMsg::GetFamilyById { family_id } => {
|
||||
client.get_family_by_id(family_id).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamilyByOwner { owner } => {
|
||||
client.get_family_by_owner(&owner.parse().unwrap()).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamilyByName { name } => {
|
||||
client.get_family_by_name(name).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamiliesPaged { start_after, limit } => {
|
||||
client.get_families_paged(start_after, limit).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamilyMembership { node_id } => {
|
||||
client.get_family_membership(node_id).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetFamilyMembersPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_family_members_paged(family_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetAllFamilyMembersPaged { start_after, limit } => client
|
||||
.get_all_family_members_paged(start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPendingInvitation { family_id, node_id } => {
|
||||
client.get_pending_invitation(family_id, node_id).ignore()
|
||||
}
|
||||
NodeFamiliesQueryMsg::GetPendingInvitationsForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_pending_invitations_for_family_paged(family_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPendingInvitationsForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_pending_invitations_for_node_paged(node_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetAllPendingInvitationsPaged { start_after, limit } => client
|
||||
.get_all_pending_invitations_paged(start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPastInvitationsForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_past_invitations_for_family_paged(family_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPastInvitationsForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_past_invitations_for_node_paged(node_id, start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetAllPastInvitationsPaged { start_after, limit } => client
|
||||
.get_all_past_invitations_paged(start_after, limit)
|
||||
.ignore(),
|
||||
NodeFamiliesQueryMsg::GetPastMembersForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_past_members_for_family_paged(family_id, start_after, limit)
|
||||
.ignore(),
|
||||
QueryMsg::GetPastMembersForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_past_members_for_node_paged(node_id, start_after, limit)
|
||||
.ignore(),
|
||||
};
|
||||
}
|
||||
}
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::nyxd::coin::Coin;
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::{Fee, SigningCosmWasmClient};
|
||||
use crate::signing::signer::OfflineSigner;
|
||||
use async_trait::async_trait;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_node_families_contract_common::{
|
||||
Config, ExecuteMsg as NodeFamiliesExecuteMsg, NodeFamilyId,
|
||||
};
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait NodeFamiliesSigningClient {
|
||||
async fn execute_node_families_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: NodeFamiliesExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError>;
|
||||
|
||||
async fn update_node_families_config(
|
||||
&self,
|
||||
config: Config,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::UpdateConfig { config },
|
||||
"NodeFamiliesContract::UpdateConfig".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn create_family(
|
||||
&self,
|
||||
name: String,
|
||||
description: String,
|
||||
fee: Option<Fee>,
|
||||
creation_fee: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::CreateFamily { name, description },
|
||||
"NodeFamiliesContract::CreateFamily".to_string(),
|
||||
creation_fee,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn disband_family(&self, fee: Option<Fee>) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::DisbandFamily {},
|
||||
"NodeFamiliesContract::DisbandFamily".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn invite_to_family(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
validity_secs: Option<u64>,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::InviteToFamily {
|
||||
node_id,
|
||||
validity_secs,
|
||||
},
|
||||
"NodeFamiliesContract::InviteToFamily".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn revoke_family_invitation(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::RevokeFamilyInvitation { node_id },
|
||||
"NodeFamiliesContract::RevokeFamilyInvitation".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn accept_family_invitation(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::AcceptFamilyInvitation { family_id, node_id },
|
||||
"NodeFamiliesContract::AcceptFamilyInvitation".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn reject_family_invitation(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::RejectFamilyInvitation { family_id, node_id },
|
||||
"NodeFamiliesContract::RejectFamilyInvitation".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn leave_family(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::LeaveFamily { node_id },
|
||||
"NodeFamiliesContract::LeaveFamily".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn kick_from_family(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::KickFromFamily { node_id },
|
||||
"NodeFamiliesContract::KickFromFamily".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Cross-contract callback fired by the mixnet contract on node unbonding.
|
||||
/// Exposed for completeness; the families contract rejects this call from
|
||||
/// any sender other than the configured mixnet contract address.
|
||||
async fn on_nym_node_unbond(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::OnNymNodeUnbond { node_id },
|
||||
"NodeFamiliesContract::OnNymNodeUnbond".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> NodeFamiliesSigningClient for C
|
||||
where
|
||||
C: SigningCosmWasmClient + NymContractsProvider + Sync,
|
||||
NyxdError: From<<Self as OfflineSigner>::Error>,
|
||||
{
|
||||
async fn execute_node_families_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: NodeFamiliesExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let node_families_contract_address = &self
|
||||
.node_families_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("node families contract"))?;
|
||||
|
||||
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
|
||||
|
||||
let signer_address = &self.signer_addresses()[0];
|
||||
self.execute(
|
||||
signer_address,
|
||||
node_families_contract_address,
|
||||
&msg,
|
||||
fee,
|
||||
memo,
|
||||
funds,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
use nym_node_families_contract_common::ExecuteMsg;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_execute_variants_are_covered<C: NodeFamiliesSigningClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: NodeFamiliesExecuteMsg,
|
||||
) {
|
||||
match msg {
|
||||
NodeFamiliesExecuteMsg::UpdateConfig { config } => {
|
||||
client.update_node_families_config(config, None).ignore()
|
||||
}
|
||||
NodeFamiliesExecuteMsg::CreateFamily { name, description } => client
|
||||
.create_family(name, description, None, vec![])
|
||||
.ignore(),
|
||||
NodeFamiliesExecuteMsg::DisbandFamily {} => client.disband_family(None).ignore(),
|
||||
NodeFamiliesExecuteMsg::InviteToFamily {
|
||||
node_id,
|
||||
validity_secs,
|
||||
} => client
|
||||
.invite_to_family(node_id, validity_secs, None)
|
||||
.ignore(),
|
||||
NodeFamiliesExecuteMsg::RevokeFamilyInvitation { node_id } => {
|
||||
client.revoke_family_invitation(node_id, None).ignore()
|
||||
}
|
||||
NodeFamiliesExecuteMsg::AcceptFamilyInvitation { family_id, node_id } => client
|
||||
.accept_family_invitation(family_id, node_id, None)
|
||||
.ignore(),
|
||||
NodeFamiliesExecuteMsg::RejectFamilyInvitation { family_id, node_id } => client
|
||||
.reject_family_invitation(family_id, node_id, None)
|
||||
.ignore(),
|
||||
NodeFamiliesExecuteMsg::LeaveFamily { node_id } => {
|
||||
client.leave_family(node_id, None).ignore()
|
||||
}
|
||||
NodeFamiliesExecuteMsg::KickFromFamily { node_id } => {
|
||||
client.kick_from_family(node_id, None).ignore()
|
||||
}
|
||||
ExecuteMsg::OnNymNodeUnbond { node_id } => {
|
||||
client.on_nym_node_unbond(node_id, None).ignore()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -286,6 +286,10 @@ impl<C, S> NyxdClient<C, S> {
|
||||
self.config.contracts.multisig_contract_address = Some(address);
|
||||
}
|
||||
|
||||
pub fn set_node_families_contract_address(&mut self, address: AccountId) {
|
||||
self.config.contracts.node_families_contract_address = Some(address);
|
||||
}
|
||||
|
||||
pub fn set_simulated_gas_multiplier(&mut self, multiplier: f32) {
|
||||
self.config.simulated_gas_multiplier = multiplier;
|
||||
}
|
||||
@@ -304,6 +308,13 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
|
||||
self.config.contracts.performance_contract_address.as_ref()
|
||||
}
|
||||
|
||||
fn node_families_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config
|
||||
.contracts
|
||||
.node_families_contract_address
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
fn ecash_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config.contracts.ecash_contract_address.as_ref()
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ pub struct Args {
|
||||
#[clap(long)]
|
||||
pub vesting_contract_address: Option<AccountId>,
|
||||
|
||||
#[clap(long)]
|
||||
pub node_families_contract_address: Option<AccountId>,
|
||||
|
||||
#[clap(long)]
|
||||
pub rewarding_denom: Option<String>,
|
||||
|
||||
@@ -130,6 +133,14 @@ pub async fn generate(args: Args) {
|
||||
.expect("Failed converting vesting contract address to AccountId")
|
||||
});
|
||||
|
||||
let node_families_contract_address = args.node_families_contract_address.unwrap_or_else(|| {
|
||||
let address =
|
||||
std::env::var(nym_network_defaults::var_names::NODE_FAMILIES_CONTRACT_ADDRESS)
|
||||
.expect("node families contract address has to be set");
|
||||
AccountId::from_str(address.as_str())
|
||||
.expect("Failed converting node families contract address to AccountId")
|
||||
});
|
||||
|
||||
let rewarding_denom = args.rewarding_denom.unwrap_or_else(|| {
|
||||
std::env::var(nym_network_defaults::var_names::MIX_DENOM)
|
||||
.expect("Rewarding (mix) denom has to be set")
|
||||
@@ -142,6 +153,7 @@ pub async fn generate(args: Args) {
|
||||
let instantiate_msg = InstantiateMsg {
|
||||
rewarding_validator_address: rewarding_validator_address.to_string(),
|
||||
vesting_contract_address: vesting_contract_address.to_string(),
|
||||
node_families_contract_address: node_families_contract_address.to_string(),
|
||||
rewarding_denom,
|
||||
epochs_in_interval: args.epochs_in_interval,
|
||||
epoch_duration: Duration::from_secs(args.epoch_duration),
|
||||
|
||||
@@ -26,6 +26,14 @@ pub trait ContractOpts {
|
||||
|
||||
fn addr_make(&self, input: &str) -> Addr;
|
||||
|
||||
fn make_sender_with_funds(&self, input: &str, funds: &[Coin]) -> MessageInfo {
|
||||
message_info(&self.addr_make(input), funds)
|
||||
}
|
||||
|
||||
fn make_sender(&self, input: &str) -> MessageInfo {
|
||||
self.make_sender_with_funds(input, &[])
|
||||
}
|
||||
|
||||
fn deps_mut_env(&mut self) -> (DepsMut<'_>, Env) {
|
||||
let env = self.env().clone();
|
||||
(self.deps_mut(), env)
|
||||
|
||||
@@ -3,12 +3,121 @@
|
||||
|
||||
use crate::error::MixnetContractError;
|
||||
use crate::mixnode::PendingMixNodeChanges;
|
||||
use crate::nym_node::NodeOwnershipResponse;
|
||||
use crate::{
|
||||
EpochEventId, IntervalEventId, MixNodeBond, MixNodeDetails, NodeId, NodeRewarding, NymNodeBond,
|
||||
NymNodeDetails, PendingNodeChanges,
|
||||
EpochEventId, EpochId, Interval, IntervalEventId, MixNodeBond, MixNodeDetails, NodeId,
|
||||
NodeRewarding, NymNodeBond, NymNodeDetails, PendingNodeChanges, QueryMsg,
|
||||
};
|
||||
use cosmwasm_std::{Coin, Decimal, StdError, StdResult, Uint128};
|
||||
use cosmwasm_std::{
|
||||
Addr, Binary, Coin, CustomQuery, Decimal, QuerierWrapper, StdError, StdResult, Uint128,
|
||||
from_json,
|
||||
};
|
||||
use cw_storage_plus::{Key, Namespace, Path, PrimaryKey};
|
||||
use nym_contracts_common::IdentityKeyRef;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::ops::Deref;
|
||||
|
||||
pub trait MixnetContractQuerier {
|
||||
#[allow(dead_code)]
|
||||
fn query_mixnet_contract<T: DeserializeOwned>(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
msg: &QueryMsg,
|
||||
) -> StdResult<T>;
|
||||
|
||||
fn query_mixnet_contract_storage(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl Into<Binary>,
|
||||
) -> StdResult<Option<Vec<u8>>>;
|
||||
|
||||
fn query_mixnet_contract_storage_value<T: DeserializeOwned>(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl Into<Binary>,
|
||||
) -> StdResult<Option<T>> {
|
||||
match self.query_mixnet_contract_storage(address, key)? {
|
||||
None => Ok(None),
|
||||
Some(value) => Ok(Some(from_json(&value)?)),
|
||||
}
|
||||
}
|
||||
|
||||
fn query_current_mixnet_interval(&self, address: impl Into<String>) -> StdResult<Interval> {
|
||||
self.query_mixnet_contract_storage_value(address, b"ci")?
|
||||
.ok_or(StdError::not_found(
|
||||
"unable to retrieve interval information from the mixnet contract storage",
|
||||
))
|
||||
}
|
||||
|
||||
fn query_current_absolute_mixnet_epoch_id(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
) -> StdResult<EpochId> {
|
||||
self.query_current_mixnet_interval(address)
|
||||
.map(|interval| interval.current_epoch_absolute_id())
|
||||
}
|
||||
|
||||
fn check_node_existence(&self, address: impl Into<String>, node_id: NodeId) -> StdResult<bool> {
|
||||
let mixnet_contract_address = address.into();
|
||||
|
||||
if let Some(nym_node) = self.query_nymnode_bond(mixnet_contract_address.clone(), node_id)? {
|
||||
return Ok(!nym_node.is_unbonding);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn query_nymnode_bond(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
node_id: NodeId,
|
||||
) -> StdResult<Option<NymNodeBond>> {
|
||||
// construct proper map key
|
||||
let pk_namespace = "nn";
|
||||
let path: Path<NymNodeBond> = Path::new(
|
||||
Namespace::from_static_str(pk_namespace).as_slice(),
|
||||
&node_id.key().iter().map(Key::as_ref).collect::<Vec<_>>(),
|
||||
);
|
||||
let storage_key = path.deref();
|
||||
|
||||
self.query_mixnet_contract_storage_value(address, storage_key)
|
||||
}
|
||||
|
||||
fn query_nymnode_ownership(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
owner: &Addr,
|
||||
) -> StdResult<Option<NymNodeBond>> {
|
||||
let resp: NodeOwnershipResponse = self.query_mixnet_contract(
|
||||
address,
|
||||
&QueryMsg::GetOwnedNymNode {
|
||||
address: owner.to_string(),
|
||||
},
|
||||
)?;
|
||||
Ok(resp.details.map(|d| d.bond_information))
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> MixnetContractQuerier for QuerierWrapper<'_, C>
|
||||
where
|
||||
C: CustomQuery,
|
||||
{
|
||||
fn query_mixnet_contract<T: DeserializeOwned>(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
msg: &QueryMsg,
|
||||
) -> StdResult<T> {
|
||||
self.query_wasm_smart(address, msg)
|
||||
}
|
||||
|
||||
fn query_mixnet_contract_storage(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl Into<Binary>,
|
||||
) -> StdResult<Option<Vec<u8>>> {
|
||||
self.query_wasm_raw(address, key)
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option<Decimal>) {
|
||||
|
||||
@@ -30,6 +30,7 @@ pub use gateway::{
|
||||
Gateway, GatewayBond, GatewayBondResponse, GatewayConfigUpdate, GatewayOwnershipResponse,
|
||||
PagedGatewayResponse,
|
||||
};
|
||||
pub use helpers::MixnetContractQuerier;
|
||||
pub use interval::{
|
||||
CurrentIntervalResponse, EpochId, EpochState, EpochStatus, Interval, IntervalId,
|
||||
};
|
||||
|
||||
@@ -190,6 +190,10 @@ impl NodeRewarding {
|
||||
truncate_reward(self.operator, denom)
|
||||
}
|
||||
|
||||
pub fn delegations_with_reward(&self, denom: impl Into<String>) -> Coin {
|
||||
truncate_reward(self.delegates, denom)
|
||||
}
|
||||
|
||||
pub fn pending_delegator_reward(&self, delegation: &Delegation) -> StdResult<Coin> {
|
||||
let delegator_reward = self.determine_delegation_reward(delegation)?;
|
||||
Ok(truncate_reward(delegator_reward, &delegation.amount.denom))
|
||||
|
||||
@@ -63,6 +63,7 @@ use nym_contracts_common::{ContractBuildInformation, signing::Nonce};
|
||||
pub struct InstantiateMsg {
|
||||
pub rewarding_validator_address: String,
|
||||
pub vesting_contract_address: String,
|
||||
pub node_families_contract_address: String,
|
||||
|
||||
pub rewarding_denom: String,
|
||||
pub epochs_in_interval: u32,
|
||||
@@ -885,4 +886,5 @@ pub enum QueryMsg {
|
||||
pub struct MigrateMsg {
|
||||
pub unsafe_skip_state_updates: Option<bool>,
|
||||
pub vesting_contract_address: Option<String>,
|
||||
pub node_families_contract_address: String,
|
||||
}
|
||||
|
||||
@@ -212,6 +212,10 @@ pub struct ContractState {
|
||||
/// track-related messages.
|
||||
pub vesting_contract_address: Addr,
|
||||
|
||||
/// Address of the node families contract. It is called whenever nym-node unbonds
|
||||
/// so that it could be removed from any family it belongs to.
|
||||
pub node_families_contract_address: Addr,
|
||||
|
||||
/// The expected denom used for rewarding (and realistically any other operation).
|
||||
/// Default: `unym`
|
||||
pub rewarding_denom: String,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "nym-node-families-contract-common"
|
||||
description = "Common crate for Nym's node families contract"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version = "1.85"
|
||||
readme.workspace = true
|
||||
publish = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
|
||||
cosmwasm-std = { workspace = true }
|
||||
cosmwasm-schema = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
cw-utils = { workspace = true }
|
||||
|
||||
nym-contracts-common = { workspace = true }
|
||||
nym-mixnet-contract-common = { workspace = true }
|
||||
|
||||
[features]
|
||||
schema = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
/// Storage key constants used by the node families contract.
|
||||
///
|
||||
/// They are kept in the common crate so that off-chain tooling (indexers, migration
|
||||
/// scripts) can reference them without depending on the contract crate itself.
|
||||
/// Changing any of these values is a breaking change for already-deployed contracts.
|
||||
pub mod storage_keys {
|
||||
/// `Item<Addr>`: address of the mixnet contract used to validate node existence.
|
||||
pub const MIXNET_CONTRACT_ADDRESS: &str = "mixnet-contract-address";
|
||||
|
||||
/// `Item<Config>`: runtime configuration (fees, length limits) set at instantiation.
|
||||
pub const CONFIG: &str = "config";
|
||||
|
||||
/// `Admin` (cw-controllers): admin allowed to perform privileged operations.
|
||||
pub const CONTRACT_ADMIN: &str = "contract-admin";
|
||||
/// `Item<NodeFamilyId>`: monotonically increasing id counter for new families.
|
||||
pub const NODE_FAMILY_ID_COUNTER: &str = "node-family-id-counter";
|
||||
/// Primary namespace for the current family-members `IndexedMap`,
|
||||
/// keyed by `NodeId` with value [`crate::FamilyMembership`].
|
||||
pub const NODE_FAMILY_MEMBERS: &str = "node-family-members";
|
||||
/// Multi-index over current family members keyed by family id —
|
||||
/// enables paginated listing of all nodes in a given family.
|
||||
pub const NODE_FAMILY_MEMBERS_FAMILY_IDX_NAMESPACE: &str = "node-family-members__family";
|
||||
|
||||
/// Primary namespace for the families `IndexedMap`.
|
||||
pub const FAMILIES_NAMESPACE: &str = "families";
|
||||
/// Secondary unique index keyed by `owner` (one family per owner).
|
||||
pub const FAMILIES_OWNER_IDX_NAMESPACE: &str = "families__owner";
|
||||
/// Secondary unique index keyed by `name` (family names are globally unique).
|
||||
pub const FAMILIES_NAME_IDX_NAMESPACE: &str = "families__name";
|
||||
|
||||
/// Primary namespace for the pending invitations `IndexedMap`.
|
||||
pub const INVITATIONS_NAMESPACE: &str = "invitations";
|
||||
/// Multi-index over pending invitations keyed by family id.
|
||||
pub const INVITATIONS_FAMILY_IDX_NAMESPACE: &str = "invitations__family";
|
||||
/// Multi-index over pending invitations keyed by node id
|
||||
/// (a node can be invited to multiple families simultaneously).
|
||||
pub const INVITATIONS_NODE_IDX_NAMESPACE: &str = "invitations__node";
|
||||
|
||||
/// Primary namespace for the archived (accepted/rejected/revoked) invitations `IndexedMap`.
|
||||
pub const PAST_INVITATIONS_NAMESPACE: &str = "past-invitations";
|
||||
/// Multi-index over past invitations keyed by family id.
|
||||
pub const PAST_INVITATIONS_FAMILY_IDX_NAMESPACE: &str = "past-invitations__family";
|
||||
/// Multi-index over past invitations keyed by node id.
|
||||
pub const PAST_INVITATIONS_NODE_IDX_NAMESPACE: &str = "past-invitations__node";
|
||||
/// `Map<(NodeFamilyId, NodeId), u64>`: per-`(family, node)` counter used to
|
||||
/// disambiguate repeat archive entries (a node can be invited and have the
|
||||
/// invitation reach a terminal state more than once).
|
||||
pub const PAST_INVITATIONS_COUNTER_NAMESPACE: &str = "past-invitations-counter";
|
||||
|
||||
/// Primary namespace for the past-members `IndexedMap`.
|
||||
pub const PAST_FAMILY_MEMBER_NAMESPACE: &str = "past-family-member";
|
||||
/// Multi-index over past members keyed by family id.
|
||||
pub const PAST_FAMILY_MEMBER_FAMILY_IDX_NAMESPACE: &str = "past-family-member__family";
|
||||
/// Multi-index over past members keyed by node id.
|
||||
pub const PAST_FAMILY_MEMBER_NODE_IDX_NAMESPACE: &str = "past-family-member__node";
|
||||
/// `Map<(NodeFamilyId, NodeId), u64>`: per-`(family, node)` counter used to
|
||||
/// disambiguate repeat past-membership entries (a node can join and leave
|
||||
/// the same family more than once).
|
||||
pub const PAST_FAMILY_MEMBER_COUNTER_NAMESPACE: &str = "past-family-member-counter";
|
||||
}
|
||||
|
||||
pub mod events {
|
||||
pub const FAMILY_CREATION_EVENT_NAME: &str = "family_creation";
|
||||
pub const FAMILY_CREATION_EVENT_FAMILY_NAME: &str = "family_name";
|
||||
pub const FAMILY_CREATION_EVENT_OWNER_ADDRESS: &str = "owner_address";
|
||||
pub const FAMILY_CREATION_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_CREATION_EVENT_PAID_FEE: &str = "paid_fee";
|
||||
|
||||
pub const FAMILY_DISBAND_EVENT_NAME: &str = "family_disband";
|
||||
pub const FAMILY_DISBAND_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_DISBAND_EVENT_OWNER_ADDRESS: &str = "owner_address";
|
||||
pub const FAMILY_DISBAND_EVENT_REFUNDED_FEE: &str = "refunded_fee";
|
||||
|
||||
pub const FAMILY_INVITATION_EVENT_NAME: &str = "family_invitation";
|
||||
pub const FAMILY_INVITATION_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_INVITATION_EVENT_NODE_ID: &str = "node_id";
|
||||
pub const FAMILY_INVITATION_EVENT_EXPIRES_AT: &str = "expires_at";
|
||||
|
||||
pub const FAMILY_INVITATION_REVOKED_EVENT_NAME: &str = "family_invitation_revoked";
|
||||
pub const FAMILY_INVITATION_REVOKED_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_INVITATION_REVOKED_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const FAMILY_INVITATION_ACCEPTED_EVENT_NAME: &str = "family_invitation_accepted";
|
||||
pub const FAMILY_INVITATION_ACCEPTED_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_INVITATION_ACCEPTED_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const FAMILY_INVITATION_REJECTED_EVENT_NAME: &str = "family_invitation_rejected";
|
||||
pub const FAMILY_INVITATION_REJECTED_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_INVITATION_REJECTED_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const FAMILY_MEMBER_LEFT_EVENT_NAME: &str = "family_member_left";
|
||||
pub const FAMILY_MEMBER_LEFT_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_MEMBER_LEFT_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const FAMILY_MEMBER_KICKED_EVENT_NAME: &str = "family_member_kicked";
|
||||
pub const FAMILY_MEMBER_KICKED_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_MEMBER_KICKED_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const NODE_UNBOND_CLEANUP_EVENT_NAME: &str = "family_node_unbond_cleanup";
|
||||
pub const NODE_UNBOND_CLEANUP_EVENT_NODE_ID: &str = "node_id";
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::NodeFamilyId;
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use cw_controllers::AdminError;
|
||||
use cw_utils::PaymentError;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors returned from any entry point of the node families contract.
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum NodeFamiliesContractError {
|
||||
/// Returned from `migrate` when the on-chain state cannot be brought forward
|
||||
/// to the current contract version (e.g. unsupported source version, malformed
|
||||
/// stored data).
|
||||
#[error("could not perform contract migration: {comment}")]
|
||||
FailedMigration { comment: String },
|
||||
|
||||
/// The referenced family does not exist (or no longer exists).
|
||||
#[error("family with id {family_id} does not exist")]
|
||||
FamilyNotFound { family_id: NodeFamilyId },
|
||||
|
||||
/// Disbanding was requested on a family that still has members.
|
||||
#[error("family {family_id} cannot be disbanded: it still has {members} member(s)")]
|
||||
FamilyNotEmpty {
|
||||
family_id: NodeFamilyId,
|
||||
members: u64,
|
||||
},
|
||||
|
||||
/// The given node is not currently a member of any family.
|
||||
#[error("node {node_id} is not currently a member of any family")]
|
||||
NodeNotInFamily { node_id: NodeId },
|
||||
|
||||
/// The given node is a member of a different family than the one the
|
||||
/// caller is acting on. Distinct from [`NodeNotInFamily`] (which means the
|
||||
/// node has no membership at all) — surfaces when, e.g., a family owner
|
||||
/// tries to kick a node that belongs to someone else's family.
|
||||
#[error("node {node_id} is not a member of family {family_id}")]
|
||||
NodeNotMemberOfFamily {
|
||||
node_id: NodeId,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// A cross-contract callback (e.g. `OnNymNodeUnbond`) was received from a
|
||||
/// sender that is not the configured mixnet contract address.
|
||||
#[error("address {sender} is not authorised to invoke the mixnet-contract callback")]
|
||||
UnauthorisedMixnetCallback { sender: Addr },
|
||||
|
||||
/// No pending invitation exists for the given `(family, node)` pair.
|
||||
#[error("no pending invitation for node {node_id} from family {family_id}")]
|
||||
InvitationNotFound {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// A pending invitation for the given `(family, node)` pair already exists;
|
||||
/// issuing a new one would silently overwrite it.
|
||||
#[error("a pending invitation for node {node_id} from family {family_id} already exists")]
|
||||
PendingInvitationAlreadyExists {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// The invitation exists but its `expires_at` is at or before the current
|
||||
/// block time, so it can no longer be acted on.
|
||||
#[error(
|
||||
"invitation for node {node_id} from family {family_id} expired at {expires_at} (now: {now})"
|
||||
)]
|
||||
InvitationExpired {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
expires_at: u64,
|
||||
now: u64,
|
||||
},
|
||||
|
||||
/// The funds attached to a paid execution failed `cw_utils` payment
|
||||
/// validation (no funds, wrong/extra denom).
|
||||
#[error("invalid fee provided: {0}")]
|
||||
InvalidDeposit(#[from] PaymentError),
|
||||
|
||||
/// The funds attached to a `CreateFamily` execution don't match the
|
||||
/// configured `create_family_fee`.
|
||||
#[error("expected exactly {expected} as family creation fee; received {received:?}")]
|
||||
InvalidFamilyCreationFee { expected: Coin, received: Vec<Coin> },
|
||||
|
||||
/// The submitted family name normalised to the empty string (i.e. it
|
||||
/// contained no ASCII alphanumeric characters).
|
||||
#[error("family name cannot be empty after normalisation")]
|
||||
EmptyFamilyName,
|
||||
|
||||
/// The submitted family name exceeds the configured length limit.
|
||||
#[error("family name length {length} exceeds the configured limit of {limit}")]
|
||||
FamilyNameTooLong { length: usize, limit: usize },
|
||||
|
||||
/// The submitted family description exceeds the configured length limit.
|
||||
#[error("family description length {length} exceeds the configured limit of {limit}")]
|
||||
FamilyDescriptionTooLong { length: usize, limit: usize },
|
||||
|
||||
/// The transaction sender already owns a family.
|
||||
#[error("address {address} already owns family {family_id}")]
|
||||
SenderAlreadyOwnsAFamily {
|
||||
address: Addr,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// The transaction sender does not currently own any family - emitted by
|
||||
/// owner-gated operations like `disband_family` when the sender has
|
||||
/// nothing to act on.
|
||||
#[error("address {address} does not currently own any family")]
|
||||
SenderDoesntOwnAFamily { address: Addr },
|
||||
|
||||
/// The transaction sender is not the controller of the bonded node
|
||||
/// referenced by the message. Covers all of: sender controls no bonded
|
||||
/// node, sender controls a different node id, and sender's node has
|
||||
/// entered the unbonding state.
|
||||
#[error("address {address} is not the controller of bonded node {node_id}")]
|
||||
SenderDoesntControlNode { address: Addr, node_id: NodeId },
|
||||
|
||||
/// A family with the requested (normalised) name already exists.
|
||||
#[error("a family with name {name:?} already exists (id {family_id})")]
|
||||
FamilyNameAlreadyTaken {
|
||||
name: String,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// A node controlled by the address is currently a member of a family,
|
||||
/// so the address cannot also become a family owner or join another family.
|
||||
#[error("address {address} controls node {node_id} which is currently in family {family_id}")]
|
||||
AlreadyInFamily {
|
||||
address: Addr,
|
||||
node_id: NodeId,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// The node referenced by an invitation does not exist as a bonded node
|
||||
/// in the mixnet contract (or has already unbonded).
|
||||
#[error("node {node_id} is not a bonded node in the mixnet contract")]
|
||||
NodeDoesntExist { node_id: NodeId },
|
||||
|
||||
/// The node referenced by an invitation is already a member of a family,
|
||||
/// so it cannot be invited to another one until it leaves / is removed.
|
||||
#[error("node {node_id} is already a member of family {family_id}")]
|
||||
NodeAlreadyInFamily {
|
||||
node_id: NodeId,
|
||||
family_id: NodeFamilyId,
|
||||
},
|
||||
|
||||
/// The sender supplied a `validity_secs` of `0` for an invitation, which
|
||||
/// would create one that is already expired at the moment it is stored.
|
||||
#[error("invitation validity must be strictly positive")]
|
||||
ZeroInvitationValidity,
|
||||
|
||||
/// Wraps errors raised by `cw-controllers::Admin` (e.g. caller is not admin).
|
||||
#[error(transparent)]
|
||||
Admin(#[from] AdminError),
|
||||
|
||||
/// Wraps any underlying `cosmwasm_std::StdError` (storage, serialization, etc.).
|
||||
#[error(transparent)]
|
||||
StdErr(#[from] cosmwasm_std::StdError),
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Common types, messages, errors and storage-key constants shared between the
|
||||
//! node families contract and any off-chain client.
|
||||
//!
|
||||
//! Keeping these in a separate crate allows clients to depend on the contract's
|
||||
//! public surface without pulling in `cw-storage-plus` and other on-chain-only
|
||||
//! dependencies.
|
||||
|
||||
/// Storage-key string constants. See [`constants::storage_keys`].
|
||||
pub mod constants;
|
||||
/// Contract-level error type.
|
||||
pub mod error;
|
||||
/// `InstantiateMsg`, `ExecuteMsg`, `QueryMsg`, `MigrateMsg` definitions.
|
||||
pub mod msg;
|
||||
/// Domain types stored in / returned by the contract.
|
||||
pub mod types;
|
||||
|
||||
pub use error::*;
|
||||
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
pub use types::*;
|
||||
@@ -0,0 +1,211 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::{
|
||||
Config, GlobalPastFamilyInvitationCursor, NodeFamilyId, PastFamilyInvitationCursor,
|
||||
PastFamilyInvitationForNodeCursor, PastFamilyMemberCursor, PastFamilyMemberForNodeCursor,
|
||||
};
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
use crate::{
|
||||
AllFamilyMembersPagedResponse, AllPastFamilyInvitationsPagedResponse, FamiliesPagedResponse,
|
||||
FamilyMembersPagedResponse, NodeFamilyByNameResponse, NodeFamilyByOwnerResponse,
|
||||
NodeFamilyMembershipResponse, NodeFamilyResponse, PastFamilyInvitationsForNodePagedResponse,
|
||||
PastFamilyInvitationsPagedResponse, PastFamilyMembersForNodePagedResponse,
|
||||
PastFamilyMembersPagedResponse, PendingFamilyInvitationResponse,
|
||||
PendingFamilyInvitationsPagedResponse, PendingInvitationsForNodePagedResponse,
|
||||
PendingInvitationsPagedResponse,
|
||||
};
|
||||
|
||||
/// Message used to instantiate the node families contract.
|
||||
#[cw_serde]
|
||||
pub struct InstantiateMsg {
|
||||
pub config: Config,
|
||||
|
||||
pub mixnet_contract_address: String,
|
||||
}
|
||||
|
||||
/// Execute messages accepted by the contract.
|
||||
#[cw_serde]
|
||||
pub enum ExecuteMsg {
|
||||
/// Replace the contract's runtime [`Config`]. Restricted to the contract
|
||||
/// admin.
|
||||
UpdateConfig { config: Config },
|
||||
|
||||
/// Create a new family owned by the message sender. The configured
|
||||
/// `create_family_fee` must be attached as funds.
|
||||
CreateFamily { name: String, description: String },
|
||||
|
||||
/// Disband the family owned by the message sender. The family must have
|
||||
/// no current members; any still-pending invitations are revoked.
|
||||
DisbandFamily {},
|
||||
|
||||
/// Invite a node to the family owned by the message sender. If
|
||||
/// `validity_secs` is omitted the invitation expires
|
||||
/// `default_invitation_validity_secs` seconds (from [`Config`]) after the
|
||||
/// current block time.
|
||||
InviteToFamily {
|
||||
node_id: NodeId,
|
||||
validity_secs: Option<u64>,
|
||||
},
|
||||
|
||||
/// Revoke a still-pending invitation previously issued by the sender's
|
||||
/// family.
|
||||
RevokeFamilyInvitation { node_id: NodeId },
|
||||
|
||||
/// Accept a pending invitation. The sender must control `node_id`.
|
||||
AcceptFamilyInvitation {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// Reject a pending invitation. The sender must control `node_id`.
|
||||
RejectFamilyInvitation {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// Leave the family `node_id` currently belongs to. The sender must
|
||||
/// control `node_id`.
|
||||
LeaveFamily { node_id: NodeId },
|
||||
|
||||
/// Remove `node_id` from the family owned by the message sender.
|
||||
KickFromFamily { node_id: NodeId },
|
||||
|
||||
/// Cross-contract callback fired by the mixnet contract the moment
|
||||
/// node with `node_id` initiates unbonding.
|
||||
/// Removes the node from any family it currently
|
||||
/// belongs to and rejects every pending invitation issued to it.
|
||||
/// Sender must be the configured mixnet contract address.
|
||||
OnNymNodeUnbond { node_id: NodeId },
|
||||
}
|
||||
|
||||
/// Query messages accepted by the contract.
|
||||
#[cw_serde]
|
||||
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
|
||||
pub enum QueryMsg {
|
||||
/// Look up a single family by its id.
|
||||
#[cfg_attr(feature = "schema", returns(NodeFamilyResponse))]
|
||||
GetFamilyById { family_id: NodeFamilyId },
|
||||
|
||||
/// Look up the (at most one) family owned by a given address.
|
||||
#[cfg_attr(feature = "schema", returns(NodeFamilyByOwnerResponse))]
|
||||
GetFamilyByOwner { owner: String },
|
||||
|
||||
/// Look up a single family by its name. The lookup is normalised
|
||||
/// contract-side (lowercased, non-alphanumerics stripped), so equivalent
|
||||
/// inputs resolve to the same family.
|
||||
#[cfg_attr(feature = "schema", returns(NodeFamilyByNameResponse))]
|
||||
GetFamilyByName { name: String },
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(FamiliesPagedResponse))]
|
||||
GetFamiliesPaged {
|
||||
start_after: Option<NodeFamilyId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Look up which family — if any — a node currently belongs to.
|
||||
#[cfg_attr(feature = "schema", returns(NodeFamilyMembershipResponse))]
|
||||
GetFamilyMembership { node_id: NodeId },
|
||||
|
||||
/// Page through every node currently in a given family.
|
||||
#[cfg_attr(feature = "schema", returns(FamilyMembersPagedResponse))]
|
||||
GetFamilyMembersPaged {
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every current family member across all families, in
|
||||
/// ascending [`NodeId`] order. Each entry carries the membership record
|
||||
/// (which in turn names the family the node belongs to).
|
||||
#[cfg_attr(feature = "schema", returns(AllFamilyMembersPagedResponse))]
|
||||
GetAllFamilyMembersPaged {
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Look up the pending invitation for a specific `(family_id, node_id)`
|
||||
/// pair.
|
||||
#[cfg_attr(feature = "schema", returns(PendingFamilyInvitationResponse))]
|
||||
GetPendingInvitation {
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
/// Page through every pending invitation issued by a given family.
|
||||
#[cfg_attr(feature = "schema", returns(PendingFamilyInvitationsPagedResponse))]
|
||||
GetPendingInvitationsForFamilyPaged {
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every pending invitation issued for a given node.
|
||||
#[cfg_attr(feature = "schema", returns(PendingInvitationsForNodePagedResponse))]
|
||||
GetPendingInvitationsForNodePaged {
|
||||
node_id: NodeId,
|
||||
start_after: Option<NodeFamilyId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every pending invitation across all families.
|
||||
#[cfg_attr(feature = "schema", returns(PendingInvitationsPagedResponse))]
|
||||
GetAllPendingInvitationsPaged {
|
||||
start_after: Option<(NodeFamilyId, NodeId)>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived (terminal-state) invitation issued by a
|
||||
/// given family.
|
||||
#[cfg_attr(feature = "schema", returns(PastFamilyInvitationsPagedResponse))]
|
||||
GetPastInvitationsForFamilyPaged {
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<PastFamilyInvitationCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived (terminal-state) invitation issued to a
|
||||
/// given node.
|
||||
#[cfg_attr(feature = "schema", returns(PastFamilyInvitationsForNodePagedResponse))]
|
||||
GetPastInvitationsForNodePaged {
|
||||
node_id: NodeId,
|
||||
start_after: Option<PastFamilyInvitationForNodeCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived (terminal-state) invitation across all
|
||||
/// families.
|
||||
#[cfg_attr(feature = "schema", returns(AllPastFamilyInvitationsPagedResponse))]
|
||||
GetAllPastInvitationsPaged {
|
||||
start_after: Option<GlobalPastFamilyInvitationCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived membership record for a given family
|
||||
/// (nodes that used to belong to it but have since been removed).
|
||||
#[cfg_attr(feature = "schema", returns(PastFamilyMembersPagedResponse))]
|
||||
GetPastMembersForFamilyPaged {
|
||||
family_id: NodeFamilyId,
|
||||
start_after: Option<PastFamilyMemberCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Page through every archived membership record for a given node
|
||||
/// (every family the node used to belong to but has since been removed
|
||||
/// from), across all families.
|
||||
#[cfg_attr(feature = "schema", returns(PastFamilyMembersForNodePagedResponse))]
|
||||
GetPastMembersForNodePaged {
|
||||
node_id: NodeId,
|
||||
start_after: Option<PastFamilyMemberForNodeCursor>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Message passed to the contract's `migrate` entry point.
|
||||
#[cw_serde]
|
||||
pub struct MigrateMsg {
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
|
||||
/// Identifier of a node family.
|
||||
///
|
||||
/// Issued sequentially by the contract on family creation; never reused even if the
|
||||
/// family is later disbanded.
|
||||
pub type NodeFamilyId = u32;
|
||||
|
||||
/// Runtime configuration of the node families contract.
|
||||
#[cw_serde]
|
||||
pub struct Config {
|
||||
/// Fee charged on each successful `create_family` execution.
|
||||
pub create_family_fee: Coin,
|
||||
|
||||
/// Maximum allowed length, in characters, of a family name.
|
||||
pub family_name_length_limit: usize,
|
||||
|
||||
/// Maximum allowed length, in characters, of a family description.
|
||||
pub family_description_length_limit: usize,
|
||||
|
||||
/// Default lifetime, in seconds, used by `invite_to_family` when the
|
||||
/// sender doesn't supply an explicit value. Senders may override this
|
||||
/// per-invitation via the optional `validity_secs` argument.
|
||||
pub default_invitation_validity_secs: u64,
|
||||
}
|
||||
|
||||
/// On-chain representation of a node family.
|
||||
#[cw_serde]
|
||||
pub struct NodeFamily {
|
||||
/// The id of the node family
|
||||
pub id: NodeFamilyId,
|
||||
|
||||
/// The name of the node family
|
||||
pub name: String,
|
||||
|
||||
/// Normalised name of the node family used for uniqueness checks
|
||||
pub normalised_name: String,
|
||||
|
||||
/// The optional description of the node family
|
||||
pub description: String,
|
||||
|
||||
/// The owner of the node family
|
||||
pub owner: Addr,
|
||||
|
||||
/// Records the fee paid when the family was created,
|
||||
/// so that the appropriate amount could be returned upon it getting disbanded.
|
||||
pub paid_fee: Coin,
|
||||
|
||||
/// Memoized value of the current number of members in the node family
|
||||
/// Used to detect if the family is empty
|
||||
pub members: u64,
|
||||
|
||||
/// Timestamp of the creation of the node family
|
||||
pub created_at: u64,
|
||||
}
|
||||
|
||||
/// A pending invitation for a node to join a particular family.
|
||||
///
|
||||
/// Invitations are stored until they are accepted, rejected, revoked, or until the
|
||||
/// chain advances past `expires_at` (in which case they remain in storage but are
|
||||
/// treated as inert — there is no background process clearing expired invitations).
|
||||
#[cw_serde]
|
||||
pub struct FamilyInvitation {
|
||||
/// The family that issued the invitation.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The node being invited.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// Block timestamp (unix seconds) after which the invitation is no longer valid.
|
||||
pub expires_at: u64,
|
||||
}
|
||||
|
||||
/// On-chain record of a node's current family membership.
|
||||
///
|
||||
/// A node belongs to at most one family at a time, so this is keyed by
|
||||
/// `NodeId` alone — `family_id` is carried in the value to support reverse
|
||||
/// lookups (all nodes in a given family) via a secondary index.
|
||||
#[cw_serde]
|
||||
pub struct FamilyMembership {
|
||||
/// The family the node is currently a member of.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// Block timestamp (unix seconds) at which the node accepted its
|
||||
/// invitation and joined the family.
|
||||
pub joined_at: u64,
|
||||
}
|
||||
|
||||
/// Historical record of a node that used to be part of a family but has since been
|
||||
/// removed (kicked, left voluntarily, or because the family was disbanded).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyMember {
|
||||
/// The family the node used to belong to.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The node that was removed.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// Block timestamp (unix seconds) at which the membership was terminated.
|
||||
pub removed_at: u64,
|
||||
}
|
||||
|
||||
/// Terminal status for an invitation that has been moved out of the pending set.
|
||||
///
|
||||
/// Note: timed-out invitations are not represented here — they are simply left in
|
||||
/// the pending set (see `FamilyInvitation::expires_at`).
|
||||
#[cw_serde]
|
||||
pub enum FamilyInvitationStatus {
|
||||
/// Still awaiting a response. Recorded with a timestamp for completeness even
|
||||
/// though pending invitations live in a separate map.
|
||||
Pending { at: u64 },
|
||||
/// The invitee accepted and joined the family at the given timestamp.
|
||||
Accepted { at: u64 },
|
||||
/// The invitee explicitly rejected the invitation at the given timestamp.
|
||||
Rejected { at: u64 },
|
||||
/// The family revoked the invitation at the given timestamp before it could
|
||||
/// be accepted or rejected.
|
||||
Revoked { at: u64 },
|
||||
}
|
||||
|
||||
/// Historical record of an invitation that has reached a terminal state
|
||||
/// (`Accepted`, `Rejected`, or `Revoked`). Timed-out invitations are **not**
|
||||
/// archived here — they remain in the pending map until explicitly cleared.
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyInvitation {
|
||||
/// The original invitation as it was issued.
|
||||
pub invitation: FamilyInvitation,
|
||||
|
||||
/// What ultimately happened to it.
|
||||
pub status: FamilyInvitationStatus,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyById`](crate::QueryMsg::GetFamilyById).
|
||||
#[cw_serde]
|
||||
pub struct NodeFamilyResponse {
|
||||
/// The id that was queried, echoed back so paginated callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The matching family, or `None` if no family with `family_id` exists.
|
||||
pub family: Option<NodeFamily>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyByOwner`](crate::QueryMsg::GetFamilyByOwner).
|
||||
#[cw_serde]
|
||||
pub struct NodeFamilyByOwnerResponse {
|
||||
/// The (validated) owner address that was queried, echoed back so callers
|
||||
/// can correlate.
|
||||
pub owner: Addr,
|
||||
|
||||
/// The matching family, or `None` if `owner` does not currently own one.
|
||||
pub family: Option<NodeFamily>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyByName`](crate::QueryMsg::GetFamilyByName).
|
||||
#[cw_serde]
|
||||
pub struct NodeFamilyByNameResponse {
|
||||
/// The name that was queried, echoed back so callers can correlate.
|
||||
pub name: String,
|
||||
|
||||
/// The matching family, or `None` if no family with that name exists.
|
||||
pub family: Option<NodeFamily>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyMembership`](crate::QueryMsg::GetFamilyMembership).
|
||||
#[cw_serde]
|
||||
pub struct NodeFamilyMembershipResponse {
|
||||
/// The node that was queried.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The id of the family the node currently belongs to, or `None` if the
|
||||
/// node is not currently a member of any family.
|
||||
pub family_id: Option<NodeFamilyId>,
|
||||
}
|
||||
|
||||
/// A pending [`FamilyInvitation`] paired with whether it has already timed
|
||||
/// out at the time the query was served.
|
||||
#[cw_serde]
|
||||
pub struct PendingFamilyInvitationDetails {
|
||||
/// The stored invitation as it was issued.
|
||||
pub invitation: FamilyInvitation,
|
||||
|
||||
/// `true` iff `now >= invitation.expires_at` at query time, i.e. the
|
||||
/// invitation is still in the pending map but can no longer be acted on.
|
||||
pub expired: bool,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPendingInvitation`](crate::QueryMsg::GetPendingInvitation).
|
||||
#[cw_serde]
|
||||
pub struct PendingFamilyInvitationResponse {
|
||||
/// The family component of the queried `(family_id, node_id)` key.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The node component of the queried `(family_id, node_id)` key.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The matching pending invitation along with an explicit expiry flag,
|
||||
/// or `None` if no such invitation exists.
|
||||
pub invitation: Option<PendingFamilyInvitationDetails>,
|
||||
}
|
||||
|
||||
/// One entry in a [`FamilyMembersPagedResponse`] page — pairs a node id with
|
||||
/// its [`FamilyMembership`] record (notably its `joined_at` timestamp).
|
||||
#[cw_serde]
|
||||
pub struct FamilyMemberRecord {
|
||||
/// The node currently in the family.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The membership record (carries `family_id` and `joined_at`).
|
||||
pub membership: FamilyMembership,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamilyMembersPaged`](crate::QueryMsg::GetFamilyMembersPaged).
|
||||
#[cw_serde]
|
||||
pub struct FamilyMembersPagedResponse {
|
||||
/// The family whose members were queried, echoed back so paginated
|
||||
/// callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The members on this page, in ascending [`NodeId`] order.
|
||||
pub members: Vec<FamilyMemberRecord>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (which the caller should treat as end-of-list).
|
||||
pub start_next_after: Option<NodeId>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetAllFamilyMembersPaged`](crate::QueryMsg::GetAllFamilyMembersPaged).
|
||||
#[cw_serde]
|
||||
pub struct AllFamilyMembersPagedResponse {
|
||||
/// The members on this page, in ascending [`NodeId`] order across every
|
||||
/// family.
|
||||
pub members: Vec<FamilyMemberRecord>,
|
||||
|
||||
/// Cursor (last `node_id`) to pass as `start_after` on the next call,
|
||||
/// or `None` if this page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<NodeId>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPendingInvitationsForFamilyPaged`](crate::QueryMsg::GetPendingInvitationsForFamilyPaged).
|
||||
#[cw_serde]
|
||||
pub struct PendingFamilyInvitationsPagedResponse {
|
||||
/// The family whose pending invitations were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The pending invitations on this page, in ascending invitee
|
||||
/// [`NodeId`] order, each stamped with whether it had already timed out
|
||||
/// at the time the query was served.
|
||||
pub invitations: Vec<PendingFamilyInvitationDetails>,
|
||||
|
||||
/// Cursor (last invitee node id) to pass as `start_after` on the next
|
||||
/// call, or `None` if this page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<NodeId>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPendingInvitationsForNodePaged`](crate::QueryMsg::GetPendingInvitationsForNodePaged).
|
||||
#[cw_serde]
|
||||
pub struct PendingInvitationsForNodePagedResponse {
|
||||
/// The node whose pending invitations were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The pending invitations addressed to this node on this page, in
|
||||
/// ascending [`NodeFamilyId`] order, each stamped with whether it had
|
||||
/// already timed out at the time the query was served.
|
||||
pub invitations: Vec<PendingFamilyInvitationDetails>,
|
||||
|
||||
/// Cursor (last issuing family id) to pass as `start_after` on the
|
||||
/// next call, or `None` if this page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<NodeFamilyId>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetAllPendingInvitationsPaged`](crate::QueryMsg::GetAllPendingInvitationsPaged).
|
||||
#[cw_serde]
|
||||
pub struct PendingInvitationsPagedResponse {
|
||||
/// The pending invitations on this page, in ascending
|
||||
/// `(family_id, node_id)` order, each stamped with whether it had
|
||||
/// already timed out at the time the query was served.
|
||||
pub invitations: Vec<PendingFamilyInvitationDetails>,
|
||||
|
||||
/// Cursor (last `(family_id, node_id)` pair) to pass as `start_after`
|
||||
/// on the next call, or `None` if this page is empty (treat as
|
||||
/// end-of-list).
|
||||
pub start_next_after: Option<(NodeFamilyId, NodeId)>,
|
||||
}
|
||||
|
||||
/// Cursor for paginating per-family past-invitation listings: identifies a
|
||||
/// single archive entry within a family by `(node_id, counter)`. The
|
||||
/// `counter` is the per-`(family, node)` archive slot — multiple archived
|
||||
/// invitations can exist for the same `(family, node)` pair (a node may be
|
||||
/// invited and have the invitation reach a terminal state more than once).
|
||||
pub type PastFamilyInvitationCursor = (NodeId, u64);
|
||||
|
||||
/// Cursor for paginating per-node past-invitation listings: identifies a
|
||||
/// single archive entry addressed to a fixed node by `(family_id, counter)`.
|
||||
pub type PastFamilyInvitationForNodeCursor = (NodeFamilyId, u64);
|
||||
|
||||
/// Cursor for paginating global past-invitation listings: identifies a
|
||||
/// single archive entry across all families by `((family_id, node_id), counter)`.
|
||||
pub type GlobalPastFamilyInvitationCursor = ((NodeFamilyId, NodeId), u64);
|
||||
|
||||
/// Response to [`QueryMsg::GetPastInvitationsForFamilyPaged`](crate::QueryMsg::GetPastInvitationsForFamilyPaged).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyInvitationsPagedResponse {
|
||||
/// The family whose archived invitations were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The archived invitations on this page, in ascending
|
||||
/// `(node_id, counter)` order across all terminal statuses.
|
||||
pub invitations: Vec<PastFamilyInvitation>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<PastFamilyInvitationCursor>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPastInvitationsForNodePaged`](crate::QueryMsg::GetPastInvitationsForNodePaged).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyInvitationsForNodePagedResponse {
|
||||
/// The node whose past invitations were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The archived invitations addressed to this node on this page, in
|
||||
/// ascending `(family_id, counter)` order across all terminal statuses.
|
||||
pub invitations: Vec<PastFamilyInvitation>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<PastFamilyInvitationForNodeCursor>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetAllPastInvitationsPaged`](crate::QueryMsg::GetAllPastInvitationsPaged).
|
||||
#[cw_serde]
|
||||
pub struct AllPastFamilyInvitationsPagedResponse {
|
||||
/// The archived invitations on this page, in ascending
|
||||
/// `((family_id, node_id), counter)` order across all terminal statuses.
|
||||
pub invitations: Vec<PastFamilyInvitation>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<GlobalPastFamilyInvitationCursor>,
|
||||
}
|
||||
|
||||
/// Cursor for paginating per-family past-member listings: identifies a single
|
||||
/// archive entry within a family by `(node_id, counter)`. The `counter` is the
|
||||
/// per-`(family, node)` archive slot — multiple archived membership entries
|
||||
/// can exist for the same `(family, node)` pair (a node may join, leave, and
|
||||
/// re-join the same family more than once).
|
||||
pub type PastFamilyMemberCursor = (NodeId, u64);
|
||||
|
||||
/// Cursor for paginating per-node past-member listings: identifies a single
|
||||
/// archive entry for a fixed node by `(family_id, counter)`.
|
||||
pub type PastFamilyMemberForNodeCursor = (NodeFamilyId, u64);
|
||||
|
||||
/// Response to [`QueryMsg::GetPastMembersForFamilyPaged`](crate::QueryMsg::GetPastMembersForFamilyPaged).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyMembersPagedResponse {
|
||||
/// The family whose archived memberships were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub family_id: NodeFamilyId,
|
||||
|
||||
/// The archived membership records on this page, in ascending
|
||||
/// `(node_id, counter)` order.
|
||||
pub members: Vec<PastFamilyMember>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<PastFamilyMemberCursor>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetPastMembersForNodePaged`](crate::QueryMsg::GetPastMembersForNodePaged).
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyMembersForNodePagedResponse {
|
||||
/// The node whose archived memberships were queried, echoed back so
|
||||
/// paginated callers can correlate.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The archived membership records for this node on this page, in
|
||||
/// ascending `(family_id, counter)` order.
|
||||
pub members: Vec<PastFamilyMember>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (treat as end-of-list).
|
||||
pub start_next_after: Option<PastFamilyMemberForNodeCursor>,
|
||||
}
|
||||
|
||||
/// Response to [`QueryMsg::GetFamiliesPaged`](crate::QueryMsg::GetFamiliesPaged).
|
||||
#[cw_serde]
|
||||
pub struct FamiliesPagedResponse {
|
||||
/// The families on this page, in ascending [`NodeFamilyId`] order.
|
||||
pub families: Vec<NodeFamily>,
|
||||
|
||||
/// Cursor to pass as `start_after` on the next call, or `None` if this
|
||||
/// page is empty (which the caller should treat as end-of-list).
|
||||
pub start_next_after: Option<NodeFamilyId>,
|
||||
}
|
||||
@@ -8,7 +8,9 @@ license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version.workspace = true
|
||||
# pinned (not inherited from workspace) because this crate is imported by the ecash contract,
|
||||
# and the contracts workspace cannot be built with rustc more recent than 1.86
|
||||
rust-version = "1.86.0"
|
||||
readme.workspace = true
|
||||
publish = true
|
||||
|
||||
|
||||
@@ -129,6 +129,41 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum OutputV2 {
|
||||
#[default]
|
||||
Json,
|
||||
Yaml,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::IntoParams, utoipa::ToSchema))]
|
||||
#[serde(default)]
|
||||
pub struct OutputParamsV2 {
|
||||
pub output: Option<OutputV2>,
|
||||
}
|
||||
|
||||
impl OutputParamsV2 {
|
||||
pub fn get_output(&self) -> OutputV2 {
|
||||
self.output.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn to_response<T: Serialize>(self, data: T) -> FormattedResponse<T> {
|
||||
self.get_output().to_response(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputV2 {
|
||||
pub fn to_response<T: Serialize>(self, data: T) -> FormattedResponse<T> {
|
||||
match self {
|
||||
OutputV2::Json => FormattedResponse::Json(Json::from(data)),
|
||||
OutputV2::Yaml => FormattedResponse::Yaml(Yaml::from(data)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
|
||||
@@ -8,7 +8,9 @@ license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version.workspace = true
|
||||
# pinned (not inherited from workspace) because this crate is imported by the ecash contract,
|
||||
# and the contracts workspace cannot be built with rustc more recent than 1.86
|
||||
rust-version = "1.86.0"
|
||||
readme.workspace = true
|
||||
publish = true
|
||||
# Exclude build.rs from published crate - it's only used for dev-time sync
|
||||
|
||||
@@ -22,6 +22,10 @@ pub const VESTING_CONTRACT_ADDRESS: &str =
|
||||
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "";
|
||||
// /\ TODO: this has to be updated once the contract is deployed
|
||||
|
||||
// \/ TODO: this has to be updated once the contract is deployed
|
||||
pub const NODE_FAMILIES_CONTRACT_ADDRESS: &str = "";
|
||||
// /\ TODO: this has to be updated once the contract is deployed
|
||||
|
||||
pub const ECASH_CONTRACT_ADDRESS: &str =
|
||||
"n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun";
|
||||
pub const GROUP_CONTRACT_ADDRESS: &str =
|
||||
|
||||
@@ -39,6 +39,8 @@ pub struct NymContracts {
|
||||
pub vesting_contract_address: Option<String>,
|
||||
#[serde(default)]
|
||||
pub performance_contract_address: Option<String>,
|
||||
#[serde(default)]
|
||||
pub node_families_contract_address: Option<String>,
|
||||
pub ecash_contract_address: Option<String>,
|
||||
pub group_contract_address: Option<String>,
|
||||
pub multisig_contract_address: Option<String>,
|
||||
@@ -174,6 +176,9 @@ impl NymNetworkDetails {
|
||||
))
|
||||
.with_mixnet_contract(get_optional_env(var_names::MIXNET_CONTRACT_ADDRESS))
|
||||
.with_vesting_contract(get_optional_env(var_names::VESTING_CONTRACT_ADDRESS))
|
||||
.with_node_families_contract(get_optional_env(
|
||||
var_names::NODE_FAMILIES_CONTRACT_ADDRESS,
|
||||
))
|
||||
.with_ecash_contract(get_optional_env(var_names::ECASH_CONTRACT_ADDRESS))
|
||||
.with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS))
|
||||
.with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
|
||||
@@ -199,6 +204,9 @@ impl NymNetworkDetails {
|
||||
performance_contract_address: parse_optional_str(
|
||||
mainnet::PERFORMANCE_CONTRACT_ADDRESS,
|
||||
),
|
||||
node_families_contract_address: parse_optional_str(
|
||||
mainnet::NODE_FAMILIES_CONTRACT_ADDRESS,
|
||||
),
|
||||
ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS),
|
||||
group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS),
|
||||
multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS),
|
||||
@@ -252,6 +260,7 @@ impl NymNetworkDetails {
|
||||
|
||||
set_optional_var(var_names::MIXNET_CONTRACT_ADDRESS, self.contracts.mixnet_contract_address);
|
||||
set_optional_var(var_names::VESTING_CONTRACT_ADDRESS, self.contracts.vesting_contract_address);
|
||||
set_optional_var(var_names::NODE_FAMILIES_CONTRACT_ADDRESS, self.contracts.node_families_contract_address);
|
||||
set_optional_var(var_names::ECASH_CONTRACT_ADDRESS, self.contracts.ecash_contract_address);
|
||||
set_optional_var(var_names::GROUP_CONTRACT_ADDRESS, self.contracts.group_contract_address);
|
||||
set_optional_var(var_names::MULTISIG_CONTRACT_ADDRESS, self.contracts.multisig_contract_address);
|
||||
@@ -340,6 +349,12 @@ impl NymNetworkDetails {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_node_families_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
|
||||
self.contracts.node_families_contract_address = contract.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_ecash_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
|
||||
self.contracts.ecash_contract_address = contract.map(Into::into);
|
||||
|
||||
@@ -17,6 +17,7 @@ pub const VESTING_CONTRACT_ADDRESS: &str = "VESTING_CONTRACT_ADDRESS";
|
||||
pub const ECASH_CONTRACT_ADDRESS: &str = "ECASH_CONTRACT_ADDRESS";
|
||||
pub const GROUP_CONTRACT_ADDRESS: &str = "GROUP_CONTRACT_ADDRESS";
|
||||
pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_CONTRACT_ADDRESS";
|
||||
pub const NODE_FAMILIES_CONTRACT_ADDRESS: &str = "NODE_FAMILIES_CONTRACT_ADDRESS";
|
||||
pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS";
|
||||
pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS";
|
||||
pub const NYXD: &str = "NYXD";
|
||||
|
||||
@@ -8,7 +8,9 @@ license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version.workspace = true
|
||||
# pinned (not inherited from workspace) because this crate is imported by the ecash contract,
|
||||
# and the contracts workspace cannot be built with rustc more recent than 1.86
|
||||
rust-version = "1.86.0"
|
||||
readme.workspace = true
|
||||
publish = true
|
||||
|
||||
|
||||
Generated
+86
-254
@@ -2,17 +2,6 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.11"
|
||||
@@ -87,7 +76,7 @@ dependencies = [
|
||||
"ark-serialize",
|
||||
"ark-std",
|
||||
"derivative",
|
||||
"digest 0.10.7",
|
||||
"digest",
|
||||
"itertools 0.10.5",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
@@ -141,7 +130,7 @@ checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5"
|
||||
dependencies = [
|
||||
"ark-serialize-derive",
|
||||
"ark-std",
|
||||
"digest 0.10.7",
|
||||
"digest",
|
||||
"num-bigint",
|
||||
]
|
||||
|
||||
@@ -167,12 +156,6 @@ dependencies = [
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.2.0"
|
||||
@@ -215,25 +198,13 @@ version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d"
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330"
|
||||
dependencies = [
|
||||
"byte-tools",
|
||||
"crypto-mac",
|
||||
"digest 0.8.1",
|
||||
"opaque-debug",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -251,12 +222,6 @@ dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byte-tools"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
@@ -321,26 +286,6 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chacha"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddf3c081b5fba1e5615640aae998e0fbd10c24cbd897ee39ed754a77601a4862"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"keystream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
@@ -380,7 +325,7 @@ dependencies = [
|
||||
"ark-serialize",
|
||||
"cosmwasm-core",
|
||||
"curve25519-dalek",
|
||||
"digest 0.10.7",
|
||||
"digest",
|
||||
"ecdsa",
|
||||
"ed25519-zebra",
|
||||
"k256",
|
||||
@@ -491,9 +436,9 @@ version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
"rand_core",
|
||||
"subtle 2.4.1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -503,29 +448,10 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-mac"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5"
|
||||
dependencies = [
|
||||
"generic-array 0.12.4",
|
||||
"subtle 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctr"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
@@ -535,10 +461,10 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"curve25519-dalek-derive",
|
||||
"digest 0.10.7",
|
||||
"digest",
|
||||
"fiat-crypto",
|
||||
"rustc_version",
|
||||
"subtle 2.4.1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -709,7 +635,7 @@ dependencies = [
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-group-contract-common",
|
||||
"nym-multisig-contract-common 1.20.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"nym-multisig-contract-common 1.21.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -796,15 +722,6 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
|
||||
dependencies = [
|
||||
"generic-array 0.12.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -814,7 +731,7 @@ dependencies = [
|
||||
"block-buffer",
|
||||
"const-oid",
|
||||
"crypto-common",
|
||||
"subtle 2.4.1",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -825,7 +742,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
||||
|
||||
[[package]]
|
||||
name = "easy-addr"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"quote",
|
||||
@@ -839,7 +756,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
|
||||
dependencies = [
|
||||
"der",
|
||||
"digest 0.10.7",
|
||||
"digest",
|
||||
"elliptic-curve",
|
||||
"rfc6979",
|
||||
"signature",
|
||||
@@ -866,7 +783,7 @@ dependencies = [
|
||||
"rand_core",
|
||||
"serde",
|
||||
"sha2",
|
||||
"subtle 2.4.1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -899,13 +816,13 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"crypto-bigint",
|
||||
"digest 0.10.7",
|
||||
"digest",
|
||||
"ff",
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
"group",
|
||||
"rand_core",
|
||||
"sec1",
|
||||
"subtle 2.4.1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -922,7 +839,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
"subtle 2.4.1",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -931,15 +848,6 @@ version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c007b1ae3abe1cb6f85a16305acd418b7ca6343b953633fee2b76d8f108b830f"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -970,7 +878,7 @@ checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
||||
dependencies = [
|
||||
"ff",
|
||||
"rand_core",
|
||||
"subtle 2.4.1",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1004,22 +912,13 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1048,15 +947,6 @@ dependencies = [
|
||||
"hashbrown 0.15.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
@@ -1102,12 +992,6 @@ dependencies = [
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keystream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28"
|
||||
|
||||
[[package]]
|
||||
name = "konst"
|
||||
version = "0.3.16"
|
||||
@@ -1141,30 +1025,32 @@ version = "0.2.180"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
||||
|
||||
[[package]]
|
||||
name = "lioness"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"blake2",
|
||||
"chacha",
|
||||
"keystream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
|
||||
|
||||
[[package]]
|
||||
name = "node-families"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"cw-storage-plus",
|
||||
"cw-utils",
|
||||
"cw2",
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-crypto",
|
||||
"nym-mixnet-contract",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-node-families-contract-common",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
@@ -1197,7 +1083,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1233,9 +1118,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-coconut-dkg-common"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a95dc43ef8954a4f79846e3224434cf389d4a9c14b77f526dae3cfd2221c6141"
|
||||
checksum = "fcdfaf17c8b2a73bd6d14a3b9432118d66cc2335a371f185791099d32c25feb0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1243,14 +1128,12 @@ dependencies = [
|
||||
"cw2",
|
||||
"cw4",
|
||||
"nym-contracts-common",
|
||||
"nym-multisig-contract-common 1.20.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"nym-multisig-contract-common 1.21.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-contracts-common"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47bb3e8427c193cd500c802274b11879086863c3643525b6ece3e9ab1c77bddc"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
@@ -1264,9 +1147,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-contracts-common-testing"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3628aac6715e844f3ab20e3b8ae8c4684f144ccb78e205f002c1c3ae375e956"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cosmwasm-std",
|
||||
@@ -1280,16 +1161,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-crypto"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b710addc28c9950dd961e7dd3837ef3b479492d2b21b5f2437eb7d2899403027"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bs58",
|
||||
"curve25519-dalek",
|
||||
"ed25519-dalek",
|
||||
"nym-pemstore",
|
||||
"nym-sphinx-types",
|
||||
"rand",
|
||||
"sha2",
|
||||
"subtle-encoding",
|
||||
@@ -1317,7 +1195,7 @@ dependencies = [
|
||||
"nym-contracts-common-testing",
|
||||
"nym-crypto",
|
||||
"nym-ecash-contract-common",
|
||||
"nym-multisig-contract-common 1.20.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"nym-multisig-contract-common 1.21.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"nym-network-defaults",
|
||||
"schemars",
|
||||
"semver",
|
||||
@@ -1328,7 +1206,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-ecash-contract-common"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
@@ -1336,15 +1214,15 @@ dependencies = [
|
||||
"cw-controllers",
|
||||
"cw-utils",
|
||||
"cw2",
|
||||
"nym-multisig-contract-common 1.20.4",
|
||||
"nym-multisig-contract-common 1.21.0",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-group-contract-common"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb13102740426a4a2b683f54bbd6614fe9ecd745f5117bcf197c49c300b15edf"
|
||||
checksum = "f6f9773b3adc4b979c4e0f86d8932a78d10149df36e136f770653ad5405ce41a"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cw-controllers",
|
||||
@@ -1370,6 +1248,7 @@ dependencies = [
|
||||
"nym-crypto",
|
||||
"nym-mixnet-contract",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-node-families-contract-common",
|
||||
"nym-vesting-contract-common",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
@@ -1379,9 +1258,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-mixnet-contract-common"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41c21bceb3bb8ee2789851b3f381fc035485af825bf7290b7c99a5af4e8f6ba1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
@@ -1401,7 +1278,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-multisig-contract-common"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1416,9 +1293,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-multisig-contract-common"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4a20b931ee849f6179ce2b387accd058720017f644ffbc8c2422f3e9ac3ff54"
|
||||
checksum = "7681c7b43201d45a4958eab012a93e285ce47f7bba405e1ba1808edd195f3347"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1433,19 +1310,30 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-defaults"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9834193b4641acdf9f360aea684a6bd841cad287930bc0d7c3241a133756464"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"cargo_metadata 0.19.2",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-families-contract-common"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"cw-utils",
|
||||
"nym-contracts-common",
|
||||
"nym-mixnet-contract-common",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-pemstore"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03077f9ebeb40caf8aa8e6f7bf8728449f73733e7a246986e492fa34ad3e70ab"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"pem",
|
||||
"tracing",
|
||||
@@ -1462,20 +1350,22 @@ dependencies = [
|
||||
"cw-controllers",
|
||||
"cw-storage-plus",
|
||||
"cw2",
|
||||
"node-families",
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-crypto",
|
||||
"nym-mixnet-contract",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-node-families-contract-common",
|
||||
"nym-performance-contract-common",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-performance-contract-common"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42129a72f4b0dc0304a48b0ca1769b27694d913687ace5692d4c6924ca9f2a13"
|
||||
checksum = "c6e47f2a04d8b0e1c492cdd7dcfc98face5efc2a4cb999c03887a5b77e4cf055"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1503,9 +1393,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-pool-contract-common"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70239cc26beda3ad19289188c50d554522af29646d7d3f855bda6fc8ed332fe7"
|
||||
checksum = "9965dce8aaee8e9943b342695ff3fda45c8a4f1daab943ed752a8a0b47aa04c7"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1515,16 +1405,6 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-sphinx-types"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ba662d39fd6da9e13166fa1162ff41c2cfaed78a77c70df72fbda6fef5eb4f5"
|
||||
dependencies = [
|
||||
"sphinx-packet",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-vesting-contract"
|
||||
version = "1.4.1"
|
||||
@@ -1548,9 +1428,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-vesting-contract-common"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "676c2793efbf9ccdf86bb788c903f778a2d5993a5174729303f9511a297f4ca8"
|
||||
checksum = "af0179bfbb30551414277ca7bed631d48f3321df7b18e3032f23f2f111a577d7"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1567,12 +1447,6 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
|
||||
|
||||
[[package]]
|
||||
name = "p256"
|
||||
version = "0.13.2"
|
||||
@@ -1743,16 +1617,6 @@ dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_distr"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.10.0"
|
||||
@@ -1809,7 +1673,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
"subtle 2.4.1",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1887,8 +1751,8 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"der",
|
||||
"generic-array 0.14.7",
|
||||
"subtle 2.4.1",
|
||||
"generic-array",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -1981,7 +1845,7 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest 0.10.7",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1990,36 +1854,10 @@ version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
"digest",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinx-packet"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c26f0c20d909fdda1c5d0ece3973127ca421984d55b000215df365e93722fc6e"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arrayref",
|
||||
"blake2",
|
||||
"bs58",
|
||||
"byteorder",
|
||||
"chacha",
|
||||
"ctr",
|
||||
"curve25519-dalek",
|
||||
"digest 0.10.7",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"lioness",
|
||||
"rand",
|
||||
"rand_distr",
|
||||
"sha2",
|
||||
"subtle 2.4.1",
|
||||
"x25519-dalek",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
@@ -2036,12 +1874,6 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.4.1"
|
||||
|
||||
+29
-18
@@ -4,11 +4,12 @@ members = [
|
||||
"coconut-dkg",
|
||||
"ecash",
|
||||
"mixnet",
|
||||
"nym-pool",
|
||||
"multisig/cw3-flex-multisig",
|
||||
"multisig/cw4-group",
|
||||
"vesting",
|
||||
"node-families",
|
||||
"nym-pool",
|
||||
"performance",
|
||||
"vesting",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
@@ -18,6 +19,8 @@ homepage = "https://nymtech.net"
|
||||
documentation = "https://nymtech.net"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
rust-version = "1.86.0"
|
||||
readme = "../README.md"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
@@ -56,31 +59,33 @@ schemars = "0.8.16"
|
||||
|
||||
thiserror = "2.0.11"
|
||||
|
||||
easy-addr = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/easy_addr" }
|
||||
easy-addr = { version = "1.21.0", path = "../common/cosmwasm-smart-contracts/easy_addr" }
|
||||
# For local development with modifications, add a [patch.crates-io] section (see bottom of file)
|
||||
nym-coconut-dkg-common = "1.20.4"
|
||||
nym-contracts-common = "1.20.4"
|
||||
nym-contracts-common-testing = "1.20.4"
|
||||
nym-crypto = { version = "1.20.4", default-features = false }
|
||||
nym-ecash-contract-common = "1.20.4"
|
||||
nym-group-contract-common = "1.20.4"
|
||||
nym-mixnet-contract-common = "1.20.4"
|
||||
nym-multisig-contract-common = "1.20.4"
|
||||
nym-network-defaults = { version = "1.20.4", default-features = false }
|
||||
nym-performance-contract-common = "1.20.4"
|
||||
nym-pool-contract-common = "1.20.4"
|
||||
nym-vesting-contract-common = "1.20.4"
|
||||
nym-coconut-dkg-common = "1.21.0"
|
||||
nym-contracts-common = "1.21.0"
|
||||
nym-contracts-common-testing = "1.21.0"
|
||||
nym-crypto = { version = "1.21.0", default-features = false }
|
||||
nym-ecash-contract-common = "1.21.0"
|
||||
nym-group-contract-common = "1.21.0"
|
||||
nym-mixnet-contract-common = "1.21.0"
|
||||
nym-multisig-contract-common = "1.21.0"
|
||||
nym-network-defaults = { version = "1.21.0", default-features = false }
|
||||
nym-performance-contract-common = "1.21.0"
|
||||
nym-pool-contract-common = "1.21.0"
|
||||
nym-vesting-contract-common = "1.21.0"
|
||||
|
||||
# Aliases for crates that some contracts import under different names
|
||||
contracts-common = { version = "1.20.4", package = "nym-contracts-common" }
|
||||
mixnet-contract-common = { version = "1.20.4", package = "nym-mixnet-contract-common" }
|
||||
vesting-contract-common = { version = "1.20.4", package = "nym-vesting-contract-common" }
|
||||
contracts-common = { version = "1.21.0", package = "nym-contracts-common" }
|
||||
mixnet-contract-common = { version = "1.21.0", package = "nym-mixnet-contract-common" }
|
||||
vesting-contract-common = { version = "1.21.0", package = "nym-vesting-contract-common" }
|
||||
nym-node-families-contract-common = { version = "1.21.0", package = "nym-node-families-contract-common" }
|
||||
|
||||
# Internal contract workspace members (for cross-contract testing)
|
||||
cw3-flex-multisig = { version = "2.0.0", path = "multisig/cw3-flex-multisig" }
|
||||
cw4-group = { version = "2.0.0", path = "multisig/cw4-group" }
|
||||
nym-mixnet-contract = { version = "1.5.1", path = "mixnet" }
|
||||
nym-vesting-contract = { version = "1.4.1", path = "vesting" }
|
||||
node-families = { version = "0.1.0", path = "node-families" }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
unwrap_used = "deny"
|
||||
@@ -97,4 +102,10 @@ unreachable = "deny"
|
||||
# nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
|
||||
[patch.crates-io]
|
||||
nym-network-defaults = { path = "../common/network-defaults" }
|
||||
nym-crypto = { path = "../common/crypto" }
|
||||
nym-contracts-common-testing = { path = "../common/cosmwasm-smart-contracts/contracts-common-testing" }
|
||||
nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-ecash-contract-common = { path = "../common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-node-families-contract-common = { path = "../common/cosmwasm-smart-contracts/node-families-contract" }
|
||||
|
||||
@@ -28,6 +28,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
[dependencies]
|
||||
mixnet-contract-common = { workspace = true }
|
||||
vesting-contract-common = { workspace = true }
|
||||
nym-node-families-contract-common = { workspace = true }
|
||||
nym-contracts-common = { workspace = true }
|
||||
nym-contracts-common-testing = { workspace = true, optional = true }
|
||||
|
||||
@@ -41,6 +42,8 @@ bs58 = { workspace = true }
|
||||
serde = { workspace = true, default-features = false, features = ["derive"] }
|
||||
semver = { workspace = true }
|
||||
|
||||
nym-crypto = { workspace = true, optional = true }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow.workspace = true
|
||||
@@ -56,7 +59,7 @@ nym-contracts-common-testing = { workspace = true }
|
||||
[features]
|
||||
default = []
|
||||
contract-testing = ["mixnet-contract-common/contract-testing"]
|
||||
testable-mixnet-contract = ["nym-contracts-common-testing"]
|
||||
testable-mixnet-contract = ["nym-contracts-common-testing", "nym-crypto", "nym-crypto/asymmetric", "nym-crypto/rand"]
|
||||
schema-gen = ["mixnet-contract-common/schema", "cosmwasm-schema"]
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"epoch_duration",
|
||||
"epochs_in_interval",
|
||||
"initial_rewarding_params",
|
||||
"node_families_contract_address",
|
||||
"rewarding_denom",
|
||||
"rewarding_validator_address",
|
||||
"vesting_contract_address"
|
||||
@@ -50,6 +51,9 @@
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_families_contract_address": {
|
||||
"type": "string"
|
||||
},
|
||||
"profit_margin": {
|
||||
"default": {
|
||||
"maximum": "1",
|
||||
@@ -3509,7 +3513,13 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "MigrateMsg",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_families_contract_address"
|
||||
],
|
||||
"properties": {
|
||||
"node_families_contract_address": {
|
||||
"type": "string"
|
||||
},
|
||||
"unsafe_skip_state_updates": {
|
||||
"type": [
|
||||
"boolean",
|
||||
@@ -10774,12 +10784,21 @@
|
||||
"description": "The current state of the mixnet contract.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_families_contract_address",
|
||||
"params",
|
||||
"rewarding_denom",
|
||||
"rewarding_validator_address",
|
||||
"vesting_contract_address"
|
||||
],
|
||||
"properties": {
|
||||
"node_families_contract_address": {
|
||||
"description": "Address of the node families contract. It is called whenever nym-node unbonds so that it could be removed from any family it belongs to.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
]
|
||||
},
|
||||
"owner": {
|
||||
"description": "Address of the contract owner.",
|
||||
"default": null,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"epoch_duration",
|
||||
"epochs_in_interval",
|
||||
"initial_rewarding_params",
|
||||
"node_families_contract_address",
|
||||
"rewarding_denom",
|
||||
"rewarding_validator_address",
|
||||
"vesting_contract_address"
|
||||
@@ -46,6 +47,9 @@
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_families_contract_address": {
|
||||
"type": "string"
|
||||
},
|
||||
"profit_margin": {
|
||||
"default": {
|
||||
"maximum": "1",
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "MigrateMsg",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_families_contract_address"
|
||||
],
|
||||
"properties": {
|
||||
"node_families_contract_address": {
|
||||
"type": "string"
|
||||
},
|
||||
"unsafe_skip_state_updates": {
|
||||
"type": [
|
||||
"boolean",
|
||||
|
||||
@@ -4,12 +4,21 @@
|
||||
"description": "The current state of the mixnet contract.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_families_contract_address",
|
||||
"params",
|
||||
"rewarding_denom",
|
||||
"rewarding_validator_address",
|
||||
"vesting_contract_address"
|
||||
],
|
||||
"properties": {
|
||||
"node_families_contract_address": {
|
||||
"description": "Address of the node families contract. It is called whenever nym-node unbonds so that it could be removed from any family it belongs to.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
]
|
||||
},
|
||||
"owner": {
|
||||
"description": "Address of the contract owner.",
|
||||
"default": null,
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::constants::INITIAL_PLEDGE_AMOUNT;
|
||||
use crate::interval::storage as interval_storage;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use crate::nodes::storage as nymnodes_storage;
|
||||
use crate::queued_migrations::introduce_key_rotation_id;
|
||||
use crate::queued_migrations::introduce_node_families_contract;
|
||||
use crate::rewards::storage::RewardingStorage;
|
||||
use cosmwasm_std::{
|
||||
entry_point, to_json_binary, Addr, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse,
|
||||
@@ -28,6 +28,7 @@ fn default_initial_state(
|
||||
owner: Addr,
|
||||
rewarding_validator_address: Addr,
|
||||
vesting_contract_address: Addr,
|
||||
node_families_contract_address: Addr,
|
||||
) -> ContractState {
|
||||
// we have to temporarily preserve this functionalities until it can be removed
|
||||
#[allow(deprecated)]
|
||||
@@ -35,6 +36,7 @@ fn default_initial_state(
|
||||
owner: Some(owner),
|
||||
rewarding_validator_address,
|
||||
vesting_contract_address,
|
||||
node_families_contract_address,
|
||||
rewarding_denom: msg.rewarding_denom.clone(),
|
||||
params: ContractStateParams {
|
||||
delegations_params: DelegationsParams {
|
||||
@@ -90,11 +92,15 @@ pub fn instantiate(
|
||||
|
||||
let rewarding_validator_address = deps.api.addr_validate(&msg.rewarding_validator_address)?;
|
||||
let vesting_contract_address = deps.api.addr_validate(&msg.vesting_contract_address)?;
|
||||
let node_families_contract_address = deps
|
||||
.api
|
||||
.addr_validate(&msg.node_families_contract_address)?;
|
||||
let state = default_initial_state(
|
||||
&msg,
|
||||
info.sender.clone(),
|
||||
rewarding_validator_address.clone(),
|
||||
vesting_contract_address,
|
||||
node_families_contract_address,
|
||||
);
|
||||
let starting_interval =
|
||||
Interval::init_interval(msg.epochs_in_interval, msg.epoch_duration, &env);
|
||||
@@ -629,7 +635,10 @@ pub fn migrate(
|
||||
let skip_state_updates = msg.unsafe_skip_state_updates.unwrap_or(false);
|
||||
|
||||
if !skip_state_updates {
|
||||
introduce_key_rotation_id(deps.branch())?;
|
||||
let addr = deps
|
||||
.api
|
||||
.addr_validate(&msg.node_families_contract_address)?;
|
||||
introduce_node_families_contract(deps.branch(), addr)?;
|
||||
}
|
||||
|
||||
// due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address
|
||||
@@ -668,6 +677,7 @@ mod tests {
|
||||
let init_msg = InstantiateMsg {
|
||||
rewarding_validator_address: deps.api.addr_make("foomp123").to_string(),
|
||||
vesting_contract_address: deps.api.addr_make("bar456").to_string(),
|
||||
node_families_contract_address: deps.api.addr_make("baz789").to_string(),
|
||||
rewarding_denom: "uatom".to_string(),
|
||||
epochs_in_interval: 1234,
|
||||
epoch_duration: Duration::from_secs(4321),
|
||||
@@ -708,6 +718,7 @@ mod tests {
|
||||
owner: Some(deps.api.addr_make("sender")),
|
||||
rewarding_validator_address: deps.api.addr_make("foomp123"),
|
||||
vesting_contract_address: deps.api.addr_make("bar456"),
|
||||
node_families_contract_address: deps.api.addr_make("baz789"),
|
||||
rewarding_denom: "uatom".into(),
|
||||
params: ContractStateParams {
|
||||
delegations_params: DelegationsParams {
|
||||
|
||||
@@ -71,7 +71,7 @@ pub(crate) fn query_current_nym_node_version(
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::support::tests::test_helpers;
|
||||
use cosmwasm_std::{coin, Addr};
|
||||
use cosmwasm_std::coin;
|
||||
use mixnet_contract_common::{ConfigScoreParams, DelegationsParams, OperatorsParams};
|
||||
|
||||
#[test]
|
||||
@@ -80,9 +80,10 @@ pub(crate) mod tests {
|
||||
|
||||
#[allow(deprecated)]
|
||||
let dummy_state = ContractState {
|
||||
owner: Some(Addr::unchecked("foomp")),
|
||||
rewarding_validator_address: Addr::unchecked("monitor"),
|
||||
vesting_contract_address: Addr::unchecked("foomp"),
|
||||
owner: Some(deps.api.addr_make("foomp")),
|
||||
rewarding_validator_address: deps.api.addr_make("monitor"),
|
||||
vesting_contract_address: deps.api.addr_make("foomp"),
|
||||
node_families_contract_address: deps.api.addr_make("bar"),
|
||||
rewarding_denom: "unym".to_string(),
|
||||
params: ContractStateParams {
|
||||
delegations_params: DelegationsParams {
|
||||
|
||||
@@ -156,6 +156,14 @@ pub(crate) fn vesting_contract_address(storage: &dyn Storage) -> Result<Addr, Mi
|
||||
.map(|state| state.vesting_contract_address)?)
|
||||
}
|
||||
|
||||
pub(crate) fn node_families_contract_address(
|
||||
storage: &dyn Storage,
|
||||
) -> Result<Addr, MixnetContractError> {
|
||||
Ok(CONTRACT_STATE
|
||||
.load(storage)
|
||||
.map(|state| state.node_families_contract_address)?)
|
||||
}
|
||||
|
||||
pub(crate) fn state_params(
|
||||
storage: &dyn Storage,
|
||||
) -> Result<ContractStateParams, MixnetContractError> {
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::support::helpers::{
|
||||
ensure_epoch_in_progress_state, ensure_no_existing_bond, ensure_operating_cost_within_range,
|
||||
ensure_profit_margin_within_range, validate_pledge,
|
||||
};
|
||||
use cosmwasm_std::{coin, Coin, DepsMut, Env, MessageInfo, Response};
|
||||
use cosmwasm_std::{coin, wasm_execute, Coin, DepsMut, Env, MessageInfo, Response};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::events::{
|
||||
new_nym_node_bonding_event, new_pending_cost_params_update_event,
|
||||
@@ -29,6 +29,7 @@ use mixnet_contract_common::{
|
||||
PendingIntervalEventKind,
|
||||
};
|
||||
use nym_contracts_common::signing::{MessageSignature, SigningPurpose};
|
||||
use nym_node_families_contract_common::msg::ExecuteMsg as NodeFamiliesExecuteMsg;
|
||||
use serde::Serialize;
|
||||
|
||||
pub fn try_add_nym_node(
|
||||
@@ -147,13 +148,24 @@ pub(crate) fn try_remove_nym_node(
|
||||
};
|
||||
interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?;
|
||||
|
||||
Ok(
|
||||
Response::new().add_event(new_pending_nym_node_unbonding_event(
|
||||
// send message to the node families contract to remove this node from any family it might be a member of
|
||||
let node_families_contract_addr =
|
||||
mixnet_params_storage::node_families_contract_address(deps.storage)?;
|
||||
let remove_from_family_exec = wasm_execute(
|
||||
node_families_contract_addr,
|
||||
&NodeFamiliesExecuteMsg::OnNymNodeUnbond {
|
||||
node_id: existing_bond.node_id,
|
||||
},
|
||||
vec![],
|
||||
)?;
|
||||
|
||||
Ok(Response::new()
|
||||
.add_message(remove_from_family_exec)
|
||||
.add_event(new_pending_nym_node_unbonding_event(
|
||||
&existing_bond.owner,
|
||||
existing_bond.identity(),
|
||||
existing_bond.node_id,
|
||||
)),
|
||||
)
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) fn try_update_node_config(
|
||||
|
||||
@@ -1,21 +1,39 @@
|
||||
// Copyright 2022-2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::interval::storage as interval_storage;
|
||||
use crate::nodes::storage as nymnodes_storage;
|
||||
use cosmwasm_std::DepsMut;
|
||||
use crate::constants::CONTRACT_STATE_KEY;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use cosmwasm_std::{Addr, DepsMut};
|
||||
use cw_storage_plus::Item;
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::KeyRotationState;
|
||||
use mixnet_contract_common::{ContractState, ContractStateParams};
|
||||
|
||||
pub fn introduce_node_families_contract(
|
||||
deps: DepsMut,
|
||||
node_families_contract_address: Addr,
|
||||
) -> Result<(), MixnetContractError> {
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct OldContractState {
|
||||
owner: Option<Addr>,
|
||||
rewarding_validator_address: Addr,
|
||||
vesting_contract_address: Addr,
|
||||
rewarding_denom: String,
|
||||
params: ContractStateParams,
|
||||
}
|
||||
|
||||
const OLD_CONTRACT_STATE: Item<OldContractState> = Item::new(CONTRACT_STATE_KEY);
|
||||
let old = OLD_CONTRACT_STATE.load(deps.storage)?;
|
||||
|
||||
#[allow(deprecated)]
|
||||
let updated = ContractState {
|
||||
owner: old.owner,
|
||||
rewarding_validator_address: old.rewarding_validator_address,
|
||||
vesting_contract_address: old.vesting_contract_address,
|
||||
rewarding_denom: old.rewarding_denom,
|
||||
params: old.params,
|
||||
node_families_contract_address,
|
||||
};
|
||||
mixnet_params_storage::CONTRACT_STATE.save(deps.storage, &updated)?;
|
||||
|
||||
pub fn introduce_key_rotation_id(deps: DepsMut) -> Result<(), MixnetContractError> {
|
||||
let current_epoch_id =
|
||||
interval_storage::current_interval(deps.storage)?.current_epoch_absolute_id();
|
||||
nymnodes_storage::KEY_ROTATION_STATE.save(
|
||||
deps.storage,
|
||||
&KeyRotationState {
|
||||
validity_epochs: 24,
|
||||
initial_epoch_id: current_epoch_id,
|
||||
},
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,16 +5,26 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::contract::{execute, instantiate, migrate, query};
|
||||
use cosmwasm_std::Decimal;
|
||||
use cosmwasm_std::testing::{message_info, mock_env};
|
||||
use cosmwasm_std::{coin, coins, Addr, Decimal, MessageInfo, StdError, StdResult};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::nym_node::{NodeDetailsResponse, NodeOwnershipResponse, Role};
|
||||
use mixnet_contract_common::reward_params::RewardedSetParams;
|
||||
use mixnet_contract_common::{
|
||||
ExecuteMsg, InitialRewardingParams, InstantiateMsg, MigrateMsg, QueryMsg,
|
||||
CurrentIntervalResponse, EpochId, ExecuteMsg, InitialRewardingParams, InstantiateMsg, Interval,
|
||||
MigrateMsg, MixnetContractQuerier, NodeCostParams, NodeId, NymNode, NymNodeBondingPayload,
|
||||
QueryMsg, RoleAssignment, SignableNymNodeBondingMsg, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT,
|
||||
DEFAULT_PROFIT_MARGIN_PERCENT,
|
||||
};
|
||||
use nym_contracts_common::signing::{ContractMessageContent, MessageSignature};
|
||||
use nym_contracts_common::Percent;
|
||||
use nym_contracts_common_testing::{
|
||||
mock_dependencies, ContractFn, PermissionedFn, QueryFn, TEST_DENOM,
|
||||
mock_dependencies, ArbitraryContractStorageReader, ArbitraryContractStorageWriter, BankExt,
|
||||
ChainOpts, ContractFn, ContractTester, PermissionedFn, QueryFn, RandExt, TEST_DENOM,
|
||||
};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::time::Duration;
|
||||
|
||||
pub use nym_contracts_common_testing::TestableNymContract;
|
||||
@@ -74,6 +84,10 @@ impl TestableNymContract for MixnetContract {
|
||||
InstantiateMsg {
|
||||
rewarding_validator_address: deps.api.addr_make("rewarder").to_string(),
|
||||
vesting_contract_address: deps.api.addr_make("vesting-contract").to_string(),
|
||||
node_families_contract_address: deps
|
||||
.api
|
||||
.addr_make("node-families-contract")
|
||||
.to_string(),
|
||||
rewarding_denom: TEST_DENOM.to_string(),
|
||||
epochs_in_interval: 720,
|
||||
epoch_duration: Duration::from_secs(60 * 60),
|
||||
@@ -87,3 +101,203 @@ impl TestableNymContract for MixnetContract {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EmbeddedMixnetContractExt:
|
||||
ChainOpts + ArbitraryContractStorageWriter + ArbitraryContractStorageReader + RandExt + BankExt
|
||||
{
|
||||
fn mixnet_contract_address(&self) -> StdResult<Addr>;
|
||||
|
||||
fn execute_mixnet_contract(&mut self, sender: MessageInfo, msg: &ExecuteMsg) -> StdResult<()> {
|
||||
let address = self.mixnet_contract_address()?;
|
||||
|
||||
self.execute_arbitrary_contract(address, sender, msg)
|
||||
.map_err(|err| {
|
||||
StdError::generic_err(format!("mixnet contract execution failure: {err}"))
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_from_mixnet_contract_storage<T: DeserializeOwned>(
|
||||
&self,
|
||||
key: impl AsRef<[u8]>,
|
||||
) -> StdResult<T> {
|
||||
let address = self.mixnet_contract_address()?;
|
||||
|
||||
self.must_read_value_from_contract_storage(address, key)
|
||||
}
|
||||
|
||||
fn write_to_mixnet_contract_storage(
|
||||
&mut self,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: impl AsRef<[u8]>,
|
||||
) -> StdResult<()> {
|
||||
let address = self.mixnet_contract_address()?;
|
||||
|
||||
<Self as ArbitraryContractStorageWriter>::set_contract_storage(self, address, key, value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_to_mixnet_contract_storage_value<T: Serialize>(
|
||||
&mut self,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: &T,
|
||||
) -> StdResult<()> {
|
||||
let address = self.mixnet_contract_address()?;
|
||||
|
||||
self.set_contract_storage_value(address, key, value)
|
||||
}
|
||||
|
||||
fn current_mixnet_epoch(&self) -> StdResult<EpochId> {
|
||||
let address = self.mixnet_contract_address()?;
|
||||
|
||||
Ok(self
|
||||
.deps()
|
||||
.querier
|
||||
.query_current_mixnet_interval(address.clone())?
|
||||
.current_epoch_absolute_id())
|
||||
}
|
||||
|
||||
fn advance_mixnet_epoch(&mut self) -> StdResult<()> {
|
||||
let interval_details: CurrentIntervalResponse = self.query_arbitrary_contract(
|
||||
self.mixnet_contract_address()?,
|
||||
&QueryMsg::GetCurrentIntervalDetails {},
|
||||
)?;
|
||||
let until_end = interval_details.time_until_current_epoch_end().as_secs();
|
||||
let timestamp = self.env().block.time.plus_seconds(until_end + 1);
|
||||
self.set_block_time(timestamp);
|
||||
self.next_block();
|
||||
|
||||
// this was hardcoded in mixnet init
|
||||
let mixnet_rewarder = self.addr_make("rewarder");
|
||||
let rewarder = message_info(&mixnet_rewarder, &[]);
|
||||
self.execute_mixnet_contract(rewarder.clone(), &ExecuteMsg::BeginEpochTransition {})?;
|
||||
self.execute_mixnet_contract(
|
||||
rewarder.clone(),
|
||||
&ExecuteMsg::ReconcileEpochEvents { limit: None },
|
||||
)?;
|
||||
|
||||
for role in [
|
||||
Role::ExitGateway,
|
||||
Role::EntryGateway,
|
||||
Role::Layer1,
|
||||
Role::Layer2,
|
||||
Role::Layer3,
|
||||
Role::Standby,
|
||||
] {
|
||||
self.execute_mixnet_contract(
|
||||
rewarder.clone(),
|
||||
&ExecuteMsg::AssignRoles {
|
||||
assignment: RoleAssignment {
|
||||
role,
|
||||
nodes: vec![],
|
||||
},
|
||||
},
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_mixnet_epoch(&mut self, epoch_id: EpochId) -> StdResult<()> {
|
||||
let address = self.mixnet_contract_address()?;
|
||||
|
||||
let interval = self
|
||||
.deps()
|
||||
.querier
|
||||
.query_current_mixnet_interval(address.clone())?;
|
||||
|
||||
let mut to_update = if interval.current_epoch_absolute_id() <= epoch_id {
|
||||
interval
|
||||
} else {
|
||||
Interval::init_interval(
|
||||
interval.epochs_in_interval(),
|
||||
interval.epoch_length(),
|
||||
&mock_env(),
|
||||
)
|
||||
};
|
||||
|
||||
let current = to_update.current_epoch_absolute_id();
|
||||
let diff = epoch_id - current;
|
||||
for _ in 0..diff {
|
||||
to_update = to_update.advance_epoch();
|
||||
}
|
||||
self.set_contract_storage_value(&address, b"ci", &to_update)
|
||||
}
|
||||
|
||||
fn bond_dummy_nymnode_for(&mut self, node_owner: &Addr) -> Result<NodeId, StdError> {
|
||||
let pledge = coins(100_000000, TEST_DENOM);
|
||||
let keypair = ed25519::KeyPair::new(self.raw_rng());
|
||||
let identity_key = keypair.public_key().to_base58_string();
|
||||
|
||||
let node = NymNode {
|
||||
host: "1.2.3.4".to_string(),
|
||||
custom_http_port: None,
|
||||
identity_key,
|
||||
};
|
||||
let cost_params = NodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT)
|
||||
.unwrap(),
|
||||
interval_operating_cost: coin(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, TEST_DENOM),
|
||||
};
|
||||
// initial signing nonce is 0 for a new address
|
||||
let signing_nonce = 0;
|
||||
|
||||
let payload = NymNodeBondingPayload::new(node.clone(), cost_params.clone());
|
||||
let content = ContractMessageContent::new(node_owner.clone(), pledge.clone(), payload);
|
||||
let msg = SignableNymNodeBondingMsg::new(signing_nonce, content);
|
||||
|
||||
let owner_signature = keypair.private_key().sign(msg.to_plaintext()?);
|
||||
let owner_signature = MessageSignature::from(owner_signature.to_bytes().as_ref());
|
||||
|
||||
self.execute_mixnet_contract(
|
||||
message_info(node_owner, &pledge),
|
||||
&ExecuteMsg::BondNymNode {
|
||||
node,
|
||||
cost_params,
|
||||
owner_signature,
|
||||
},
|
||||
)?;
|
||||
|
||||
let bond: NodeOwnershipResponse = self.query_arbitrary_contract(
|
||||
self.mixnet_contract_address()?,
|
||||
&QueryMsg::GetOwnedNymNode {
|
||||
address: node_owner.to_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(bond.details.unwrap().bond_information.node_id)
|
||||
}
|
||||
|
||||
fn bond_dummy_nymnode(&mut self) -> Result<NodeId, StdError> {
|
||||
let node_owner = self.generate_account_with_balance();
|
||||
self.bond_dummy_nymnode_for(&node_owner)
|
||||
}
|
||||
|
||||
fn unbond_nymnode(&mut self, node_id: NodeId) -> Result<(), StdError> {
|
||||
let bond: NodeDetailsResponse = self.query_arbitrary_contract(
|
||||
self.mixnet_contract_address()?,
|
||||
&QueryMsg::GetNymNodeDetails { node_id },
|
||||
)?;
|
||||
|
||||
let node_owner = bond.details.unwrap().bond_information.owner;
|
||||
|
||||
self.execute_mixnet_contract(
|
||||
message_info(&node_owner, &[]),
|
||||
&ExecuteMsg::UnbondNymNode {},
|
||||
)?;
|
||||
|
||||
self.advance_mixnet_epoch()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> EmbeddedMixnetContractExt for ContractTester<C>
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
fn mixnet_contract_address(&self) -> StdResult<Addr> {
|
||||
self.well_known_contracts
|
||||
.get(MixnetContract::NAME)
|
||||
.ok_or_else(|| StdError::generic_err("mixnet contract not part of the tester"))
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,62 @@
|
||||
[package]
|
||||
name = "node-families"
|
||||
description = "Nym Node Families contract"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "schema"
|
||||
required-features = ["schema-gen"]
|
||||
|
||||
[lib]
|
||||
name = "node_families_contract"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
cw-storage-plus = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
cosmwasm-schema = { workspace = true, optional = true }
|
||||
cw-utils = { workspace = true }
|
||||
|
||||
nym-contracts-common = { workspace = true }
|
||||
nym-node-families-contract-common = { workspace = true }
|
||||
nym-mixnet-contract-common = { workspace = true }
|
||||
|
||||
# Optional deps activated by the `testable-node-families-contract` feature so
|
||||
# downstream crates can pull in `crate::testing` for their own test harnesses.
|
||||
nym-contracts-common-testing = { workspace = true, optional = true }
|
||||
nym-mixnet-contract = { workspace = true, optional = true, features = ["testable-mixnet-contract"] }
|
||||
nym-crypto = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
# make the testing helpers available for this crate's own unit tests via
|
||||
# `cfg(test)`; downstream crates instead pull these in through the
|
||||
# `testable-node-families-contract` feature.
|
||||
nym-contracts-common-testing = { workspace = true }
|
||||
nym-mixnet-contract = { workspace = true, features = ["testable-mixnet-contract"] }
|
||||
nym-crypto = { workspace = true, features = ["asymmetric"] }
|
||||
|
||||
[features]
|
||||
schema-gen = ["nym-node-families-contract-common/schema", "cosmwasm-schema"]
|
||||
testable-node-families-contract = [
|
||||
"nym-contracts-common-testing",
|
||||
"nym-mixnet-contract",
|
||||
"nym-crypto",
|
||||
"nym-crypto/asymmetric",
|
||||
]
|
||||
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,5 @@
|
||||
wasm:
|
||||
RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown
|
||||
|
||||
generate-schema:
|
||||
cargo schema
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,318 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ExecuteMsg",
|
||||
"description": "Execute messages accepted by the contract.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Replace the contract's runtime [`Config`]. Restricted to the contract admin.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"update_config"
|
||||
],
|
||||
"properties": {
|
||||
"update_config": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"config"
|
||||
],
|
||||
"properties": {
|
||||
"config": {
|
||||
"$ref": "#/definitions/Config"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Create a new family owned by the message sender. The configured `create_family_fee` must be attached as funds.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"create_family"
|
||||
],
|
||||
"properties": {
|
||||
"create_family": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Disband the family owned by the message sender. The family must have no current members; any still-pending invitations are revoked.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"disband_family"
|
||||
],
|
||||
"properties": {
|
||||
"disband_family": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Invite a node to the family owned by the message sender. If `validity_secs` is omitted the invitation expires `default_invitation_validity_secs` seconds (from [`Config`]) after the current block time.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"invite_to_family"
|
||||
],
|
||||
"properties": {
|
||||
"invite_to_family": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"validity_secs": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Revoke a still-pending invitation previously issued by the sender's family.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"revoke_family_invitation"
|
||||
],
|
||||
"properties": {
|
||||
"revoke_family_invitation": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Accept a pending invitation. The sender must control `node_id`.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"accept_family_invitation"
|
||||
],
|
||||
"properties": {
|
||||
"accept_family_invitation": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Reject a pending invitation. The sender must control `node_id`.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"reject_family_invitation"
|
||||
],
|
||||
"properties": {
|
||||
"reject_family_invitation": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Leave the family `node_id` currently belongs to. The sender must control `node_id`.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"leave_family"
|
||||
],
|
||||
"properties": {
|
||||
"leave_family": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Remove `node_id` from the family owned by the message sender.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kick_from_family"
|
||||
],
|
||||
"properties": {
|
||||
"kick_from_family": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Cross-contract callback fired by the mixnet contract the moment node with `node_id` initiates unbonding. Removes the node from any family it currently belongs to and rejects every pending invitation issued to it. Sender must be the configured mixnet contract address.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"on_nym_node_unbond"
|
||||
],
|
||||
"properties": {
|
||||
"on_nym_node_unbond": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"Coin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Config": {
|
||||
"description": "Runtime configuration of the node families contract.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"create_family_fee",
|
||||
"default_invitation_validity_secs",
|
||||
"family_description_length_limit",
|
||||
"family_name_length_limit"
|
||||
],
|
||||
"properties": {
|
||||
"create_family_fee": {
|
||||
"description": "Fee charged on each successful `create_family` execution.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_invitation_validity_secs": {
|
||||
"description": "Default lifetime, in seconds, used by `invite_to_family` when the sender doesn't supply an explicit value. Senders may override this per-invitation via the optional `validity_secs` argument.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"family_description_length_limit": {
|
||||
"description": "Maximum allowed length, in characters, of a family description.",
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"family_name_length_limit": {
|
||||
"description": "Maximum allowed length, in characters, of a family name.",
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Uint128": {
|
||||
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "InstantiateMsg",
|
||||
"description": "Message used to instantiate the node families contract.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"config",
|
||||
"mixnet_contract_address"
|
||||
],
|
||||
"properties": {
|
||||
"config": {
|
||||
"$ref": "#/definitions/Config"
|
||||
},
|
||||
"mixnet_contract_address": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Coin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Config": {
|
||||
"description": "Runtime configuration of the node families contract.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"create_family_fee",
|
||||
"default_invitation_validity_secs",
|
||||
"family_description_length_limit",
|
||||
"family_name_length_limit"
|
||||
],
|
||||
"properties": {
|
||||
"create_family_fee": {
|
||||
"description": "Fee charged on each successful `create_family` execution.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_invitation_validity_secs": {
|
||||
"description": "Default lifetime, in seconds, used by `invite_to_family` when the sender doesn't supply an explicit value. Senders may override this per-invitation via the optional `validity_secs` argument.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"family_description_length_limit": {
|
||||
"description": "Maximum allowed length, in characters, of a family description.",
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"family_name_length_limit": {
|
||||
"description": "Maximum allowed length, in characters, of a family name.",
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Uint128": {
|
||||
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "MigrateMsg",
|
||||
"description": "Message passed to the contract's `migrate` entry point.",
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,620 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "QueryMsg",
|
||||
"description": "Query messages accepted by the contract.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Look up a single family by its id.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_family_by_id"
|
||||
],
|
||||
"properties": {
|
||||
"get_family_by_id": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Look up the (at most one) family owned by a given address.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_family_by_owner"
|
||||
],
|
||||
"properties": {
|
||||
"get_family_by_owner": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Look up a single family by its name. The lookup is normalised contract-side (lowercased, non-alphanumerics stripped), so equivalent inputs resolve to the same family.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_family_by_name"
|
||||
],
|
||||
"properties": {
|
||||
"get_family_by_name": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_families_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_families_paged": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Look up which family — if any — a node currently belongs to.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_family_membership"
|
||||
],
|
||||
"properties": {
|
||||
"get_family_membership": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Page through every node currently in a given family.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_family_members_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_family_members_paged": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Page through every current family member across all families, in ascending [`NodeId`] order. Each entry carries the membership record (which in turn names the family the node belongs to).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_all_family_members_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_all_family_members_paged": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Look up the pending invitation for a specific `(family_id, node_id)` pair.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_pending_invitation"
|
||||
],
|
||||
"properties": {
|
||||
"get_pending_invitation": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Page through every pending invitation issued by a given family.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_pending_invitations_for_family_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_pending_invitations_for_family_paged": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Page through every pending invitation issued for a given node.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_pending_invitations_for_node_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_pending_invitations_for_node_paged": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Page through every pending invitation across all families.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_all_pending_invitations_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_all_pending_invitations_paged": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Page through every archived (terminal-state) invitation issued by a given family.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_past_invitations_for_family_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_past_invitations_for_family_paged": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Page through every archived (terminal-state) invitation issued to a given node.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_past_invitations_for_node_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_past_invitations_for_node_paged": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Page through every archived (terminal-state) invitation across all families.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_all_past_invitations_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_all_past_invitations_paged": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Page through every archived membership record for a given family (nodes that used to belong to it but have since been removed).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_past_members_for_family_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_past_members_for_family_paged": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Page through every archived membership record for a given node (every family the node used to belong to but has since been removed from), across all families.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_past_members_for_node_paged"
|
||||
],
|
||||
"properties": {
|
||||
"get_past_members_for_node_paged": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "AllFamilyMembersPagedResponse",
|
||||
"description": "Response to [`QueryMsg::GetAllFamilyMembersPaged`](crate::QueryMsg::GetAllFamilyMembersPaged).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"members"
|
||||
],
|
||||
"properties": {
|
||||
"members": {
|
||||
"description": "The members on this page, in ascending [`NodeId`] order across every family.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/FamilyMemberRecord"
|
||||
}
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Cursor (last `node_id`) to pass as `start_after` on the next call, or `None` if this page is empty (treat as end-of-list).",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyMemberRecord": {
|
||||
"description": "One entry in a [`FamilyMembersPagedResponse`] page — pairs a node id with its [`FamilyMembership`] record (notably its `joined_at` timestamp).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"membership",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"membership": {
|
||||
"description": "The membership record (carries `family_id` and `joined_at`).",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FamilyMembership"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_id": {
|
||||
"description": "The node currently in the family.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"FamilyMembership": {
|
||||
"description": "On-chain record of a node's current family membership.\n\nA node belongs to at most one family at a time, so this is keyed by `NodeId` alone — `family_id` is carried in the value to support reverse lookups (all nodes in a given family) via a secondary index.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id",
|
||||
"joined_at"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"description": "The family the node is currently a member of.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"joined_at": {
|
||||
"description": "Block timestamp (unix seconds) at which the node accepted its invitation and joined the family.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "AllPastFamilyInvitationsPagedResponse",
|
||||
"description": "Response to [`QueryMsg::GetAllPastInvitationsPaged`](crate::QueryMsg::GetAllPastInvitationsPaged).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"invitations"
|
||||
],
|
||||
"properties": {
|
||||
"invitations": {
|
||||
"description": "The archived invitations on this page, in ascending `((family_id, node_id), counter)` order across all terminal statuses.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PastFamilyInvitation"
|
||||
}
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Cursor to pass as `start_after` on the next call, or `None` if this page is empty (treat as end-of-list).",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
"family_id",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"expires_at": {
|
||||
"description": "Block timestamp (unix seconds) after which the invitation is no longer valid.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"family_id": {
|
||||
"description": "The family that issued the invitation.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"description": "The node being invited.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"FamilyInvitationStatus": {
|
||||
"description": "Terminal status for an invitation that has been moved out of the pending set.\n\nNote: timed-out invitations are not represented here — they are simply left in the pending set (see `FamilyInvitation::expires_at`).",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Still awaiting a response. Recorded with a timestamp for completeness even though pending invitations live in a separate map.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"pending"
|
||||
],
|
||||
"properties": {
|
||||
"pending": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "The invitee accepted and joined the family at the given timestamp.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"accepted"
|
||||
],
|
||||
"properties": {
|
||||
"accepted": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "The invitee explicitly rejected the invitation at the given timestamp.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"rejected"
|
||||
],
|
||||
"properties": {
|
||||
"rejected": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "The family revoked the invitation at the given timestamp before it could be accepted or rejected.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"revoked"
|
||||
],
|
||||
"properties": {
|
||||
"revoked": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"PastFamilyInvitation": {
|
||||
"description": "Historical record of an invitation that has reached a terminal state (`Accepted`, `Rejected`, or `Revoked`). Timed-out invitations are **not** archived here — they remain in the pending map until explicitly cleared.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"invitation",
|
||||
"status"
|
||||
],
|
||||
"properties": {
|
||||
"invitation": {
|
||||
"description": "The original invitation as it was issued.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FamilyInvitation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"description": "What ultimately happened to it.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FamilyInvitationStatus"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PendingInvitationsPagedResponse",
|
||||
"description": "Response to [`QueryMsg::GetAllPendingInvitationsPaged`](crate::QueryMsg::GetAllPendingInvitationsPaged).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"invitations"
|
||||
],
|
||||
"properties": {
|
||||
"invitations": {
|
||||
"description": "The pending invitations on this page, in ascending `(family_id, node_id)` order, each stamped with whether it had already timed out at the time the query was served.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PendingFamilyInvitationDetails"
|
||||
}
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Cursor (last `(family_id, node_id)` pair) to pass as `start_after` on the next call, or `None` if this page is empty (treat as end-of-list).",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
"family_id",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"expires_at": {
|
||||
"description": "Block timestamp (unix seconds) after which the invitation is no longer valid.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"family_id": {
|
||||
"description": "The family that issued the invitation.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"description": "The node being invited.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PendingFamilyInvitationDetails": {
|
||||
"description": "A pending [`FamilyInvitation`] paired with whether it has already timed out at the time the query was served.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expired",
|
||||
"invitation"
|
||||
],
|
||||
"properties": {
|
||||
"expired": {
|
||||
"description": "`true` iff `now >= invitation.expires_at` at query time, i.e. the invitation is still in the pending map but can no longer be acted on.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"invitation": {
|
||||
"description": "The stored invitation as it was issued.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FamilyInvitation"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "FamiliesPagedResponse",
|
||||
"description": "Response to [`QueryMsg::GetFamiliesPaged`](crate::QueryMsg::GetFamiliesPaged).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"families"
|
||||
],
|
||||
"properties": {
|
||||
"families": {
|
||||
"description": "The families on this page, in ascending [`NodeFamilyId`] order.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/NodeFamily"
|
||||
}
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Cursor to pass as `start_after` on the next call, or `None` if this page is empty (which the caller should treat as end-of-list).",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"Coin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"NodeFamily": {
|
||||
"description": "On-chain representation of a node family.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"created_at",
|
||||
"description",
|
||||
"id",
|
||||
"members",
|
||||
"name",
|
||||
"normalised_name",
|
||||
"owner",
|
||||
"paid_fee"
|
||||
],
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"description": "Timestamp of the creation of the node family",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"description": {
|
||||
"description": "The optional description of the node family",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "The id of the node family",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"members": {
|
||||
"description": "Memoized value of the current number of members in the node family Used to detect if the family is empty",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"name": {
|
||||
"description": "The name of the node family",
|
||||
"type": "string"
|
||||
},
|
||||
"normalised_name": {
|
||||
"description": "Normalised name of the node family used for uniqueness checks",
|
||||
"type": "string"
|
||||
},
|
||||
"owner": {
|
||||
"description": "The owner of the node family",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
]
|
||||
},
|
||||
"paid_fee": {
|
||||
"description": "Records the fee paid when the family was created, so that the appropriate amount could be returned upon it getting disbanded.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Uint128": {
|
||||
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "NodeFamilyResponse",
|
||||
"description": "Response to [`QueryMsg::GetFamilyById`](crate::QueryMsg::GetFamilyById).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id"
|
||||
],
|
||||
"properties": {
|
||||
"family": {
|
||||
"description": "The matching family, or `None` if no family with `family_id` exists.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/NodeFamily"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"family_id": {
|
||||
"description": "The id that was queried, echoed back so paginated callers can correlate.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"Coin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"NodeFamily": {
|
||||
"description": "On-chain representation of a node family.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"created_at",
|
||||
"description",
|
||||
"id",
|
||||
"members",
|
||||
"name",
|
||||
"normalised_name",
|
||||
"owner",
|
||||
"paid_fee"
|
||||
],
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"description": "Timestamp of the creation of the node family",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"description": {
|
||||
"description": "The optional description of the node family",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "The id of the node family",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"members": {
|
||||
"description": "Memoized value of the current number of members in the node family Used to detect if the family is empty",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"name": {
|
||||
"description": "The name of the node family",
|
||||
"type": "string"
|
||||
},
|
||||
"normalised_name": {
|
||||
"description": "Normalised name of the node family used for uniqueness checks",
|
||||
"type": "string"
|
||||
},
|
||||
"owner": {
|
||||
"description": "The owner of the node family",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
]
|
||||
},
|
||||
"paid_fee": {
|
||||
"description": "Records the fee paid when the family was created, so that the appropriate amount could be returned upon it getting disbanded.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Uint128": {
|
||||
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "NodeFamilyByNameResponse",
|
||||
"description": "Response to [`QueryMsg::GetFamilyByName`](crate::QueryMsg::GetFamilyByName).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"family": {
|
||||
"description": "The matching family, or `None` if no family with that name exists.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/NodeFamily"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"description": "The name that was queried, echoed back so callers can correlate.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"Coin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"NodeFamily": {
|
||||
"description": "On-chain representation of a node family.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"created_at",
|
||||
"description",
|
||||
"id",
|
||||
"members",
|
||||
"name",
|
||||
"normalised_name",
|
||||
"owner",
|
||||
"paid_fee"
|
||||
],
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"description": "Timestamp of the creation of the node family",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"description": {
|
||||
"description": "The optional description of the node family",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "The id of the node family",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"members": {
|
||||
"description": "Memoized value of the current number of members in the node family Used to detect if the family is empty",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"name": {
|
||||
"description": "The name of the node family",
|
||||
"type": "string"
|
||||
},
|
||||
"normalised_name": {
|
||||
"description": "Normalised name of the node family used for uniqueness checks",
|
||||
"type": "string"
|
||||
},
|
||||
"owner": {
|
||||
"description": "The owner of the node family",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
]
|
||||
},
|
||||
"paid_fee": {
|
||||
"description": "Records the fee paid when the family was created, so that the appropriate amount could be returned upon it getting disbanded.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Uint128": {
|
||||
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "NodeFamilyByOwnerResponse",
|
||||
"description": "Response to [`QueryMsg::GetFamilyByOwner`](crate::QueryMsg::GetFamilyByOwner).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"family": {
|
||||
"description": "The matching family, or `None` if `owner` does not currently own one.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/NodeFamily"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"owner": {
|
||||
"description": "The (validated) owner address that was queried, echoed back so callers can correlate.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"Coin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"NodeFamily": {
|
||||
"description": "On-chain representation of a node family.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"created_at",
|
||||
"description",
|
||||
"id",
|
||||
"members",
|
||||
"name",
|
||||
"normalised_name",
|
||||
"owner",
|
||||
"paid_fee"
|
||||
],
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"description": "Timestamp of the creation of the node family",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"description": {
|
||||
"description": "The optional description of the node family",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "The id of the node family",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"members": {
|
||||
"description": "Memoized value of the current number of members in the node family Used to detect if the family is empty",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"name": {
|
||||
"description": "The name of the node family",
|
||||
"type": "string"
|
||||
},
|
||||
"normalised_name": {
|
||||
"description": "Normalised name of the node family used for uniqueness checks",
|
||||
"type": "string"
|
||||
},
|
||||
"owner": {
|
||||
"description": "The owner of the node family",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
]
|
||||
},
|
||||
"paid_fee": {
|
||||
"description": "Records the fee paid when the family was created, so that the appropriate amount could be returned upon it getting disbanded.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Uint128": {
|
||||
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "FamilyMembersPagedResponse",
|
||||
"description": "Response to [`QueryMsg::GetFamilyMembersPaged`](crate::QueryMsg::GetFamilyMembersPaged).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id",
|
||||
"members"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"description": "The family whose members were queried, echoed back so paginated callers can correlate.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"members": {
|
||||
"description": "The members on this page, in ascending [`NodeId`] order.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/FamilyMemberRecord"
|
||||
}
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Cursor to pass as `start_after` on the next call, or `None` if this page is empty (which the caller should treat as end-of-list).",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyMemberRecord": {
|
||||
"description": "One entry in a [`FamilyMembersPagedResponse`] page — pairs a node id with its [`FamilyMembership`] record (notably its `joined_at` timestamp).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"membership",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"membership": {
|
||||
"description": "The membership record (carries `family_id` and `joined_at`).",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FamilyMembership"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_id": {
|
||||
"description": "The node currently in the family.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"FamilyMembership": {
|
||||
"description": "On-chain record of a node's current family membership.\n\nA node belongs to at most one family at a time, so this is keyed by `NodeId` alone — `family_id` is carried in the value to support reverse lookups (all nodes in a given family) via a secondary index.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id",
|
||||
"joined_at"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"description": "The family the node is currently a member of.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"joined_at": {
|
||||
"description": "Block timestamp (unix seconds) at which the node accepted its invitation and joined the family.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "NodeFamilyMembershipResponse",
|
||||
"description": "Response to [`QueryMsg::GetFamilyMembership`](crate::QueryMsg::GetFamilyMembership).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"description": "The id of the family the node currently belongs to, or `None` if the node is not currently a member of any family.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"description": "The node that was queried.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PastFamilyInvitationsPagedResponse",
|
||||
"description": "Response to [`QueryMsg::GetPastInvitationsForFamilyPaged`](crate::QueryMsg::GetPastInvitationsForFamilyPaged).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id",
|
||||
"invitations"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"description": "The family whose archived invitations were queried, echoed back so paginated callers can correlate.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"invitations": {
|
||||
"description": "The archived invitations on this page, in ascending `(node_id, counter)` order across all terminal statuses.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PastFamilyInvitation"
|
||||
}
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Cursor to pass as `start_after` on the next call, or `None` if this page is empty (treat as end-of-list).",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
"family_id",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"expires_at": {
|
||||
"description": "Block timestamp (unix seconds) after which the invitation is no longer valid.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"family_id": {
|
||||
"description": "The family that issued the invitation.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"description": "The node being invited.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"FamilyInvitationStatus": {
|
||||
"description": "Terminal status for an invitation that has been moved out of the pending set.\n\nNote: timed-out invitations are not represented here — they are simply left in the pending set (see `FamilyInvitation::expires_at`).",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Still awaiting a response. Recorded with a timestamp for completeness even though pending invitations live in a separate map.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"pending"
|
||||
],
|
||||
"properties": {
|
||||
"pending": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "The invitee accepted and joined the family at the given timestamp.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"accepted"
|
||||
],
|
||||
"properties": {
|
||||
"accepted": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "The invitee explicitly rejected the invitation at the given timestamp.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"rejected"
|
||||
],
|
||||
"properties": {
|
||||
"rejected": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "The family revoked the invitation at the given timestamp before it could be accepted or rejected.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"revoked"
|
||||
],
|
||||
"properties": {
|
||||
"revoked": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"PastFamilyInvitation": {
|
||||
"description": "Historical record of an invitation that has reached a terminal state (`Accepted`, `Rejected`, or `Revoked`). Timed-out invitations are **not** archived here — they remain in the pending map until explicitly cleared.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"invitation",
|
||||
"status"
|
||||
],
|
||||
"properties": {
|
||||
"invitation": {
|
||||
"description": "The original invitation as it was issued.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FamilyInvitation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"description": "What ultimately happened to it.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FamilyInvitationStatus"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PastFamilyInvitationsForNodePagedResponse",
|
||||
"description": "Response to [`QueryMsg::GetPastInvitationsForNodePaged`](crate::QueryMsg::GetPastInvitationsForNodePaged).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"invitations",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"invitations": {
|
||||
"description": "The archived invitations addressed to this node on this page, in ascending `(family_id, counter)` order across all terminal statuses.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PastFamilyInvitation"
|
||||
}
|
||||
},
|
||||
"node_id": {
|
||||
"description": "The node whose past invitations were queried, echoed back so paginated callers can correlate.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Cursor to pass as `start_after` on the next call, or `None` if this page is empty (treat as end-of-list).",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
"family_id",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"expires_at": {
|
||||
"description": "Block timestamp (unix seconds) after which the invitation is no longer valid.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"family_id": {
|
||||
"description": "The family that issued the invitation.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"description": "The node being invited.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"FamilyInvitationStatus": {
|
||||
"description": "Terminal status for an invitation that has been moved out of the pending set.\n\nNote: timed-out invitations are not represented here — they are simply left in the pending set (see `FamilyInvitation::expires_at`).",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Still awaiting a response. Recorded with a timestamp for completeness even though pending invitations live in a separate map.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"pending"
|
||||
],
|
||||
"properties": {
|
||||
"pending": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "The invitee accepted and joined the family at the given timestamp.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"accepted"
|
||||
],
|
||||
"properties": {
|
||||
"accepted": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "The invitee explicitly rejected the invitation at the given timestamp.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"rejected"
|
||||
],
|
||||
"properties": {
|
||||
"rejected": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "The family revoked the invitation at the given timestamp before it could be accepted or rejected.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"revoked"
|
||||
],
|
||||
"properties": {
|
||||
"revoked": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"PastFamilyInvitation": {
|
||||
"description": "Historical record of an invitation that has reached a terminal state (`Accepted`, `Rejected`, or `Revoked`). Timed-out invitations are **not** archived here — they remain in the pending map until explicitly cleared.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"invitation",
|
||||
"status"
|
||||
],
|
||||
"properties": {
|
||||
"invitation": {
|
||||
"description": "The original invitation as it was issued.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FamilyInvitation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"description": "What ultimately happened to it.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FamilyInvitationStatus"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PastFamilyMembersPagedResponse",
|
||||
"description": "Response to [`QueryMsg::GetPastMembersForFamilyPaged`](crate::QueryMsg::GetPastMembersForFamilyPaged).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id",
|
||||
"members"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"description": "The family whose archived memberships were queried, echoed back so paginated callers can correlate.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"members": {
|
||||
"description": "The archived membership records on this page, in ascending `(node_id, counter)` order.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PastFamilyMember"
|
||||
}
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Cursor to pass as `start_after` on the next call, or `None` if this page is empty (treat as end-of-list).",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"PastFamilyMember": {
|
||||
"description": "Historical record of a node that used to be part of a family but has since been removed (kicked, left voluntarily, or because the family was disbanded).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id",
|
||||
"node_id",
|
||||
"removed_at"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"description": "The family the node used to belong to.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"description": "The node that was removed.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"removed_at": {
|
||||
"description": "Block timestamp (unix seconds) at which the membership was terminated.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PastFamilyMembersForNodePagedResponse",
|
||||
"description": "Response to [`QueryMsg::GetPastMembersForNodePaged`](crate::QueryMsg::GetPastMembersForNodePaged).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"members",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"members": {
|
||||
"description": "The archived membership records for this node on this page, in ascending `(family_id, counter)` order.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PastFamilyMember"
|
||||
}
|
||||
},
|
||||
"node_id": {
|
||||
"description": "The node whose archived memberships were queried, echoed back so paginated callers can correlate.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Cursor to pass as `start_after` on the next call, or `None` if this page is empty (treat as end-of-list).",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"PastFamilyMember": {
|
||||
"description": "Historical record of a node that used to be part of a family but has since been removed (kicked, left voluntarily, or because the family was disbanded).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id",
|
||||
"node_id",
|
||||
"removed_at"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"description": "The family the node used to belong to.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"description": "The node that was removed.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"removed_at": {
|
||||
"description": "Block timestamp (unix seconds) at which the membership was terminated.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PendingFamilyInvitationResponse",
|
||||
"description": "Response to [`QueryMsg::GetPendingInvitation`](crate::QueryMsg::GetPendingInvitation).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"description": "The family component of the queried `(family_id, node_id)` key.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"invitation": {
|
||||
"description": "The matching pending invitation along with an explicit expiry flag, or `None` if no such invitation exists.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PendingFamilyInvitationDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_id": {
|
||||
"description": "The node component of the queried `(family_id, node_id)` key.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
"family_id",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"expires_at": {
|
||||
"description": "Block timestamp (unix seconds) after which the invitation is no longer valid.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"family_id": {
|
||||
"description": "The family that issued the invitation.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"description": "The node being invited.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PendingFamilyInvitationDetails": {
|
||||
"description": "A pending [`FamilyInvitation`] paired with whether it has already timed out at the time the query was served.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expired",
|
||||
"invitation"
|
||||
],
|
||||
"properties": {
|
||||
"expired": {
|
||||
"description": "`true` iff `now >= invitation.expires_at` at query time, i.e. the invitation is still in the pending map but can no longer be acted on.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"invitation": {
|
||||
"description": "The stored invitation as it was issued.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FamilyInvitation"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PendingFamilyInvitationsPagedResponse",
|
||||
"description": "Response to [`QueryMsg::GetPendingInvitationsForFamilyPaged`](crate::QueryMsg::GetPendingInvitationsForFamilyPaged).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"family_id",
|
||||
"invitations"
|
||||
],
|
||||
"properties": {
|
||||
"family_id": {
|
||||
"description": "The family whose pending invitations were queried, echoed back so paginated callers can correlate.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"invitations": {
|
||||
"description": "The pending invitations on this page, in ascending invitee [`NodeId`] order, each stamped with whether it had already timed out at the time the query was served.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PendingFamilyInvitationDetails"
|
||||
}
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Cursor (last invitee node id) to pass as `start_after` on the next call, or `None` if this page is empty (treat as end-of-list).",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
"family_id",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"expires_at": {
|
||||
"description": "Block timestamp (unix seconds) after which the invitation is no longer valid.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"family_id": {
|
||||
"description": "The family that issued the invitation.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"description": "The node being invited.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PendingFamilyInvitationDetails": {
|
||||
"description": "A pending [`FamilyInvitation`] paired with whether it has already timed out at the time the query was served.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expired",
|
||||
"invitation"
|
||||
],
|
||||
"properties": {
|
||||
"expired": {
|
||||
"description": "`true` iff `now >= invitation.expires_at` at query time, i.e. the invitation is still in the pending map but can no longer be acted on.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"invitation": {
|
||||
"description": "The stored invitation as it was issued.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FamilyInvitation"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PendingInvitationsForNodePagedResponse",
|
||||
"description": "Response to [`QueryMsg::GetPendingInvitationsForNodePaged`](crate::QueryMsg::GetPendingInvitationsForNodePaged).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"invitations",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"invitations": {
|
||||
"description": "The pending invitations addressed to this node on this page, in ascending [`NodeFamilyId`] order, each stamped with whether it had already timed out at the time the query was served.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PendingFamilyInvitationDetails"
|
||||
}
|
||||
},
|
||||
"node_id": {
|
||||
"description": "The node whose pending invitations were queried, echoed back so paginated callers can correlate.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Cursor (last issuing family id) to pass as `start_after` on the next call, or `None` if this page is empty (treat as end-of-list).",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
"family_id",
|
||||
"node_id"
|
||||
],
|
||||
"properties": {
|
||||
"expires_at": {
|
||||
"description": "Block timestamp (unix seconds) after which the invitation is no longer valid.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"family_id": {
|
||||
"description": "The family that issued the invitation.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_id": {
|
||||
"description": "The node being invited.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PendingFamilyInvitationDetails": {
|
||||
"description": "A pending [`FamilyInvitation`] paired with whether it has already timed out at the time the query was served.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expired",
|
||||
"invitation"
|
||||
],
|
||||
"properties": {
|
||||
"expired": {
|
||||
"description": "`true` iff `now >= invitation.expires_at` at query time, i.e. the invitation is still in the pending map but can no longer be acted on.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"invitation": {
|
||||
"description": "The stored invitation as it was issued.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/FamilyInvitation"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_schema::write_api;
|
||||
use nym_node_families_contract_common::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
|
||||
fn main() {
|
||||
write_api! {
|
||||
instantiate: InstantiateMsg,
|
||||
query: QueryMsg,
|
||||
execute: ExecuteMsg,
|
||||
migrate: MigrateMsg,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
//! CosmWasm entry points for the node families contract.
|
||||
|
||||
use crate::queries::{
|
||||
query_all_family_members_paged, query_all_past_invitations_paged,
|
||||
query_all_pending_invitations_paged, query_families_paged, query_family_by_id,
|
||||
query_family_by_name, query_family_by_owner, query_family_members_paged,
|
||||
query_family_membership, query_past_invitations_for_family_paged,
|
||||
query_past_invitations_for_node_paged, query_past_members_for_family_paged,
|
||||
query_past_members_for_node_paged, query_pending_invitation,
|
||||
query_pending_invitations_for_family_paged, query_pending_invitations_for_node_paged,
|
||||
};
|
||||
use crate::storage::NodeFamiliesStorage;
|
||||
use crate::transactions::{
|
||||
try_accept_family_invitation, try_create_family, try_disband_family, try_handle_node_unbonding,
|
||||
try_invite_to_family, try_kick_from_family, try_leave_family, try_reject_family_invitation,
|
||||
try_revoke_family_invitation, try_update_config,
|
||||
};
|
||||
use cosmwasm_std::{
|
||||
entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response,
|
||||
};
|
||||
use nym_contracts_common::set_build_information;
|
||||
use nym_node_families_contract_common::{
|
||||
ExecuteMsg, InstantiateMsg, MigrateMsg, NodeFamiliesContractError, QueryMsg,
|
||||
};
|
||||
|
||||
const CONTRACT_NAME: &str = "crate:nym-node-families-contract";
|
||||
|
||||
/// Contract semver, taken from `Cargo.toml` at build time. Bumped on every
|
||||
/// release; recorded in cw2 storage so migrations can detect the source version.
|
||||
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// One-time initialisation of contract storage on code instantiation.
|
||||
#[entry_point]
|
||||
pub fn instantiate(
|
||||
deps: DepsMut,
|
||||
_env: Env,
|
||||
info: MessageInfo,
|
||||
msg: InstantiateMsg,
|
||||
) -> Result<Response, NodeFamiliesContractError> {
|
||||
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)?;
|
||||
|
||||
NodeFamiliesStorage::new().initialise(
|
||||
deps,
|
||||
info.sender,
|
||||
mixnet_contract_address,
|
||||
msg.config,
|
||||
)?;
|
||||
|
||||
Ok(Response::default())
|
||||
}
|
||||
|
||||
/// State-mutating dispatcher. Concrete handlers live in [`crate::transactions`]
|
||||
/// and are wired up here as variants are added to [`ExecuteMsg`].
|
||||
#[entry_point]
|
||||
pub fn execute(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: ExecuteMsg,
|
||||
) -> Result<Response, NodeFamiliesContractError> {
|
||||
match msg {
|
||||
ExecuteMsg::UpdateConfig { config } => try_update_config(deps, env, info, config),
|
||||
ExecuteMsg::CreateFamily { name, description } => {
|
||||
try_create_family(deps, env, info, name, description)
|
||||
}
|
||||
ExecuteMsg::DisbandFamily {} => try_disband_family(deps, env, info),
|
||||
ExecuteMsg::InviteToFamily {
|
||||
node_id,
|
||||
validity_secs,
|
||||
} => try_invite_to_family(deps, env, info, node_id, validity_secs),
|
||||
ExecuteMsg::RevokeFamilyInvitation { node_id } => {
|
||||
try_revoke_family_invitation(deps, env, info, node_id)
|
||||
}
|
||||
ExecuteMsg::AcceptFamilyInvitation { family_id, node_id } => {
|
||||
try_accept_family_invitation(deps, env, info, family_id, node_id)
|
||||
}
|
||||
ExecuteMsg::RejectFamilyInvitation { family_id, node_id } => {
|
||||
try_reject_family_invitation(deps, env, info, family_id, node_id)
|
||||
}
|
||||
ExecuteMsg::LeaveFamily { node_id } => try_leave_family(deps, env, info, node_id),
|
||||
ExecuteMsg::KickFromFamily { node_id } => try_kick_from_family(deps, env, info, node_id),
|
||||
ExecuteMsg::OnNymNodeUnbond { node_id } => {
|
||||
try_handle_node_unbonding(deps, env, info, node_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read-only dispatcher. Concrete handlers live in [`crate::queries`] and are
|
||||
/// wired up here as variants are added to [`QueryMsg`].
|
||||
#[entry_point]
|
||||
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result<Binary, NodeFamiliesContractError> {
|
||||
match msg {
|
||||
QueryMsg::GetFamilyById { family_id } => {
|
||||
Ok(to_json_binary(&query_family_by_id(deps, family_id)?)?)
|
||||
}
|
||||
QueryMsg::GetFamilyByOwner { owner } => {
|
||||
Ok(to_json_binary(&query_family_by_owner(deps, owner)?)?)
|
||||
}
|
||||
QueryMsg::GetFamilyByName { name } => {
|
||||
Ok(to_json_binary(&query_family_by_name(deps, name)?)?)
|
||||
}
|
||||
QueryMsg::GetFamilyMembership { node_id } => {
|
||||
Ok(to_json_binary(&query_family_membership(deps, node_id)?)?)
|
||||
}
|
||||
QueryMsg::GetFamilyMembersPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => Ok(to_json_binary(&query_family_members_paged(
|
||||
deps,
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
)?)?),
|
||||
QueryMsg::GetAllFamilyMembersPaged { start_after, limit } => Ok(to_json_binary(
|
||||
&query_all_family_members_paged(deps, start_after, limit)?,
|
||||
)?),
|
||||
QueryMsg::GetPendingInvitation { family_id, node_id } => Ok(to_json_binary(
|
||||
&query_pending_invitation(deps, env, family_id, node_id)?,
|
||||
)?),
|
||||
QueryMsg::GetPendingInvitationsForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => Ok(to_json_binary(
|
||||
&query_pending_invitations_for_family_paged(deps, env, family_id, start_after, limit)?,
|
||||
)?),
|
||||
QueryMsg::GetPendingInvitationsForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => Ok(to_json_binary(&query_pending_invitations_for_node_paged(
|
||||
deps,
|
||||
env,
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
)?)?),
|
||||
QueryMsg::GetAllPendingInvitationsPaged { start_after, limit } => Ok(to_json_binary(
|
||||
&query_all_pending_invitations_paged(deps, env, start_after, limit)?,
|
||||
)?),
|
||||
QueryMsg::GetPastInvitationsForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => Ok(to_json_binary(&query_past_invitations_for_family_paged(
|
||||
deps,
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
)?)?),
|
||||
QueryMsg::GetPastInvitationsForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => Ok(to_json_binary(&query_past_invitations_for_node_paged(
|
||||
deps,
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
)?)?),
|
||||
QueryMsg::GetAllPastInvitationsPaged { start_after, limit } => Ok(to_json_binary(
|
||||
&query_all_past_invitations_paged(deps, start_after, limit)?,
|
||||
)?),
|
||||
QueryMsg::GetPastMembersForFamilyPaged {
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => Ok(to_json_binary(&query_past_members_for_family_paged(
|
||||
deps,
|
||||
family_id,
|
||||
start_after,
|
||||
limit,
|
||||
)?)?),
|
||||
QueryMsg::GetPastMembersForNodePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => Ok(to_json_binary(&query_past_members_for_node_paged(
|
||||
deps,
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
)?)?),
|
||||
QueryMsg::GetFamiliesPaged { start_after, limit } => Ok(to_json_binary(
|
||||
&query_families_paged(deps, start_after, limit)?,
|
||||
)?),
|
||||
}
|
||||
}
|
||||
|
||||
/// Migration entry point.
|
||||
///
|
||||
/// Refreshes recorded build information and ensures the existing on-chain
|
||||
/// contract version is at most the current `CONTRACT_VERSION` (i.e. forbids
|
||||
/// downgrades). Any data migrations are dispatched via
|
||||
/// [`crate::queued_migrations`].
|
||||
#[entry_point]
|
||||
pub fn migrate(
|
||||
deps: DepsMut,
|
||||
_env: Env,
|
||||
_msg: MigrateMsg,
|
||||
) -> Result<Response, NodeFamiliesContractError> {
|
||||
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 cosmwasm_std::coin;
|
||||
use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env};
|
||||
use nym_node_families_contract_common::Config;
|
||||
|
||||
fn mock_config() -> Config {
|
||||
Config {
|
||||
create_family_fee: coin(123, "unym"),
|
||||
family_name_length_limit: 20,
|
||||
family_description_length_limit: 100,
|
||||
default_invitation_validity_secs: 24 * 60 * 60,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sets_contract_admin_to_the_message_sender() -> anyhow::Result<()> {
|
||||
let mut deps = mock_dependencies();
|
||||
let env = mock_env();
|
||||
let mixnet_contract_address = deps.api.addr_make("mixnet-contract");
|
||||
let some_sender = deps.api.addr_make("some_sender");
|
||||
|
||||
instantiate(
|
||||
deps.as_mut(),
|
||||
env,
|
||||
message_info(&some_sender, &[]),
|
||||
InstantiateMsg {
|
||||
config: mock_config(),
|
||||
mixnet_contract_address: mixnet_contract_address.to_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let deps = deps.as_ref();
|
||||
|
||||
NodeFamiliesStorage::new()
|
||||
.contract_admin
|
||||
.assert_admin(deps, &some_sender)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persists_the_provided_config() -> anyhow::Result<()> {
|
||||
let mut deps = mock_dependencies();
|
||||
let env = mock_env();
|
||||
let mixnet_contract_address = deps.api.addr_make("mixnet-contract");
|
||||
let sender = deps.api.addr_make("some_sender");
|
||||
let config = mock_config();
|
||||
|
||||
instantiate(
|
||||
deps.as_mut(),
|
||||
env,
|
||||
message_info(&sender, &[]),
|
||||
InstantiateMsg {
|
||||
config: config.clone(),
|
||||
mixnet_contract_address: mixnet_contract_address.to_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let stored = NodeFamiliesStorage::new()
|
||||
.config
|
||||
.load(deps.as_ref().storage)?;
|
||||
assert_eq!(stored, config);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persists_the_validated_mixnet_contract_address() -> anyhow::Result<()> {
|
||||
let mut deps = mock_dependencies();
|
||||
let env = mock_env();
|
||||
let mixnet_contract_address = deps.api.addr_make("mixnet-contract");
|
||||
let sender = deps.api.addr_make("some_sender");
|
||||
|
||||
instantiate(
|
||||
deps.as_mut(),
|
||||
env,
|
||||
message_info(&sender, &[]),
|
||||
InstantiateMsg {
|
||||
config: mock_config(),
|
||||
mixnet_contract_address: mixnet_contract_address.to_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let stored = NodeFamiliesStorage::new()
|
||||
.mixnet_contract_address
|
||||
.load(deps.as_ref().storage)?;
|
||||
assert_eq!(stored, mixnet_contract_address);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_on_invalid_mixnet_contract_address() {
|
||||
let mut deps = mock_dependencies();
|
||||
let env = mock_env();
|
||||
let sender = deps.api.addr_make("some_sender");
|
||||
|
||||
let res = instantiate(
|
||||
deps.as_mut(),
|
||||
env,
|
||||
message_info(&sender, &[]),
|
||||
InstantiateMsg {
|
||||
config: mock_config(),
|
||||
mixnet_contract_address: "not-a-valid-bech32-address".to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_the_cw2_contract_version() -> anyhow::Result<()> {
|
||||
let mut deps = mock_dependencies();
|
||||
let env = mock_env();
|
||||
let mixnet_contract_address = deps.api.addr_make("mixnet-contract");
|
||||
let sender = deps.api.addr_make("some_sender");
|
||||
|
||||
instantiate(
|
||||
deps.as_mut(),
|
||||
env,
|
||||
message_info(&sender, &[]),
|
||||
InstantiateMsg {
|
||||
config: mock_config(),
|
||||
mixnet_contract_address: mixnet_contract_address.to_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let version = cw2::get_contract_version(deps.as_ref().storage)?;
|
||||
assert_eq!(version.contract, CONTRACT_NAME);
|
||||
assert_eq!(version.version, CONTRACT_VERSION);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::storage::NodeFamiliesStorage;
|
||||
use cosmwasm_std::{Addr, Deps};
|
||||
use nym_mixnet_contract_common::{MixnetContractQuerier, NodeId};
|
||||
use nym_node_families_contract_common::NodeFamiliesContractError;
|
||||
|
||||
/// Normalise a family name into the canonical form used as the unique-index key.
|
||||
///
|
||||
/// Drops every character that isn't an ASCII letter or digit and lowercases
|
||||
/// the rest, so `" Foo-Bar! "`, `"foobar"` and `"FOO BAR"` all collide on
|
||||
/// the storage layer's unique-name index.
|
||||
pub fn normalise_family_name(name: &str) -> String {
|
||||
name.chars()
|
||||
.filter(|c| c.is_ascii_alphanumeric())
|
||||
.map(|c| c.to_ascii_lowercase())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Ensure no node controlled by `address` is currently a member of any family.
|
||||
pub(crate) fn ensure_address_holds_no_family_membership(
|
||||
storage: &NodeFamiliesStorage,
|
||||
deps: Deps,
|
||||
address: &Addr,
|
||||
) -> Result<(), NodeFamiliesContractError> {
|
||||
let mixnet_contract = storage.mixnet_contract_address.load(deps.storage)?;
|
||||
let Some(nym_node) = deps
|
||||
.querier
|
||||
.query_nymnode_ownership(&mixnet_contract, address)?
|
||||
else {
|
||||
// if the owner has no nym-node, it can't possibly be in a family
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// check if that node is in a family
|
||||
if let Some(family) = storage
|
||||
.family_members
|
||||
.may_load(deps.storage, nym_node.node_id)?
|
||||
{
|
||||
return Err(NodeFamiliesContractError::AlreadyInFamily {
|
||||
address: address.clone(),
|
||||
node_id: nym_node.node_id,
|
||||
family_id: family.family_id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cross-contract query: ensure `node_id` is a currently-bonded node in the
|
||||
/// mixnet contract. Returns [`NodeDoesntExist`] otherwise.
|
||||
///
|
||||
/// [`NodeDoesntExist`]: NodeFamiliesContractError::NodeDoesntExist
|
||||
pub(crate) fn ensure_node_is_bonded(
|
||||
storage: &NodeFamiliesStorage,
|
||||
deps: Deps,
|
||||
node_id: NodeId,
|
||||
) -> Result<(), NodeFamiliesContractError> {
|
||||
let mixnet_contract = storage.mixnet_contract_address.load(deps.storage)?;
|
||||
if !deps
|
||||
.querier
|
||||
.check_node_existence(&mixnet_contract, node_id)?
|
||||
{
|
||||
return Err(NodeFamiliesContractError::NodeDoesntExist { node_id });
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure `address` is the controller of the bonded node `node_id` per the
|
||||
/// mixnet contract. Errors with [`SenderDoesntControlNode`] when `address`
|
||||
/// owns no bonded node, owns a node with a different id, or owns it but it
|
||||
/// has entered the unbonding state.
|
||||
///
|
||||
/// [`SenderDoesntControlNode`]: NodeFamiliesContractError::SenderDoesntControlNode
|
||||
pub(crate) fn ensure_has_bonded_node(
|
||||
storage: &NodeFamiliesStorage,
|
||||
deps: Deps,
|
||||
address: &Addr,
|
||||
node_id: NodeId,
|
||||
) -> Result<(), NodeFamiliesContractError> {
|
||||
let mixnet_contract = storage.mixnet_contract_address.load(deps.storage)?;
|
||||
match deps
|
||||
.querier
|
||||
.query_nymnode_ownership(&mixnet_contract, address)?
|
||||
{
|
||||
Some(bond) if bond.node_id == node_id && !bond.is_unbonding => Ok(()),
|
||||
_ => Err(NodeFamiliesContractError::SenderDoesntControlNode {
|
||||
address: address.clone(),
|
||||
node_id,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure `node_id` is not currently a member of any family. Returns
|
||||
/// [`NodeAlreadyInFamily`] if it is.
|
||||
///
|
||||
/// [`NodeAlreadyInFamily`]: NodeFamiliesContractError::NodeAlreadyInFamily
|
||||
pub(crate) fn ensure_node_not_in_family(
|
||||
storage: &NodeFamiliesStorage,
|
||||
deps: Deps,
|
||||
node_id: NodeId,
|
||||
) -> Result<(), NodeFamiliesContractError> {
|
||||
if let Some(membership) = storage.family_members.may_load(deps.storage, node_id)? {
|
||||
return Err(NodeFamiliesContractError::NodeAlreadyInFamily {
|
||||
node_id,
|
||||
family_id: membership.family_id,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod normalise_family_name {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty_input_yields_empty() {
|
||||
assert_eq!(normalise_family_name(""), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn already_canonical_is_unchanged() {
|
||||
assert_eq!(normalise_family_name("foobar42"), "foobar42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lowercases_uppercase_letters() {
|
||||
assert_eq!(normalise_family_name("FOOBAR"), "foobar");
|
||||
assert_eq!(normalise_family_name("FooBar"), "foobar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_whitespace() {
|
||||
assert_eq!(normalise_family_name(" foo bar "), "foobar");
|
||||
assert_eq!(normalise_family_name("foo\tbar\nbaz"), "foobarbaz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_punctuation_and_symbols() {
|
||||
assert_eq!(normalise_family_name("foo-bar!"), "foobar");
|
||||
assert_eq!(normalise_family_name("a.b_c@d"), "abcd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_digits() {
|
||||
assert_eq!(normalise_family_name("squad-2026"), "squad2026");
|
||||
assert_eq!(normalise_family_name("0123456789"), "0123456789");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drops_non_ascii_letters() {
|
||||
// is_ascii_alphanumeric is strict — accented and non-Latin chars are dropped.
|
||||
assert_eq!(normalise_family_name("café"), "caf");
|
||||
assert_eq!(normalise_family_name("Ω-team"), "team");
|
||||
assert_eq!(normalise_family_name("名前"), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_symbols_input_normalises_to_empty() {
|
||||
// try_create_family relies on this to surface EmptyFamilyName.
|
||||
assert_eq!(normalise_family_name(" "), "");
|
||||
assert_eq!(normalise_family_name("!!!---"), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distinct_inputs_collide_under_normalisation() {
|
||||
// The collision behaviour the unique-name index depends on.
|
||||
let canonical = normalise_family_name("Foo Bar");
|
||||
assert_eq!(canonical, normalise_family_name("foobar"));
|
||||
assert_eq!(canonical, normalise_family_name("FOO-BAR"));
|
||||
assert_eq!(canonical, normalise_family_name(" f.o.o.b.a.r "));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! CosmWasm contract that manages "node families" — owner-led groupings of
|
||||
//! Nym nodes — including their members, pending invitations, and historical
|
||||
//! records of past members and rejected/revoked invitations.
|
||||
//!
|
||||
//! The shared message and type surface lives in
|
||||
//! [`node_families_contract_common`]; this crate contains only the on-chain logic
|
||||
//! and storage layout.
|
||||
|
||||
/// CosmWasm entry points (`instantiate`, `execute`, `query`, `migrate`).
|
||||
pub mod contract;
|
||||
/// One-shot data migrations executed by the `migrate` entry point.
|
||||
pub mod queued_migrations;
|
||||
/// `cw-storage-plus` definitions: typed maps, items and secondary indexes.
|
||||
pub mod storage;
|
||||
|
||||
mod helpers;
|
||||
/// Read-only query handlers backing [`contract::query`].
|
||||
mod queries;
|
||||
/// Test-only helpers — always compiled for this crate's own unit tests via
|
||||
/// `cfg(test)`; downstream crates can pull them in for their own test
|
||||
/// harnesses by enabling the `testable-node-families-contract` feature.
|
||||
#[cfg(any(test, feature = "testable-node-families-contract"))]
|
||||
pub mod testing;
|
||||
/// State-mutating execute handlers backing [`contract::execute`].
|
||||
mod transactions;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
/// Default page size for paginated family listings when the caller omits `limit`.
|
||||
pub const FAMILIES_DEFAULT_LIMIT: u32 = 50;
|
||||
|
||||
/// Hard cap on the page size for paginated family listings; larger values are clamped.
|
||||
pub const FAMILIES_MAX_LIMIT: u32 = 100;
|
||||
|
||||
/// Default page size for paginated family-member listings when the caller omits `limit`.
|
||||
pub const FAMILY_MEMBERS_DEFAULT_LIMIT: u32 = 50;
|
||||
|
||||
/// Hard cap on the page size for paginated family-member listings; larger values are clamped.
|
||||
pub const FAMILY_MEMBERS_MAX_LIMIT: u32 = 100;
|
||||
|
||||
/// Default page size for paginated pending-invitation listings (both per-family
|
||||
/// and global) when the caller omits `limit`.
|
||||
pub const PENDING_INVITATIONS_DEFAULT_LIMIT: u32 = 50;
|
||||
|
||||
/// Hard cap on the page size for paginated pending-invitation listings; larger values are clamped.
|
||||
pub const PENDING_INVITATIONS_MAX_LIMIT: u32 = 100;
|
||||
|
||||
/// Default page size for paginated past-invitation (archive) listings (both
|
||||
/// per-family and global) when the caller omits `limit`.
|
||||
pub const PAST_INVITATIONS_DEFAULT_LIMIT: u32 = 50;
|
||||
|
||||
/// Hard cap on the page size for paginated past-invitation listings; larger values are clamped.
|
||||
pub const PAST_INVITATIONS_MAX_LIMIT: u32 = 100;
|
||||
|
||||
/// Default page size for paginated past-member (archive) listings when the caller omits `limit`.
|
||||
pub const PAST_MEMBERS_DEFAULT_LIMIT: u32 = 50;
|
||||
|
||||
/// Hard cap on the page size for paginated past-member listings; larger values are clamped.
|
||||
pub const PAST_MEMBERS_MAX_LIMIT: u32 = 100;
|
||||
@@ -0,0 +1,174 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::storage::FamilyMember;
|
||||
use cosmwasm_std::Addr;
|
||||
use cw_storage_plus::{Index, IndexList, MultiIndex, UniqueIndex};
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_node_families_contract_common::constants::storage_keys;
|
||||
use nym_node_families_contract_common::{
|
||||
FamilyInvitation, FamilyMembership, NodeFamily, NodeFamilyId, PastFamilyInvitation,
|
||||
PastFamilyMember,
|
||||
};
|
||||
|
||||
/// Secondary indexes over [`NodeFamily`]. Enforces one-family-per-owner and
|
||||
/// globally-unique family names via `UniqueIndex`es on `owner` and `name`.
|
||||
pub(crate) struct NodeFamiliesIndex<'a> {
|
||||
/// Unique index: at most one family per owner [`Addr`].
|
||||
pub(crate) owner: UniqueIndex<'a, Addr, NodeFamily, NodeFamilyId>,
|
||||
/// Unique index keyed on [`NodeFamily::normalised_name`]: enforces global
|
||||
/// uniqueness on the normalised form, so families with the same
|
||||
/// canonical name but different user-submitted formatting collide.
|
||||
pub(crate) normalised_name: UniqueIndex<'a, String, NodeFamily, NodeFamilyId>,
|
||||
}
|
||||
|
||||
impl NodeFamiliesIndex<'_> {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub(crate) fn new() -> Self {
|
||||
NodeFamiliesIndex {
|
||||
owner: UniqueIndex::new(
|
||||
|family| family.owner.clone(),
|
||||
storage_keys::FAMILIES_OWNER_IDX_NAMESPACE,
|
||||
),
|
||||
normalised_name: UniqueIndex::new(
|
||||
|family| family.normalised_name.clone(),
|
||||
storage_keys::FAMILIES_NAME_IDX_NAMESPACE,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexList<NodeFamily> for NodeFamiliesIndex<'_> {
|
||||
fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<NodeFamily>> + '_> {
|
||||
let v: Vec<&dyn Index<NodeFamily>> = vec![&self.owner, &self.normalised_name];
|
||||
Box::new(v.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
/// Secondary indexes over current [`FamilyMembership`] records. The PK is
|
||||
/// `NodeId` (one family per node), and the family-id multi-index enables
|
||||
/// paginated listing of all nodes belonging to a given family.
|
||||
pub(crate) struct FamilyMembersIndex<'a> {
|
||||
/// Multi-index: every node currently in a given family.
|
||||
pub(crate) family: MultiIndex<'a, NodeFamilyId, FamilyMembership, NodeId>,
|
||||
}
|
||||
|
||||
impl FamilyMembersIndex<'_> {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub(crate) fn new() -> Self {
|
||||
FamilyMembersIndex {
|
||||
family: MultiIndex::new(
|
||||
|_pk, m| m.family_id,
|
||||
storage_keys::NODE_FAMILY_MEMBERS,
|
||||
storage_keys::NODE_FAMILY_MEMBERS_FAMILY_IDX_NAMESPACE,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexList<FamilyMembership> for FamilyMembersIndex<'_> {
|
||||
fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<FamilyMembership>> + '_> {
|
||||
let v: Vec<&dyn Index<FamilyMembership>> = vec![&self.family];
|
||||
Box::new(v.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
/// Secondary indexes over pending [`FamilyInvitation`]s, allowing lookup by
|
||||
/// either family id or node id.
|
||||
pub(crate) struct NodeFamilyInvitationIndex<'a> {
|
||||
/// Multi-index: all pending invitations issued by a given family.
|
||||
pub(crate) family: MultiIndex<'a, NodeFamilyId, FamilyInvitation, FamilyMember>,
|
||||
/// Multi-index: all pending invitations addressed to a given node.
|
||||
pub(crate) node: MultiIndex<'a, NodeId, FamilyInvitation, FamilyMember>,
|
||||
}
|
||||
|
||||
impl NodeFamilyInvitationIndex<'_> {
|
||||
pub(crate) fn new() -> Self {
|
||||
NodeFamilyInvitationIndex {
|
||||
family: MultiIndex::new(
|
||||
|_pk, inv| inv.family_id,
|
||||
storage_keys::INVITATIONS_NAMESPACE,
|
||||
storage_keys::INVITATIONS_FAMILY_IDX_NAMESPACE,
|
||||
),
|
||||
node: MultiIndex::new(
|
||||
|_pk, inv| inv.node_id,
|
||||
storage_keys::INVITATIONS_NAMESPACE,
|
||||
storage_keys::INVITATIONS_NODE_IDX_NAMESPACE,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexList<FamilyInvitation> for NodeFamilyInvitationIndex<'_> {
|
||||
fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<FamilyInvitation>> + '_> {
|
||||
let v: Vec<&dyn Index<FamilyInvitation>> = vec![&self.family, &self.node];
|
||||
Box::new(v.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
/// Secondary indexes over the [`PastFamilyMember`] archive.
|
||||
pub(crate) struct PastFamilyMembersIndex<'a> {
|
||||
/// Multi-index: every past membership record for a given family.
|
||||
pub(crate) family: MultiIndex<'a, NodeFamilyId, PastFamilyMember, (FamilyMember, u64)>,
|
||||
/// Multi-index: every past membership record for a given node.
|
||||
pub(crate) node: MultiIndex<'a, NodeId, PastFamilyMember, (FamilyMember, u64)>,
|
||||
}
|
||||
|
||||
impl PastFamilyMembersIndex<'_> {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub(crate) fn new() -> Self {
|
||||
PastFamilyMembersIndex {
|
||||
family: MultiIndex::new(
|
||||
|_pk, mem| mem.family_id,
|
||||
storage_keys::PAST_FAMILY_MEMBER_NAMESPACE,
|
||||
storage_keys::PAST_FAMILY_MEMBER_FAMILY_IDX_NAMESPACE,
|
||||
),
|
||||
node: MultiIndex::new(
|
||||
|_pk, mem| mem.node_id,
|
||||
storage_keys::PAST_FAMILY_MEMBER_NAMESPACE,
|
||||
storage_keys::PAST_FAMILY_MEMBER_NODE_IDX_NAMESPACE,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexList<PastFamilyMember> for PastFamilyMembersIndex<'_> {
|
||||
fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<PastFamilyMember>> + '_> {
|
||||
let v: Vec<&dyn Index<PastFamilyMember>> = vec![&self.family, &self.node];
|
||||
Box::new(v.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
/// Secondary indexes over the [`PastFamilyInvitation`] archive
|
||||
/// (rejected / revoked invitations).
|
||||
pub(crate) struct PastFamilyInvitationsIndex<'a> {
|
||||
/// Multi-index: every archived invitation issued by a given family.
|
||||
pub(crate) family: MultiIndex<'a, NodeFamilyId, PastFamilyInvitation, (FamilyMember, u64)>,
|
||||
/// Multi-index: every archived invitation addressed to a given node.
|
||||
pub(crate) node: MultiIndex<'a, NodeId, PastFamilyInvitation, (FamilyMember, u64)>,
|
||||
}
|
||||
|
||||
impl PastFamilyInvitationsIndex<'_> {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub(crate) fn new() -> Self {
|
||||
PastFamilyInvitationsIndex {
|
||||
family: MultiIndex::new(
|
||||
|_pk, inv| inv.invitation.family_id,
|
||||
storage_keys::PAST_INVITATIONS_NAMESPACE,
|
||||
storage_keys::PAST_INVITATIONS_FAMILY_IDX_NAMESPACE,
|
||||
),
|
||||
node: MultiIndex::new(
|
||||
|_pk, inv| inv.invitation.node_id,
|
||||
storage_keys::PAST_INVITATIONS_NAMESPACE,
|
||||
storage_keys::PAST_INVITATIONS_NODE_IDX_NAMESPACE,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexList<PastFamilyInvitation> for PastFamilyInvitationsIndex<'_> {
|
||||
fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<PastFamilyInvitation>> + '_> {
|
||||
let v: Vec<&dyn Index<PastFamilyInvitation>> = vec![&self.family, &self.node];
|
||||
Box::new(v.into_iter())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
// fine in test code
|
||||
#![allow(clippy::unwrap_used)]
|
||||
#![allow(clippy::expect_used)]
|
||||
|
||||
use crate::contract::{execute, instantiate, migrate, query};
|
||||
use crate::helpers::normalise_family_name;
|
||||
use crate::storage::NodeFamiliesStorage;
|
||||
use cosmwasm_std::{coin, Addr, Coin, Storage};
|
||||
use mixnet_contract::testable_mixnet_contract::{EmbeddedMixnetContractExt, MixnetContract};
|
||||
use nym_contracts_common_testing::{
|
||||
AdminExt, ArbitraryContractStorageReader, ArbitraryContractStorageWriter, BankExt, ChainOpts,
|
||||
CommonStorageKeys, ContractFn, ContractOpts, ContractTester, ContractTesterBuilder, DenomExt,
|
||||
PermissionedFn, QueryFn, RandExt, TestableNymContract, TEST_DENOM,
|
||||
};
|
||||
use nym_mixnet_contract_common::ContractState;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_node_families_contract_common::constants::storage_keys;
|
||||
use nym_node_families_contract_common::{
|
||||
Config, ExecuteMsg, FamilyInvitation, InstantiateMsg, MigrateMsg, NodeFamiliesContractError,
|
||||
NodeFamily, NodeFamilyId, QueryMsg,
|
||||
};
|
||||
|
||||
pub struct NodeFamiliesContract;
|
||||
|
||||
impl TestableNymContract for NodeFamiliesContract {
|
||||
const NAME: &'static str = "node-families-contract";
|
||||
type InitMsg = InstantiateMsg;
|
||||
type ExecuteMsg = ExecuteMsg;
|
||||
type QueryMsg = QueryMsg;
|
||||
type MigrateMsg = MigrateMsg;
|
||||
type ContractError = NodeFamiliesContractError;
|
||||
|
||||
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 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 {
|
||||
config: Config {
|
||||
create_family_fee: coin(100_000000, TEST_DENOM),
|
||||
family_name_length_limit: 20,
|
||||
family_description_length_limit: 200,
|
||||
default_invitation_validity_secs: 24 * 60 * 60,
|
||||
},
|
||||
mixnet_contract_address: mixnet_address.to_string(),
|
||||
}))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
/// Storage key the mixnet contract uses for its `ContractState` `Item`
|
||||
/// (mirrors `mixnet/src/constants.rs::CONTRACT_STATE_KEY`).
|
||||
const MIXNET_CONTRACT_STATE_STORAGE_KEY: &str = "state";
|
||||
|
||||
pub fn init_contract_tester() -> ContractTester<NodeFamiliesContract> {
|
||||
let mut tester = NodeFamiliesContract::init()
|
||||
.with_common_storage_key(CommonStorageKeys::Admin, storage_keys::CONTRACT_ADMIN);
|
||||
|
||||
// Chicken-and-egg: the mixnet contract is instantiated first and is given
|
||||
// a placeholder `node_families_contract_address` because the families
|
||||
// contract doesn't exist yet. Once the families contract has been
|
||||
// instantiated we patch the mixnet's stored `ContractState` so that the
|
||||
// unbond callback (`OnNymNodeUnbond`) actually dispatches to the right
|
||||
// contract. In production this fixup happens via a contract migration;
|
||||
// here we go straight to storage to avoid jumping through cw2 version
|
||||
// checks that don't apply on a fresh tester.
|
||||
let families_address = tester.contract_address.clone();
|
||||
let mut mixnet_state: ContractState = tester
|
||||
.read_from_mixnet_contract_storage(MIXNET_CONTRACT_STATE_STORAGE_KEY)
|
||||
.expect("mixnet contract state should be loadable");
|
||||
mixnet_state.node_families_contract_address = families_address;
|
||||
tester
|
||||
.write_to_mixnet_contract_storage_value(MIXNET_CONTRACT_STATE_STORAGE_KEY, &mixnet_state)
|
||||
.expect("should be able to patch mixnet contract state");
|
||||
|
||||
tester
|
||||
}
|
||||
|
||||
pub trait NodeFamiliesContractTesterExt:
|
||||
ContractOpts<
|
||||
ExecuteMsg = ExecuteMsg,
|
||||
QueryMsg = QueryMsg,
|
||||
ContractError = NodeFamiliesContractError,
|
||||
> + ChainOpts
|
||||
+ AdminExt
|
||||
+ DenomExt
|
||||
+ BankExt
|
||||
+ RandExt
|
||||
+ Storage
|
||||
+ ArbitraryContractStorageReader
|
||||
+ ArbitraryContractStorageWriter
|
||||
+ EmbeddedMixnetContractExt
|
||||
+ Sized
|
||||
{
|
||||
fn family_fee(&self) -> Coin {
|
||||
let s = NodeFamiliesStorage::new();
|
||||
s.config.load(self).unwrap().create_family_fee
|
||||
}
|
||||
|
||||
fn make_named_family(&mut self, owner: &Addr, name: &str) -> NodeFamily {
|
||||
let normalised = normalise_family_name(name);
|
||||
let env = self.env();
|
||||
let fee = self.family_fee();
|
||||
NodeFamiliesStorage::new()
|
||||
.register_new_family(
|
||||
self,
|
||||
&env,
|
||||
fee,
|
||||
owner.clone(),
|
||||
name.to_string(),
|
||||
normalised,
|
||||
"dummy".to_string(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn make_family(&mut self, owner: &Addr) -> NodeFamily {
|
||||
// names must be globally unique; derive from owner addr (also unique)
|
||||
let name = format!("family-{owner}");
|
||||
self.make_named_family(owner, &name)
|
||||
}
|
||||
|
||||
fn disband_family(&mut self, family: NodeFamilyId) {
|
||||
let env = self.env();
|
||||
NodeFamiliesStorage::new()
|
||||
.disband_family(self, &env, family)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn add_dummy_family(&mut self) -> NodeFamily {
|
||||
let owner = self.generate_account();
|
||||
self.make_family(&owner)
|
||||
}
|
||||
|
||||
fn invite_to_family_with_expiration(
|
||||
&mut self,
|
||||
family: NodeFamilyId,
|
||||
node: NodeId,
|
||||
expiration: u64,
|
||||
) -> FamilyInvitation {
|
||||
NodeFamiliesStorage::new()
|
||||
.add_pending_invitation(self, family, node, expiration)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn invite_to_family(&mut self, family: NodeFamilyId, node: NodeId) -> FamilyInvitation {
|
||||
let exp = self.env().block.time.seconds() + 100;
|
||||
self.invite_to_family_with_expiration(family, node, exp)
|
||||
}
|
||||
|
||||
fn accept_invitation(&mut self, family: NodeFamilyId, node: NodeId) {
|
||||
let env = self.env();
|
||||
NodeFamiliesStorage::new()
|
||||
.accept_invitation(self, &env, family, node)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn reject_invitation(&mut self, family: NodeFamilyId, node: NodeId) {
|
||||
let env = self.env();
|
||||
NodeFamiliesStorage::new()
|
||||
.reject_pending_invitation(self, &env, family, node)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn revoke_invitation(&mut self, family: NodeFamilyId, node: NodeId) {
|
||||
let env = self.env();
|
||||
NodeFamiliesStorage::new()
|
||||
.revoke_pending_invitation(self, &env, family, node)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn add_to_family(&mut self, family: NodeFamilyId, node: NodeId) {
|
||||
self.invite_to_family(family, node);
|
||||
self.accept_invitation(family, node);
|
||||
}
|
||||
|
||||
fn remove_from_family(&mut self, node: NodeId) {
|
||||
let env = self.env();
|
||||
NodeFamiliesStorage::new()
|
||||
.remove_family_member(self, &env, node)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn add_n_family_members(&mut self, family: NodeFamilyId, count: u32) {
|
||||
for n in 1..=count {
|
||||
self.add_to_family(family, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeFamiliesContractTesterExt for ContractTester<NodeFamiliesContract> {}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,13 @@ nym-contracts-common-testing = { workspace = true }
|
||||
nym-mixnet-contract = { workspace = true, features = ["testable-mixnet-contract"] }
|
||||
nym-crypto = { workspace = true, features = ["asymmetric", "rand"] }
|
||||
|
||||
# Needed only by the test harness: the embedded mixnet contract dispatches an
|
||||
# `OnNymNodeUnbond` WasmMsg on `try_remove_nym_node` and the target must be a
|
||||
# real contract. We instantiate the families contract alongside so the call
|
||||
# lands somewhere that knows how to handle it.
|
||||
node-families = { workspace = true, features = ["testable-node-families-contract"] }
|
||||
nym-node-families-contract-common = { workspace = true }
|
||||
|
||||
[features]
|
||||
schema-gen = ["nym-performance-contract-common/schema", "cosmwasm-schema"]
|
||||
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
// 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, 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();
|
||||
|
||||
if let Some(nym_node) = self.query_nymnode_bond(mixnet_contract_address.clone(), node_id)? {
|
||||
return Ok(!nym_node.is_unbonding);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn query_nymnode_bond(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
node_id: NodeId,
|
||||
) -> StdResult<Option<NymNodeBond>> {
|
||||
// construct proper map key
|
||||
let pk_namespace = "nn";
|
||||
let path: Path<NymNodeBond> = Path::new(
|
||||
Namespace::from_static_str(pk_namespace).as_slice(),
|
||||
&node_id.key().iter().map(Key::as_ref).collect::<Vec<_>>(),
|
||||
);
|
||||
let storage_key = path.deref();
|
||||
|
||||
self.query_mixnet_contract_storage_value(address, storage_key)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ pub mod contract;
|
||||
pub mod queued_migrations;
|
||||
pub mod storage;
|
||||
|
||||
mod helpers;
|
||||
mod queries;
|
||||
mod transactions;
|
||||
|
||||
|
||||
@@ -316,6 +316,7 @@ pub fn query_last_submission(deps: Deps) -> Result<LastSubmission, NymPerformanc
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::testing::{init_contract_tester, PerformanceContractTesterExt};
|
||||
use mixnet_contract::testable_mixnet_contract::EmbeddedMixnetContractExt;
|
||||
use nym_contracts_common_testing::{ChainOpts, ContractOpts, RandExt};
|
||||
use nym_performance_contract_common::LastSubmittedData;
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::helpers::MixnetContractQuerier;
|
||||
use cosmwasm_std::{Addr, Deps, DepsMut, Env, StdError, Storage};
|
||||
use cw_controllers::Admin;
|
||||
use cw_storage_plus::{Item, Map};
|
||||
use nym_contracts_common::Percent;
|
||||
use nym_mixnet_contract_common::MixnetContractQuerier;
|
||||
use nym_performance_contract_common::constants::storage_keys;
|
||||
use nym_performance_contract_common::{
|
||||
BatchSubmissionResult, EpochId, LastSubmission, LastSubmittedData, NetworkMonitorDetails,
|
||||
@@ -580,6 +580,7 @@ mod tests {
|
||||
mod performance_contract_storage {
|
||||
use super::*;
|
||||
use crate::testing::{init_contract_tester, PerformanceContractTesterExt, PreInitContract};
|
||||
use mixnet_contract::testable_mixnet_contract::EmbeddedMixnetContractExt;
|
||||
use nym_contracts_common_testing::{AdminExt, ContractOpts};
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -2856,6 +2857,7 @@ mod tests {
|
||||
mod performance_storage {
|
||||
use super::*;
|
||||
use crate::testing::{init_contract_tester, PerformanceContractTesterExt};
|
||||
use mixnet_contract::testable_mixnet_contract::EmbeddedMixnetContractExt;
|
||||
use nym_contracts_common_testing::ContractOpts;
|
||||
use std::str::FromStr;
|
||||
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
// 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::testing::{mock_env, MockApi};
|
||||
use cosmwasm_std::{
|
||||
coin, coins, Addr, ContractInfo, Deps, DepsMut, Env, MessageInfo, QuerierWrapper, StdError,
|
||||
StdResult,
|
||||
coin, Addr, ContractInfo, Deps, DepsMut, Env, QuerierWrapper, StdError, StdResult,
|
||||
};
|
||||
use mixnet_contract::testable_mixnet_contract::MixnetContract;
|
||||
use nym_contracts_common::signing::{ContractMessageContent, MessageSignature};
|
||||
use mixnet_contract::testable_mixnet_contract::{EmbeddedMixnetContractExt, MixnetContract};
|
||||
use node_families_contract::testing::NodeFamiliesContract;
|
||||
use nym_contracts_common::Percent;
|
||||
use nym_contracts_common_testing::{
|
||||
addr, AdminExt, ArbitraryContractStorageReader, ArbitraryContractStorageWriter, BankExt,
|
||||
@@ -18,19 +16,15 @@ use nym_contracts_common_testing::{
|
||||
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, NodeCostParams, NymNode, NymNodeBondingPayload,
|
||||
RoleAssignment, SignableNymNodeBondingMsg, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT,
|
||||
DEFAULT_PROFIT_MARGIN_PERCENT,
|
||||
use nym_mixnet_contract_common::{ContractState, EpochId};
|
||||
use nym_node_families_contract_common::{
|
||||
Config as NodeFamiliesConfig, InstantiateMsg as NodeFamiliesInstantiateMsg,
|
||||
};
|
||||
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::Serialize;
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -80,6 +74,22 @@ impl TestableNymContract for PerformanceContract {
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
// The embedded mixnet's `try_remove_nym_node` always emits an
|
||||
// `OnNymNodeUnbond` WasmMsg to the configured families contract;
|
||||
// instantiate one alongside so that target exists and accepts the call.
|
||||
// `init_contract_tester` patches the mixnet's stored families address
|
||||
// to point at this instance after the build completes.
|
||||
let builder =
|
||||
builder.instantiate::<NodeFamiliesContract>(Some(NodeFamiliesInstantiateMsg {
|
||||
config: NodeFamiliesConfig {
|
||||
create_family_fee: coin(100_000000, TEST_DENOM),
|
||||
family_name_length_limit: 20,
|
||||
family_description_length_limit: 200,
|
||||
default_invitation_validity_secs: 24 * 60 * 60,
|
||||
},
|
||||
mixnet_contract_address: mixnet_address.to_string(),
|
||||
}));
|
||||
|
||||
builder
|
||||
.instantiate::<Self>(Some(InstantiateMsg {
|
||||
mixnet_contract_address: mixnet_address.to_string(),
|
||||
@@ -89,9 +99,35 @@ impl TestableNymContract for PerformanceContract {
|
||||
}
|
||||
}
|
||||
|
||||
/// Storage key the mixnet contract uses for its `ContractState` `Item`
|
||||
/// (mirrors `mixnet/src/constants.rs::CONTRACT_STATE_KEY`).
|
||||
const MIXNET_CONTRACT_STATE_STORAGE_KEY: &str = "state";
|
||||
|
||||
pub fn init_contract_tester() -> ContractTester<PerformanceContract> {
|
||||
PerformanceContract::init()
|
||||
.with_common_storage_key(CommonStorageKeys::Admin, storage_keys::CONTRACT_ADMIN)
|
||||
let mut tester = PerformanceContract::init()
|
||||
.with_common_storage_key(CommonStorageKeys::Admin, storage_keys::CONTRACT_ADMIN);
|
||||
|
||||
// Chicken-and-egg: the mixnet contract was instantiated first with a
|
||||
// placeholder `node_families_contract_address`. Now that the families
|
||||
// contract exists, patch the mixnet's `ContractState` so its
|
||||
// `OnNymNodeUnbond` dispatch lands on the real contract instead of the
|
||||
// placeholder. In production this fixup happens via a contract migration;
|
||||
// here we go straight to storage to avoid the cw2 version check that
|
||||
// blocks migrating a freshly-instantiated contract.
|
||||
let families_address = tester
|
||||
.well_known_contracts
|
||||
.get(NodeFamiliesContract::NAME)
|
||||
.expect("families contract should have been instantiated")
|
||||
.clone();
|
||||
let mut mixnet_state: ContractState = tester
|
||||
.read_from_mixnet_contract_storage(MIXNET_CONTRACT_STATE_STORAGE_KEY)
|
||||
.expect("mixnet contract state should be loadable");
|
||||
mixnet_state.node_families_contract_address = families_address;
|
||||
tester
|
||||
.write_to_mixnet_contract_storage_value(MIXNET_CONTRACT_STATE_STORAGE_KEY, &mixnet_state)
|
||||
.expect("should be able to patch mixnet contract state");
|
||||
|
||||
tester
|
||||
}
|
||||
|
||||
// we need to be able to test instantiation, but for that we require
|
||||
@@ -214,136 +250,8 @@ pub(crate) trait PerformanceContractTesterExt:
|
||||
+ BankExt
|
||||
+ ArbitraryContractStorageReader
|
||||
+ ArbitraryContractStorageWriter
|
||||
+ EmbeddedMixnetContractExt
|
||||
{
|
||||
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,
|
||||
@@ -435,68 +343,6 @@ pub(crate) trait PerformanceContractTesterExt:
|
||||
.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(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PerformanceContractTesterExt for ContractTester<PerformanceContract> {}
|
||||
|
||||
@@ -132,6 +132,7 @@ mod tests {
|
||||
use crate::storage::retrieval_limits;
|
||||
use crate::testing::{init_contract_tester, PerformanceContractTesterExt};
|
||||
use cosmwasm_std::from_json;
|
||||
use mixnet_contract::testable_mixnet_contract::EmbeddedMixnetContractExt;
|
||||
use nym_contracts_common_testing::{AdminExt, ContractOpts};
|
||||
use nym_performance_contract_common::RemoveEpochMeasurementsResponse;
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ nym-serde-helpers = { workspace = true, features = ["date"] }
|
||||
nym-ticketbooks-merkle = { workspace = true }
|
||||
nym-statistics-common = { workspace = true }
|
||||
nym-ecash-signer-check = { workspace = true }
|
||||
nym-node-families-contract-common = { workspace = true }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
@@ -15,6 +15,7 @@ pub mod legacy;
|
||||
pub mod mixnet;
|
||||
pub mod network;
|
||||
pub mod network_monitor;
|
||||
pub mod node_families;
|
||||
pub mod node_status;
|
||||
pub mod schema_helpers;
|
||||
pub mod utility;
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use super::CoinSchema;
|
||||
use cosmwasm_std::Coin;
|
||||
use nym_mixnet_contract_common::{NodeId, NodeRewarding};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Pending family invitation as exposed by the nym-api node-families endpoints.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct PendingFamilyInvitation {
|
||||
/// Node the invitation is addressed to.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// Block-time after which the invitation can no longer be accepted.
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
#[schema(value_type = String)]
|
||||
pub expires_at: OffsetDateTime,
|
||||
}
|
||||
|
||||
/// Per-node stake snapshot derived from the mixnet contract's rewarding state.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct NodeStakeInformation {
|
||||
/// Operator bond + all delegations, with accrued rewards applied.
|
||||
#[schema(value_type = CoinSchema)]
|
||||
pub stake: Coin,
|
||||
|
||||
/// Operator pledge component of `stake`.
|
||||
#[schema(value_type = CoinSchema)]
|
||||
pub bond: Coin,
|
||||
|
||||
/// Delegations component of `stake`.
|
||||
#[schema(value_type = CoinSchema)]
|
||||
pub delegations: Coin,
|
||||
|
||||
/// Number of unique delegators backing this node.
|
||||
pub delegators: usize,
|
||||
}
|
||||
|
||||
impl From<&NodeRewarding> for NodeStakeInformation {
|
||||
fn from(rewarding: &NodeRewarding) -> Self {
|
||||
let denom = &rewarding.cost_params.interval_operating_cost.denom;
|
||||
|
||||
let bond = rewarding.operator_pledge_with_reward(denom);
|
||||
let delegations = rewarding.delegations_with_reward(denom);
|
||||
let mut stake = bond.clone();
|
||||
stake.amount += delegations.amount;
|
||||
|
||||
NodeStakeInformation {
|
||||
stake,
|
||||
bond,
|
||||
delegations,
|
||||
delegators: rewarding.unique_delegations as usize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Family member view as exposed by the nym-api node-families endpoints.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct NodeFamilyMember {
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// Block-time at which the node joined the family.
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
#[schema(value_type = String)]
|
||||
pub joined_at: OffsetDateTime,
|
||||
|
||||
/// Stake/bond/delegation snapshot; `None` if the node was not in the
|
||||
/// mixnet-contract cache at refresh time.
|
||||
pub stake_information: Option<NodeStakeInformation>,
|
||||
}
|
||||
|
||||
/// Family view as exposed by the nym-api node-families endpoints, carrying
|
||||
/// current members, pending invitations and aggregated stats.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct NodeFamily {
|
||||
/// Unique family identifier assigned by the contract.
|
||||
pub id: u32,
|
||||
|
||||
/// Display name (canonical form — see `normalise_family_name`).
|
||||
pub name: String,
|
||||
|
||||
/// Free-form family description.
|
||||
pub description: String,
|
||||
|
||||
/// Owner address (cosmos `Addr` rendered as a string).
|
||||
pub owner: String,
|
||||
|
||||
/// Average age of members.
|
||||
#[serde(with = "humantime_serde")]
|
||||
#[schema(value_type = String)]
|
||||
pub average_node_age: Duration,
|
||||
|
||||
/// Sum of member stakes; `None` when no member has reportable stake.
|
||||
#[schema(value_type = Option<CoinSchema>)]
|
||||
pub total_stake: Option<Coin>,
|
||||
|
||||
/// Block-time the family was created.
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
#[schema(value_type = String)]
|
||||
pub created_at: OffsetDateTime,
|
||||
|
||||
/// Current members of the family.
|
||||
pub members: Vec<NodeFamilyMember>,
|
||||
|
||||
/// Outstanding invitations issued by the family owner.
|
||||
pub pending_invitations: Vec<PendingFamilyInvitation>,
|
||||
}
|
||||
|
||||
/// Response wrapper for endpoints that look up a single family. `family` is
|
||||
/// `None` when the lookup did not match (rather than returning a 404).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct NodeFamilyResponse {
|
||||
pub family: Option<NodeFamily>,
|
||||
}
|
||||
|
||||
/// Response wrapper for endpoints that look up the family a given node
|
||||
/// belongs to. `family` is `None` when the node is not currently a member of
|
||||
/// any cached family.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct NodeFamilyForNodeResponse {
|
||||
/// The node the lookup was performed for.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// The family this node belongs to, if any.
|
||||
pub family: Option<NodeFamily>,
|
||||
}
|
||||
+107
-133
@@ -5,24 +5,11 @@ use crate::ecash::api_routes::handlers::ecash_routes;
|
||||
use crate::ecash::error::{EcashError, Result};
|
||||
use crate::ecash::keys::KeyPairWithEpoch;
|
||||
use crate::ecash::state::EcashState;
|
||||
use crate::mixnet_contract_cache::cache::MixnetContractCache;
|
||||
use crate::network::models::NetworkDetails;
|
||||
use crate::node_describe_cache::cache::DescribedNodes;
|
||||
use crate::node_status_api::handlers::unstable;
|
||||
use crate::node_status_api::NodeStatusCache;
|
||||
use crate::status::ApiStatusState;
|
||||
use crate::node_families::cache::NodeFamiliesCacheData;
|
||||
use crate::support::caching::cache::SharedCache;
|
||||
use crate::support::caching::refresher::RefreshRequester;
|
||||
use crate::support::config;
|
||||
use crate::support::http::state::chain_status::ChainStatusCache;
|
||||
use crate::support::http::state::contract_details::ContractDetailsCache;
|
||||
use crate::support::http::state::force_refresh::ForcedRefresh;
|
||||
use crate::support::http::state::mixnet_contract_cache::MixnetContractCacheState;
|
||||
use crate::support::http::state::node_annotations_cache::NodeAnnotationsCache;
|
||||
use crate::support::http::state::AppState;
|
||||
use crate::support::nyxd::Client;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use crate::unstable_routes::v1::account::cache::AddressInfoCache;
|
||||
use async_trait::async_trait;
|
||||
use axum::Router;
|
||||
use axum_test::http::StatusCode;
|
||||
@@ -78,8 +65,6 @@ use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use tempfile::{tempdir, TempDir};
|
||||
use time::Date;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
@@ -88,7 +73,8 @@ pub(crate) mod helpers;
|
||||
mod issued_ticketbooks;
|
||||
|
||||
const TEST_COIN_DENOM: &str = "unym";
|
||||
const TEST_REWARDING_VALIDATOR_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
|
||||
pub(crate) const TEST_REWARDING_VALIDATOR_ADDRESS: &str =
|
||||
"n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct InternalCounters {
|
||||
@@ -1271,133 +1257,124 @@ struct TestFixture {
|
||||
chain_state: SharedFakeChain,
|
||||
epoch: Arc<AtomicU64>,
|
||||
ecash_clients: Arc<RwLock<HashMap<EpochId, Vec<EcashApiClient>>>>,
|
||||
}
|
||||
|
||||
_tmp_dir: TempDir,
|
||||
/// Test-only bundle returned by [`build_dummy_ecash_state`]. Carries the
|
||||
/// constructed [`EcashState`] plus the test handles the caller may want to
|
||||
/// poke at directly (chain state, registered ecash clients, epoch counter).
|
||||
pub(crate) struct DummyEcashBundle {
|
||||
pub ecash_state: EcashState,
|
||||
pub chain_state: SharedFakeChain,
|
||||
pub ecash_clients: Arc<RwLock<HashMap<EpochId, Vec<EcashApiClient>>>>,
|
||||
/// A real [`Client`] (not the [`DummyClient`]) suitable for the
|
||||
/// `nyxd_client` field on [`AppState`]. Built from a global env-var dance
|
||||
/// (see body), which is required because `AppState` is not generic.
|
||||
pub real_client: Client,
|
||||
pub epoch: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
/// Build a self-contained [`EcashState`] suitable for handler tests. Pulls
|
||||
/// every dependency it needs (coconut keypair, identity, fake chain, dummy
|
||||
/// comm channel, etc.) from a deterministic RNG seed so callers get
|
||||
/// reproducible setups.
|
||||
pub(crate) async fn build_dummy_ecash_state(
|
||||
config: &config::Config,
|
||||
storage: NymApiStorage,
|
||||
rng_seed: [u8; 32],
|
||||
) -> DummyEcashBundle {
|
||||
let mut rng = crate::ecash::tests::fixtures::test_rng(rng_seed);
|
||||
let coconut_keypair = ttp_keygen(1, 1).unwrap().remove(0);
|
||||
let identity = ed25519::KeyPair::new(&mut rng);
|
||||
let epoch = Arc::new(AtomicU64::new(1));
|
||||
let address = AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap();
|
||||
let comm_channel = DummyCommunicationChannel::new_single_dummy(
|
||||
coconut_keypair.verification_key().clone(),
|
||||
address.clone(),
|
||||
)
|
||||
.with_epoch(epoch.clone());
|
||||
let ecash_clients = comm_channel.clients_arc();
|
||||
|
||||
let staged_key_pair = crate::ecash::keys::KeyPair::new();
|
||||
staged_key_pair
|
||||
.set(KeyPairWithEpoch {
|
||||
keys: coconut_keypair,
|
||||
issued_for_epoch: 1,
|
||||
})
|
||||
.await;
|
||||
staged_key_pair.validate();
|
||||
|
||||
let chain_state = SharedFakeChain::default();
|
||||
let nyxd_client = DummyClient::new(address, chain_state.clone());
|
||||
|
||||
let ecash_contract = chain_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.ecash_contract
|
||||
.address
|
||||
.clone()
|
||||
.as_str()
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
let ecash_state = EcashState::new(
|
||||
config,
|
||||
ecash_contract,
|
||||
nyxd_client,
|
||||
identity,
|
||||
staged_key_pair,
|
||||
comm_channel,
|
||||
storage,
|
||||
&ShutdownManager::empty_mock(),
|
||||
);
|
||||
|
||||
// ideally this would have been generic, but that's way too much work
|
||||
// since then `AppState` would have had to be made generic
|
||||
// also, this is such a disgusting workaround to make it 'work'. yuck
|
||||
let mut dummy = NymNetworkDetails::new_empty();
|
||||
dummy.endpoints = vec![ValidatorDetails::new(
|
||||
"http://127.0.0.1:26657",
|
||||
Some("http://why-do-we-even-need-api-url-set-here.wtf"),
|
||||
None,
|
||||
)];
|
||||
dummy.export_to_env();
|
||||
let real_client = Client::new(config).unwrap();
|
||||
|
||||
DummyEcashBundle {
|
||||
ecash_state,
|
||||
chain_state,
|
||||
ecash_clients,
|
||||
real_client,
|
||||
epoch,
|
||||
}
|
||||
}
|
||||
|
||||
impl TestFixture {
|
||||
fn build_app_state(
|
||||
storage: NymApiStorage,
|
||||
ecash_state: EcashState,
|
||||
nyxd_client: Client,
|
||||
tmp_dir: &TempDir,
|
||||
) -> AppState {
|
||||
let mixnet_contract_paths = tmp_dir.path().join("mixnet_contract");
|
||||
let node_annotations_paths = tmp_dir.path().join("node_annotations");
|
||||
|
||||
let mixnet_contract_cache_state =
|
||||
MixnetContractCache::new(&mixnet_contract_paths, Duration::from_secs(42));
|
||||
let mixnet_contract_cache =
|
||||
MixnetContractCacheState::new(mixnet_contract_cache_state, RefreshRequester::default());
|
||||
|
||||
let node_status_cache_state =
|
||||
NodeStatusCache::new(&node_annotations_paths, Duration::from_secs(42));
|
||||
let node_annotations_cache =
|
||||
NodeAnnotationsCache::new(node_status_cache_state, RefreshRequester::default());
|
||||
|
||||
AppState {
|
||||
nyxd_client,
|
||||
chain_status_cache: ChainStatusCache::new(Duration::from_secs(42)),
|
||||
ecash_signers_cache: Default::default(),
|
||||
address_info_cache: AddressInfoCache::new(Duration::from_secs(42), 1000),
|
||||
forced_refresh: ForcedRefresh::new(true),
|
||||
mixnet_contract_cache,
|
||||
node_annotations_cache,
|
||||
storage,
|
||||
described_nodes_cache: SharedCache::<DescribedNodes>::new(),
|
||||
network_details: NetworkDetails::new(
|
||||
"localhost".to_string(),
|
||||
NymNetworkDetails::new_empty(),
|
||||
),
|
||||
node_info_cache: unstable::NodeInfoCache::default(),
|
||||
contract_info_cache: ContractDetailsCache::new(Duration::from_secs(42)),
|
||||
api_status: ApiStatusState::new(None),
|
||||
ecash_state: Arc::new(ecash_state),
|
||||
}
|
||||
}
|
||||
|
||||
async fn new() -> Self {
|
||||
let mut rng = crate::ecash::tests::fixtures::test_rng([69u8; 32]);
|
||||
let coconut_keypair = ttp_keygen(1, 1).unwrap().remove(0);
|
||||
let identity = ed25519::KeyPair::new(&mut rng);
|
||||
let epoch = Arc::new(AtomicU64::new(1));
|
||||
let address = AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap();
|
||||
let comm_channel = DummyCommunicationChannel::new_single_dummy(
|
||||
coconut_keypair.verification_key().clone(),
|
||||
address.clone(),
|
||||
)
|
||||
.with_epoch(epoch.clone());
|
||||
let ecash_clients = comm_channel.clients_arc();
|
||||
|
||||
// TODO: it's AWFUL to test with actual storage, we should somehow abstract it away
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let storage = NymApiStorage::init(tmp_dir.path().join("TESTING_STORAGE.db"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let staged_key_pair = crate::ecash::keys::KeyPair::new();
|
||||
staged_key_pair
|
||||
.set(KeyPairWithEpoch {
|
||||
keys: coconut_keypair,
|
||||
issued_for_epoch: 1,
|
||||
})
|
||||
.await;
|
||||
staged_key_pair.validate();
|
||||
|
||||
let chain_state = SharedFakeChain::default();
|
||||
let nyxd_client = DummyClient::new(address, chain_state.clone());
|
||||
|
||||
let ecash_contract = chain_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.ecash_contract
|
||||
.address
|
||||
.clone()
|
||||
.as_str()
|
||||
.parse()
|
||||
.unwrap();
|
||||
let storage = NymApiStorage::init_in_memory().await.unwrap();
|
||||
|
||||
let mut config = config::Config::new("test");
|
||||
config.ecash_signer.enabled = true;
|
||||
|
||||
let ecash_state = EcashState::new(
|
||||
&config,
|
||||
ecash_contract,
|
||||
nyxd_client,
|
||||
identity,
|
||||
staged_key_pair,
|
||||
comm_channel,
|
||||
let bundle = build_dummy_ecash_state(&config, storage.clone(), [69u8; 32]).await;
|
||||
|
||||
let app_state = crate::support::http::state::test_helpers::build_app_state(
|
||||
storage.clone(),
|
||||
&ShutdownManager::empty_mock(),
|
||||
bundle.ecash_state,
|
||||
bundle.real_client,
|
||||
SharedCache::<NodeFamiliesCacheData>::new(),
|
||||
);
|
||||
|
||||
// ideally this would have been generic, but that's way too much work
|
||||
// since then `AppState` would have had to be made generic
|
||||
// also, this is such a disgusting workaround to make it 'work'. yuck
|
||||
let mut dummy = NymNetworkDetails::new_empty();
|
||||
dummy.endpoints = vec![ValidatorDetails::new(
|
||||
"http://127.0.0.1:26657",
|
||||
Some("http://why-do-we-even-need-api-url-set-here.wtf"),
|
||||
None,
|
||||
)];
|
||||
dummy.export_to_env();
|
||||
let another_fake_nyxd_client = Client::new(&config).unwrap();
|
||||
|
||||
TestFixture {
|
||||
axum: TestServer::new(Router::new().nest("/v1/ecash", ecash_routes()).with_state(
|
||||
Self::build_app_state(
|
||||
storage.clone(),
|
||||
ecash_state,
|
||||
another_fake_nyxd_client,
|
||||
&tmp_dir,
|
||||
),
|
||||
))
|
||||
axum: TestServer::new(
|
||||
Router::new()
|
||||
.nest("/v1/ecash", ecash_routes())
|
||||
.with_state(app_state),
|
||||
)
|
||||
.unwrap(),
|
||||
storage,
|
||||
chain_state,
|
||||
epoch,
|
||||
ecash_clients,
|
||||
_tmp_dir: tmp_dir,
|
||||
chain_state: bundle.chain_state,
|
||||
epoch: bundle.epoch,
|
||||
ecash_clients: bundle.ecash_clients,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1573,11 +1550,8 @@ mod credential_tests {
|
||||
|
||||
let nyxd_client = DummyClient::new(address.clone(), Default::default());
|
||||
let key_pair = ttp_keygen(1, 1).unwrap().remove(0);
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
|
||||
let storage = NymApiStorage::init(tmp_dir.path().join("storage.db"))
|
||||
.await
|
||||
.unwrap();
|
||||
let storage = NymApiStorage::init_in_memory().await.unwrap();
|
||||
let comm_channel = DummyCommunicationChannel::new_single_dummy(
|
||||
key_pair.verification_key().clone(),
|
||||
address,
|
||||
|
||||
@@ -17,6 +17,7 @@ pub(crate) mod mixnet_contract_cache;
|
||||
pub(crate) mod network;
|
||||
mod network_monitor;
|
||||
pub(crate) mod node_describe_cache;
|
||||
mod node_families;
|
||||
mod node_performance;
|
||||
pub(crate) mod node_status_api;
|
||||
pub(crate) mod nym_nodes;
|
||||
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmwasm_std::Coin;
|
||||
use nym_api_requests::models::node_families::{
|
||||
NodeFamily, NodeFamilyMember, NodeStakeInformation, PendingFamilyInvitation,
|
||||
};
|
||||
use nym_mixnet_contract_common::{NodeId, NymNodeDetails};
|
||||
use nym_node_families_contract_common::{FamilyMemberRecord, NodeFamilyId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
pub(crate) mod refresher;
|
||||
|
||||
/// Cached view of a single family member, joining the contract membership
|
||||
/// record with mixnet-contract node details (bond height + stake).
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub(crate) struct CachedFamilyMember {
|
||||
pub(crate) node_id: NodeId,
|
||||
|
||||
/// Block-time at which the node joined the family.
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub(crate) joined_at: OffsetDateTime,
|
||||
|
||||
/// Bonding height from the mixnet contract; `None` if the node is no
|
||||
/// longer in the mixnet cache snapshot.
|
||||
pub(crate) bonding_height: Option<u64>,
|
||||
|
||||
/// Stake/bond/delegation snapshot; `None` if the node is no longer in the
|
||||
/// mixnet cache snapshot.
|
||||
pub(crate) node_stake_information: Option<NodeStakeInformation>,
|
||||
}
|
||||
|
||||
impl CachedFamilyMember {
|
||||
pub(crate) fn new(
|
||||
record: FamilyMemberRecord,
|
||||
node_information: Option<&NymNodeDetails>,
|
||||
) -> Self {
|
||||
CachedFamilyMember {
|
||||
node_id: record.node_id,
|
||||
joined_at: OffsetDateTime::from_unix_timestamp(record.membership.joined_at as i64)
|
||||
.unwrap_or(OffsetDateTime::UNIX_EPOCH),
|
||||
bonding_height: node_information.map(|n| n.bond_information.bonding_height),
|
||||
node_stake_information: node_information.map(|n| (&n.rewarding_details).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cached pending invitation entry for a family.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub(crate) struct CachedFamilyInvitation {
|
||||
/// Node the invitation is addressed to.
|
||||
pub(crate) node_id: NodeId,
|
||||
|
||||
/// Block-time after which the invitation can no longer be accepted.
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub(crate) expires_at: OffsetDateTime,
|
||||
}
|
||||
|
||||
impl From<nym_node_families_contract_common::PendingFamilyInvitationDetails>
|
||||
for CachedFamilyInvitation
|
||||
{
|
||||
fn from(invitation: nym_node_families_contract_common::PendingFamilyInvitationDetails) -> Self {
|
||||
CachedFamilyInvitation {
|
||||
node_id: invitation.invitation.node_id,
|
||||
expires_at: OffsetDateTime::from_unix_timestamp(
|
||||
invitation.invitation.expires_at as i64,
|
||||
)
|
||||
.unwrap_or(OffsetDateTime::UNIX_EPOCH),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cached family record with its current members, pending invitations and
|
||||
/// aggregated stats (`average_node_age`, `total_stake`).
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub(crate) struct CachedFamily {
|
||||
/// Unique family identifier assigned by the contract.
|
||||
pub(crate) id: NodeFamilyId,
|
||||
|
||||
/// Display name (canonical form — see `normalise_family_name`).
|
||||
pub(crate) name: String,
|
||||
|
||||
/// Free-form family description.
|
||||
pub(crate) description: String,
|
||||
|
||||
/// Owner address (cosmos `Addr` rendered as a string).
|
||||
pub(crate) owner: String,
|
||||
|
||||
/// Average age of members, computed from cached per-height
|
||||
/// block timestamps.
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub(crate) average_node_age: Duration,
|
||||
|
||||
/// Sum of member stakes; `None` when no member has reportable stake (e.g.
|
||||
/// all bonds unbonding).
|
||||
pub(crate) total_stake: Option<Coin>,
|
||||
|
||||
/// Block-time the family was created.
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub(crate) created_at: OffsetDateTime,
|
||||
|
||||
/// Current members of the family.
|
||||
pub(crate) members: Vec<CachedFamilyMember>,
|
||||
|
||||
/// Outstanding invitations issued by the family owner.
|
||||
pub(crate) pending_invitations: Vec<CachedFamilyInvitation>,
|
||||
}
|
||||
|
||||
/// Full nym-api node-families cache snapshot — combined families-contract
|
||||
/// state plus mixnet-contract stake/bond information.
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct NodeFamiliesCacheData {
|
||||
/// Every family known to the contract, keyed by family id. `BTreeMap` so
|
||||
/// iteration order is deterministic across refreshes (stable list
|
||||
/// endpoint output, deterministic on-disk bincode).
|
||||
pub(crate) families: BTreeMap<NodeFamilyId, CachedFamily>,
|
||||
|
||||
/// Secondary index: member node id → family id. Built alongside
|
||||
/// `families` during refresh; lets `by-node` lookups avoid an O(families
|
||||
/// × members) scan.
|
||||
pub(crate) family_by_member: HashMap<NodeId, NodeFamilyId>,
|
||||
|
||||
/// Persistent block-height → block-time cache used by the refresher when
|
||||
/// computing per-member age. Survives restarts via the on-disk cache file.
|
||||
pub(crate) block_timestamps: HashMap<u64, OffsetDateTime>,
|
||||
}
|
||||
|
||||
/// Intermediate accumulator used while folding contract data into a
|
||||
/// [`CachedFamily`]; finalised via [`Self::build`] once `average_node_age` is
|
||||
/// known.
|
||||
pub(crate) struct CachedFamilyBuilder {
|
||||
/// Unique family identifier assigned by the contract.
|
||||
pub(crate) id: NodeFamilyId,
|
||||
|
||||
/// Display name (canonical form — see `normalise_family_name`).
|
||||
pub(crate) name: String,
|
||||
|
||||
/// Free-form family description.
|
||||
pub(crate) description: String,
|
||||
|
||||
/// Owner address (cosmos `Addr` rendered as a string).
|
||||
pub(crate) owner: String,
|
||||
|
||||
/// Block-time the family was created.
|
||||
pub(crate) created_at: OffsetDateTime,
|
||||
|
||||
/// Members accumulated as the refresher iterates the contract response.
|
||||
pub(crate) members: Vec<CachedFamilyMember>,
|
||||
|
||||
/// Pending invitations accumulated as the refresher iterates the contract
|
||||
/// response.
|
||||
pub(crate) pending_invitations: Vec<CachedFamilyInvitation>,
|
||||
}
|
||||
|
||||
impl CachedFamilyBuilder {
|
||||
pub(crate) fn build(self, average_node_age: Duration) -> CachedFamily {
|
||||
let total_stake = self.total_family_stake();
|
||||
|
||||
CachedFamily {
|
||||
id: self.id,
|
||||
name: self.name,
|
||||
description: self.description,
|
||||
owner: self.owner,
|
||||
created_at: self.created_at,
|
||||
members: self.members,
|
||||
pending_invitations: self.pending_invitations,
|
||||
total_stake,
|
||||
average_node_age,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sum the per-member stake into a single family total. Returns `None` if
|
||||
/// no member has a known stake.
|
||||
pub(crate) fn total_family_stake(&self) -> Option<Coin> {
|
||||
self.members
|
||||
.iter()
|
||||
.filter_map(|m| m.node_stake_information.as_ref().map(|s| s.stake.clone()))
|
||||
.reduce(|acc, e| {
|
||||
let mut updated = acc;
|
||||
updated.amount += e.amount;
|
||||
updated
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CachedFamilyInvitation> for PendingFamilyInvitation {
|
||||
fn from(value: &CachedFamilyInvitation) -> Self {
|
||||
PendingFamilyInvitation {
|
||||
node_id: value.node_id,
|
||||
expires_at: value.expires_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CachedFamilyMember> for NodeFamilyMember {
|
||||
fn from(value: &CachedFamilyMember) -> Self {
|
||||
NodeFamilyMember {
|
||||
node_id: value.node_id,
|
||||
joined_at: value.joined_at,
|
||||
stake_information: value.node_stake_information.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CachedFamily> for NodeFamily {
|
||||
fn from(value: &CachedFamily) -> Self {
|
||||
NodeFamily {
|
||||
id: value.id,
|
||||
name: value.name.clone(),
|
||||
description: value.description.clone(),
|
||||
owner: value.owner.clone(),
|
||||
average_node_age: value.average_node_age,
|
||||
total_stake: value.total_stake.clone(),
|
||||
created_at: value.created_at,
|
||||
members: value.members.iter().map(Into::into).collect(),
|
||||
pending_invitations: value.pending_invitations.iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// initial conversion with empty details
|
||||
impl From<nym_node_families_contract_common::NodeFamily> for CachedFamilyBuilder {
|
||||
fn from(value: nym_node_families_contract_common::NodeFamily) -> Self {
|
||||
CachedFamilyBuilder {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
description: value.description,
|
||||
owner: value.owner.to_string(),
|
||||
created_at: OffsetDateTime::from_unix_timestamp(value.created_at as i64)
|
||||
.unwrap_or(OffsetDateTime::UNIX_EPOCH),
|
||||
members: Vec::new(),
|
||||
pending_invitations: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::mixnet_contract_cache::cache::MixnetContractCache;
|
||||
use crate::node_families::cache::{CachedFamilyBuilder, CachedFamilyMember, NodeFamiliesCacheData};
|
||||
use crate::support::caching::cache::SharedCache;
|
||||
use crate::support::caching::refresher::CacheItemProvider;
|
||||
use crate::support::nyxd::Client;
|
||||
use async_trait::async_trait;
|
||||
use futures::{stream, StreamExt};
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_validator_client::nyxd::contract_traits::PagedNodeFamiliesQueryClient;
|
||||
use nym_validator_client::nyxd::error::NyxdError;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::error;
|
||||
|
||||
/// Periodic refresher feeding the [`NodeFamiliesCacheData`] cache from the
|
||||
/// node-families contract, joined with mixnet-contract stake snapshots.
|
||||
pub struct NodeFamiliesDataProvider {
|
||||
/// Nyxd client used for contract queries and block timestamp lookups.
|
||||
nyxd_client: Client,
|
||||
|
||||
/// Source of per-node stake/delegation information.
|
||||
mixnet_contract_cache: MixnetContractCache,
|
||||
|
||||
/// Read-only handle to the cache this provider feeds. Used to recover the
|
||||
/// previously-known block-height → block-time map (rehydrated from disk on
|
||||
/// startup) so we only RPC heights we haven't already seen.
|
||||
shared_cache: SharedCache<NodeFamiliesCacheData>,
|
||||
|
||||
/// Maximum number of `block_timestamp` lookups in flight in parallel during a
|
||||
/// single refresh tick.
|
||||
block_timestamp_fetch_concurrency: usize,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CacheItemProvider for NodeFamiliesDataProvider {
|
||||
type Item = NodeFamiliesCacheData;
|
||||
type Error = NyxdError;
|
||||
|
||||
async fn wait_until_ready(&self) {
|
||||
self.mixnet_contract_cache
|
||||
.naive_wait_for_initial_values()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn try_refresh(&mut self) -> Result<Option<Self::Item>, Self::Error> {
|
||||
self.refresh().await.map(Some)
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeFamiliesDataProvider {
|
||||
pub(crate) fn new(
|
||||
block_timestamp_fetch_concurrency: usize,
|
||||
nyxd_client: Client,
|
||||
mixnet_contract_cache: MixnetContractCache,
|
||||
shared_cache: SharedCache<NodeFamiliesCacheData>,
|
||||
) -> Self {
|
||||
NodeFamiliesDataProvider {
|
||||
nyxd_client,
|
||||
mixnet_contract_cache,
|
||||
shared_cache,
|
||||
block_timestamp_fetch_concurrency,
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshot of the previously-cached block timestamps (rehydrated from
|
||||
/// disk on startup). Empty if the cache hasn't been initialised yet.
|
||||
async fn previous_block_timestamps(&self) -> HashMap<u64, OffsetDateTime> {
|
||||
let Ok(prev) = self.shared_cache.get().await else {
|
||||
return HashMap::new();
|
||||
};
|
||||
prev.block_timestamps.clone()
|
||||
}
|
||||
|
||||
/// Pull the full families/members/pending-invitations snapshot from the
|
||||
/// node-families contract and join it with the latest mixnet-contract node
|
||||
/// information for stake/bonding data.
|
||||
async fn refresh(&self) -> Result<NodeFamiliesCacheData, NyxdError> {
|
||||
// retrieve the base data from the contract
|
||||
let raw_families = self.nyxd_client.get_all_families().await?;
|
||||
let raw_members = self.nyxd_client.get_all_family_members().await?;
|
||||
let pending_invites = self.nyxd_client.get_all_pending_invitations().await?;
|
||||
|
||||
let nym_nodes = self
|
||||
.mixnet_contract_cache
|
||||
.nym_nodes()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|node| (node.node_id(), node))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let mut families: HashMap<_, CachedFamilyBuilder> = raw_families
|
||||
.into_iter()
|
||||
.map(|family| (family.id, family.into()))
|
||||
.collect();
|
||||
let mut family_by_member: HashMap<NodeId, _> = HashMap::new();
|
||||
|
||||
// insert all member information into appropriate families
|
||||
for member_record in raw_members {
|
||||
let family_id = member_record.membership.family_id;
|
||||
let node_id = member_record.node_id;
|
||||
let Some(family) = families.get_mut(&family_id) else {
|
||||
error!(
|
||||
"node {node_id} belongs to family {family_id}, but this family does not exist!",
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let node_info = nym_nodes.get(&node_id);
|
||||
family
|
||||
.members
|
||||
.push(CachedFamilyMember::new(member_record, node_info));
|
||||
family_by_member.insert(node_id, family_id);
|
||||
}
|
||||
|
||||
// insert all invitations into appropriate families
|
||||
for invitation in pending_invites {
|
||||
let family_id = invitation.invitation.family_id;
|
||||
let node_id = invitation.invitation.node_id;
|
||||
let Some(family) = families.get_mut(&family_id) else {
|
||||
error!(
|
||||
"node {node_id} has been invited to family {family_id}, but this family does not exist!",
|
||||
);
|
||||
continue;
|
||||
};
|
||||
family.pending_invitations.push(invitation.into());
|
||||
}
|
||||
|
||||
let referenced_heights: HashSet<u64> = families
|
||||
.values()
|
||||
.flat_map(|f| f.members.iter().filter_map(|m| m.bonding_height))
|
||||
.collect();
|
||||
|
||||
let block_timestamps = self.resolve_block_timestamps(&referenced_heights).await;
|
||||
|
||||
let family_details: BTreeMap<_, _> = families
|
||||
.into_values()
|
||||
.map(|family| {
|
||||
let average_node_age = average_node_age(&family.members, &block_timestamps);
|
||||
let built = family.build(average_node_age);
|
||||
(built.id, built)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(NodeFamiliesCacheData {
|
||||
families: family_details,
|
||||
family_by_member,
|
||||
block_timestamps,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build the block-height → block-time map for this refresh: keep entries
|
||||
/// from the previous cache that we still need, parallel-fetch the rest.
|
||||
async fn resolve_block_timestamps(
|
||||
&self,
|
||||
referenced_heights: &HashSet<u64>,
|
||||
) -> HashMap<u64, OffsetDateTime> {
|
||||
let mut block_timestamps = self.previous_block_timestamps().await;
|
||||
|
||||
let to_fetch: Vec<u64> = referenced_heights
|
||||
.iter()
|
||||
.filter(|h| !block_timestamps.contains_key(h))
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
let fetched: Vec<(u64, OffsetDateTime)> = stream::iter(to_fetch)
|
||||
.map(|h| async move {
|
||||
match self.nyxd_client.block_timestamp(h as u32).await {
|
||||
Ok(t) => Some((h, OffsetDateTime::from(t))),
|
||||
Err(err) => {
|
||||
error!("failed to retrieve block timestamp for height {h}: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.buffer_unordered(self.block_timestamp_fetch_concurrency)
|
||||
.filter_map(|x| async move { x })
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
block_timestamps.extend(fetched);
|
||||
block_timestamps
|
||||
}
|
||||
}
|
||||
|
||||
/// Average member age: for each member with a known bonding
|
||||
/// height we have a cached block-time, take `now - t` and average. Heights we
|
||||
/// failed to resolve are skipped rather than poisoning the average.
|
||||
fn average_node_age(
|
||||
members: &[CachedFamilyMember],
|
||||
block_timestamps: &HashMap<u64, OffsetDateTime>,
|
||||
) -> Duration {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let mut total_secs: i64 = 0;
|
||||
let mut count: i64 = 0;
|
||||
for height in members.iter().filter_map(|m| m.bonding_height) {
|
||||
let Some(ts) = block_timestamps.get(&height) else {
|
||||
continue;
|
||||
};
|
||||
let age = (now - *ts).whole_seconds();
|
||||
if age < 0 {
|
||||
continue;
|
||||
}
|
||||
total_secs += age;
|
||||
count += 1;
|
||||
}
|
||||
if count == 0 {
|
||||
return Duration::ZERO;
|
||||
}
|
||||
Duration::from_secs((total_secs / count) as u64)
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node_status_api::models::AxumResult;
|
||||
use crate::support::http::helpers::PaginationRequestV2;
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use nym_api_requests::models::node_families::{
|
||||
NodeFamily, NodeFamilyForNodeResponse, NodeFamilyResponse,
|
||||
};
|
||||
use nym_api_requests::pagination::{PaginatedResponse, Pagination};
|
||||
use nym_http_api_common::{FormattedResponse, OutputParamsV2};
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_node_families_contract_common::NodeFamilyId;
|
||||
use std::cmp::min;
|
||||
|
||||
const DEFAULT_FAMILIES_PAGE_SIZE: u32 = 50;
|
||||
const MAX_FAMILIES_PAGE_SIZE: u32 = 200;
|
||||
|
||||
// /v1/node-families
|
||||
pub(crate) fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(get_families))
|
||||
.route("/:family_id", get(get_family_by_id))
|
||||
.route("/by-node/:node_id", get(get_family_for_node))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Node Families",
|
||||
get,
|
||||
path = "",
|
||||
context_path = "/v1/node-families",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(PaginatedResponse<NodeFamily> = "application/json"),
|
||||
(PaginatedResponse<NodeFamily> = "application/yaml"),
|
||||
))
|
||||
),
|
||||
params(PaginationRequestV2)
|
||||
)]
|
||||
async fn get_families(
|
||||
Query(pagination): Query<PaginationRequestV2>,
|
||||
State(state): State<AppState>,
|
||||
) -> AxumResult<FormattedResponse<PaginatedResponse<NodeFamily>>> {
|
||||
let page = pagination.page.unwrap_or_default();
|
||||
let per_page = min(
|
||||
pagination.per_page.unwrap_or(DEFAULT_FAMILIES_PAGE_SIZE),
|
||||
MAX_FAMILIES_PAGE_SIZE,
|
||||
);
|
||||
let output = pagination.output.unwrap_or_default();
|
||||
|
||||
let cache = state.node_families_cache.get().await?;
|
||||
let total = cache.families.len();
|
||||
let offset = (page as usize).saturating_mul(per_page as usize);
|
||||
|
||||
// BTreeMap ascending-id iteration is stable across refreshes, so paging
|
||||
// by offset is well-defined: page N is the same window of ids on every
|
||||
// call (until the underlying set changes).
|
||||
let data: Vec<NodeFamily> = cache
|
||||
.families
|
||||
.values()
|
||||
.skip(offset)
|
||||
.take(per_page as usize)
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
|
||||
Ok(output.to_response(PaginatedResponse {
|
||||
pagination: Pagination {
|
||||
total,
|
||||
page,
|
||||
size: data.len(),
|
||||
},
|
||||
data,
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Node Families",
|
||||
get,
|
||||
path = "/{family_id}",
|
||||
context_path = "/v1/node-families",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(NodeFamilyResponse = "application/json"),
|
||||
(NodeFamilyResponse = "application/yaml"),
|
||||
))
|
||||
),
|
||||
params(
|
||||
("family_id" = u32, Path, description = "Identifier of the family"),
|
||||
OutputParamsV2,
|
||||
)
|
||||
)]
|
||||
async fn get_family_by_id(
|
||||
Path(family_id): Path<NodeFamilyId>,
|
||||
Query(output): Query<OutputParamsV2>,
|
||||
State(state): State<AppState>,
|
||||
) -> AxumResult<FormattedResponse<NodeFamilyResponse>> {
|
||||
let output = output.get_output();
|
||||
|
||||
let cache = state.node_families_cache.get().await?;
|
||||
let family = cache.families.get(&family_id).map(NodeFamily::from);
|
||||
|
||||
Ok(output.to_response(NodeFamilyResponse { family }))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Node Families",
|
||||
get,
|
||||
path = "/by-node/{node_id}",
|
||||
context_path = "/v1/node-families",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(NodeFamilyForNodeResponse = "application/json"),
|
||||
(NodeFamilyForNodeResponse = "application/yaml"),
|
||||
))
|
||||
),
|
||||
params(
|
||||
("node_id" = u32, Path, description = "Identifier of the member node"),
|
||||
OutputParamsV2,
|
||||
)
|
||||
)]
|
||||
async fn get_family_for_node(
|
||||
Path(node_id): Path<NodeId>,
|
||||
Query(output): Query<OutputParamsV2>,
|
||||
State(state): State<AppState>,
|
||||
) -> AxumResult<FormattedResponse<NodeFamilyForNodeResponse>> {
|
||||
let output = output.get_output();
|
||||
|
||||
let cache = state.node_families_cache.get().await?;
|
||||
let family = cache
|
||||
.family_by_member
|
||||
.get(&node_id)
|
||||
.and_then(|family_id| cache.families.get(family_id))
|
||||
.map(NodeFamily::from);
|
||||
|
||||
Ok(output.to_response(NodeFamilyForNodeResponse { node_id, family }))
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::mixnet_contract_cache::cache::MixnetContractCache;
|
||||
use crate::node_families::cache::refresher::NodeFamiliesDataProvider;
|
||||
use crate::node_families::cache::NodeFamiliesCacheData;
|
||||
use crate::support::caching::cache::SharedCache;
|
||||
use crate::support::caching::refresher::CacheRefresher;
|
||||
use crate::support::{config, nyxd};
|
||||
use nym_validator_client::nyxd::error::NyxdError;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub(crate) mod cache;
|
||||
pub(crate) mod handlers;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub(crate) fn build_refresher(
|
||||
config: &config::NodeFamiliesCache,
|
||||
mixnet_contract_cache: &MixnetContractCache,
|
||||
node_families_cache: &SharedCache<NodeFamiliesCacheData>,
|
||||
nyxd_client: nyxd::Client,
|
||||
on_disk_file: PathBuf,
|
||||
) -> CacheRefresher<NodeFamiliesCacheData, NyxdError> {
|
||||
CacheRefresher::new_with_initial_value(
|
||||
Box::new(NodeFamiliesDataProvider::new(
|
||||
config.debug.node_families_block_timestamp_fetch_concurrency,
|
||||
nyxd_client,
|
||||
mixnet_contract_cache.clone(),
|
||||
node_families_cache.clone(),
|
||||
)),
|
||||
config.debug.caching_interval,
|
||||
node_families_cache.clone(),
|
||||
)
|
||||
.named("node-families-cache-refresher")
|
||||
.with_persistent_cache(on_disk_file)
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::ecash::tests::build_dummy_ecash_state;
|
||||
use crate::node_families::cache::{CachedFamily, CachedFamilyMember, NodeFamiliesCacheData};
|
||||
use crate::node_families::handlers;
|
||||
use crate::support::caching::cache::SharedCache;
|
||||
use crate::support::config;
|
||||
use crate::support::http::state::test_helpers::build_app_state;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use axum::Router;
|
||||
use axum_test::http::StatusCode;
|
||||
use axum_test::TestServer;
|
||||
use cosmwasm_std::Coin;
|
||||
use nym_api_requests::models::node_families::{
|
||||
NodeFamily, NodeFamilyForNodeResponse, NodeFamilyResponse, NodeStakeInformation,
|
||||
};
|
||||
use nym_api_requests::pagination::PaginatedResponse;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_node_families_contract_common::NodeFamilyId;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
struct NodeFamiliesTestFixture {
|
||||
axum: TestServer,
|
||||
cache: SharedCache<NodeFamiliesCacheData>,
|
||||
}
|
||||
|
||||
impl NodeFamiliesTestFixture {
|
||||
/// Build a test server with the node-families router mounted and an empty
|
||||
/// (but initialised) shared cache. Use [`seed`] to populate it.
|
||||
async fn new() -> Self {
|
||||
let storage = NymApiStorage::init_in_memory().await.unwrap();
|
||||
|
||||
let cache = SharedCache::<NodeFamiliesCacheData>::new_with_value(Default::default());
|
||||
|
||||
// node-families handlers don't read `AppState.ecash_state`, but
|
||||
// `AppState` requires one; we just need a valid construction.
|
||||
let mut cfg = config::Config::new("test");
|
||||
cfg.ecash_signer.enabled = false;
|
||||
let bundle = build_dummy_ecash_state(&cfg, storage.clone(), [7u8; 32]).await;
|
||||
|
||||
let app_state = build_app_state(
|
||||
storage,
|
||||
bundle.ecash_state,
|
||||
bundle.real_client,
|
||||
cache.clone(),
|
||||
);
|
||||
|
||||
let server = TestServer::new(
|
||||
Router::new()
|
||||
.nest("/v1/node-families", handlers::routes())
|
||||
.with_state(app_state),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
NodeFamiliesTestFixture {
|
||||
axum: server,
|
||||
cache,
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the cached data with the provided snapshot.
|
||||
async fn seed(&self, data: NodeFamiliesCacheData) {
|
||||
// try_overwrite_old_value swaps the entire cached value
|
||||
let res = self
|
||||
.cache
|
||||
.try_overwrite_old_value(data, "node-families-test")
|
||||
.await;
|
||||
assert!(res.is_ok(), "failed to seed node-families cache: {res:?}");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- fixtures ----------
|
||||
|
||||
fn stake_coin(amount: u128) -> Coin {
|
||||
Coin::new(amount, "unym")
|
||||
}
|
||||
|
||||
fn member(node_id: NodeId, with_stake: Option<u128>) -> CachedFamilyMember {
|
||||
CachedFamilyMember {
|
||||
node_id,
|
||||
joined_at: OffsetDateTime::UNIX_EPOCH,
|
||||
bonding_height: Some(1),
|
||||
node_stake_information: with_stake.map(|amt| NodeStakeInformation {
|
||||
stake: stake_coin(amt),
|
||||
bond: stake_coin(amt),
|
||||
delegations: stake_coin(0),
|
||||
delegators: 0,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn family(id: NodeFamilyId, name: &str, members: Vec<CachedFamilyMember>) -> CachedFamily {
|
||||
CachedFamily {
|
||||
id,
|
||||
name: name.to_string(),
|
||||
description: format!("{name} description"),
|
||||
owner: format!("n1owner{id}"),
|
||||
average_node_age: Duration::ZERO,
|
||||
total_stake: None,
|
||||
created_at: OffsetDateTime::UNIX_EPOCH,
|
||||
members,
|
||||
pending_invitations: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot(families: Vec<CachedFamily>) -> NodeFamiliesCacheData {
|
||||
let mut family_by_member: HashMap<NodeId, NodeFamilyId> = HashMap::new();
|
||||
let mut families_map: BTreeMap<NodeFamilyId, CachedFamily> = BTreeMap::new();
|
||||
for family in families {
|
||||
for m in &family.members {
|
||||
family_by_member.insert(m.node_id, family.id);
|
||||
}
|
||||
families_map.insert(family.id, family);
|
||||
}
|
||||
NodeFamiliesCacheData {
|
||||
families: families_map,
|
||||
family_by_member,
|
||||
block_timestamps: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- /v1/node-families (list) ----------
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_returns_empty_when_cache_has_no_families() {
|
||||
let fx = NodeFamiliesTestFixture::new().await;
|
||||
|
||||
let res = fx.axum.get("/v1/node-families").await;
|
||||
assert_eq!(res.status_code(), StatusCode::OK);
|
||||
let body: PaginatedResponse<NodeFamily> = res.json();
|
||||
assert_eq!(body.pagination.total, 0);
|
||||
assert_eq!(body.pagination.size, 0);
|
||||
assert_eq!(body.pagination.page, 0);
|
||||
assert!(body.data.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_returns_every_seeded_family() {
|
||||
let fx = NodeFamiliesTestFixture::new().await;
|
||||
fx.seed(snapshot(vec![
|
||||
family(1, "alpha", vec![member(10, Some(100))]),
|
||||
family(2, "beta", vec![member(20, None)]),
|
||||
]))
|
||||
.await;
|
||||
|
||||
let res = fx.axum.get("/v1/node-families").await;
|
||||
let body: PaginatedResponse<NodeFamily> = res.json();
|
||||
assert_eq!(body.pagination.total, 2);
|
||||
let ids: Vec<_> = body.data.iter().map(|f| f.id).collect();
|
||||
assert_eq!(ids, vec![1, 2]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_paginates_by_offset_in_ascending_id_order() {
|
||||
let fx = NodeFamiliesTestFixture::new().await;
|
||||
let families: Vec<_> = (1u32..=5)
|
||||
.map(|i| family(i, &format!("f{i}"), vec![]))
|
||||
.collect();
|
||||
fx.seed(snapshot(families)).await;
|
||||
|
||||
// page 0 / per_page 2 → ids [1, 2]
|
||||
let res = fx.axum.get("/v1/node-families?page=0&per_page=2").await;
|
||||
let page0: PaginatedResponse<NodeFamily> = res.json();
|
||||
assert_eq!(page0.pagination.total, 5);
|
||||
assert_eq!(page0.pagination.page, 0);
|
||||
assert_eq!(page0.pagination.size, 2);
|
||||
assert_eq!(
|
||||
page0.data.iter().map(|f| f.id).collect::<Vec<_>>(),
|
||||
vec![1, 2]
|
||||
);
|
||||
|
||||
// page 1 / per_page 2 → ids [3, 4]
|
||||
let res = fx.axum.get("/v1/node-families?page=1&per_page=2").await;
|
||||
let page1: PaginatedResponse<NodeFamily> = res.json();
|
||||
assert_eq!(
|
||||
page1.data.iter().map(|f| f.id).collect::<Vec<_>>(),
|
||||
vec![3, 4]
|
||||
);
|
||||
|
||||
// page 2 / per_page 2 → just id 5 on the last page
|
||||
let res = fx.axum.get("/v1/node-families?page=2&per_page=2").await;
|
||||
let page2: PaginatedResponse<NodeFamily> = res.json();
|
||||
assert_eq!(page2.pagination.size, 1);
|
||||
assert_eq!(page2.data.iter().map(|f| f.id).collect::<Vec<_>>(), vec![5]);
|
||||
|
||||
// page 3 / per_page 2 → past the end; data empty but total still reported
|
||||
let res = fx.axum.get("/v1/node-families?page=3&per_page=2").await;
|
||||
let page3: PaginatedResponse<NodeFamily> = res.json();
|
||||
assert_eq!(page3.pagination.total, 5);
|
||||
assert_eq!(page3.pagination.size, 0);
|
||||
assert!(page3.data.is_empty());
|
||||
}
|
||||
|
||||
// ---------- /v1/node-families/{family_id} ----------
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_family_by_id_returns_some_on_hit() {
|
||||
let fx = NodeFamiliesTestFixture::new().await;
|
||||
fx.seed(snapshot(vec![family(7, "seven", vec![])])).await;
|
||||
|
||||
let res = fx.axum.get("/v1/node-families/7").await;
|
||||
assert_eq!(res.status_code(), StatusCode::OK);
|
||||
let body: NodeFamilyResponse = res.json();
|
||||
let family = body.family.expect("family should be present");
|
||||
assert_eq!(family.id, 7);
|
||||
assert_eq!(family.name, "seven");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_family_by_id_returns_none_on_miss() {
|
||||
let fx = NodeFamiliesTestFixture::new().await;
|
||||
fx.seed(snapshot(vec![family(1, "alpha", vec![])])).await;
|
||||
|
||||
let res = fx.axum.get("/v1/node-families/999").await;
|
||||
// still 200 — `family: None` signals absence
|
||||
assert_eq!(res.status_code(), StatusCode::OK);
|
||||
let body: NodeFamilyResponse = res.json();
|
||||
assert!(body.family.is_none());
|
||||
}
|
||||
|
||||
// ---------- /v1/node-families/by-node/{node_id} ----------
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_family_for_node_returns_some_on_hit() {
|
||||
let fx = NodeFamiliesTestFixture::new().await;
|
||||
fx.seed(snapshot(vec![family(
|
||||
3,
|
||||
"gamma",
|
||||
vec![member(42, None), member(43, None)],
|
||||
)]))
|
||||
.await;
|
||||
|
||||
let res = fx.axum.get("/v1/node-families/by-node/42").await;
|
||||
let body: NodeFamilyForNodeResponse = res.json();
|
||||
assert_eq!(body.node_id, 42);
|
||||
let family = body.family.expect("expected to find the family");
|
||||
assert_eq!(family.id, 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_family_for_node_returns_none_on_miss() {
|
||||
let fx = NodeFamiliesTestFixture::new().await;
|
||||
fx.seed(snapshot(vec![family(1, "alpha", vec![member(10, None)])]))
|
||||
.await;
|
||||
|
||||
let res = fx.axum.get("/v1/node-families/by-node/9999").await;
|
||||
assert_eq!(res.status_code(), StatusCode::OK);
|
||||
let body: NodeFamilyForNodeResponse = res.json();
|
||||
assert_eq!(body.node_id, 9999);
|
||||
assert!(body.family.is_none());
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use crate::key_rotation::KeyRotationController;
|
||||
use crate::mixnet_contract_cache::cache::MixnetContractCache;
|
||||
use crate::network::models::NetworkDetails;
|
||||
use crate::node_describe_cache::cache::DescribedNodes;
|
||||
use crate::node_families::cache::NodeFamiliesCacheData;
|
||||
use crate::node_performance::provider::contract_provider::ContractPerformanceProvider;
|
||||
use crate::node_performance::provider::legacy_storage_provider::LegacyStoragePerformanceProvider;
|
||||
use crate::node_performance::provider::NodePerformanceProvider;
|
||||
@@ -36,7 +37,7 @@ use crate::support::storage::NymApiStorage;
|
||||
use crate::unstable_routes::v1::account::cache::AddressInfoCache;
|
||||
use crate::{
|
||||
ecash, epoch_operations, mixnet_contract_cache, network_monitor, node_describe_cache,
|
||||
node_performance, node_status_api, signers_cache,
|
||||
node_families, node_performance, node_status_api, signers_cache,
|
||||
};
|
||||
use anyhow::{bail, Context};
|
||||
use nym_config::defaults::NymNetworkDetails;
|
||||
@@ -188,6 +189,19 @@ async fn start_nym_api_tasks(mut config: Config) -> anyhow::Result<ShutdownManag
|
||||
described_path,
|
||||
);
|
||||
|
||||
// NODE FAMILIES
|
||||
let node_families_path = storage_cfg.cache_file("node_families");
|
||||
let ttl = config.node_families_cache.debug.caching_interval;
|
||||
let node_families_cache =
|
||||
SharedCache::<NodeFamiliesCacheData>::new_with_persistent(&node_families_path, ttl, None);
|
||||
let node_families_cache_refresher = node_families::build_refresher(
|
||||
&config.node_families_cache,
|
||||
&mixnet_contract_cache_state.clone(),
|
||||
&node_families_cache,
|
||||
nyxd_client.clone(),
|
||||
node_families_path,
|
||||
);
|
||||
|
||||
// NODES ANNOTATIONS
|
||||
let annotations_path = storage_cfg.cache_file("node_annotations");
|
||||
let ttl = config.node_status_api.debug.caching_interval;
|
||||
@@ -316,6 +330,8 @@ async fn start_nym_api_tasks(mut config: Config) -> anyhow::Result<ShutdownManag
|
||||
&shutdown_manager,
|
||||
);
|
||||
|
||||
node_families_cache_refresher.start(shutdown_manager.clone_shutdown_token());
|
||||
|
||||
// start dkg task
|
||||
if config.ecash_signer.enabled {
|
||||
let dkg_bte_keypair = load_bte_keypair(&config.ecash_signer)?;
|
||||
@@ -395,6 +411,7 @@ async fn start_nym_api_tasks(mut config: Config) -> anyhow::Result<ShutdownManag
|
||||
),
|
||||
forced_refresh: ForcedRefresh::new(config.describe_cache.debug.allow_illegal_ips),
|
||||
mixnet_contract_cache,
|
||||
node_families_cache,
|
||||
node_annotations_cache,
|
||||
storage,
|
||||
described_nodes_cache,
|
||||
|
||||
@@ -52,6 +52,11 @@ const DEFAULT_PER_NODE_TEST_PACKETS: usize = 3;
|
||||
|
||||
const DEFAULT_NODE_STATUS_CACHE_REFRESH_INTERVAL: Duration = Duration::from_secs(305);
|
||||
const DEFAULT_MIXNET_CACHE_REFRESH_INTERVAL: Duration = Duration::from_secs(150);
|
||||
const DEFAULT_NODE_FAMILIES_CACHE_REFRESH_INTERVAL: Duration = Duration::from_secs(600);
|
||||
|
||||
/// Maximum number of `block_timestamp` lookups in flight in parallel during a
|
||||
/// single refresh tick.
|
||||
const DEFAULT_NODE_FAMILIES_BLOCK_TIMESTAMP_FETCH_CONCURRENCY: usize = 8;
|
||||
const DEFAULT_PERFORMANCE_CONTRACT_POLLING_INTERVAL: Duration = Duration::from_secs(150);
|
||||
const DEFAULT_PERFORMANCE_CONTRACT_FALLBACK_EPOCHS: u32 = 12;
|
||||
const DEFAULT_PERFORMANCE_CONTRACT_RETAINED_EPOCHS: usize = 25;
|
||||
@@ -121,6 +126,9 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
pub mixnet_contract_cache: MixnetContractCache,
|
||||
|
||||
#[serde(default)]
|
||||
pub node_families_cache: NodeFamiliesCache,
|
||||
|
||||
pub node_status_api: NodeStatusAPI,
|
||||
|
||||
#[serde(alias = "topology_cacher")]
|
||||
@@ -156,6 +164,7 @@ impl Config {
|
||||
performance_provider: Default::default(),
|
||||
network_monitor: NetworkMonitor::new_default(id.as_ref()),
|
||||
mixnet_contract_cache: Default::default(),
|
||||
node_families_cache: Default::default(),
|
||||
node_status_api: NodeStatusAPI::new_default(id.as_ref()),
|
||||
describe_cache: Default::default(),
|
||||
contracts_info_cache: Default::default(),
|
||||
@@ -389,6 +398,40 @@ impl Default for MixnetContractCacheDebug {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
pub struct NodeFamiliesCache {
|
||||
#[serde(default)]
|
||||
pub debug: NodeFamiliesCacheDebug,
|
||||
}
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for NodeFamiliesCache {
|
||||
fn default() -> Self {
|
||||
NodeFamiliesCache {
|
||||
debug: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct NodeFamiliesCacheDebug {
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub caching_interval: Duration,
|
||||
|
||||
pub node_families_block_timestamp_fetch_concurrency: usize,
|
||||
}
|
||||
|
||||
impl Default for NodeFamiliesCacheDebug {
|
||||
fn default() -> Self {
|
||||
NodeFamiliesCacheDebug {
|
||||
caching_interval: DEFAULT_NODE_FAMILIES_CACHE_REFRESH_INTERVAL,
|
||||
node_families_block_timestamp_fetch_concurrency:
|
||||
DEFAULT_NODE_FAMILIES_BLOCK_TIMESTAMP_FETCH_CONCURRENCY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
pub struct PerformanceProvider {
|
||||
/// Specifies whether this nym-api should attempt to retrieve node performance
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use nym_http_api_common::Output;
|
||||
use nym_http_api_common::{Output, OutputV2};
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
@@ -14,6 +14,14 @@ pub struct PaginationRequest {
|
||||
pub per_page: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, ToSchema, IntoParams)]
|
||||
#[into_params(parameter_in = Query)]
|
||||
pub struct PaginationRequestV2 {
|
||||
pub output: Option<OutputV2>,
|
||||
pub page: Option<u32>,
|
||||
pub per_page: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, IntoParams, ToSchema)]
|
||||
#[schema(title = "NodeId")]
|
||||
#[schema(as = NodeId)]
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::circulating_supply_api::handlers::circulating_supply_routes;
|
||||
use crate::ecash::api_routes::handlers::ecash_routes;
|
||||
use crate::mixnet_contract_cache::handlers::{epoch_routes, legacy_nodes_routes};
|
||||
use crate::network::handlers::nym_network_routes;
|
||||
use crate::node_families::handlers as node_families_handlers;
|
||||
use crate::node_status_api::handlers::status_routes;
|
||||
use crate::support::http::openapi::ApiDoc;
|
||||
use crate::support::http::state::AppState;
|
||||
@@ -49,6 +50,7 @@ impl RouterBuilder {
|
||||
.nest("/network", nym_network_routes())
|
||||
.nest("/api-status", status::handlers::api_status_routes())
|
||||
.nest("/nym-nodes", nym_nodes::handlers::v1::routes())
|
||||
.nest("/node-families", node_families_handlers::routes())
|
||||
.nest("/ecash", ecash_routes())
|
||||
.nest("/unstable", unstable_routes_v1())
|
||||
.nest("/legacy", legacy_nodes_routes()); // CORS layer needs to be "outside" of routes
|
||||
|
||||
@@ -55,6 +55,7 @@ async fn refresh(nyxd_client: &Client) -> Result<CachedContractsInfo, NyxdError>
|
||||
let multisig = query_guard!(client_guard, multisig_contract_address());
|
||||
let ecash = query_guard!(client_guard, ecash_contract_address());
|
||||
let performance = query_guard!(client_guard, performance_contract_address());
|
||||
let node_families = query_guard!(client_guard, node_families_contract_address());
|
||||
|
||||
for (address, name) in [
|
||||
(mixnet, "nym-mixnet-contract"),
|
||||
@@ -64,6 +65,7 @@ async fn refresh(nyxd_client: &Client) -> Result<CachedContractsInfo, NyxdError>
|
||||
(multisig, "nym-cw3-multisig-contract"),
|
||||
(ecash, "nym-ecash-contract"),
|
||||
(performance, "nym-performance-contract"),
|
||||
(node_families, "nym-node-families-contract"),
|
||||
] {
|
||||
let (cw2, build_info) = if let Some(address) = address {
|
||||
let cw2 = query_guard!(client_guard, try_get_cw2_contract_version(address).await);
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::ecash::state::EcashState;
|
||||
use crate::mixnet_contract_cache::cache::MixnetContractCache;
|
||||
use crate::network::models::NetworkDetails;
|
||||
use crate::node_describe_cache::cache::DescribedNodes;
|
||||
use crate::node_families::cache::NodeFamiliesCacheData;
|
||||
use crate::node_status_api::handlers::unstable;
|
||||
use crate::node_status_api::models::AxumErrorResponse;
|
||||
use crate::node_status_api::NodeStatusCache;
|
||||
@@ -33,6 +34,8 @@ pub(crate) mod force_refresh;
|
||||
pub(crate) mod helpers;
|
||||
pub(crate) mod mixnet_contract_cache;
|
||||
pub(crate) mod node_annotations_cache;
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test_helpers;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AppState {
|
||||
@@ -59,6 +62,9 @@ pub(crate) struct AppState {
|
||||
/// Holds cached state of the Nym Mixnet contract, e.g. bonded nym-nodes, rewarded set, current interval.
|
||||
pub(crate) mixnet_contract_cache: MixnetContractCacheState,
|
||||
|
||||
/// Hold cached state of node families, e.g. membership, invitations, age, etc.
|
||||
pub(crate) node_families_cache: SharedCache<NodeFamiliesCacheData>,
|
||||
|
||||
/// Holds processed information on network nodes, i.e. performance, config scores, etc.
|
||||
// TODO: also perhaps redundant?
|
||||
pub(crate) node_annotations_cache: NodeAnnotationsCache,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user