Compare commits

...

1 Commits

Author SHA1 Message Date
Jędrzej Stuczyński aaab2939d1 squashed feature/offline-signers-contract branch (#5901)
removed placeholder mod

updated contract schema

remove duplicate import

queries tests

tx tests

ensure placeholder is deprecated so that clippy catches it

contract storage tests

ignore panics in legacy tests

missing performance contract schema changes

they weren't caught as the contract was missing from the makefile

dealing with CI

generated contract schema

client traits

query for exposing signing status at particular height

uses placeholder data due to another PR in progress at the time of writing

wip

removed placeholder mod

removed dead code

contract queries

resetting offline status of sender

implemented TestableNymContract for OfflineSigner, DKG, CW4 and CW3 contracts

basic logic for voting on offline signers

init offline signers contract
2025-10-06 10:42:06 +01:00
62 changed files with 7888 additions and 58 deletions
Generated
+14 -1
View File
@@ -5373,7 +5373,7 @@ dependencies = [
[[package]]
name = "nym-credential-proxy"
version = "0.2.0"
version = "0.3.0"
dependencies = [
"anyhow",
"axum",
@@ -6605,6 +6605,18 @@ dependencies = [
"tokio",
]
[[package]]
name = "nym-offline-signers-contract-common"
version = "0.1.0"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"schemars 0.8.22",
"serde",
"thiserror 2.0.12",
]
[[package]]
name = "nym-ordered-buffer"
version = "0.1.0"
@@ -7300,6 +7312,7 @@ dependencies = [
"nym-mixnet-contract-common",
"nym-multisig-contract-common",
"nym-network-defaults",
"nym-offline-signers-contract-common",
"nym-performance-contract-common",
"nym-serde-helpers",
"nym-vesting-contract-common",
+8 -4
View File
@@ -42,6 +42,7 @@ members = [
"common/cosmwasm-smart-contracts/multisig-contract",
"common/cosmwasm-smart-contracts/nym-performance-contract",
"common/cosmwasm-smart-contracts/nym-pool-contract",
"common/cosmwasm-smart-contracts/offline-signers",
"common/cosmwasm-smart-contracts/vesting-contract",
"common/credential-proxy",
"common/credential-storage",
@@ -58,7 +59,8 @@ members = [
"common/gateway-requests",
"common/gateway-stats-storage",
"common/gateway-storage",
"common/http-api-client", "common/http-api-client-macro",
"common/http-api-client",
"common/http-api-client-macro",
"common/http-api-common",
"common/inclusion-probability",
"common/ip-packet-requests",
@@ -66,7 +68,8 @@ members = [
"common/mixnode-common",
"common/network-defaults",
"common/node-tester-utils",
"common/nonexhaustive-delayqueue", "common/nym-cache",
"common/nonexhaustive-delayqueue",
"common/nym-cache",
"common/nym-id",
"common/nym-metrics",
"common/nym_offline_compact_ecash",
@@ -98,7 +101,8 @@ members = [
"common/ticketbooks-merkle",
"common/topology",
"common/tun",
"common/types", "common/upgrade-mode-check",
"common/types",
"common/upgrade-mode-check",
"common/verloc",
"common/wasm/client-core",
"common/wasm/storage",
@@ -127,7 +131,7 @@ members = [
"nym-node-status-api/nym-node-status-client",
"nym-node/nym-node-metrics",
"nym-node/nym-node-requests",
"nym-outfox",
"nym-outfox",
"nym-registration-client",
"nym-signers-monitor",
"nym-statistics-api",
@@ -19,6 +19,7 @@ nym-vesting-contract-common = { path = "../../cosmwasm-smart-contracts/vesting-c
nym-ecash-contract-common = { path = "../../cosmwasm-smart-contracts/ecash-contract" }
nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" }
nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" }
nym-offline-signers-contract-common = { path = "../../cosmwasm-smart-contracts/offline-signers" }
nym-performance-contract-common = { path = "../../cosmwasm-smart-contracts/nym-performance-contract" }
nym-serde-helpers = { path = "../../serde-helpers", features = ["hex", "base64"] }
serde = { workspace = true, features = ["derive"] }
@@ -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 offline_signers_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 offline_signers_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 offline_signers_query_client::{OfflineSignersQueryClient, PagedOfflineSignersQueryClient};
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 offline_signers_signing_client::OfflineSignersSigningClient;
pub use performance_signing_client::PerformanceSigningClient;
pub use vesting_signing_client::VestingSigningClient;
@@ -55,6 +59,7 @@ pub trait NymContractsProvider {
fn dkg_contract_address(&self) -> Option<&AccountId>;
fn group_contract_address(&self) -> Option<&AccountId>;
fn multisig_contract_address(&self) -> Option<&AccountId>;
fn offline_signers_contract_address(&self) -> Option<&AccountId>;
}
#[derive(Debug, Clone)]
@@ -67,6 +72,7 @@ pub struct TypedNymContracts {
pub group_contract_address: Option<AccountId>,
pub multisig_contract_address: Option<AccountId>,
pub coconut_dkg_contract_address: Option<AccountId>,
pub offline_signers_contract_address: Option<AccountId>,
}
impl TryFrom<NymContracts> for TypedNymContracts {
@@ -102,6 +108,10 @@ impl TryFrom<NymContracts> for TypedNymContracts {
.coconut_dkg_contract_address
.map(|addr| addr.parse())
.transpose()?,
offline_signers_contract_address: value
.offline_signers_contract_address
.map(|addr| addr.parse())
.transpose()?,
})
}
}
@@ -0,0 +1,283 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::collect_paged;
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::error::NyxdError;
use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use cosmrs::AccountId;
use cw_controllers::AdminResponse;
use nym_offline_signers_contract_common::msg::QueryMsg as OfflineSignersQueryMsg;
use serde::Deserialize;
pub use nym_offline_signers_contract_common::{
ActiveProposalResponse, ActiveProposalsPagedResponse, Config, LastStatusResetDetails,
LastStatusResetPagedResponse, LastStatusResetResponse, OfflineSignerDetails,
OfflineSignerInformation, OfflineSignerResponse, OfflineSignersAddressesResponse,
OfflineSignersPagedResponse, Proposal, ProposalId, ProposalResponse, ProposalWithResolution,
ProposalsPagedResponse, SigningStatusAtHeightResponse, SigningStatusResponse, VoteDetails,
VoteInformation, VoteResponse, VotesPagedResponse,
};
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait OfflineSignersQueryClient {
async fn query_offline_signers_contract<T>(
&self,
query: OfflineSignersQueryMsg,
) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>;
async fn admin(&self) -> Result<AdminResponse, NyxdError> {
self.query_offline_signers_contract(OfflineSignersQueryMsg::Admin {})
.await
}
async fn get_config(&self) -> Result<Config, NyxdError> {
self.query_offline_signers_contract(OfflineSignersQueryMsg::GetConfig {})
.await
}
async fn get_active_proposal(
&self,
signer: AccountId,
) -> Result<ActiveProposalResponse, NyxdError> {
self.query_offline_signers_contract(OfflineSignersQueryMsg::GetActiveProposal {
signer: signer.to_string(),
})
.await
}
async fn get_proposal(&self, proposal_id: ProposalId) -> Result<ProposalResponse, NyxdError> {
self.query_offline_signers_contract(OfflineSignersQueryMsg::GetProposal { proposal_id })
.await
}
async fn get_vote_information(
&self,
voter: AccountId,
proposal: ProposalId,
) -> Result<VoteResponse, NyxdError> {
self.query_offline_signers_contract(OfflineSignersQueryMsg::GetVoteInformation {
voter: voter.to_string(),
proposal,
})
.await
}
async fn get_offline_signer_information(
&self,
signer: AccountId,
) -> Result<OfflineSignerResponse, NyxdError> {
self.query_offline_signers_contract(OfflineSignersQueryMsg::GetOfflineSignerInformation {
signer: signer.to_string(),
})
.await
}
async fn get_offline_signers_addresses_at_height(
&self,
height: Option<u64>,
) -> Result<OfflineSignersAddressesResponse, NyxdError> {
self.query_offline_signers_contract(
OfflineSignersQueryMsg::GetOfflineSignersAddressesAtHeight { height },
)
.await
}
async fn get_last_status_reset(
&self,
signer: AccountId,
) -> Result<LastStatusResetResponse, NyxdError> {
self.query_offline_signers_contract(OfflineSignersQueryMsg::GetLastStatusReset {
signer: signer.to_string(),
})
.await
}
async fn get_active_proposals_paged(
&self,
start_after: Option<String>,
limit: Option<u32>,
) -> Result<ActiveProposalsPagedResponse, NyxdError> {
self.query_offline_signers_contract(OfflineSignersQueryMsg::GetActiveProposalsPaged {
start_after,
limit,
})
.await
}
async fn get_proposals_paged(
&self,
start_after: Option<ProposalId>,
limit: Option<u32>,
) -> Result<ProposalsPagedResponse, NyxdError> {
self.query_offline_signers_contract(OfflineSignersQueryMsg::GetProposalsPaged {
start_after,
limit,
})
.await
}
async fn get_votes_paged(
&self,
proposal: ProposalId,
start_after: Option<String>,
limit: Option<u32>,
) -> Result<VotesPagedResponse, NyxdError> {
self.query_offline_signers_contract(OfflineSignersQueryMsg::GetVotesPaged {
proposal,
start_after,
limit,
})
.await
}
async fn get_offline_signers_paged(
&self,
start_after: Option<String>,
limit: Option<u32>,
) -> Result<OfflineSignersPagedResponse, NyxdError> {
self.query_offline_signers_contract(OfflineSignersQueryMsg::GetOfflineSignersPaged {
start_after,
limit,
})
.await
}
async fn get_last_status_reset_paged(
&self,
start_after: Option<String>,
limit: Option<u32>,
) -> Result<LastStatusResetPagedResponse, NyxdError> {
self.query_offline_signers_contract(OfflineSignersQueryMsg::GetLastStatusResetPaged {
start_after,
limit,
})
.await
}
async fn current_signing_status(&self) -> Result<SigningStatusResponse, NyxdError> {
self.query_offline_signers_contract(OfflineSignersQueryMsg::CurrentSigningStatus {})
.await
}
async fn signing_status_at_height(
&self,
block_height: u64,
) -> Result<SigningStatusAtHeightResponse, NyxdError> {
self.query_offline_signers_contract(OfflineSignersQueryMsg::SigningStatusAtHeight {
block_height,
})
.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 PagedOfflineSignersQueryClient: OfflineSignersQueryClient {
async fn get_all_active_proposals(&self) -> Result<Vec<ProposalWithResolution>, NyxdError> {
collect_paged!(self, get_active_proposals_paged, active_proposals)
}
async fn get_all_proposals(&self) -> Result<Vec<Proposal>, NyxdError> {
collect_paged!(self, get_proposals_paged, proposals)
}
async fn get_all_votes(&self, proposal: ProposalId) -> Result<Vec<VoteDetails>, NyxdError> {
collect_paged!(self, get_votes_paged, votes, proposal)
}
async fn get_all_offline_signers(&self) -> Result<Vec<OfflineSignerDetails>, NyxdError> {
collect_paged!(self, get_offline_signers_paged, offline_signers)
}
async fn get_all_last_status_reset(&self) -> Result<Vec<LastStatusResetDetails>, NyxdError> {
collect_paged!(self, get_last_status_reset_paged, status_resets)
}
}
#[async_trait]
impl<T> PagedOfflineSignersQueryClient for T where T: OfflineSignersQueryClient {}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> OfflineSignersQueryClient for C
where
C: CosmWasmClient + NymContractsProvider + Send + Sync,
{
async fn query_offline_signers_contract<T>(
&self,
query: OfflineSignersQueryMsg,
) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>,
{
let offline_signers_contract_address = &self
.offline_signers_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("offline signers contract"))?;
self.query_contract_smart(offline_signers_contract_address, &query)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
fn all_query_variants_are_covered<C: OfflineSignersQueryClient + Send + Sync>(
client: C,
msg: OfflineSignersQueryMsg,
) {
match msg {
OfflineSignersQueryMsg::Admin {} => client.admin().ignore(),
OfflineSignersQueryMsg::GetConfig {} => client.get_config().ignore(),
OfflineSignersQueryMsg::GetActiveProposal { signer } => {
client.get_active_proposal(signer.parse().unwrap()).ignore()
}
OfflineSignersQueryMsg::GetProposal { proposal_id } => {
client.get_proposal(proposal_id).ignore()
}
OfflineSignersQueryMsg::GetVoteInformation { voter, proposal } => client
.get_vote_information(voter.parse().unwrap(), proposal)
.ignore(),
OfflineSignersQueryMsg::GetOfflineSignerInformation { signer } => client
.get_offline_signer_information(signer.parse().unwrap())
.ignore(),
OfflineSignersQueryMsg::GetOfflineSignersAddressesAtHeight { height } => client
.get_offline_signers_addresses_at_height(height)
.ignore(),
OfflineSignersQueryMsg::GetLastStatusReset { signer } => client
.get_last_status_reset(signer.parse().unwrap())
.ignore(),
OfflineSignersQueryMsg::GetActiveProposalsPaged { start_after, limit } => client
.get_active_proposals_paged(start_after, limit)
.ignore(),
OfflineSignersQueryMsg::GetProposalsPaged { start_after, limit } => {
client.get_proposals_paged(start_after, limit).ignore()
}
OfflineSignersQueryMsg::GetVotesPaged {
proposal,
start_after,
limit,
} => client
.get_votes_paged(proposal, start_after, limit)
.ignore(),
OfflineSignersQueryMsg::GetOfflineSignersPaged { start_after, limit } => client
.get_offline_signers_paged(start_after, limit)
.ignore(),
OfflineSignersQueryMsg::GetLastStatusResetPaged { start_after, limit } => client
.get_last_status_reset_paged(start_after, limit)
.ignore(),
OfflineSignersQueryMsg::CurrentSigningStatus {} => {
client.current_signing_status().ignore()
}
OfflineSignersQueryMsg::SigningStatusAtHeight { block_height } => {
client.signing_status_at_height(block_height).ignore()
}
};
}
}
@@ -0,0 +1,120 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
use crate::nyxd::error::NyxdError;
use crate::nyxd::{Coin, Fee, SigningCosmWasmClient};
use crate::signing::signer::OfflineSigner;
use async_trait::async_trait;
use cosmrs::AccountId;
use nym_offline_signers_contract_common::msg::ExecuteMsg as OfflineSignersExecuteMsg;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait OfflineSignersSigningClient {
async fn execute_offline_signers_contract(
&self,
fee: Option<Fee>,
msg: OfflineSignersExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError>;
async fn update_admin(
&self,
admin: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_offline_signers_contract(
fee,
OfflineSignersExecuteMsg::UpdateAdmin { admin },
"OfflineSignersContract::UpdateAdmin".to_string(),
vec![],
)
.await
}
async fn propose_or_vote(
&self,
signer: AccountId,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_offline_signers_contract(
fee,
OfflineSignersExecuteMsg::ProposeOrVote {
signer: signer.to_string(),
},
"OfflineSignersContract::ProposeOrVote".to_string(),
vec![],
)
.await
}
async fn reset_offline_status(&self, fee: Option<Fee>) -> Result<ExecuteResult, NyxdError> {
self.execute_offline_signers_contract(
fee,
OfflineSignersExecuteMsg::ResetOfflineStatus {},
"OfflineSignersContract::ResetOfflineStatus".to_string(),
vec![],
)
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> OfflineSignersSigningClient for C
where
C: SigningCosmWasmClient + NymContractsProvider + Sync,
NyxdError: From<<Self as OfflineSigner>::Error>,
{
async fn execute_offline_signers_contract(
&self,
fee: Option<Fee>,
msg: OfflineSignersExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError> {
let multisig_contract_address = self
.multisig_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("multisig contract"))?;
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
let signer_address = &self.signer_addresses()?[0];
self.execute(
signer_address,
multisig_contract_address,
&msg,
fee,
memo,
funds,
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
fn all_execute_variants_are_covered<C: OfflineSignersSigningClient + Send + Sync>(
client: C,
msg: OfflineSignersExecuteMsg,
) {
match msg {
OfflineSignersExecuteMsg::UpdateAdmin { admin } => {
client.update_admin(admin, None).ignore()
}
OfflineSignersExecuteMsg::ProposeOrVote { signer } => client
.propose_or_vote(signer.parse().unwrap(), None)
.ignore(),
OfflineSignersExecuteMsg::ResetOfflineStatus {} => {
client.reset_offline_status(None).ignore()
}
};
}
}
@@ -305,6 +305,13 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
fn multisig_contract_address(&self) -> Option<&AccountId> {
self.config.contracts.multisig_contract_address.as_ref()
}
fn offline_signers_contract_address(&self) -> Option<&AccountId> {
self.config
.contracts
.offline_signers_contract_address
.as_ref()
}
}
// queries
@@ -14,7 +14,6 @@ readme.workspace = true
anyhow = { workspace = true }
cosmwasm-std = { workspace = true }
cw-storage-plus = { workspace = true }
serde = { workspace = true }
rand_chacha = { workspace = true }
rand = { workspace = true }
@@ -142,6 +142,7 @@ pub trait ChainOpts: ContractOpts {
fn set_contract_balance(&mut self, balance: Coin);
fn update_block<F: Fn(&mut BlockInfo)>(&mut self, action: F);
fn set_to_epoch(&mut self) {
self.set_block_time(Timestamp::from_seconds(0))
}
@@ -88,6 +88,7 @@ pub enum QueryMsg {
#[cw_serde]
pub struct MigrateMsg {
// that's actually the ecash contract now
pub coconut_bandwidth_address: String,
pub coconut_dkg_address: String,
}
@@ -0,0 +1,28 @@
[package]
name = "nym-offline-signers-contract-common"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
[dependencies]
thiserror = { workspace = true }
serde = { workspace = true }
schemars = { workspace = true }
cosmwasm-std = { workspace = true }
cosmwasm-schema = { workspace = true }
cw-controllers = { workspace = true }
[features]
schema = []
[lints]
workspace = true
@@ -0,0 +1,25 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Decimal;
pub const DEFAULT_REQUIRED_QUORUM: Decimal = Decimal::percent(50);
pub const DEFAULT_MAXIMUM_PROPOSAL_LIFETIME_SECS: u64 = 4 * 60 * 60; // 4h
pub const DEFAULT_STATUS_CHANGE_COOLDOWN_SECS: u64 = 300; // 5min
pub mod storage_keys {
pub const CONTRACT_ADMIN: &str = "contract-admin";
pub const DKG_CONTRACT: &str = "dkg_contract";
pub const CONFIG: &str = "config";
pub const ACTIVE_PROPOSALS: &str = "active_proposals";
pub const PROPOSALS: &str = "proposals";
pub const VOTES: &str = "votes";
pub const OFFLINE_SIGNERS_INFORMATION: &str = "offline_signers_information";
pub const OFFLINE_SIGNERS: &str = "offline_signers";
pub const OFFLINE_SIGNERS_CHECKPOINTS: &str = "offline_signers__check";
pub const OFFLINE_SIGNERS_CHANGELOG: &str = "offline_signers__change";
pub const LAST_STATUS_RESET: &str = "last_status_reset";
pub const PROPOSAL_COUNT: &str = "proposal_count";
}
@@ -0,0 +1,44 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::ProposalId;
use cosmwasm_std::Addr;
use cw_controllers::AdminError;
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum NymOfflineSignersContractError {
#[error("could not perform contract migration: {comment}")]
FailedMigration { comment: String },
#[error("can't require more than 100% of signers for quorum")]
RequiredQuorumBiggerThanOne,
#[error(transparent)]
Admin(#[from] AdminError),
#[error(transparent)]
StdErr(#[from] cosmwasm_std::StdError),
#[error("{address} is not a member of the authorised DKG group")]
NotGroupMember { address: Addr },
#[error("{address} is already marked as offline")]
AlreadyOffline { address: Addr },
#[error("{address} is not marked as offline nor in the process of being voted on")]
NotOffline { address: Addr },
#[error("{voter} has already voted to mark {target} as offline in proposal {proposal}")]
AlreadyVoted {
voter: Addr,
proposal: ProposalId,
target: Addr,
},
#[error("{address} has only recently came back online")]
RecentlyCameOnline { address: Addr },
#[error("{address} has only recently came offline")]
RecentlyCameOffline { address: Addr },
}
@@ -0,0 +1,2 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,12 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod constants;
pub mod error;
pub mod helpers;
pub mod msg;
pub mod types;
pub use error::*;
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
pub use types::*;
@@ -0,0 +1,118 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[cfg(feature = "schema")]
use crate::types::{
ActiveProposalResponse, ActiveProposalsPagedResponse, LastStatusResetPagedResponse,
LastStatusResetResponse, OfflineSignerResponse, OfflineSignersAddressesResponse,
OfflineSignersPagedResponse, ProposalResponse, ProposalsPagedResponse,
SigningStatusAtHeightResponse, SigningStatusResponse, VoteResponse, VotesPagedResponse,
};
use crate::{Config, ProposalId};
use cosmwasm_schema::cw_serde;
#[cw_serde]
pub struct InstantiateMsg {
/// Address of the DKG contract that's used as the base of the signer information
pub dkg_contract_address: String,
#[serde(default)]
pub config: Config,
}
#[cw_serde]
pub enum ExecuteMsg {
/// Change the admin
UpdateAdmin { admin: String },
/// Propose or cast vote on particular DKG signer being offline
ProposeOrVote { signer: String },
/// Attempt to reset own offline status
ResetOfflineStatus {},
}
#[cw_serde]
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
pub enum QueryMsg {
#[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))]
Admin {},
/// Returns current config values of the contract
#[cfg_attr(feature = "schema", returns(Config))]
GetConfig {},
/// Returns information of the current active proposal against specific signer
#[cfg_attr(feature = "schema", returns(ActiveProposalResponse))]
GetActiveProposal { signer: String },
/// Returns information about proposal with the specified id
#[cfg_attr(feature = "schema", returns(ProposalResponse))]
GetProposal { proposal_id: ProposalId },
/// Returns information on the vote from the provided voter for the specified proposal
#[cfg_attr(feature = "schema", returns(VoteResponse))]
GetVoteInformation { voter: String, proposal: ProposalId },
/// Returns offline signer information for the provided signer
#[cfg_attr(feature = "schema", returns(OfflineSignerResponse))]
GetOfflineSignerInformation { signer: String },
/// Returns list of addresses of all signers marked as offline at provided height.
/// If no height is given, the current value is returned instead
#[cfg_attr(feature = "schema", returns(OfflineSignersAddressesResponse))]
GetOfflineSignersAddressesAtHeight { height: Option<u64> },
/// Returns information on the last status reset of the provided signer
#[cfg_attr(feature = "schema", returns(LastStatusResetResponse))]
GetLastStatusReset { signer: String },
/// Returns all (paged) active proposals
#[cfg_attr(feature = "schema", returns(ActiveProposalsPagedResponse))]
GetActiveProposalsPaged {
start_after: Option<String>,
limit: Option<u32>,
},
/// Returns all (paged) proposals
#[cfg_attr(feature = "schema", returns(ProposalsPagedResponse))]
GetProposalsPaged {
start_after: Option<ProposalId>,
limit: Option<u32>,
},
/// Returns all (paged) votes for the specified proposal
#[cfg_attr(feature = "schema", returns(VotesPagedResponse))]
GetVotesPaged {
proposal: ProposalId,
start_after: Option<String>,
limit: Option<u32>,
},
/// Returns all (paged) offline signers
#[cfg_attr(feature = "schema", returns(OfflineSignersPagedResponse))]
GetOfflineSignersPaged {
start_after: Option<String>,
limit: Option<u32>,
},
/// Returns all (paged) status resets
#[cfg_attr(feature = "schema", returns(LastStatusResetPagedResponse))]
GetLastStatusResetPaged {
start_after: Option<String>,
limit: Option<u32>,
},
/// Returns the current signing status, i.e. whether credential issuance is still possible
#[cfg_attr(feature = "schema", returns(SigningStatusResponse))]
CurrentSigningStatus {},
/// Returns the signing status at provided block height, i.e. whether credential issuance was possible at that point
#[cfg_attr(feature = "schema", returns(SigningStatusAtHeightResponse))]
SigningStatusAtHeight { block_height: u64 },
}
#[cw_serde]
pub struct MigrateMsg {
//
}
@@ -0,0 +1,196 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::constants::{
DEFAULT_MAXIMUM_PROPOSAL_LIFETIME_SECS, DEFAULT_REQUIRED_QUORUM,
DEFAULT_STATUS_CHANGE_COOLDOWN_SECS,
};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, BlockInfo, Decimal};
pub type ProposalId = u64;
#[cw_serde]
pub struct Proposal {
pub created_at: BlockInfo,
pub id: ProposalId,
pub proposed_offline_signer: Addr,
// not strictly necessary but the address of the first sender who has managed to get the message through
pub proposer: Addr,
}
impl Proposal {
pub fn expired(&self, current_block: &BlockInfo, lifetime_secs: u64) -> bool {
self.created_at.time.plus_seconds(lifetime_secs) <= current_block.time
}
}
#[cw_serde]
pub struct VoteInformation {
pub voted_at: BlockInfo,
}
impl VoteInformation {
pub fn new(voted_at: &BlockInfo) -> Self {
VoteInformation {
voted_at: voted_at.clone(),
}
}
}
#[cw_serde]
pub struct OfflineSignerInformation {
pub marked_offline_at: BlockInfo,
pub associated_proposal: ProposalId,
}
impl OfflineSignerInformation {
pub fn recently_marked_offline(&self, current_block: &BlockInfo, threshold_secs: u64) -> bool {
self.marked_offline_at.time.plus_seconds(threshold_secs) > current_block.time
}
}
#[cw_serde]
pub struct StatusResetInformation {
pub status_reset_at: BlockInfo,
}
impl StatusResetInformation {
pub fn recently_marked_online(&self, current_block: &BlockInfo, threshold_secs: u64) -> bool {
self.status_reset_at.time.plus_seconds(threshold_secs) >= current_block.time
}
}
#[cw_serde]
#[derive(Copy)]
#[serde(default)]
pub struct Config {
// needed % of eligible voters for a proposal to pass
pub required_quorum: Decimal,
// maximum duration (in seconds) a proposal can exist for
// before its votes are reset if not passed
pub maximum_proposal_lifetime_secs: u64,
// minimum time between two consecutive status changes
// (to prevent signer from going online-offline multiple times a minute)
pub status_change_cooldown_secs: u64,
}
impl Default for Config {
fn default() -> Self {
Config {
required_quorum: DEFAULT_REQUIRED_QUORUM,
maximum_proposal_lifetime_secs: DEFAULT_MAXIMUM_PROPOSAL_LIFETIME_SECS,
status_change_cooldown_secs: DEFAULT_STATUS_CHANGE_COOLDOWN_SECS,
}
}
}
#[cw_serde]
pub struct ProposalWithResolution {
pub proposal: Proposal,
pub passed: bool,
pub voting_finished: bool,
}
#[cw_serde]
pub struct ActiveProposalResponse {
pub proposal: Option<ProposalWithResolution>,
}
#[cw_serde]
pub struct ActiveProposalsPagedResponse {
pub start_next_after: Option<String>,
pub active_proposals: Vec<ProposalWithResolution>,
}
#[cw_serde]
pub struct LastStatusResetDetails {
pub information: StatusResetInformation,
pub signer: Addr,
}
#[cw_serde]
pub struct LastStatusResetPagedResponse {
pub start_next_after: Option<String>,
pub status_resets: Vec<LastStatusResetDetails>,
}
#[cw_serde]
pub struct LastStatusResetResponse {
pub information: Option<StatusResetInformation>,
}
#[cw_serde]
pub struct OfflineSignerResponse {
pub information: Option<OfflineSignerInformation>,
}
#[cw_serde]
pub struct OfflineSignersAddressesResponse {
pub addresses: Vec<Addr>,
}
#[cw_serde]
pub struct OfflineSignerDetails {
pub information: OfflineSignerInformation,
pub signer: Addr,
}
#[cw_serde]
pub struct OfflineSignersPagedResponse {
pub start_next_after: Option<String>,
pub offline_signers: Vec<OfflineSignerDetails>,
}
#[cw_serde]
pub struct ProposalResponse {
pub proposal: Option<ProposalWithResolution>,
}
#[cw_serde]
pub struct ProposalsPagedResponse {
pub start_next_after: Option<ProposalId>,
pub proposals: Vec<Proposal>,
}
#[cw_serde]
pub struct VoteResponse {
pub vote: Option<VoteInformation>,
}
#[cw_serde]
pub struct VoteDetails {
pub voter: Addr,
pub information: VoteInformation,
}
#[cw_serde]
pub struct VotesPagedResponse {
pub start_next_after: Option<String>,
pub votes: Vec<VoteDetails>,
}
#[cw_serde]
pub struct SigningStatusResponse {
pub dkg_epoch_id: u64,
pub signing_threshold: u64,
pub total_group_members: u32,
pub current_registered_dealers: u32,
pub offline_signers: u32,
pub threshold_available: bool,
}
#[cw_serde]
pub struct SigningStatusAtHeightResponse {
pub block_height: u64,
pub dkg_epoch_id: u64,
pub signing_threshold: u64,
pub current_registered_dealers: u32,
pub offline_signers: u32,
pub threshold_available: bool,
}
+1
View File
@@ -20,6 +20,7 @@ pub const VESTING_CONTRACT_ADDRESS: &str =
// \/ TODO: this has to be updated once the contract is deployed
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "";
pub const OFFLINE_SIGNERS_CONTRACT_ADDRESS: &str = "";
// /\ TODO: this has to be updated once the contract is deployed
pub const ECASH_CONTRACT_ADDRESS: &str =
+6
View File
@@ -26,6 +26,9 @@ pub struct NymContracts {
pub group_contract_address: Option<String>,
pub multisig_contract_address: Option<String>,
pub coconut_dkg_contract_address: Option<String>,
#[serde(default)]
pub offline_signers_contract_address: Option<String>,
}
// I wanted to use the simpler `NetworkDetails` name, but there's a clash
@@ -187,6 +190,9 @@ impl NymNetworkDetails {
coconut_dkg_contract_address: parse_optional_str(
mainnet::COCONUT_DKG_CONTRACT_ADDRESS,
),
offline_signers_contract_address: parse_optional_str(
mainnet::OFFLINE_SIGNERS_CONTRACT_ADDRESS,
),
},
nym_vpn_api_url: parse_optional_str(mainnet::NYM_VPN_API),
nym_api_urls: Some(mainnet::NYM_APIS.iter().copied().map(Into::into).collect()),
+32
View File
@@ -1288,6 +1288,38 @@ dependencies = [
"regex",
]
[[package]]
name = "nym-offline-signers-contract"
version = "0.1.0"
dependencies = [
"anyhow",
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"cw-storage-plus",
"cw2",
"cw4",
"itertools 0.14.0",
"nym-coconut-dkg",
"nym-coconut-dkg-common",
"nym-contracts-common",
"nym-contracts-common-testing",
"nym-offline-signers-contract-common",
"serde",
]
[[package]]
name = "nym-offline-signers-contract-common"
version = "0.1.0"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"schemars",
"serde",
"thiserror 2.0.12",
]
[[package]]
name = "nym-pemstore"
version = "0.3.0"
+2
View File
@@ -10,6 +10,7 @@ members = [
"multisig/cw4-group",
"vesting",
"performance",
"offline-signers",
]
[workspace.package]
@@ -54,6 +55,7 @@ semver = "1.0.21"
serde = "1.0.196"
sylvia = "1.3.3"
schemars = "0.8.16"
itertools = "0.14.0"
thiserror = "2.0.11"
+10 -1
View File
@@ -1,4 +1,4 @@
schema: coconut-dkg-schema mixnet-schema vesting-schema multisig-schema group-schema ecash-schema
schema: coconut-dkg-schema mixnet-schema vesting-schema multisig-schema group-schema ecash-schema nym-pool-schema performance-schema offline-signers-schema
coconut-dkg-schema:
$(MAKE) -C coconut-dkg generate-schema
@@ -17,3 +17,12 @@ multisig-schema:
group-schema:
$(MAKE) -C multisig/cw4-group generate-schema
nym-pool-schema:
$(MAKE) -C nym-pool generate-schema
performance-schema:
$(MAKE) -C performance generate-schema
offline-signers-schema:
$(MAKE) -C offline-signers generate-schema
+1 -1
View File
@@ -36,8 +36,8 @@ cw4-group = { path = "../multisig/cw4-group", features = ["testable-cw4-contract
[dev-dependencies]
anyhow = { workspace = true }
easy-addr = { path = "../../common/cosmwasm-smart-contracts/easy_addr" }
nym-group-contract-common = { path = "../../common/cosmwasm-smart-contracts/group-contract" }
cw-multi-test = { workspace = true }
nym-group-contract-common = { path = "../../common/cosmwasm-smart-contracts/group-contract" }
cw4-group = { path = "../multisig/cw4-group" }
[features]
+4
View File
@@ -0,0 +1,4 @@
[alias]
wasm = "build --release --lib --target wasm32-unknown-unknown"
unit-test = "test --lib"
schema = "run --bin schema --features=schema-gen"
+44
View File
@@ -0,0 +1,44 @@
[package]
name = "nym-offline-signers-contract"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
[[bin]]
name = "schema"
required-features = ["schema-gen"]
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
cosmwasm-std = { workspace = true }
cw2 = { workspace = true }
cw4 = { workspace = true }
cw-storage-plus = { workspace = true }
cosmwasm-schema = { workspace = true, optional = true }
cw-controllers = { workspace = true }
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
nym-offline-signers-contract-common = { path = "../../common/cosmwasm-smart-contracts/offline-signers" }
nym-coconut-dkg-common = { path = "../../common/cosmwasm-smart-contracts/coconut-dkg" }
# TEMPORARY UNTIL BRANCH IS REBASED
serde = { workspace = true, features = ["derive"] }
[dev-dependencies]
itertools = { workspace = true }
anyhow = { workspace = true }
nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing" }
nym-coconut-dkg = { path = "../coconut-dkg", features = ["testable-dkg-contract"] }
[features]
schema-gen = ["nym-offline-signers-contract-common/schema", "cosmwasm-schema"]
[lints]
workspace = true
+5
View File
@@ -0,0 +1,5 @@
wasm:
RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown
generate-schema:
cargo schema
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,64 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ExecuteMsg",
"oneOf": [
{
"description": "Change the admin",
"type": "object",
"required": [
"update_admin"
],
"properties": {
"update_admin": {
"type": "object",
"required": [
"admin"
],
"properties": {
"admin": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Propose or cast vote on particular DKG signer being offline",
"type": "object",
"required": [
"propose_or_vote"
],
"properties": {
"propose_or_vote": {
"type": "object",
"required": [
"signer"
],
"properties": {
"signer": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Attempt to reset own offline status",
"type": "object",
"required": [
"reset_offline_status"
],
"properties": {
"reset_offline_status": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
}
]
}
@@ -0,0 +1,59 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "InstantiateMsg",
"type": "object",
"required": [
"dkg_contract_address"
],
"properties": {
"config": {
"default": {
"maximum_proposal_lifetime_secs": 14400,
"required_quorum": "0.5",
"status_change_cooldown_secs": 300
},
"allOf": [
{
"$ref": "#/definitions/Config"
}
]
},
"dkg_contract_address": {
"description": "Address of the DKG contract that's used as the base of the signer information",
"type": "string"
}
},
"additionalProperties": false,
"definitions": {
"Config": {
"type": "object",
"properties": {
"maximum_proposal_lifetime_secs": {
"default": 14400,
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"required_quorum": {
"default": "0.5",
"allOf": [
{
"$ref": "#/definitions/Decimal"
}
]
},
"status_change_cooldown_secs": {
"default": 300,
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
},
"Decimal": {
"description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)",
"type": "string"
}
}
}
@@ -0,0 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MigrateMsg",
"type": "object",
"additionalProperties": false
}
@@ -0,0 +1,373 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "QueryMsg",
"oneOf": [
{
"type": "object",
"required": [
"admin"
],
"properties": {
"admin": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Returns current config values of the contract",
"type": "object",
"required": [
"get_config"
],
"properties": {
"get_config": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Returns information of the current active proposal against specific signer",
"type": "object",
"required": [
"get_active_proposal"
],
"properties": {
"get_active_proposal": {
"type": "object",
"required": [
"signer"
],
"properties": {
"signer": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Returns information about proposal with the specified id",
"type": "object",
"required": [
"get_proposal"
],
"properties": {
"get_proposal": {
"type": "object",
"required": [
"proposal_id"
],
"properties": {
"proposal_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Returns information on the vote from the provided voter for the specified proposal",
"type": "object",
"required": [
"get_vote_information"
],
"properties": {
"get_vote_information": {
"type": "object",
"required": [
"proposal",
"voter"
],
"properties": {
"proposal": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"voter": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Returns offline signer information for the provided signer",
"type": "object",
"required": [
"get_offline_signer_information"
],
"properties": {
"get_offline_signer_information": {
"type": "object",
"required": [
"signer"
],
"properties": {
"signer": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Returns list of addresses of all signers marked as offline at provided height. If no height is given, the current value is returned instead",
"type": "object",
"required": [
"get_offline_signers_addresses_at_height"
],
"properties": {
"get_offline_signers_addresses_at_height": {
"type": "object",
"properties": {
"height": {
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Returns information on the last status reset of the provided signer",
"type": "object",
"required": [
"get_last_status_reset"
],
"properties": {
"get_last_status_reset": {
"type": "object",
"required": [
"signer"
],
"properties": {
"signer": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Returns all (paged) active proposals",
"type": "object",
"required": [
"get_active_proposals_paged"
],
"properties": {
"get_active_proposals_paged": {
"type": "object",
"properties": {
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Returns all (paged) proposals",
"type": "object",
"required": [
"get_proposals_paged"
],
"properties": {
"get_proposals_paged": {
"type": "object",
"properties": {
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Returns all (paged) votes for the specified proposal",
"type": "object",
"required": [
"get_votes_paged"
],
"properties": {
"get_votes_paged": {
"type": "object",
"required": [
"proposal"
],
"properties": {
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"proposal": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"start_after": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Returns all (paged) offline signers",
"type": "object",
"required": [
"get_offline_signers_paged"
],
"properties": {
"get_offline_signers_paged": {
"type": "object",
"properties": {
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Returns all (paged) status resets",
"type": "object",
"required": [
"get_last_status_reset_paged"
],
"properties": {
"get_last_status_reset_paged": {
"type": "object",
"properties": {
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Returns the current signing status, i.e. whether credential issuance is still possible",
"type": "object",
"required": [
"current_signing_status"
],
"properties": {
"current_signing_status": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Returns the signing status at provided block height, i.e. whether credential issuance was possible at that point",
"type": "object",
"required": [
"signing_status_at_height"
],
"properties": {
"signing_status_at_height": {
"type": "object",
"required": [
"block_height"
],
"properties": {
"block_height": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
]
}
@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AdminResponse",
"description": "Returned from Admin.query_admin()",
"type": "object",
"properties": {
"admin": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
}
@@ -0,0 +1,44 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SigningStatusResponse",
"type": "object",
"required": [
"current_registered_dealers",
"dkg_epoch_id",
"offline_signers",
"signing_threshold",
"threshold_available",
"total_group_members"
],
"properties": {
"current_registered_dealers": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"dkg_epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"offline_signers": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"signing_threshold": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"threshold_available": {
"type": "boolean"
},
"total_group_members": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
@@ -0,0 +1,110 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ActiveProposalResponse",
"type": "object",
"properties": {
"proposal": {
"anyOf": [
{
"$ref": "#/definitions/ProposalWithResolution"
},
{
"type": "null"
}
]
}
},
"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"
},
"BlockInfo": {
"type": "object",
"required": [
"chain_id",
"height",
"time"
],
"properties": {
"chain_id": {
"type": "string"
},
"height": {
"description": "The height of a block is the number of blocks preceding it in the blockchain.",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"time": {
"description": "Absolute time of the block creation in seconds since the UNIX epoch (00:00:00 on 1970-01-01 UTC).\n\nThe source of this is the [BFT Time in Tendermint](https://github.com/tendermint/tendermint/blob/58dc1726/spec/consensus/bft-time.md), which has the same nanosecond precision as the `Timestamp` type.\n\n# Examples\n\nUsing chrono:\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; # extern crate chrono; use chrono::NaiveDateTime; let seconds = env.block.time.seconds(); let nsecs = env.block.time.subsec_nanos(); let dt = NaiveDateTime::from_timestamp(seconds as i64, nsecs as u32); ```\n\nCreating a simple millisecond-precision timestamp (as used in JavaScript):\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; let millis = env.block.time.nanos() / 1_000_000; ```",
"allOf": [
{
"$ref": "#/definitions/Timestamp"
}
]
}
},
"additionalProperties": false
},
"Proposal": {
"type": "object",
"required": [
"created_at",
"id",
"proposed_offline_signer",
"proposer"
],
"properties": {
"created_at": {
"$ref": "#/definitions/BlockInfo"
},
"id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"proposed_offline_signer": {
"$ref": "#/definitions/Addr"
},
"proposer": {
"$ref": "#/definitions/Addr"
}
},
"additionalProperties": false
},
"ProposalWithResolution": {
"type": "object",
"required": [
"passed",
"proposal",
"voting_finished"
],
"properties": {
"passed": {
"type": "boolean"
},
"proposal": {
"$ref": "#/definitions/Proposal"
},
"voting_finished": {
"type": "boolean"
}
},
"additionalProperties": false
},
"Timestamp": {
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
"allOf": [
{
"$ref": "#/definitions/Uint64"
}
]
},
"Uint64": {
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 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 `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
"type": "string"
}
}
}
@@ -0,0 +1,115 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ActiveProposalsPagedResponse",
"type": "object",
"required": [
"active_proposals"
],
"properties": {
"active_proposals": {
"type": "array",
"items": {
"$ref": "#/definitions/ProposalWithResolution"
}
},
"start_next_after": {
"type": [
"string",
"null"
]
}
},
"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"
},
"BlockInfo": {
"type": "object",
"required": [
"chain_id",
"height",
"time"
],
"properties": {
"chain_id": {
"type": "string"
},
"height": {
"description": "The height of a block is the number of blocks preceding it in the blockchain.",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"time": {
"description": "Absolute time of the block creation in seconds since the UNIX epoch (00:00:00 on 1970-01-01 UTC).\n\nThe source of this is the [BFT Time in Tendermint](https://github.com/tendermint/tendermint/blob/58dc1726/spec/consensus/bft-time.md), which has the same nanosecond precision as the `Timestamp` type.\n\n# Examples\n\nUsing chrono:\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; # extern crate chrono; use chrono::NaiveDateTime; let seconds = env.block.time.seconds(); let nsecs = env.block.time.subsec_nanos(); let dt = NaiveDateTime::from_timestamp(seconds as i64, nsecs as u32); ```\n\nCreating a simple millisecond-precision timestamp (as used in JavaScript):\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; let millis = env.block.time.nanos() / 1_000_000; ```",
"allOf": [
{
"$ref": "#/definitions/Timestamp"
}
]
}
},
"additionalProperties": false
},
"Proposal": {
"type": "object",
"required": [
"created_at",
"id",
"proposed_offline_signer",
"proposer"
],
"properties": {
"created_at": {
"$ref": "#/definitions/BlockInfo"
},
"id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"proposed_offline_signer": {
"$ref": "#/definitions/Addr"
},
"proposer": {
"$ref": "#/definitions/Addr"
}
},
"additionalProperties": false
},
"ProposalWithResolution": {
"type": "object",
"required": [
"passed",
"proposal",
"voting_finished"
],
"properties": {
"passed": {
"type": "boolean"
},
"proposal": {
"$ref": "#/definitions/Proposal"
},
"voting_finished": {
"type": "boolean"
}
},
"additionalProperties": false
},
"Timestamp": {
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
"allOf": [
{
"$ref": "#/definitions/Uint64"
}
]
},
"Uint64": {
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 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 `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
"type": "string"
}
}
}
@@ -0,0 +1,34 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Config",
"type": "object",
"properties": {
"maximum_proposal_lifetime_secs": {
"default": 14400,
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"required_quorum": {
"default": "0.5",
"allOf": [
{
"$ref": "#/definitions/Decimal"
}
]
},
"status_change_cooldown_secs": {
"default": 300,
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false,
"definitions": {
"Decimal": {
"description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)",
"type": "string"
}
}
}
@@ -0,0 +1,72 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "LastStatusResetResponse",
"type": "object",
"properties": {
"information": {
"anyOf": [
{
"$ref": "#/definitions/StatusResetInformation"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false,
"definitions": {
"BlockInfo": {
"type": "object",
"required": [
"chain_id",
"height",
"time"
],
"properties": {
"chain_id": {
"type": "string"
},
"height": {
"description": "The height of a block is the number of blocks preceding it in the blockchain.",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"time": {
"description": "Absolute time of the block creation in seconds since the UNIX epoch (00:00:00 on 1970-01-01 UTC).\n\nThe source of this is the [BFT Time in Tendermint](https://github.com/tendermint/tendermint/blob/58dc1726/spec/consensus/bft-time.md), which has the same nanosecond precision as the `Timestamp` type.\n\n# Examples\n\nUsing chrono:\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; # extern crate chrono; use chrono::NaiveDateTime; let seconds = env.block.time.seconds(); let nsecs = env.block.time.subsec_nanos(); let dt = NaiveDateTime::from_timestamp(seconds as i64, nsecs as u32); ```\n\nCreating a simple millisecond-precision timestamp (as used in JavaScript):\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; let millis = env.block.time.nanos() / 1_000_000; ```",
"allOf": [
{
"$ref": "#/definitions/Timestamp"
}
]
}
},
"additionalProperties": false
},
"StatusResetInformation": {
"type": "object",
"required": [
"status_reset_at"
],
"properties": {
"status_reset_at": {
"$ref": "#/definitions/BlockInfo"
}
},
"additionalProperties": false
},
"Timestamp": {
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
"allOf": [
{
"$ref": "#/definitions/Uint64"
}
]
},
"Uint64": {
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 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 `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
"type": "string"
}
}
}
@@ -0,0 +1,97 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "LastStatusResetPagedResponse",
"type": "object",
"required": [
"status_resets"
],
"properties": {
"start_next_after": {
"type": [
"string",
"null"
]
},
"status_resets": {
"type": "array",
"items": {
"$ref": "#/definitions/LastStatusResetDetails"
}
}
},
"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"
},
"BlockInfo": {
"type": "object",
"required": [
"chain_id",
"height",
"time"
],
"properties": {
"chain_id": {
"type": "string"
},
"height": {
"description": "The height of a block is the number of blocks preceding it in the blockchain.",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"time": {
"description": "Absolute time of the block creation in seconds since the UNIX epoch (00:00:00 on 1970-01-01 UTC).\n\nThe source of this is the [BFT Time in Tendermint](https://github.com/tendermint/tendermint/blob/58dc1726/spec/consensus/bft-time.md), which has the same nanosecond precision as the `Timestamp` type.\n\n# Examples\n\nUsing chrono:\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; # extern crate chrono; use chrono::NaiveDateTime; let seconds = env.block.time.seconds(); let nsecs = env.block.time.subsec_nanos(); let dt = NaiveDateTime::from_timestamp(seconds as i64, nsecs as u32); ```\n\nCreating a simple millisecond-precision timestamp (as used in JavaScript):\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; let millis = env.block.time.nanos() / 1_000_000; ```",
"allOf": [
{
"$ref": "#/definitions/Timestamp"
}
]
}
},
"additionalProperties": false
},
"LastStatusResetDetails": {
"type": "object",
"required": [
"information",
"signer"
],
"properties": {
"information": {
"$ref": "#/definitions/StatusResetInformation"
},
"signer": {
"$ref": "#/definitions/Addr"
}
},
"additionalProperties": false
},
"StatusResetInformation": {
"type": "object",
"required": [
"status_reset_at"
],
"properties": {
"status_reset_at": {
"$ref": "#/definitions/BlockInfo"
}
},
"additionalProperties": false
},
"Timestamp": {
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
"allOf": [
{
"$ref": "#/definitions/Uint64"
}
]
},
"Uint64": {
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 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 `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
"type": "string"
}
}
}
@@ -0,0 +1,78 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "OfflineSignerResponse",
"type": "object",
"properties": {
"information": {
"anyOf": [
{
"$ref": "#/definitions/OfflineSignerInformation"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false,
"definitions": {
"BlockInfo": {
"type": "object",
"required": [
"chain_id",
"height",
"time"
],
"properties": {
"chain_id": {
"type": "string"
},
"height": {
"description": "The height of a block is the number of blocks preceding it in the blockchain.",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"time": {
"description": "Absolute time of the block creation in seconds since the UNIX epoch (00:00:00 on 1970-01-01 UTC).\n\nThe source of this is the [BFT Time in Tendermint](https://github.com/tendermint/tendermint/blob/58dc1726/spec/consensus/bft-time.md), which has the same nanosecond precision as the `Timestamp` type.\n\n# Examples\n\nUsing chrono:\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; # extern crate chrono; use chrono::NaiveDateTime; let seconds = env.block.time.seconds(); let nsecs = env.block.time.subsec_nanos(); let dt = NaiveDateTime::from_timestamp(seconds as i64, nsecs as u32); ```\n\nCreating a simple millisecond-precision timestamp (as used in JavaScript):\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; let millis = env.block.time.nanos() / 1_000_000; ```",
"allOf": [
{
"$ref": "#/definitions/Timestamp"
}
]
}
},
"additionalProperties": false
},
"OfflineSignerInformation": {
"type": "object",
"required": [
"associated_proposal",
"marked_offline_at"
],
"properties": {
"associated_proposal": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"marked_offline_at": {
"$ref": "#/definitions/BlockInfo"
}
},
"additionalProperties": false
},
"Timestamp": {
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
"allOf": [
{
"$ref": "#/definitions/Uint64"
}
]
},
"Uint64": {
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 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 `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
"type": "string"
}
}
}
@@ -0,0 +1,23 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "OfflineSignersAddressesResponse",
"type": "object",
"required": [
"addresses"
],
"properties": {
"addresses": {
"type": "array",
"items": {
"$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"
}
}
}
@@ -0,0 +1,103 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "OfflineSignersPagedResponse",
"type": "object",
"required": [
"offline_signers"
],
"properties": {
"offline_signers": {
"type": "array",
"items": {
"$ref": "#/definitions/OfflineSignerDetails"
}
},
"start_next_after": {
"type": [
"string",
"null"
]
}
},
"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"
},
"BlockInfo": {
"type": "object",
"required": [
"chain_id",
"height",
"time"
],
"properties": {
"chain_id": {
"type": "string"
},
"height": {
"description": "The height of a block is the number of blocks preceding it in the blockchain.",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"time": {
"description": "Absolute time of the block creation in seconds since the UNIX epoch (00:00:00 on 1970-01-01 UTC).\n\nThe source of this is the [BFT Time in Tendermint](https://github.com/tendermint/tendermint/blob/58dc1726/spec/consensus/bft-time.md), which has the same nanosecond precision as the `Timestamp` type.\n\n# Examples\n\nUsing chrono:\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; # extern crate chrono; use chrono::NaiveDateTime; let seconds = env.block.time.seconds(); let nsecs = env.block.time.subsec_nanos(); let dt = NaiveDateTime::from_timestamp(seconds as i64, nsecs as u32); ```\n\nCreating a simple millisecond-precision timestamp (as used in JavaScript):\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; let millis = env.block.time.nanos() / 1_000_000; ```",
"allOf": [
{
"$ref": "#/definitions/Timestamp"
}
]
}
},
"additionalProperties": false
},
"OfflineSignerDetails": {
"type": "object",
"required": [
"information",
"signer"
],
"properties": {
"information": {
"$ref": "#/definitions/OfflineSignerInformation"
},
"signer": {
"$ref": "#/definitions/Addr"
}
},
"additionalProperties": false
},
"OfflineSignerInformation": {
"type": "object",
"required": [
"associated_proposal",
"marked_offline_at"
],
"properties": {
"associated_proposal": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"marked_offline_at": {
"$ref": "#/definitions/BlockInfo"
}
},
"additionalProperties": false
},
"Timestamp": {
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
"allOf": [
{
"$ref": "#/definitions/Uint64"
}
]
},
"Uint64": {
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 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 `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
"type": "string"
}
}
}
@@ -0,0 +1,110 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ProposalResponse",
"type": "object",
"properties": {
"proposal": {
"anyOf": [
{
"$ref": "#/definitions/ProposalWithResolution"
},
{
"type": "null"
}
]
}
},
"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"
},
"BlockInfo": {
"type": "object",
"required": [
"chain_id",
"height",
"time"
],
"properties": {
"chain_id": {
"type": "string"
},
"height": {
"description": "The height of a block is the number of blocks preceding it in the blockchain.",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"time": {
"description": "Absolute time of the block creation in seconds since the UNIX epoch (00:00:00 on 1970-01-01 UTC).\n\nThe source of this is the [BFT Time in Tendermint](https://github.com/tendermint/tendermint/blob/58dc1726/spec/consensus/bft-time.md), which has the same nanosecond precision as the `Timestamp` type.\n\n# Examples\n\nUsing chrono:\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; # extern crate chrono; use chrono::NaiveDateTime; let seconds = env.block.time.seconds(); let nsecs = env.block.time.subsec_nanos(); let dt = NaiveDateTime::from_timestamp(seconds as i64, nsecs as u32); ```\n\nCreating a simple millisecond-precision timestamp (as used in JavaScript):\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; let millis = env.block.time.nanos() / 1_000_000; ```",
"allOf": [
{
"$ref": "#/definitions/Timestamp"
}
]
}
},
"additionalProperties": false
},
"Proposal": {
"type": "object",
"required": [
"created_at",
"id",
"proposed_offline_signer",
"proposer"
],
"properties": {
"created_at": {
"$ref": "#/definitions/BlockInfo"
},
"id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"proposed_offline_signer": {
"$ref": "#/definitions/Addr"
},
"proposer": {
"$ref": "#/definitions/Addr"
}
},
"additionalProperties": false
},
"ProposalWithResolution": {
"type": "object",
"required": [
"passed",
"proposal",
"voting_finished"
],
"properties": {
"passed": {
"type": "boolean"
},
"proposal": {
"$ref": "#/definitions/Proposal"
},
"voting_finished": {
"type": "boolean"
}
},
"additionalProperties": false
},
"Timestamp": {
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
"allOf": [
{
"$ref": "#/definitions/Uint64"
}
]
},
"Uint64": {
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 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 `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
"type": "string"
}
}
}
@@ -0,0 +1,97 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ProposalsPagedResponse",
"type": "object",
"required": [
"proposals"
],
"properties": {
"proposals": {
"type": "array",
"items": {
"$ref": "#/definitions/Proposal"
}
},
"start_next_after": {
"type": [
"integer",
"null"
],
"format": "uint64",
"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"
},
"BlockInfo": {
"type": "object",
"required": [
"chain_id",
"height",
"time"
],
"properties": {
"chain_id": {
"type": "string"
},
"height": {
"description": "The height of a block is the number of blocks preceding it in the blockchain.",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"time": {
"description": "Absolute time of the block creation in seconds since the UNIX epoch (00:00:00 on 1970-01-01 UTC).\n\nThe source of this is the [BFT Time in Tendermint](https://github.com/tendermint/tendermint/blob/58dc1726/spec/consensus/bft-time.md), which has the same nanosecond precision as the `Timestamp` type.\n\n# Examples\n\nUsing chrono:\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; # extern crate chrono; use chrono::NaiveDateTime; let seconds = env.block.time.seconds(); let nsecs = env.block.time.subsec_nanos(); let dt = NaiveDateTime::from_timestamp(seconds as i64, nsecs as u32); ```\n\nCreating a simple millisecond-precision timestamp (as used in JavaScript):\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; let millis = env.block.time.nanos() / 1_000_000; ```",
"allOf": [
{
"$ref": "#/definitions/Timestamp"
}
]
}
},
"additionalProperties": false
},
"Proposal": {
"type": "object",
"required": [
"created_at",
"id",
"proposed_offline_signer",
"proposer"
],
"properties": {
"created_at": {
"$ref": "#/definitions/BlockInfo"
},
"id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"proposed_offline_signer": {
"$ref": "#/definitions/Addr"
},
"proposer": {
"$ref": "#/definitions/Addr"
}
},
"additionalProperties": false
},
"Timestamp": {
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
"allOf": [
{
"$ref": "#/definitions/Uint64"
}
]
},
"Uint64": {
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 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 `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
"type": "string"
}
}
}
@@ -0,0 +1,72 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "VoteResponse",
"type": "object",
"properties": {
"vote": {
"anyOf": [
{
"$ref": "#/definitions/VoteInformation"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false,
"definitions": {
"BlockInfo": {
"type": "object",
"required": [
"chain_id",
"height",
"time"
],
"properties": {
"chain_id": {
"type": "string"
},
"height": {
"description": "The height of a block is the number of blocks preceding it in the blockchain.",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"time": {
"description": "Absolute time of the block creation in seconds since the UNIX epoch (00:00:00 on 1970-01-01 UTC).\n\nThe source of this is the [BFT Time in Tendermint](https://github.com/tendermint/tendermint/blob/58dc1726/spec/consensus/bft-time.md), which has the same nanosecond precision as the `Timestamp` type.\n\n# Examples\n\nUsing chrono:\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; # extern crate chrono; use chrono::NaiveDateTime; let seconds = env.block.time.seconds(); let nsecs = env.block.time.subsec_nanos(); let dt = NaiveDateTime::from_timestamp(seconds as i64, nsecs as u32); ```\n\nCreating a simple millisecond-precision timestamp (as used in JavaScript):\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; let millis = env.block.time.nanos() / 1_000_000; ```",
"allOf": [
{
"$ref": "#/definitions/Timestamp"
}
]
}
},
"additionalProperties": false
},
"Timestamp": {
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
"allOf": [
{
"$ref": "#/definitions/Uint64"
}
]
},
"Uint64": {
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 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 `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
"type": "string"
},
"VoteInformation": {
"type": "object",
"required": [
"voted_at"
],
"properties": {
"voted_at": {
"$ref": "#/definitions/BlockInfo"
}
},
"additionalProperties": false
}
}
}
@@ -0,0 +1,97 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "VotesPagedResponse",
"type": "object",
"required": [
"votes"
],
"properties": {
"start_next_after": {
"type": [
"string",
"null"
]
},
"votes": {
"type": "array",
"items": {
"$ref": "#/definitions/VoteDetails"
}
}
},
"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"
},
"BlockInfo": {
"type": "object",
"required": [
"chain_id",
"height",
"time"
],
"properties": {
"chain_id": {
"type": "string"
},
"height": {
"description": "The height of a block is the number of blocks preceding it in the blockchain.",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"time": {
"description": "Absolute time of the block creation in seconds since the UNIX epoch (00:00:00 on 1970-01-01 UTC).\n\nThe source of this is the [BFT Time in Tendermint](https://github.com/tendermint/tendermint/blob/58dc1726/spec/consensus/bft-time.md), which has the same nanosecond precision as the `Timestamp` type.\n\n# Examples\n\nUsing chrono:\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; # extern crate chrono; use chrono::NaiveDateTime; let seconds = env.block.time.seconds(); let nsecs = env.block.time.subsec_nanos(); let dt = NaiveDateTime::from_timestamp(seconds as i64, nsecs as u32); ```\n\nCreating a simple millisecond-precision timestamp (as used in JavaScript):\n\n``` # use cosmwasm_std::{Addr, BlockInfo, ContractInfo, Env, MessageInfo, Timestamp, TransactionInfo}; # let env = Env { # block: BlockInfo { # height: 12_345, # time: Timestamp::from_nanos(1_571_797_419_879_305_533), # chain_id: \"cosmos-testnet-14002\".to_string(), # }, # transaction: Some(TransactionInfo { index: 3 }), # contract: ContractInfo { # address: Addr::unchecked(\"contract\"), # }, # }; let millis = env.block.time.nanos() / 1_000_000; ```",
"allOf": [
{
"$ref": "#/definitions/Timestamp"
}
]
}
},
"additionalProperties": false
},
"Timestamp": {
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
"allOf": [
{
"$ref": "#/definitions/Uint64"
}
]
},
"Uint64": {
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 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 `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
"type": "string"
},
"VoteDetails": {
"type": "object",
"required": [
"information",
"voter"
],
"properties": {
"information": {
"$ref": "#/definitions/VoteInformation"
},
"voter": {
"$ref": "#/definitions/Addr"
}
},
"additionalProperties": false
},
"VoteInformation": {
"type": "object",
"required": [
"voted_at"
],
"properties": {
"voted_at": {
"$ref": "#/definitions/BlockInfo"
}
},
"additionalProperties": false
}
}
}
@@ -0,0 +1,44 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SigningStatusAtHeightResponse",
"type": "object",
"required": [
"block_height",
"current_registered_dealers",
"dkg_epoch_id",
"offline_signers",
"signing_threshold",
"threshold_available"
],
"properties": {
"block_height": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"current_registered_dealers": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"dkg_epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"offline_signers": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"signing_threshold": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"threshold_available": {
"type": "boolean"
}
},
"additionalProperties": false
}
@@ -0,0 +1,14 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::write_api;
use nym_offline_signers_contract_common::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
fn main() {
write_api! {
instantiate: InstantiateMsg,
query: QueryMsg,
execute: ExecuteMsg,
migrate: MigrateMsg,
}
}
+167
View File
@@ -0,0 +1,167 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::queries::{
query_active_proposal, query_active_proposals_paged, query_admin, query_config,
query_current_signing_status, query_last_status_reset, query_last_status_reset_paged,
query_offline_signer_information, query_offline_signers_addresses_at_height,
query_offline_signers_paged, query_proposal, query_proposals_paged,
query_signing_status_at_height, query_vote_information, query_votes_paged,
};
use crate::storage::NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE;
use crate::transactions::{
try_propose_or_vote, try_reset_offline_status, try_update_contract_admin,
};
use cosmwasm_std::{
entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response,
};
use nym_contracts_common::set_build_information;
use nym_offline_signers_contract_common::{
ExecuteMsg, InstantiateMsg, MigrateMsg, NymOfflineSignersContractError, QueryMsg,
};
const CONTRACT_NAME: &str = "crate:nym-offline-signers-contract";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
#[entry_point]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, NymOfflineSignersContractError> {
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
set_build_information!(deps.storage)?;
let dkg_contract_address = deps.api.addr_validate(&msg.dkg_contract_address)?;
NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE.initialise(
deps,
env,
info.sender,
dkg_contract_address,
msg.config,
)?;
Ok(Response::default())
}
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, NymOfflineSignersContractError> {
match msg {
ExecuteMsg::UpdateAdmin { admin } => try_update_contract_admin(deps, info, admin),
ExecuteMsg::ProposeOrVote { signer } => try_propose_or_vote(deps, env, info, signer),
ExecuteMsg::ResetOfflineStatus {} => try_reset_offline_status(deps, env, info),
}
}
#[entry_point]
pub fn query(
deps: Deps,
env: Env,
msg: QueryMsg,
) -> Result<Binary, NymOfflineSignersContractError> {
match msg {
QueryMsg::Admin {} => Ok(to_json_binary(&query_admin(deps)?)?),
QueryMsg::GetConfig {} => Ok(to_json_binary(&query_config(deps)?)?),
QueryMsg::GetActiveProposal { signer } => {
Ok(to_json_binary(&query_active_proposal(deps, env, signer)?)?)
}
QueryMsg::GetProposal { proposal_id } => {
Ok(to_json_binary(&query_proposal(deps, env, proposal_id)?)?)
}
QueryMsg::GetVoteInformation { voter, proposal } => Ok(to_json_binary(
&query_vote_information(deps, voter, proposal)?,
)?),
QueryMsg::GetOfflineSignerInformation { signer } => Ok(to_json_binary(
&query_offline_signer_information(deps, signer)?,
)?),
QueryMsg::GetOfflineSignersAddressesAtHeight { height } => Ok(to_json_binary(
&query_offline_signers_addresses_at_height(deps, height)?,
)?),
QueryMsg::GetLastStatusReset { signer } => {
Ok(to_json_binary(&query_last_status_reset(deps, signer)?)?)
}
QueryMsg::GetActiveProposalsPaged { start_after, limit } => Ok(to_json_binary(
&query_active_proposals_paged(deps, env, start_after, limit)?,
)?),
QueryMsg::GetProposalsPaged { start_after, limit } => Ok(to_json_binary(
&query_proposals_paged(deps, start_after, limit)?,
)?),
QueryMsg::GetVotesPaged {
proposal,
start_after,
limit,
} => Ok(to_json_binary(&query_votes_paged(
deps,
proposal,
start_after,
limit,
)?)?),
QueryMsg::GetOfflineSignersPaged { start_after, limit } => Ok(to_json_binary(
&query_offline_signers_paged(deps, start_after, limit)?,
)?),
QueryMsg::GetLastStatusResetPaged { start_after, limit } => Ok(to_json_binary(
&query_last_status_reset_paged(deps, start_after, limit)?,
)?),
QueryMsg::CurrentSigningStatus {} => {
Ok(to_json_binary(&query_current_signing_status(deps)?)?)
}
QueryMsg::SigningStatusAtHeight { block_height } => Ok(to_json_binary(
&query_signing_status_at_height(deps, block_height)?,
)?),
}
}
#[entry_point]
pub fn migrate(
deps: DepsMut,
_: Env,
_msg: MigrateMsg,
) -> Result<Response, NymOfflineSignersContractError> {
set_build_information!(deps.storage)?;
cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
Ok(Default::default())
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(test)]
mod contract_instantiation {
use super::*;
use crate::storage::NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE;
use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env};
#[test]
fn sets_contract_admin_to_the_message_sender() -> anyhow::Result<()> {
let mut deps = mock_dependencies();
let env = mock_env();
let some_sender = deps.api.addr_make("some_sender");
let dummy_dkg_contract = deps.api.addr_make("dkg_contract");
instantiate(
deps.as_mut(),
env,
message_info(&some_sender, &[]),
InstantiateMsg {
dkg_contract_address: dummy_dkg_contract.to_string(),
config: Default::default(),
},
)?;
NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE
.contract_admin
.assert_admin(deps.as_ref(), &some_sender)?;
Ok(())
}
}
}
+212
View File
@@ -0,0 +1,212 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::storage::NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE;
use cosmwasm_std::{Addr, Deps, QuerierWrapper, StdError, StdResult};
use nym_coconut_dkg_common::dealer::PagedDealerAddressesResponse;
use nym_coconut_dkg_common::types::EpochId;
use nym_coconut_dkg_common::{
msg::QueryMsg as DkgQueryMsg,
types::{Cw4Contract, Epoch},
};
use nym_contracts_common::contract_querier::ContractQuerier;
use nym_offline_signers_contract_common::{NymOfflineSignersContractError, SigningStatusResponse};
pub(crate) trait DkgContractQuerier: ContractQuerier {
fn query_dkg_cw4_contract_address(&self, dkg_contract: impl Into<String>) -> StdResult<Addr> {
Ok(self.query_dkg_contract_state(dkg_contract)?.group_addr.0)
}
fn query_dkg_contract_state(
&self,
dkg_contract: impl Into<String>,
) -> StdResult<nym_coconut_dkg_common::types::State> {
self.query_contract_storage_value(dkg_contract, b"state")?
.ok_or(StdError::not_found(
"unable to retrieve state information from the DKG contract storage",
))
}
fn query_current_dkg_epoch(&self, dkg_contract: impl Into<String>) -> StdResult<Epoch> {
self.query_contract_storage_value(dkg_contract, b"current_epoch")?
.ok_or(StdError::not_found(
"unable to retrieve epoch information from the DKG contract storage",
))
}
fn query_dkg_epoch_at_height(
&self,
dkg_contract: impl Into<String>,
height: u64,
) -> StdResult<Epoch> {
let res: Option<Epoch> =
self.query_contract(dkg_contract, &DkgQueryMsg::GetEpochStateAtHeight { height })?;
res.ok_or(StdError::not_found(format!(
"epoch hasn't been initialised/migrated to new format at height {height} yet"
)))
}
fn query_dkg_dealers(
&self,
dkg_contract: impl Into<String>,
epoch_id: EpochId,
) -> StdResult<Vec<Addr>> {
let dkg_contract = dkg_contract.into();
let mut dealers_addresses = Vec::new();
// current max limit
let limit = 50;
let mut start_after = None;
loop {
let mut response: PagedDealerAddressesResponse = self.query_contract(
&dkg_contract,
&DkgQueryMsg::GetEpochDealersAddresses {
epoch_id,
limit: Some(limit),
start_after,
},
)?;
start_after = response.start_next_after.as_ref().map(|d| d.to_string());
if response.dealers.len() < limit as usize || response.start_next_after.is_none() {
dealers_addresses.append(&mut response.dealers);
// we have already exhausted the data
break;
} else {
dealers_addresses.append(&mut response.dealers);
}
}
Ok(dealers_addresses)
}
fn query_dkg_threshold(
&self,
dkg_contract: impl Into<String>,
epoch_id: EpochId,
) -> StdResult<u64> {
self.query_contract(dkg_contract, &DkgQueryMsg::GetEpochThreshold { epoch_id })
}
}
impl<T> DkgContractQuerier for T where T: ContractQuerier {}
pub(crate) fn group_members(
querier_wrapper: &QuerierWrapper,
contract: &Cw4Contract,
) -> Result<Vec<Addr>, NymOfflineSignersContractError> {
// we shouldn't ever have more group members than the default limit but IN CASE
// something changes down the line, do go through the pagination flow
let mut group_members = Vec::new();
// current max limit
let limit = 30;
let mut start_after = None;
loop {
let members = contract.list_members(querier_wrapper, start_after, Some(limit))?;
start_after = members.last().as_ref().map(|d| d.addr.clone());
for member in &members {
group_members.push(Addr::unchecked(&member.addr));
}
if members.len() < limit as usize {
// we have already exhausted the data
break;
}
}
Ok(group_members)
}
// TODO: change our testing frameworks to allow testing this
// (the current problem is that it relies on very particular intermediate states of the DKG contract)
pub(crate) fn basic_signing_status(
deps: Deps,
block_height: Option<u64>,
) -> Result<SigningStatusResponse, NymOfflineSignersContractError> {
let dkg_contract_address = NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE
.dkg_contract
.load(deps.storage)?;
let dkg_epoch = match block_height {
Some(block_height) => deps
.querier
.query_dkg_epoch_at_height(&dkg_contract_address, block_height)?,
None => deps
.querier
.query_current_dkg_epoch(&dkg_contract_address)?,
};
// if DKG exchange is currently in progress, retrieve dealers and threshold from the PREVIOUS epoch
// as that'd be the set used for issuing credentials
let epoch_id = if dkg_epoch.state.is_final() {
dkg_epoch.epoch_id
} else {
dkg_epoch.epoch_id.saturating_sub(1)
};
let dkg_threshold = deps
.querier
.query_dkg_threshold(&dkg_contract_address, epoch_id)?;
let group_contract = Cw4Contract::new(
deps.querier
.query_dkg_cw4_contract_address(&dkg_contract_address)?,
);
let total_group_members = group_members(&deps.querier, &group_contract)?.len() as u32;
let dkg_dealers = deps
.querier
.query_dkg_dealers(&dkg_contract_address, epoch_id)?;
let offline_signers = match block_height {
Some(block_height) => NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE
.offline_signers
.addresses
.may_load_at_height(deps.storage, block_height)?
.unwrap_or_default(),
None => NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE
.offline_signers
.addresses
.load(deps.storage)?,
}
.into_iter()
.filter(|offline_signer| dkg_dealers.contains(offline_signer))
.count() as u32;
let available_signers = (dkg_dealers.len() as u32).saturating_sub(offline_signers);
Ok(SigningStatusResponse {
dkg_epoch_id: epoch_id,
signing_threshold: dkg_threshold,
total_group_members,
current_registered_dealers: dkg_dealers.len() as u32,
offline_signers,
threshold_available: available_signers as u64 >= dkg_threshold,
})
}
#[cfg(test)]
mod tests {
use crate::helpers::group_members;
use crate::testing::init_contract_tester_with_group_members;
use cw4::Cw4Contract;
use nym_coconut_dkg::testable_dkg_contract::GroupContract;
use nym_contracts_common_testing::ContractOpts;
#[test]
fn getting_group_members() -> anyhow::Result<()> {
for members in [0, 10, 100, 1000] {
let tester = init_contract_tester_with_group_members(members);
let group_contract =
Cw4Contract::new(tester.unchecked_contract_address::<GroupContract>());
let querier = tester.deps().querier;
let addresses = group_members(&querier, &group_contract)?;
assert_eq!(addresses.len(), members);
}
Ok(())
}
}
+13
View File
@@ -0,0 +1,13 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod contract;
pub mod queued_migrations;
pub mod storage;
mod helpers;
mod queries;
mod transactions;
#[cfg(test)]
pub mod testing;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,233 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// that's fine in test code
#![allow(clippy::panic)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]
use crate::contract::{execute, instantiate, migrate, query};
use crate::helpers::{group_members, DkgContractQuerier};
use crate::storage::NymOfflineSignersStorage;
use cosmwasm_std::Addr;
use cw4::Cw4Contract;
use nym_coconut_dkg::testable_dkg_contract::DkgContract;
use nym_contracts_common_testing::{
AdminExt, ArbitraryContractStorageReader, ArbitraryContractStorageWriter, BankExt, ChainOpts,
CommonStorageKeys, ContractFn, ContractOpts, ContractTester, DenomExt, PermissionedFn, QueryFn,
RandExt, SliceRandom, TestableNymContract,
};
use nym_offline_signers_contract_common::constants::storage_keys;
use nym_offline_signers_contract_common::{
ExecuteMsg, InstantiateMsg, MigrateMsg, NymOfflineSignersContractError, Proposal, ProposalId,
QueryMsg,
};
pub struct OfflineSignersContract;
const DEFAULT_GROUP_MEMBERS: usize = 15;
impl TestableNymContract for OfflineSignersContract {
const NAME: &'static str = "offline-signers-contract";
type InitMsg = InstantiateMsg;
type ExecuteMsg = ExecuteMsg;
type QueryMsg = QueryMsg;
type MigrateMsg = MigrateMsg;
type ContractError = NymOfflineSignersContractError;
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,
{
init_contract_tester_with_group_members(DEFAULT_GROUP_MEMBERS)
}
}
pub fn init_contract_tester() -> ContractTester<OfflineSignersContract> {
OfflineSignersContract::init()
.with_common_storage_key(CommonStorageKeys::Admin, storage_keys::CONTRACT_ADMIN)
}
pub fn init_contract_tester_with_group_members(
members: usize,
) -> ContractTester<OfflineSignersContract> {
init_custom_contract_tester(
members,
InstantiateMsg {
dkg_contract_address: "PLACEHOLDER".to_string(),
config: Default::default(),
},
)
}
// this will OVERWRITE placeholder you put for dkg contract address with correct value
pub(crate) fn init_custom_contract_tester(
members: usize,
mut instantiate_msg: InstantiateMsg,
) -> ContractTester<OfflineSignersContract> {
// prepare the dkg contract and using that initial setup, add the offline signers contract
let builder =
nym_coconut_dkg::testable_dkg_contract::prepare_contract_tester_builder_with_group_members(
members,
);
// we just instantiated it
let dkg_contract_address = builder.unchecked_contract_address::<DkgContract>();
instantiate_msg.dkg_contract_address = dkg_contract_address.to_string();
// 5. finally init the offline signers contract
builder
.instantiate::<OfflineSignersContract>(Some(instantiate_msg))
.build()
}
pub(crate) trait OfflineSignersContractTesterExt:
ContractOpts<
ExecuteMsg = ExecuteMsg,
QueryMsg = QueryMsg,
ContractError = NymOfflineSignersContractError,
> + ChainOpts
+ AdminExt
+ DenomExt
+ RandExt
+ BankExt
+ ArbitraryContractStorageReader
+ ArbitraryContractStorageWriter
{
fn group_contract_wrapper(&self) -> Cw4Contract {
let storage = NymOfflineSignersStorage::new();
let dkg_contract_address = storage.dkg_contract.load(self.storage()).unwrap();
Cw4Contract::new(
self.deps()
.querier
.query_dkg_cw4_contract_address(dkg_contract_address)
.unwrap(),
)
}
fn group_members(&self) -> Vec<Addr> {
let querier = self.deps().querier;
let group_contract = self.group_contract_wrapper();
group_members(&querier, &group_contract).unwrap()
}
fn random_group_member(&mut self) -> Addr {
let members = self.group_members();
members
.choose(&mut self.raw_rng())
.expect("no group members available")
.clone()
}
#[track_caller]
fn add_votes(&mut self, proposal_id: ProposalId) {
let storage = NymOfflineSignersStorage::new();
let members = self.group_members();
let proposal = storage.proposals.load(self.storage(), proposal_id).unwrap();
for member in members {
// check if we already voted
if !storage.votes.has(self.storage(), (proposal_id, &member)) {
let env = self.env();
storage
.propose_or_vote(
self.deps_mut(),
env,
member,
proposal.proposed_offline_signer.clone(),
)
.unwrap();
}
}
}
#[track_caller]
fn add_vote(&mut self, proposal_id: ProposalId, voter: &Addr) {
let storage = NymOfflineSignersStorage::new();
let proposal = storage.proposals.load(self.storage(), proposal_id).unwrap();
let env = self.env();
storage
.propose_or_vote(
self.deps_mut(),
env,
voter.clone(),
proposal.proposed_offline_signer.clone(),
)
.unwrap();
}
fn next_proposal_id(&self) -> ProposalId {
NymOfflineSignersStorage::new()
.proposal_count
.may_load(self.storage())
.unwrap()
.unwrap_or_default()
+ 1
}
#[track_caller]
fn make_proposal(&mut self, target: &Addr) -> ProposalId {
let proposer = self.random_group_member();
let storage = NymOfflineSignersStorage::new();
let id = self.next_proposal_id();
let env = self.env();
storage
.propose_or_vote(self.deps_mut(), env, proposer, target.clone())
.unwrap();
id
}
fn load_proposal(&mut self, proposal_id: ProposalId) -> Option<Proposal> {
NymOfflineSignersStorage::new()
.proposals
.may_load(self.storage(), proposal_id)
.unwrap()
}
#[track_caller]
fn insert_empty_proposal(&mut self, target: &Addr) -> ProposalId {
let proposer = self.generate_account();
let storage = NymOfflineSignersStorage::new();
let env = self.env();
storage
.insert_new_active_proposal(self.storage_mut(), &env, &proposer, target)
.unwrap()
}
#[track_caller]
fn insert_offline_signer(&mut self, signer: &Addr) -> ProposalId {
let proposal_id = self.make_proposal(signer);
self.add_votes(proposal_id);
proposal_id
}
#[track_caller]
fn reset_offline_status(&mut self, signer: &Addr) {
let storage = NymOfflineSignersStorage::new();
let env = self.env();
storage
.reset_offline_status(self.deps_mut(), env, signer.clone())
.unwrap();
}
}
impl OfflineSignersContractTesterExt for ContractTester<OfflineSignersContract> {}
@@ -0,0 +1,232 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::storage::NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE;
use cosmwasm_std::{DepsMut, Env, Event, MessageInfo, Response};
use nym_offline_signers_contract_common::NymOfflineSignersContractError;
pub fn try_update_contract_admin(
deps: DepsMut<'_>,
info: MessageInfo,
new_admin: String,
) -> Result<Response, NymOfflineSignersContractError> {
let new_admin = deps.api.addr_validate(&new_admin)?;
let res = NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE
.contract_admin
.execute_update_admin(deps, info, Some(new_admin))?;
Ok(res)
}
pub fn try_propose_or_vote(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
signer: String,
) -> Result<Response, NymOfflineSignersContractError> {
let signer = deps.api.addr_validate(&signer)?;
let reached_quorum =
NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE.propose_or_vote(deps, env, info.sender, signer)?;
Ok(Response::new().add_event(
Event::new("offline_signer_vote")
.add_attribute("quorum_reached", reached_quorum.to_string()),
))
}
pub fn try_reset_offline_status(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
) -> Result<Response, NymOfflineSignersContractError> {
NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE.reset_offline_status(deps, env, info.sender)?;
Ok(Response::default())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::NymOfflineSignersStorage;
use crate::testing::{
init_contract_tester, init_custom_contract_tester, OfflineSignersContractTesterExt,
};
use cosmwasm_std::testing::message_info;
use cosmwasm_std::{Decimal, StdError};
use itertools::Itertools;
use nym_contracts_common_testing::{ChainOpts, ContractOpts, FindAttribute};
use nym_offline_signers_contract_common::{Config, InstantiateMsg};
#[cfg(test)]
mod updating_contract_admin {
use super::*;
use crate::testing::init_contract_tester;
use cw_controllers::AdminError;
use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt};
use nym_offline_signers_contract_common::ExecuteMsg;
#[test]
fn can_only_be_performed_by_current_admin() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let random_acc = test.generate_account();
let new_admin = test.generate_account();
let res = test
.execute_raw(
random_acc,
ExecuteMsg::UpdateAdmin {
admin: new_admin.to_string(),
},
)
.unwrap_err();
assert_eq!(
res,
NymOfflineSignersContractError::Admin(AdminError::NotAdmin {})
);
let actual_admin = test.admin_unchecked();
let res = test.execute_raw(
actual_admin.clone(),
ExecuteMsg::UpdateAdmin {
admin: new_admin.to_string(),
},
);
assert!(res.is_ok());
let updated_admin = test.admin_unchecked();
assert_eq!(new_admin, updated_admin);
Ok(())
}
#[test]
fn requires_providing_valid_address() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let bad_account = "definitely-not-valid-account";
let res = test.execute_raw(
test.admin_unchecked(),
ExecuteMsg::UpdateAdmin {
admin: bad_account.to_string(),
},
);
assert!(res.is_err());
let empty_account = "";
let res = test.execute_raw(
test.admin_unchecked(),
ExecuteMsg::UpdateAdmin {
admin: empty_account.to_string(),
},
);
assert!(res.is_err());
Ok(())
}
}
#[test]
fn try_propose_or_vote() -> anyhow::Result<()> {
let mut tester = init_custom_contract_tester(
10,
InstantiateMsg {
dkg_contract_address: "".to_string(),
config: Config {
required_quorum: Decimal::percent(30),
..Default::default()
},
},
);
let voter1 = tester.random_group_member();
let voter2 = tester.random_group_member();
let voter3 = tester.random_group_member();
let good_signer = tester.random_group_member();
assert!([&voter1, &voter2, &voter3, &good_signer]
.iter()
.duplicates()
.next()
.is_none());
let bad_signer = "invalid-address".to_string();
let env = tester.env();
let err = super::try_propose_or_vote(
tester.deps_mut(),
env,
message_info(&voter1, &[]),
bad_signer,
)
.unwrap_err();
assert!(matches!(
err,
NymOfflineSignersContractError::StdErr(StdError::GenericErr { msg, .. }) if msg == "Error decoding bech32"
));
// emits quorum information as an event
let env = tester.env();
let res = super::try_propose_or_vote(
tester.deps_mut(),
env,
message_info(&voter1, &[]),
good_signer.to_string(),
)?;
assert!(!res.parsed_attribute::<_, _, bool>("offline_signer_vote", "quorum_reached"));
let env = tester.env();
let res = super::try_propose_or_vote(
tester.deps_mut(),
env,
message_info(&voter2, &[]),
good_signer.to_string(),
)?;
assert!(!res.parsed_attribute::<_, _, bool>("offline_signer_vote", "quorum_reached"));
let env = tester.env();
let res = super::try_propose_or_vote(
tester.deps_mut(),
env,
message_info(&voter3, &[]),
good_signer.to_string(),
)?;
assert!(res.parsed_attribute::<_, _, bool>("offline_signer_vote", "quorum_reached"));
Ok(())
}
#[test]
fn try_reset_offline_status() -> anyhow::Result<()> {
let storage = NymOfflineSignersStorage::new();
let mut tester = init_contract_tester();
let signer = tester.random_group_member();
tester.insert_offline_signer(&signer);
tester.advance_day_of_blocks();
assert!(storage
.offline_signers
.addresses
.load(&tester)?
.contains(&signer));
assert!(storage.offline_signers.information.has(&tester, &signer));
assert!(storage.active_proposals.has(&tester, &signer));
let env = tester.env();
super::try_reset_offline_status(tester.deps_mut(), env, message_info(&signer, &[]))?;
assert!(!storage
.offline_signers
.addresses
.load(&tester)?
.contains(&signer));
assert!(!storage.offline_signers.information.has(&tester, &signer));
assert!(!storage.active_proposals.has(&tester, &signer));
Ok(())
}
}
@@ -151,6 +151,60 @@
}
},
"additionalProperties": false
},
{
"description": "An admin method to remove submitted node measurements. Used as an escape hatch should the data stored get too unwieldy.",
"type": "object",
"required": [
"remove_node_measurements"
],
"properties": {
"remove_node_measurements": {
"type": "object",
"required": [
"epoch_id",
"node_id"
],
"properties": {
"epoch_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"node_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "An admin method to remove submitted nodes measurements. Used as an escape hatch should the data stored get too unwieldy. Note: it is expected to get called multiple times until the response indicates all the epoch data has been removed.",
"type": "object",
"required": [
"remove_epoch_measurements"
],
"properties": {
"remove_epoch_measurements": {
"type": "object",
"required": [
"epoch_id"
],
"properties": {
"epoch_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
],
"definitions": {
@@ -126,6 +126,60 @@
}
},
"additionalProperties": false
},
{
"description": "An admin method to remove submitted node measurements. Used as an escape hatch should the data stored get too unwieldy.",
"type": "object",
"required": [
"remove_node_measurements"
],
"properties": {
"remove_node_measurements": {
"type": "object",
"required": [
"epoch_id",
"node_id"
],
"properties": {
"epoch_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"node_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "An admin method to remove submitted nodes measurements. Used as an escape hatch should the data stored get too unwieldy. Note: it is expected to get called multiple times until the response indicates all the epoch data has been removed.",
"type": "object",
"required": [
"remove_epoch_measurements"
],
"properties": {
"remove_epoch_measurements": {
"type": "object",
"required": [
"epoch_id"
],
"properties": {
"epoch_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
],
"definitions": {
+7 -50
View File
@@ -1,40 +1,16 @@
// 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 cosmwasm_std::{StdError, StdResult};
use cw_storage_plus::{Key, Namespace, Path, PrimaryKey};
use nym_contracts_common::contract_querier::ContractQuerier;
use nym_mixnet_contract_common::{Interval, MixNodeBond, NymNodeBond};
use nym_performance_contract_common::{EpochId, NodeId};
use serde::de::DeserializeOwned;
use std::ops::Deref;
pub(crate) trait MixnetContractQuerier {
#[allow(dead_code)]
fn query_mixnet_contract<T: DeserializeOwned>(
&self,
address: impl Into<String>,
msg: &nym_mixnet_contract_common::QueryMsg,
) -> StdResult<T>;
fn query_mixnet_contract_storage(
&self,
address: impl Into<String>,
key: impl Into<Binary>,
) -> StdResult<Option<Vec<u8>>>;
fn query_mixnet_contract_storage_value<T: DeserializeOwned>(
&self,
address: impl Into<String>,
key: impl Into<Binary>,
) -> StdResult<Option<T>> {
match self.query_mixnet_contract_storage(address, key)? {
None => Ok(None),
Some(value) => Ok(Some(from_json(&value)?)),
}
}
pub(crate) trait MixnetContractQuerier: ContractQuerier {
fn query_current_mixnet_interval(&self, address: impl Into<String>) -> StdResult<Interval> {
self.query_mixnet_contract_storage_value(address, b"ci")?
self.query_contract_storage_value(address, b"ci")?
.ok_or(StdError::not_found(
"unable to retrieve interval information from the mixnet contract storage",
))
@@ -76,7 +52,7 @@ pub(crate) trait MixnetContractQuerier {
);
let storage_key = path.deref();
self.query_mixnet_contract_storage_value(address, storage_key)
self.query_contract_storage_value(address, storage_key)
}
fn query_mixnode_bond(
@@ -92,27 +68,8 @@ pub(crate) trait MixnetContractQuerier {
);
let storage_key = path.deref();
self.query_mixnet_contract_storage_value(address, storage_key)
self.query_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)
}
}
impl<T> MixnetContractQuerier for T where T: ContractQuerier {}
+13
View File
@@ -4350,6 +4350,18 @@ dependencies = [
"utoipa",
]
[[package]]
name = "nym-offline-signers-contract-common"
version = "0.1.0"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"schemars",
"serde",
"thiserror 2.0.12",
]
[[package]]
name = "nym-pemstore"
version = "0.3.0"
@@ -4480,6 +4492,7 @@ dependencies = [
"nym-mixnet-contract-common",
"nym-multisig-contract-common",
"nym-network-defaults",
"nym-offline-signers-contract-common",
"nym-performance-contract-common",
"nym-serde-helpers",
"nym-vesting-contract-common",
@@ -26,6 +26,7 @@ pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str =
// \/ TODO: this has to be updated once the contract is deployed
pub(crate) const PERFORMANCE_CONTRACT_ADDRESS: &str = "";
pub(crate) const OFFLINE_SIGNERS_CONTRACT_ADDRESS: &str = "";
// /\ TODO: this has to be updated once the contract is deployed
// -- Constructor functions --
@@ -55,6 +56,7 @@ pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails {
group_contract_address: parse_optional_str(GROUP_CONTRACT_ADDRESS),
multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS),
coconut_dkg_contract_address: parse_optional_str(COCONUT_DKG_CONTRACT_ADDRESS),
offline_signers_contract_address: parse_optional_str(OFFLINE_SIGNERS_CONTRACT_ADDRESS),
},
nym_vpn_api_url: None,
nym_vpn_api_urls: None,
@@ -44,6 +44,7 @@ pub(crate) struct NymContracts {
pub(crate) cw4_group: Contract,
pub(crate) dkg: Contract,
pub(crate) performance: Contract,
pub(crate) offline_signers: Contract,
}
impl NymContracts {
@@ -131,6 +132,7 @@ impl Default for NymContracts {
cw3_multisig: Contract::new("cw3_multisig"),
dkg: Contract::new("dkg"),
performance: Contract::new("performance"),
offline_signers: Contract::new("offline_signers"),
}
}
}
@@ -70,6 +70,7 @@ impl<'a> From<&'a LoadedNetwork> for nym_config::defaults::NymNetworkDetails {
group_contract_address: Some(value.contracts.cw4_group.address.to_string()),
multisig_contract_address: Some(value.contracts.cw3_multisig.address.to_string()),
coconut_dkg_contract_address: Some(value.contracts.dkg.address.to_string()),
offline_signers_contract_address: None,
};
// ASSUMPTION: same chain details like prefix, denoms, etc. as mainnet
let mainnet = NymNetworkDetails::new_mainnet();