diff --git a/Cargo.lock b/Cargo.lock index cc43189a112..700ee9b0006 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 11aad891fe3..a38c09b7ee7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index ea4e8beeb48..a0cbc8fdfb0 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -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"] } diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs index ad7db0991b4..6e8c42330ab 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs @@ -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, pub multisig_contract_address: Option, pub coconut_dkg_contract_address: Option, + pub offline_signers_contract_address: Option, } impl TryFrom for TypedNymContracts { @@ -102,6 +108,10 @@ impl TryFrom 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()?, }) } } diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/offline_signers_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/offline_signers_query_client.rs new file mode 100644 index 00000000000..9927b98e52e --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/offline_signers_query_client.rs @@ -0,0 +1,283 @@ +// Copyright 2025 - Nym Technologies SA +// 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( + &self, + query: OfflineSignersQueryMsg, + ) -> Result + where + for<'a> T: Deserialize<'a>; + + async fn admin(&self) -> Result { + self.query_offline_signers_contract(OfflineSignersQueryMsg::Admin {}) + .await + } + + async fn get_config(&self) -> Result { + self.query_offline_signers_contract(OfflineSignersQueryMsg::GetConfig {}) + .await + } + + async fn get_active_proposal( + &self, + signer: AccountId, + ) -> Result { + self.query_offline_signers_contract(OfflineSignersQueryMsg::GetActiveProposal { + signer: signer.to_string(), + }) + .await + } + + async fn get_proposal(&self, proposal_id: ProposalId) -> Result { + self.query_offline_signers_contract(OfflineSignersQueryMsg::GetProposal { proposal_id }) + .await + } + + async fn get_vote_information( + &self, + voter: AccountId, + proposal: ProposalId, + ) -> Result { + self.query_offline_signers_contract(OfflineSignersQueryMsg::GetVoteInformation { + voter: voter.to_string(), + proposal, + }) + .await + } + + async fn get_offline_signer_information( + &self, + signer: AccountId, + ) -> Result { + self.query_offline_signers_contract(OfflineSignersQueryMsg::GetOfflineSignerInformation { + signer: signer.to_string(), + }) + .await + } + + async fn get_offline_signers_addresses_at_height( + &self, + height: Option, + ) -> Result { + self.query_offline_signers_contract( + OfflineSignersQueryMsg::GetOfflineSignersAddressesAtHeight { height }, + ) + .await + } + + async fn get_last_status_reset( + &self, + signer: AccountId, + ) -> Result { + self.query_offline_signers_contract(OfflineSignersQueryMsg::GetLastStatusReset { + signer: signer.to_string(), + }) + .await + } + + async fn get_active_proposals_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_offline_signers_contract(OfflineSignersQueryMsg::GetActiveProposalsPaged { + start_after, + limit, + }) + .await + } + + async fn get_proposals_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_offline_signers_contract(OfflineSignersQueryMsg::GetProposalsPaged { + start_after, + limit, + }) + .await + } + + async fn get_votes_paged( + &self, + proposal: ProposalId, + start_after: Option, + limit: Option, + ) -> Result { + self.query_offline_signers_contract(OfflineSignersQueryMsg::GetVotesPaged { + proposal, + start_after, + limit, + }) + .await + } + + async fn get_offline_signers_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_offline_signers_contract(OfflineSignersQueryMsg::GetOfflineSignersPaged { + start_after, + limit, + }) + .await + } + + async fn get_last_status_reset_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_offline_signers_contract(OfflineSignersQueryMsg::GetLastStatusResetPaged { + start_after, + limit, + }) + .await + } + + async fn current_signing_status(&self) -> Result { + self.query_offline_signers_contract(OfflineSignersQueryMsg::CurrentSigningStatus {}) + .await + } + + async fn signing_status_at_height( + &self, + block_height: u64, + ) -> Result { + 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, NyxdError> { + collect_paged!(self, get_active_proposals_paged, active_proposals) + } + async fn get_all_proposals(&self) -> Result, NyxdError> { + collect_paged!(self, get_proposals_paged, proposals) + } + async fn get_all_votes(&self, proposal: ProposalId) -> Result, NyxdError> { + collect_paged!(self, get_votes_paged, votes, proposal) + } + async fn get_all_offline_signers(&self) -> Result, NyxdError> { + collect_paged!(self, get_offline_signers_paged, offline_signers) + } + async fn get_all_last_status_reset(&self) -> Result, NyxdError> { + collect_paged!(self, get_last_status_reset_paged, status_resets) + } +} + +#[async_trait] +impl PagedOfflineSignersQueryClient for T where T: OfflineSignersQueryClient {} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl OfflineSignersQueryClient for C +where + C: CosmWasmClient + NymContractsProvider + Send + Sync, +{ + async fn query_offline_signers_contract( + &self, + query: OfflineSignersQueryMsg, + ) -> Result + 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( + 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() + } + }; + } +} diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/offline_signers_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/offline_signers_signing_client.rs new file mode 100644 index 00000000000..6571821c018 --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/offline_signers_signing_client.rs @@ -0,0 +1,120 @@ +// Copyright 2025 - Nym Technologies SA +// 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, + msg: OfflineSignersExecuteMsg, + memo: String, + funds: Vec, + ) -> Result; + + async fn update_admin( + &self, + admin: String, + fee: Option, + ) -> Result { + 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, + ) -> Result { + 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) -> Result { + 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 OfflineSignersSigningClient for C +where + C: SigningCosmWasmClient + NymContractsProvider + Sync, + NyxdError: From<::Error>, +{ + async fn execute_offline_signers_contract( + &self, + fee: Option, + msg: OfflineSignersExecuteMsg, + memo: String, + funds: Vec, + ) -> Result { + 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( + 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() + } + }; + } +} diff --git a/common/client-libs/validator-client/src/nyxd/mod.rs b/common/client-libs/validator-client/src/nyxd/mod.rs index 85b08ab12a0..5957e52a251 100644 --- a/common/client-libs/validator-client/src/nyxd/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/mod.rs @@ -305,6 +305,13 @@ impl NymContractsProvider for NyxdClient { 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 diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/Cargo.toml b/common/cosmwasm-smart-contracts/contracts-common-testing/Cargo.toml index 0bb81f17921..906390d575a 100644 --- a/common/cosmwasm-smart-contracts/contracts-common-testing/Cargo.toml +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/Cargo.toml @@ -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 } diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/basic_traits.rs b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/basic_traits.rs index f471b64be49..c551fbaafde 100644 --- a/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/basic_traits.rs +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/basic_traits.rs @@ -142,6 +142,7 @@ pub trait ChainOpts: ContractOpts { fn set_contract_balance(&mut self, balance: Coin); fn update_block(&mut self, action: F); + fn set_to_epoch(&mut self) { self.set_block_time(Timestamp::from_seconds(0)) } diff --git a/common/cosmwasm-smart-contracts/multisig-contract/src/msg.rs b/common/cosmwasm-smart-contracts/multisig-contract/src/msg.rs index 15b959fa144..770fadf21b4 100644 --- a/common/cosmwasm-smart-contracts/multisig-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/multisig-contract/src/msg.rs @@ -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, } diff --git a/common/cosmwasm-smart-contracts/offline-signers/Cargo.toml b/common/cosmwasm-smart-contracts/offline-signers/Cargo.toml new file mode 100644 index 00000000000..f1fd227e811 --- /dev/null +++ b/common/cosmwasm-smart-contracts/offline-signers/Cargo.toml @@ -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 diff --git a/common/cosmwasm-smart-contracts/offline-signers/src/constants.rs b/common/cosmwasm-smart-contracts/offline-signers/src/constants.rs new file mode 100644 index 00000000000..9f0a4630d09 --- /dev/null +++ b/common/cosmwasm-smart-contracts/offline-signers/src/constants.rs @@ -0,0 +1,25 @@ +// Copyright 2025 - Nym Technologies SA +// 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"; +} diff --git a/common/cosmwasm-smart-contracts/offline-signers/src/error.rs b/common/cosmwasm-smart-contracts/offline-signers/src/error.rs new file mode 100644 index 00000000000..d34abea0776 --- /dev/null +++ b/common/cosmwasm-smart-contracts/offline-signers/src/error.rs @@ -0,0 +1,44 @@ +// Copyright 2025 - Nym Technologies SA +// 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 }, +} diff --git a/common/cosmwasm-smart-contracts/offline-signers/src/helpers.rs b/common/cosmwasm-smart-contracts/offline-signers/src/helpers.rs new file mode 100644 index 00000000000..7e1e3cacd64 --- /dev/null +++ b/common/cosmwasm-smart-contracts/offline-signers/src/helpers.rs @@ -0,0 +1,2 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 diff --git a/common/cosmwasm-smart-contracts/offline-signers/src/lib.rs b/common/cosmwasm-smart-contracts/offline-signers/src/lib.rs new file mode 100644 index 00000000000..4fc43b8bfe8 --- /dev/null +++ b/common/cosmwasm-smart-contracts/offline-signers/src/lib.rs @@ -0,0 +1,12 @@ +// Copyright 2025 - Nym Technologies SA +// 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::*; diff --git a/common/cosmwasm-smart-contracts/offline-signers/src/msg.rs b/common/cosmwasm-smart-contracts/offline-signers/src/msg.rs new file mode 100644 index 00000000000..cb855918496 --- /dev/null +++ b/common/cosmwasm-smart-contracts/offline-signers/src/msg.rs @@ -0,0 +1,118 @@ +// Copyright 2025 - Nym Technologies SA +// 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 }, + + /// 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, + limit: Option, + }, + + /// Returns all (paged) proposals + #[cfg_attr(feature = "schema", returns(ProposalsPagedResponse))] + GetProposalsPaged { + start_after: Option, + limit: Option, + }, + + /// Returns all (paged) votes for the specified proposal + #[cfg_attr(feature = "schema", returns(VotesPagedResponse))] + GetVotesPaged { + proposal: ProposalId, + start_after: Option, + limit: Option, + }, + + /// Returns all (paged) offline signers + #[cfg_attr(feature = "schema", returns(OfflineSignersPagedResponse))] + GetOfflineSignersPaged { + start_after: Option, + limit: Option, + }, + + /// Returns all (paged) status resets + #[cfg_attr(feature = "schema", returns(LastStatusResetPagedResponse))] + GetLastStatusResetPaged { + start_after: Option, + limit: Option, + }, + + /// 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 { + // +} diff --git a/common/cosmwasm-smart-contracts/offline-signers/src/types.rs b/common/cosmwasm-smart-contracts/offline-signers/src/types.rs new file mode 100644 index 00000000000..366a322bb4d --- /dev/null +++ b/common/cosmwasm-smart-contracts/offline-signers/src/types.rs @@ -0,0 +1,196 @@ +// Copyright 2025 - Nym Technologies SA +// 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, +} + +#[cw_serde] +pub struct ActiveProposalsPagedResponse { + pub start_next_after: Option, + pub active_proposals: Vec, +} + +#[cw_serde] +pub struct LastStatusResetDetails { + pub information: StatusResetInformation, + pub signer: Addr, +} + +#[cw_serde] +pub struct LastStatusResetPagedResponse { + pub start_next_after: Option, + pub status_resets: Vec, +} + +#[cw_serde] +pub struct LastStatusResetResponse { + pub information: Option, +} + +#[cw_serde] +pub struct OfflineSignerResponse { + pub information: Option, +} + +#[cw_serde] +pub struct OfflineSignersAddressesResponse { + pub addresses: Vec, +} + +#[cw_serde] +pub struct OfflineSignerDetails { + pub information: OfflineSignerInformation, + pub signer: Addr, +} + +#[cw_serde] +pub struct OfflineSignersPagedResponse { + pub start_next_after: Option, + pub offline_signers: Vec, +} + +#[cw_serde] +pub struct ProposalResponse { + pub proposal: Option, +} + +#[cw_serde] +pub struct ProposalsPagedResponse { + pub start_next_after: Option, + pub proposals: Vec, +} + +#[cw_serde] +pub struct VoteResponse { + pub vote: Option, +} + +#[cw_serde] +pub struct VoteDetails { + pub voter: Addr, + pub information: VoteInformation, +} + +#[cw_serde] +pub struct VotesPagedResponse { + pub start_next_after: Option, + pub votes: Vec, +} + +#[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, +} diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index af64bd4d630..e996c34dc0c 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -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 = diff --git a/common/network-defaults/src/network.rs b/common/network-defaults/src/network.rs index 3fdf03671e0..f8767384c69 100644 --- a/common/network-defaults/src/network.rs +++ b/common/network-defaults/src/network.rs @@ -26,6 +26,9 @@ pub struct NymContracts { pub group_contract_address: Option, pub multisig_contract_address: Option, pub coconut_dkg_contract_address: Option, + + #[serde(default)] + pub offline_signers_contract_address: Option, } // 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()), diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 42e5e1352a2..ba4404c96cd 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -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" diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index ec3239772cf..3853d9ba399 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -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" diff --git a/contracts/Makefile b/contracts/Makefile index 9c826771c76..493fdbd53b7 100644 --- a/contracts/Makefile +++ b/contracts/Makefile @@ -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 \ No newline at end of file diff --git a/contracts/coconut-dkg/Cargo.toml b/contracts/coconut-dkg/Cargo.toml index 977448b9e73..e673e73c2ed 100644 --- a/contracts/coconut-dkg/Cargo.toml +++ b/contracts/coconut-dkg/Cargo.toml @@ -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] diff --git a/contracts/offline-signers/.cargo/config b/contracts/offline-signers/.cargo/config new file mode 100644 index 00000000000..2fb2c1afdbc --- /dev/null +++ b/contracts/offline-signers/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --bin schema --features=schema-gen" \ No newline at end of file diff --git a/contracts/offline-signers/Cargo.toml b/contracts/offline-signers/Cargo.toml new file mode 100644 index 00000000000..c3fec9b551f --- /dev/null +++ b/contracts/offline-signers/Cargo.toml @@ -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 diff --git a/contracts/offline-signers/Makefile b/contracts/offline-signers/Makefile new file mode 100644 index 00000000000..8ca651ccec8 --- /dev/null +++ b/contracts/offline-signers/Makefile @@ -0,0 +1,5 @@ +wasm: + RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown + +generate-schema: + cargo schema \ No newline at end of file diff --git a/contracts/offline-signers/schema/nym-offline-signers-contract.json b/contracts/offline-signers/schema/nym-offline-signers-contract.json new file mode 100644 index 00000000000..47db9b5b292 --- /dev/null +++ b/contracts/offline-signers/schema/nym-offline-signers-contract.json @@ -0,0 +1,1621 @@ +{ + "contract_name": "nym-offline-signers-contract", + "contract_version": "0.1.0", + "idl_version": "1.0.0", + "instantiate": { + "$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" + } + } + }, + "execute": { + "$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 + } + ] + }, + "query": { + "$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 + } + ] + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false + }, + "sudo": null, + "responses": { + "admin": { + "$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 + }, + "current_signing_status": { + "$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 + }, + "get_active_proposal": { + "$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" + } + } + }, + "get_active_proposals_paged": { + "$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" + } + } + }, + "get_config": { + "$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" + } + } + }, + "get_last_status_reset": { + "$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" + } + } + }, + "get_last_status_reset_paged": { + "$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" + } + } + }, + "get_offline_signer_information": { + "$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" + } + } + }, + "get_offline_signers_addresses_at_height": { + "$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" + } + } + }, + "get_offline_signers_paged": { + "$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" + } + } + }, + "get_proposal": { + "$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" + } + } + }, + "get_proposals_paged": { + "$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" + } + } + }, + "get_vote_information": { + "$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 + } + } + }, + "get_votes_paged": { + "$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 + } + } + }, + "signing_status_at_height": { + "$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 + } + } +} diff --git a/contracts/offline-signers/schema/raw/execute.json b/contracts/offline-signers/schema/raw/execute.json new file mode 100644 index 00000000000..120ca579b4a --- /dev/null +++ b/contracts/offline-signers/schema/raw/execute.json @@ -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 + } + ] +} diff --git a/contracts/offline-signers/schema/raw/instantiate.json b/contracts/offline-signers/schema/raw/instantiate.json new file mode 100644 index 00000000000..2798cce99b8 --- /dev/null +++ b/contracts/offline-signers/schema/raw/instantiate.json @@ -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" + } + } +} diff --git a/contracts/offline-signers/schema/raw/migrate.json b/contracts/offline-signers/schema/raw/migrate.json new file mode 100644 index 00000000000..7fbe8c5708e --- /dev/null +++ b/contracts/offline-signers/schema/raw/migrate.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false +} diff --git a/contracts/offline-signers/schema/raw/query.json b/contracts/offline-signers/schema/raw/query.json new file mode 100644 index 00000000000..888ea909c0a --- /dev/null +++ b/contracts/offline-signers/schema/raw/query.json @@ -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 + } + ] +} diff --git a/contracts/offline-signers/schema/raw/response_to_admin.json b/contracts/offline-signers/schema/raw/response_to_admin.json new file mode 100644 index 00000000000..c73969ab04b --- /dev/null +++ b/contracts/offline-signers/schema/raw/response_to_admin.json @@ -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 +} diff --git a/contracts/offline-signers/schema/raw/response_to_current_signing_status.json b/contracts/offline-signers/schema/raw/response_to_current_signing_status.json new file mode 100644 index 00000000000..b8459497e1c --- /dev/null +++ b/contracts/offline-signers/schema/raw/response_to_current_signing_status.json @@ -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 +} diff --git a/contracts/offline-signers/schema/raw/response_to_get_active_proposal.json b/contracts/offline-signers/schema/raw/response_to_get_active_proposal.json new file mode 100644 index 00000000000..3171a49366e --- /dev/null +++ b/contracts/offline-signers/schema/raw/response_to_get_active_proposal.json @@ -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" + } + } +} diff --git a/contracts/offline-signers/schema/raw/response_to_get_active_proposals_paged.json b/contracts/offline-signers/schema/raw/response_to_get_active_proposals_paged.json new file mode 100644 index 00000000000..e01b311445c --- /dev/null +++ b/contracts/offline-signers/schema/raw/response_to_get_active_proposals_paged.json @@ -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" + } + } +} diff --git a/contracts/offline-signers/schema/raw/response_to_get_config.json b/contracts/offline-signers/schema/raw/response_to_get_config.json new file mode 100644 index 00000000000..e8f4c9e7c6e --- /dev/null +++ b/contracts/offline-signers/schema/raw/response_to_get_config.json @@ -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" + } + } +} diff --git a/contracts/offline-signers/schema/raw/response_to_get_last_status_reset.json b/contracts/offline-signers/schema/raw/response_to_get_last_status_reset.json new file mode 100644 index 00000000000..c90a01d25e0 --- /dev/null +++ b/contracts/offline-signers/schema/raw/response_to_get_last_status_reset.json @@ -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" + } + } +} diff --git a/contracts/offline-signers/schema/raw/response_to_get_last_status_reset_paged.json b/contracts/offline-signers/schema/raw/response_to_get_last_status_reset_paged.json new file mode 100644 index 00000000000..1181e2dd583 --- /dev/null +++ b/contracts/offline-signers/schema/raw/response_to_get_last_status_reset_paged.json @@ -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" + } + } +} diff --git a/contracts/offline-signers/schema/raw/response_to_get_offline_signer_information.json b/contracts/offline-signers/schema/raw/response_to_get_offline_signer_information.json new file mode 100644 index 00000000000..7d796568eac --- /dev/null +++ b/contracts/offline-signers/schema/raw/response_to_get_offline_signer_information.json @@ -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" + } + } +} diff --git a/contracts/offline-signers/schema/raw/response_to_get_offline_signers_addresses_at_height.json b/contracts/offline-signers/schema/raw/response_to_get_offline_signers_addresses_at_height.json new file mode 100644 index 00000000000..587652a2bb7 --- /dev/null +++ b/contracts/offline-signers/schema/raw/response_to_get_offline_signers_addresses_at_height.json @@ -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" + } + } +} diff --git a/contracts/offline-signers/schema/raw/response_to_get_offline_signers_paged.json b/contracts/offline-signers/schema/raw/response_to_get_offline_signers_paged.json new file mode 100644 index 00000000000..e07f3b8614c --- /dev/null +++ b/contracts/offline-signers/schema/raw/response_to_get_offline_signers_paged.json @@ -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" + } + } +} diff --git a/contracts/offline-signers/schema/raw/response_to_get_proposal.json b/contracts/offline-signers/schema/raw/response_to_get_proposal.json new file mode 100644 index 00000000000..e5707be2dca --- /dev/null +++ b/contracts/offline-signers/schema/raw/response_to_get_proposal.json @@ -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" + } + } +} diff --git a/contracts/offline-signers/schema/raw/response_to_get_proposals_paged.json b/contracts/offline-signers/schema/raw/response_to_get_proposals_paged.json new file mode 100644 index 00000000000..78b8fe0cf89 --- /dev/null +++ b/contracts/offline-signers/schema/raw/response_to_get_proposals_paged.json @@ -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" + } + } +} diff --git a/contracts/offline-signers/schema/raw/response_to_get_vote_information.json b/contracts/offline-signers/schema/raw/response_to_get_vote_information.json new file mode 100644 index 00000000000..a9cc7a2bfb4 --- /dev/null +++ b/contracts/offline-signers/schema/raw/response_to_get_vote_information.json @@ -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 + } + } +} diff --git a/contracts/offline-signers/schema/raw/response_to_get_votes_paged.json b/contracts/offline-signers/schema/raw/response_to_get_votes_paged.json new file mode 100644 index 00000000000..bc892dade79 --- /dev/null +++ b/contracts/offline-signers/schema/raw/response_to_get_votes_paged.json @@ -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 + } + } +} diff --git a/contracts/offline-signers/schema/raw/response_to_signing_status_at_height.json b/contracts/offline-signers/schema/raw/response_to_signing_status_at_height.json new file mode 100644 index 00000000000..7ee232a333e --- /dev/null +++ b/contracts/offline-signers/schema/raw/response_to_signing_status_at_height.json @@ -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 +} diff --git a/contracts/offline-signers/src/bin/schema.rs b/contracts/offline-signers/src/bin/schema.rs new file mode 100644 index 00000000000..6561dd1cac1 --- /dev/null +++ b/contracts/offline-signers/src/bin/schema.rs @@ -0,0 +1,14 @@ +// Copyright 2025 - Nym Technologies SA +// 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, + } +} diff --git a/contracts/offline-signers/src/contract.rs b/contracts/offline-signers/src/contract.rs new file mode 100644 index 00000000000..5d07eff558a --- /dev/null +++ b/contracts/offline-signers/src/contract.rs @@ -0,0 +1,167 @@ +// Copyright 2025 - Nym Technologies SA +// 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 { + 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 { + 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 { + 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 { + 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(()) + } + } +} diff --git a/contracts/offline-signers/src/helpers.rs b/contracts/offline-signers/src/helpers.rs new file mode 100644 index 00000000000..e4dfeba470e --- /dev/null +++ b/contracts/offline-signers/src/helpers.rs @@ -0,0 +1,212 @@ +// Copyright 2025 - Nym Technologies SA +// 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) -> StdResult { + Ok(self.query_dkg_contract_state(dkg_contract)?.group_addr.0) + } + + fn query_dkg_contract_state( + &self, + dkg_contract: impl Into, + ) -> StdResult { + 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) -> StdResult { + 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, + height: u64, + ) -> StdResult { + let res: Option = + 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, + epoch_id: EpochId, + ) -> StdResult> { + 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, + epoch_id: EpochId, + ) -> StdResult { + self.query_contract(dkg_contract, &DkgQueryMsg::GetEpochThreshold { epoch_id }) + } +} + +impl DkgContractQuerier for T where T: ContractQuerier {} + +pub(crate) fn group_members( + querier_wrapper: &QuerierWrapper, + contract: &Cw4Contract, +) -> Result, 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, +) -> Result { + 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::()); + let querier = tester.deps().querier; + + let addresses = group_members(&querier, &group_contract)?; + assert_eq!(addresses.len(), members); + } + + Ok(()) + } +} diff --git a/contracts/offline-signers/src/lib.rs b/contracts/offline-signers/src/lib.rs new file mode 100644 index 00000000000..08a2b9651d8 --- /dev/null +++ b/contracts/offline-signers/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright 2025 - Nym Technologies SA +// 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; diff --git a/contracts/offline-signers/src/queries.rs b/contracts/offline-signers/src/queries.rs new file mode 100644 index 00000000000..0f64584b399 --- /dev/null +++ b/contracts/offline-signers/src/queries.rs @@ -0,0 +1,1065 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::helpers::basic_signing_status; +use crate::storage::{retrieval_limits, NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE}; +use cosmwasm_std::{Deps, Env, Order, StdResult}; +use cw_controllers::AdminResponse; +use cw_storage_plus::Bound; +use nym_offline_signers_contract_common::{ + ActiveProposalResponse, ActiveProposalsPagedResponse, Config, LastStatusResetDetails, + LastStatusResetPagedResponse, LastStatusResetResponse, NymOfflineSignersContractError, + OfflineSignerDetails, OfflineSignerResponse, OfflineSignersAddressesResponse, + OfflineSignersPagedResponse, ProposalId, ProposalResponse, ProposalsPagedResponse, + SigningStatusAtHeightResponse, SigningStatusResponse, VoteDetails, VoteResponse, + VotesPagedResponse, +}; + +pub fn query_admin(deps: Deps) -> Result { + NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE + .contract_admin + .query_admin(deps) + .map_err(Into::into) +} + +pub fn query_config(deps: Deps) -> Result { + NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE + .config + .load(deps.storage) + .map_err(Into::into) +} + +pub fn query_active_proposal( + deps: Deps, + env: Env, + signer: String, +) -> Result { + let signer = deps.api.addr_validate(&signer)?; + + let Some(proposal) = + NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE.try_load_active_proposal(deps.storage, &signer)? + else { + return Ok(ActiveProposalResponse { proposal: None }); + }; + + Ok(ActiveProposalResponse { + proposal: Some( + NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE.add_proposal_resolution(deps, &env, proposal)?, + ), + }) +} + +pub fn query_proposal( + deps: Deps, + env: Env, + proposal_id: ProposalId, +) -> Result { + let Some(proposal) = NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE + .proposals + .may_load(deps.storage, proposal_id)? + else { + return Ok(ProposalResponse { proposal: None }); + }; + + Ok(ProposalResponse { + proposal: Some( + NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE.add_proposal_resolution(deps, &env, proposal)?, + ), + }) +} + +pub fn query_vote_information( + deps: Deps, + voter: String, + proposal_id: ProposalId, +) -> Result { + let voter = deps.api.addr_validate(&voter)?; + + let vote = NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE + .votes + .may_load(deps.storage, (proposal_id, &voter))?; + Ok(VoteResponse { vote }) +} + +pub fn query_offline_signer_information( + deps: Deps, + signer: String, +) -> Result { + let signer = deps.api.addr_validate(&signer)?; + + let information = NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE + .offline_signers + .information + .may_load(deps.storage, &signer)?; + + Ok(OfflineSignerResponse { information }) +} + +pub fn query_offline_signers_addresses_at_height( + deps: Deps, + height: Option, +) -> Result { + let addresses = match height { + Some(height) => NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE + .offline_signers + .addresses + .may_load_at_height(deps.storage, height)? + .unwrap_or_default(), + None => NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE + .offline_signers + .addresses + .load(deps.storage)?, + }; + + Ok(OfflineSignersAddressesResponse { addresses }) +} + +pub fn query_last_status_reset( + deps: Deps, + signer: String, +) -> Result { + let signer = deps.api.addr_validate(&signer)?; + + let information = NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE + .last_status_reset + .may_load(deps.storage, &signer)?; + + Ok(LastStatusResetResponse { information }) +} + +pub fn query_active_proposals_paged( + deps: Deps, + env: Env, + start_after: Option, + limit: Option, +) -> Result { + let limit = limit + .unwrap_or(retrieval_limits::ACTIVE_PROPOSALS_DEFAULT_LIMIT) + .min(retrieval_limits::ACTIVE_PROPOSALS_MAX_LIMIT) as usize; + + let signer = start_after + .map(|signer| deps.api.addr_validate(&signer)) + .transpose()?; + + let start = signer.as_ref().map(Bound::exclusive); + let active_proposals = NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE + .active_proposals + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| { + res.map_err(Into::into) + .and_then(|(_, proposal_id)| { + NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE + .proposals + .load(deps.storage, proposal_id) + .map_err(Into::into) + }) + .and_then(|proposal| { + NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE + .add_proposal_resolution(deps, &env, proposal) + }) + }) + .collect::, _>>()?; + + let start_next_after = active_proposals + .last() + .map(|p| p.proposal.proposed_offline_signer.to_string()); + + Ok(ActiveProposalsPagedResponse { + start_next_after, + active_proposals, + }) +} + +pub fn query_proposals_paged( + deps: Deps, + start_after: Option, + limit: Option, +) -> Result { + let limit = limit + .unwrap_or(retrieval_limits::PROPOSALS_DEFAULT_LIMIT) + .min(retrieval_limits::PROPOSALS_MAX_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let proposals = NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE + .proposals + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|(_, proposal)| proposal)) + .collect::, _>>()?; + + let start_next_after = proposals.last().map(|p| p.id); + + Ok(ProposalsPagedResponse { + start_next_after, + proposals, + }) +} + +pub fn query_votes_paged( + deps: Deps, + proposal_id: ProposalId, + start_after: Option, + limit: Option, +) -> Result { + let limit = limit + .unwrap_or(retrieval_limits::VOTES_DEFAULT_LIMIT) + .min(retrieval_limits::VOTES_MAX_LIMIT) as usize; + + let voter = start_after + .map(|voter| deps.api.addr_validate(&voter)) + .transpose()?; + let start = voter.as_ref().map(Bound::exclusive); + + let votes = NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE + .votes + .prefix(proposal_id) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|(voter, information)| VoteDetails { voter, information })) + .collect::, _>>()?; + + let start_next_after = votes.last().map(|vote| vote.voter.to_string()); + + Ok(VotesPagedResponse { + start_next_after, + votes, + }) +} + +pub fn query_offline_signers_paged( + deps: Deps, + start_after: Option, + limit: Option, +) -> Result { + let limit = limit + .unwrap_or(retrieval_limits::OFFLINE_SIGNERS_DEFAULT_LIMIT) + .min(retrieval_limits::OFFLINE_SIGNERS_MAX_LIMIT) as usize; + + let signer = start_after + .map(|signer| deps.api.addr_validate(&signer)) + .transpose()?; + let start = signer.as_ref().map(Bound::exclusive); + + let offline_signers = NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE + .offline_signers + .information + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| { + res.map(|(signer, information)| OfflineSignerDetails { + information, + signer, + }) + }) + .collect::>>()?; + + let start_next_after = offline_signers + .last() + .map(|details| details.signer.to_string()); + + Ok(OfflineSignersPagedResponse { + start_next_after, + offline_signers, + }) +} + +pub fn query_last_status_reset_paged( + deps: Deps, + start_after: Option, + limit: Option, +) -> Result { + let limit = limit + .unwrap_or(retrieval_limits::LAST_STATUS_RESET_DEFAULT_LIMIT) + .min(retrieval_limits::LAST_STATUS_RESET_MAX_LIMIT) as usize; + + let signer = start_after + .map(|signer| deps.api.addr_validate(&signer)) + .transpose()?; + let start = signer.as_ref().map(Bound::exclusive); + + let status_resets = NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE + .last_status_reset + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| { + res.map(|(signer, information)| LastStatusResetDetails { + information, + signer, + }) + }) + .collect::>>()?; + + let start_next_after = status_resets + .last() + .map(|details| details.signer.to_string()); + + Ok(LastStatusResetPagedResponse { + start_next_after, + status_resets, + }) +} + +pub fn query_current_signing_status( + deps: Deps, +) -> Result { + basic_signing_status(deps, None) +} + +pub fn query_signing_status_at_height( + deps: Deps, + block_height: u64, +) -> Result { + let basic = basic_signing_status(deps, Some(block_height))?; + + Ok(SigningStatusAtHeightResponse { + block_height, + dkg_epoch_id: basic.dkg_epoch_id, + signing_threshold: basic.signing_threshold, + current_registered_dealers: basic.current_registered_dealers, + offline_signers: basic.offline_signers, + threshold_available: basic.threshold_available, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::testing::{ + init_contract_tester, init_custom_contract_tester, OfflineSignersContractTesterExt, + }; + use cosmwasm_std::Decimal; + use nym_contracts_common_testing::{ChainOpts, ContractOpts}; + use nym_offline_signers_contract_common::{ + InstantiateMsg, OfflineSignerInformation, ProposalWithResolution, StatusResetInformation, + VoteInformation, + }; + + #[cfg(test)] + mod admin_query { + use super::*; + use crate::testing::init_contract_tester; + use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, RandExt}; + use nym_offline_signers_contract_common::ExecuteMsg; + + #[test] + fn returns_current_admin() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let initial_admin = test.admin_unchecked(); + + // initial + let res = query_admin(test.deps())?; + assert_eq!(res.admin, Some(initial_admin.to_string())); + + let new_admin = test.generate_account(); + + // sanity check + assert_ne!(initial_admin, new_admin); + + // after update + test.execute_msg( + initial_admin.clone(), + &ExecuteMsg::UpdateAdmin { + admin: new_admin.to_string(), + }, + )?; + + let updated_admin = query_admin(test.deps())?; + assert_eq!(updated_admin.admin, Some(new_admin.to_string())); + + Ok(()) + } + } + + #[test] + #[allow(clippy::panic)] + fn active_proposal_query() -> anyhow::Result<()> { + let mut tester = init_contract_tester(); + + let signer = tester.random_group_member(); + + // invalid address + let res = query_active_proposal(tester.deps(), tester.env(), "bad-address".to_string()); + assert!(res.is_err()); + + // new signer - no active proposals + let res = query_active_proposal(tester.deps(), tester.env(), signer.to_string())?; + assert!(res.proposal.is_none()); + + // initial proposal + let id1 = tester.make_proposal(&signer); + + let Some(proposal) = + query_active_proposal(tester.deps(), tester.env(), signer.to_string())?.proposal + else { + panic!("test failure - no proposal") + }; + assert_eq!(id1, proposal.proposal.id); + assert_eq!(signer.clone(), proposal.proposal.proposed_offline_signer); + + assert!(!proposal.passed); + assert!(!proposal.voting_finished); + + // passed + tester.add_votes(id1); + let Some(proposal) = + query_active_proposal(tester.deps(), tester.env(), signer.to_string())?.proposal + else { + panic!("test failure - no proposal") + }; + assert_eq!(id1, proposal.proposal.id); + assert_eq!(signer.clone(), proposal.proposal.proposed_offline_signer); + + assert!(proposal.passed); + assert!(!proposal.voting_finished); + + // voting passed + tester.advance_day_of_blocks(); + tester.add_votes(id1); + let Some(proposal) = + query_active_proposal(tester.deps(), tester.env(), signer.to_string())?.proposal + else { + panic!("test failure - no proposal") + }; + assert_eq!(id1, proposal.proposal.id); + assert_eq!(signer.clone(), proposal.proposal.proposed_offline_signer); + + assert!(proposal.passed); + assert!(proposal.voting_finished); + + // marked online - no proposals again + tester.reset_offline_status(&signer); + let res = query_active_proposal(tester.deps(), tester.env(), signer.to_string())?; + assert!(res.proposal.is_none()); + tester.advance_day_of_blocks(); + + let id2 = tester.make_proposal(&signer); + let Some(proposal) = + query_active_proposal(tester.deps(), tester.env(), signer.to_string())?.proposal + else { + panic!("test failure - no proposal") + }; + assert_ne!(id1, id2); + assert_eq!(id2, proposal.proposal.id); + assert_eq!(signer.clone(), proposal.proposal.proposed_offline_signer); + + assert!(!proposal.passed); + assert!(!proposal.voting_finished); + + tester.advance_day_of_blocks(); + + let Some(proposal) = + query_active_proposal(tester.deps(), tester.env(), signer.to_string())?.proposal + else { + panic!("test failure - no proposal") + }; + assert_eq!(id2, proposal.proposal.id); + assert_eq!(signer.clone(), proposal.proposal.proposed_offline_signer); + + assert!(!proposal.passed); + assert!(proposal.voting_finished); + + Ok(()) + } + + #[test] + #[allow(clippy::panic)] + fn proposal_query() -> anyhow::Result<()> { + let mut tester = init_contract_tester(); + + let signer = tester.random_group_member(); + + // new signer - no active proposals + let res = query_proposal(tester.deps(), tester.env(), 1)?; + assert!(res.proposal.is_none()); + + // initial proposal + let id1 = tester.make_proposal(&signer); + // sanity check + assert_eq!(id1, 1); + + let Some(proposal) = query_proposal(tester.deps(), tester.env(), 1)?.proposal else { + panic!("test failure - no proposal") + }; + assert_eq!(id1, proposal.proposal.id); + assert_eq!(signer.clone(), proposal.proposal.proposed_offline_signer); + + assert!(!proposal.passed); + assert!(!proposal.voting_finished); + + // passed + tester.add_votes(id1); + let Some(proposal) = query_proposal(tester.deps(), tester.env(), 1)?.proposal else { + panic!("test failure - no proposal") + }; + assert_eq!(id1, proposal.proposal.id); + assert_eq!(signer.clone(), proposal.proposal.proposed_offline_signer); + + assert!(proposal.passed); + assert!(!proposal.voting_finished); + + // voting passed + tester.advance_day_of_blocks(); + tester.add_votes(id1); + let Some(proposal) = query_proposal(tester.deps(), tester.env(), 1)?.proposal else { + panic!("test failure - no proposal") + }; + assert_eq!(id1, proposal.proposal.id); + assert_eq!(signer.clone(), proposal.proposal.proposed_offline_signer); + + assert!(proposal.passed); + assert!(proposal.voting_finished); + + // marked online - proposals still exists! + tester.reset_offline_status(&signer); + let Some(proposal) = query_proposal(tester.deps(), tester.env(), 1)?.proposal else { + panic!("test failure - no proposal") + }; + assert_eq!(id1, proposal.proposal.id); + assert_eq!(signer.clone(), proposal.proposal.proposed_offline_signer); + + assert!(proposal.passed); + assert!(proposal.voting_finished); + + tester.advance_day_of_blocks(); + + let id2 = tester.make_proposal(&signer); + let Some(proposal) = query_proposal(tester.deps(), tester.env(), 2)?.proposal else { + panic!("test failure - no proposal") + }; + assert_eq!(id2, 2); + assert_eq!(id2, proposal.proposal.id); + assert_eq!(signer.clone(), proposal.proposal.proposed_offline_signer); + + assert!(!proposal.passed); + assert!(!proposal.voting_finished); + + tester.advance_day_of_blocks(); + + let Some(proposal) = query_proposal(tester.deps(), tester.env(), 2)?.proposal else { + panic!("test failure - no proposal") + }; + assert_eq!(id2, proposal.proposal.id); + assert_eq!(signer.clone(), proposal.proposal.proposed_offline_signer); + + assert!(!proposal.passed); + assert!(proposal.voting_finished); + + Ok(()) + } + + #[test] + fn vote_information_query() -> anyhow::Result<()> { + let mut tester = init_contract_tester(); + let signer = tester.random_group_member(); + let voter1 = tester.random_group_member(); + let voter2 = tester.random_group_member(); + + let proposal_id = tester.insert_empty_proposal(&signer); + let res = query_vote_information(tester.deps(), "bad-address".to_string(), proposal_id); + assert!(res.is_err()); + + let res1 = query_vote_information(tester.deps(), voter1.to_string(), proposal_id)?; + let res2 = query_vote_information(tester.deps(), voter2.to_string(), proposal_id)?; + + assert!(res1.vote.is_none()); + assert!(res2.vote.is_none()); + + tester.add_vote(proposal_id, &voter1); + let res1 = query_vote_information(tester.deps(), voter1.to_string(), proposal_id)?; + let res2 = query_vote_information(tester.deps(), voter2.to_string(), proposal_id)?; + + assert_eq!( + res1.vote.unwrap(), + VoteInformation { + voted_at: tester.env().block, + } + ); + assert!(res2.vote.is_none()); + + tester.next_block(); + tester.add_vote(proposal_id, &voter2); + let res1 = query_vote_information(tester.deps(), voter1.to_string(), proposal_id)?; + let res2 = query_vote_information(tester.deps(), voter2.to_string(), proposal_id)?; + assert_ne!(res1, res2); + assert_eq!( + res2.vote.unwrap(), + VoteInformation { + voted_at: tester.env().block, + } + ); + + Ok(()) + } + + #[test] + fn offline_signer_information_query() -> anyhow::Result<()> { + let mut tester = init_contract_tester(); + let signer = tester.random_group_member(); + + assert!( + query_offline_signer_information(tester.deps(), "bad-address".to_string()).is_err() + ); + assert!( + query_offline_signer_information(tester.deps(), signer.to_string())? + .information + .is_none() + ); + + tester.insert_offline_signer(&signer); + let res = query_offline_signer_information(tester.deps(), signer.to_string())? + .information + .unwrap(); + assert_eq!( + res, + OfflineSignerInformation { + marked_offline_at: tester.env().block, + associated_proposal: 1, + } + ); + + Ok(()) + } + + #[cfg(test)] + mod offline_signers_at_height { + use super::*; + + #[test] + fn current_height() -> anyhow::Result<()> { + let mut tester = init_contract_tester(); + let signer1 = tester.random_group_member(); + let signer2 = tester.random_group_member(); + let signer3 = tester.random_group_member(); + + assert!( + query_offline_signers_addresses_at_height(tester.deps(), None)? + .addresses + .is_empty() + ); + + tester.insert_offline_signer(&signer1); + assert_eq!( + query_offline_signers_addresses_at_height(tester.deps(), None)?.addresses, + vec![signer1.clone()] + ); + tester.insert_offline_signer(&signer2); + assert_eq!( + query_offline_signers_addresses_at_height(tester.deps(), None)?.addresses, + vec![signer1.clone(), signer2.clone()] + ); + tester.insert_offline_signer(&signer3); + assert_eq!( + query_offline_signers_addresses_at_height(tester.deps(), None)?.addresses, + vec![signer1.clone(), signer2.clone(), signer3.clone()] + ); + + tester.advance_day_of_blocks(); + tester.reset_offline_status(&signer2); + assert_eq!( + query_offline_signers_addresses_at_height(tester.deps(), None)?.addresses, + vec![signer1.clone(), signer3] + ); + Ok(()) + } + + #[test] + fn specific_height() -> anyhow::Result<()> { + let mut tester = init_contract_tester(); + let signer1 = tester.random_group_member(); + let signer2 = tester.random_group_member(); + let signer3 = tester.random_group_member(); + + let h1 = tester.env().block.height; + assert!( + query_offline_signers_addresses_at_height(tester.deps(), None)? + .addresses + .is_empty() + ); + + tester.next_block(); + tester.insert_offline_signer(&signer1); + let h2 = tester.env().block.height; + + tester.next_block(); + tester.insert_offline_signer(&signer2); + let h3 = tester.env().block.height; + + tester.next_block(); + tester.insert_offline_signer(&signer3); + let h4 = tester.env().block.height; + + tester.advance_day_of_blocks(); + tester.reset_offline_status(&signer2); + let h5 = tester.env().block.height; + + assert!( + query_offline_signers_addresses_at_height(tester.deps(), Some(h1 + 1))? + .addresses + .is_empty() + ); + assert_eq!( + query_offline_signers_addresses_at_height(tester.deps(), Some(h2 + 1))?.addresses, + vec![signer1.clone()] + ); + assert_eq!( + query_offline_signers_addresses_at_height(tester.deps(), Some(h3 + 1))?.addresses, + vec![signer1.clone(), signer2.clone()] + ); + assert_eq!( + query_offline_signers_addresses_at_height(tester.deps(), Some(h4 + 1))?.addresses, + vec![signer1.clone(), signer2.clone(), signer3.clone()] + ); + assert_eq!( + query_offline_signers_addresses_at_height(tester.deps(), Some(h5 + 1))?.addresses, + vec![signer1.clone(), signer3] + ); + Ok(()) + } + } + + #[test] + fn last_status_reset_query() -> anyhow::Result<()> { + let mut tester = init_contract_tester(); + let signer = tester.random_group_member(); + + assert!(query_last_status_reset(tester.deps(), "bad-address".to_string()).is_err()); + assert!(query_last_status_reset(tester.deps(), signer.to_string())? + .information + .is_none()); + + tester.insert_offline_signer(&signer); + assert!(query_last_status_reset(tester.deps(), signer.to_string())? + .information + .is_none()); + tester.advance_day_of_blocks(); + tester.reset_offline_status(&signer); + + let res1 = query_last_status_reset(tester.deps(), signer.to_string())? + .information + .unwrap(); + assert_eq!( + res1, + StatusResetInformation { + status_reset_at: tester.env().block, + } + ); + + tester.advance_day_of_blocks(); + tester.insert_offline_signer(&signer); + let res2 = query_last_status_reset(tester.deps(), signer.to_string())? + .information + .unwrap(); + assert_eq!(res1, res2); + tester.advance_day_of_blocks(); + tester.reset_offline_status(&signer); + + let res3 = query_last_status_reset(tester.deps(), signer.to_string())? + .information + .unwrap(); + assert_eq!( + res3, + StatusResetInformation { + status_reset_at: tester.env().block, + } + ); + + Ok(()) + } + + #[test] + fn active_proposals_paged_query() -> anyhow::Result<()> { + let mut tester = init_custom_contract_tester( + 10, + InstantiateMsg { + dkg_contract_address: "".to_string(), + config: Config { + required_quorum: Decimal::percent(20), + ..Default::default() + }, + }, + ); + + let signer1 = tester.random_group_member(); + let signer2 = tester.random_group_member(); + let signer3 = tester.random_group_member(); + let signer4 = tester.random_group_member(); + + // expired + let id1 = tester.insert_empty_proposal(&signer1); + tester.advance_day_of_blocks(); + + // passed + let id2 = tester.insert_empty_proposal(&signer2); + + // not passed + let id3 = tester.insert_empty_proposal(&signer3); + + // not voted on + let id4 = tester.insert_empty_proposal(&signer4); + + let mut signers_with_proposals = [ + (signer1, id1), + (signer2, id2), + (signer3, id3), + (signer4, id4), + ]; + signers_with_proposals.sort_by_key(|a| a.0.clone()); + + let voter1 = tester.random_group_member(); + let voter2 = tester.random_group_member(); + + tester.add_vote(id2, &voter1); + tester.add_vote(id2, &voter2); + + tester.add_vote(id3, &voter1); + + let active = query_active_proposals_paged(tester.deps(), tester.env(), None, None)?; + + let prop1 = tester.load_proposal(signers_with_proposals[0].1).unwrap(); + let prop2 = tester.load_proposal(signers_with_proposals[1].1).unwrap(); + let prop3 = tester.load_proposal(signers_with_proposals[2].1).unwrap(); + let prop4 = tester.load_proposal(signers_with_proposals[3].1).unwrap(); + + assert_eq!( + active.active_proposals, + vec![ + ProposalWithResolution { + proposal: prop1, + passed: false, + voting_finished: true, + }, + ProposalWithResolution { + proposal: prop2, + passed: false, + voting_finished: false, + }, + ProposalWithResolution { + proposal: prop3, + passed: false, + voting_finished: false, + }, + ProposalWithResolution { + proposal: prop4, + passed: true, + voting_finished: false, + } + ] + ); + + let res = query_active_proposals_paged(tester.deps(), tester.env(), None, Some(0))?; + assert!(res.active_proposals.is_empty()); + + let res = query_active_proposals_paged( + tester.deps(), + tester.env(), + Some(signers_with_proposals[3].0.to_string()), + None, + )?; + assert!(res.active_proposals.is_empty()); + + Ok(()) + } + + #[test] + fn proposals_paged_query() -> anyhow::Result<()> { + let mut tester = init_contract_tester(); + + let signer1 = tester.random_group_member(); + let signer2 = tester.random_group_member(); + let signer3 = tester.random_group_member(); + let signer4 = tester.random_group_member(); + + let id1 = tester.insert_empty_proposal(&signer1); + let id2 = tester.insert_empty_proposal(&signer2); + let id3 = tester.insert_empty_proposal(&signer3); + let id4 = tester.insert_empty_proposal(&signer4); + + let active = query_proposals_paged(tester.deps(), None, None)?; + + let prop1 = tester.load_proposal(id1).unwrap(); + let prop2 = tester.load_proposal(id2).unwrap(); + let prop3 = tester.load_proposal(id3).unwrap(); + let prop4 = tester.load_proposal(id4).unwrap(); + + assert_eq!(active.proposals, vec![prop1, prop2, prop3, prop4,]); + + let res = query_proposals_paged(tester.deps(), None, Some(0))?; + assert!(res.proposals.is_empty()); + + let res = query_proposals_paged(tester.deps(), Some(id4), None)?; + assert!(res.proposals.is_empty()); + + Ok(()) + } + + #[test] + fn votes_paged_query() -> anyhow::Result<()> { + let mut tester = init_contract_tester(); + + let signer1 = tester.random_group_member(); + let signer2 = tester.random_group_member(); + let signer3 = tester.random_group_member(); + + let id1 = tester.insert_empty_proposal(&signer1); + let id2 = tester.insert_empty_proposal(&signer2); + let id3 = tester.insert_empty_proposal(&signer3); + + let voter1 = tester.random_group_member(); + let voter2 = tester.random_group_member(); + + tester.add_vote(id2, &voter1); + tester.add_vote(id2, &voter2); + + tester.add_vote(id3, &voter1); + + let votes1 = query_votes_paged(tester.deps(), id1, None, None)?; + let votes2 = query_votes_paged(tester.deps(), id2, None, None)?; + let votes3 = query_votes_paged(tester.deps(), id3, None, None)?; + + assert!(votes1.votes.is_empty()); + assert_eq!( + votes2.votes, + vec![ + VoteDetails { + voter: voter1.clone(), + information: VoteInformation { + voted_at: tester.env().block, + }, + }, + VoteDetails { + voter: voter2.clone(), + information: VoteInformation { + voted_at: tester.env().block, + }, + } + ] + ); + + assert_eq!( + votes3.votes, + vec![VoteDetails { + voter: voter1.clone(), + information: VoteInformation { + voted_at: tester.env().block + }, + }] + ); + + let res = query_votes_paged(tester.deps(), 2, None, Some(0))?; + assert!(res.votes.is_empty()); + + let res = query_votes_paged(tester.deps(), id3, Some(voter1.to_string()), None)?; + assert!(res.votes.is_empty()); + + Ok(()) + } + + #[test] + fn offline_signers_paged_query() -> anyhow::Result<()> { + let mut tester = init_contract_tester(); + + let mut signers = [ + tester.random_group_member(), + tester.random_group_member(), + tester.random_group_member(), + ]; + signers.sort_unstable(); + + tester.insert_offline_signer(&signers[0]); + tester.insert_offline_signer(&signers[1]); + tester.insert_offline_signer(&signers[2]); + + let res = query_offline_signers_paged(tester.deps(), None, None)?; + assert_eq!( + res.offline_signers, + vec![ + OfflineSignerDetails { + information: OfflineSignerInformation { + marked_offline_at: tester.env().block, + associated_proposal: 1 + }, + signer: signers[0].clone() + }, + OfflineSignerDetails { + information: OfflineSignerInformation { + marked_offline_at: tester.env().block, + associated_proposal: 2 + }, + signer: signers[1].clone() + }, + OfflineSignerDetails { + information: OfflineSignerInformation { + marked_offline_at: tester.env().block, + associated_proposal: 3 + }, + signer: signers[2].clone() + } + ] + ); + + let res = query_offline_signers_paged(tester.deps(), None, Some(0))?; + assert!(res.offline_signers.is_empty()); + + let res = query_offline_signers_paged(tester.deps(), Some(signers[2].to_string()), None)?; + assert!(res.offline_signers.is_empty()); + + Ok(()) + } + + #[test] + fn last_status_reset_paged_query() -> anyhow::Result<()> { + let mut tester = init_contract_tester(); + + let mut signers = [ + tester.random_group_member(), + tester.random_group_member(), + tester.random_group_member(), + ]; + signers.sort_unstable(); + + tester.insert_offline_signer(&signers[0]); + tester.insert_offline_signer(&signers[1]); + tester.insert_offline_signer(&signers[2]); + + tester.advance_day_of_blocks(); + tester.reset_offline_status(&signers[0]); + tester.reset_offline_status(&signers[1]); + tester.reset_offline_status(&signers[2]); + + let res = query_last_status_reset_paged(tester.deps(), None, None)?; + assert_eq!( + res.status_resets, + vec![ + LastStatusResetDetails { + information: StatusResetInformation { + status_reset_at: tester.env().block + }, + signer: signers[0].clone() + }, + LastStatusResetDetails { + information: StatusResetInformation { + status_reset_at: tester.env().block + }, + signer: signers[1].clone() + }, + LastStatusResetDetails { + information: StatusResetInformation { + status_reset_at: tester.env().block + }, + signer: signers[2].clone() + } + ] + ); + + let res = query_last_status_reset_paged(tester.deps(), None, Some(0))?; + assert!(res.status_resets.is_empty()); + + let res = query_last_status_reset_paged(tester.deps(), Some(signers[2].to_string()), None)?; + assert!(res.status_resets.is_empty()); + + Ok(()) + } +} diff --git a/contracts/offline-signers/src/queued_migrations.rs b/contracts/offline-signers/src/queued_migrations.rs new file mode 100644 index 00000000000..7e1e3cacd64 --- /dev/null +++ b/contracts/offline-signers/src/queued_migrations.rs @@ -0,0 +1,2 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 diff --git a/contracts/offline-signers/src/storage.rs b/contracts/offline-signers/src/storage.rs new file mode 100644 index 00000000000..47a34765373 --- /dev/null +++ b/contracts/offline-signers/src/storage.rs @@ -0,0 +1,1608 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::helpers::{group_members, DkgContractQuerier}; +use cosmwasm_std::{Addr, Decimal, Deps, DepsMut, Env, Order, StdResult, Storage}; +use cw4::Cw4Contract; +use cw_controllers::Admin; +use cw_storage_plus::{Item, Map, SnapshotItem, Strategy}; +use nym_offline_signers_contract_common::constants::storage_keys; +use nym_offline_signers_contract_common::constants::storage_keys::PROPOSAL_COUNT; +use nym_offline_signers_contract_common::{ + Config, NymOfflineSignersContractError, OfflineSignerInformation, Proposal, ProposalId, + ProposalWithResolution, StatusResetInformation, VoteInformation, +}; + +pub const NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE: NymOfflineSignersStorage = + NymOfflineSignersStorage::new(); + +pub struct NymOfflineSignersStorage { + // address of the contract admin + pub(crate) contract_admin: Admin, + + // address of the associated DKG contract + pub(crate) dkg_contract: Item, + + // configurable (by the admin) values of this contract + pub(crate) config: Item, + + // map between given signer and a currently active (if applicable) proposal id + // note: one signer can have only a single active proposal against them at a given time + pub(crate) active_proposals: Map<&'static Addr, ProposalId>, + + // all proposals ever created - realistically we'll ever see a handful of them, + // so leaving them is fine + pub(crate) proposals: Map, + + // votes information (proposal, voter) => vote + pub(crate) votes: Map<(ProposalId, &'static Addr), VoteInformation>, + + // details on signers marked as offline + pub(crate) offline_signers: OfflineSignersStorage, + + // holds information on when signers last reset their status after going back online + // (for full history you'd have to scrape the chain data; the system doesn't need it so it doesn't hold it) + pub(crate) last_status_reset: Map<&'static Addr, StatusResetInformation>, + + // keep track of the current proposal id counter + pub(crate) proposal_count: Item, +} + +impl NymOfflineSignersStorage { + #[allow(clippy::new_without_default)] + pub(crate) const fn new() -> Self { + NymOfflineSignersStorage { + contract_admin: Admin::new(storage_keys::CONTRACT_ADMIN), + dkg_contract: Item::new(storage_keys::DKG_CONTRACT), + config: Item::new(storage_keys::CONFIG), + active_proposals: Map::new(storage_keys::ACTIVE_PROPOSALS), + proposals: Map::new(storage_keys::PROPOSALS), + votes: Map::new(storage_keys::VOTES), + offline_signers: OfflineSignersStorage::new(), + last_status_reset: Map::new(storage_keys::LAST_STATUS_RESET), + proposal_count: Item::new(PROPOSAL_COUNT), + } + } + + fn next_proposal_id(&self, storage: &mut dyn Storage) -> StdResult { + let id: ProposalId = self.proposal_count.may_load(storage)?.unwrap_or_default() + 1; + self.proposal_count.save(storage, &id)?; + Ok(id) + } + + pub(crate) fn insert_new_active_proposal( + &self, + storage: &mut dyn Storage, + env: &Env, + proposer: &Addr, + proposed_offline_signer: &Addr, + ) -> Result { + let id = self.next_proposal_id(storage)?; + self.proposals.save( + storage, + id, + &Proposal { + created_at: env.block.clone(), + id, + proposed_offline_signer: proposed_offline_signer.clone(), + proposer: proposer.clone(), + }, + )?; + self.active_proposals + .save(storage, proposed_offline_signer, &id)?; + Ok(id) + } + + #[cfg(test)] + fn ensure_is_admin( + &self, + deps: Deps, + addr: &Addr, + ) -> Result<(), NymOfflineSignersContractError> { + self.contract_admin + .assert_admin(deps, addr) + .map_err(Into::into) + } + + pub fn initialise( + &self, + mut deps: DepsMut, + env: Env, + admin: Addr, + dkg_contract_address: Addr, + config: Config, + ) -> Result<(), NymOfflineSignersContractError> { + // set the dkg contract address + self.dkg_contract + .save(deps.storage, &dkg_contract_address)?; + + // check quorum and set config values + if config.required_quorum > Decimal::one() { + return Err(NymOfflineSignersContractError::RequiredQuorumBiggerThanOne); + } + self.config.save(deps.storage, &config)?; + + // set the contract admin + self.contract_admin + .set(deps.branch(), Some(admin.clone()))?; + + // finally initialise the inner offline signers storage wrapper + self.offline_signers.initialise(deps, env) + } + + pub(crate) fn try_load_active_proposal( + &self, + storage: &dyn Storage, + signer: &Addr, + ) -> Result, NymOfflineSignersContractError> { + let Some(active_proposal_id) = self.active_proposals.may_load(storage, signer)? else { + return Ok(None); + }; + self.proposals + .may_load(storage, active_proposal_id) + .map_err(Into::into) + } + + fn recently_marked_online( + &self, + storage: &dyn Storage, + env: &Env, + signer: &Addr, + ) -> Result { + let Some(last_status_reset) = self.last_status_reset.may_load(storage, signer)? else { + return Ok(false); + }; + + let config = self.config.load(storage)?; + Ok( + last_status_reset + .recently_marked_online(&env.block, config.status_change_cooldown_secs), + ) + } + + fn recently_marked_offline( + &self, + storage: &dyn Storage, + env: &Env, + signer: &Addr, + ) -> Result { + let Some(signer_info) = self + .offline_signers + .load_signer_information(storage, signer)? + else { + return Ok(false); + }; + + let config = self.config.load(storage)?; + Ok(signer_info.recently_marked_offline(&env.block, config.status_change_cooldown_secs)) + } + + pub(crate) fn proposal_expired( + &self, + storage: &dyn Storage, + env: &Env, + proposal: &Proposal, + ) -> Result { + let config = self.config.load(storage)?; + Ok(proposal.expired(&env.block, config.maximum_proposal_lifetime_secs)) + } + + fn try_vote( + &self, + storage: &mut dyn Storage, + env: &Env, + proposer: &Addr, + signer: &Addr, + ) -> Result { + // 1. retrieve existing proposal or make a new one + let active_proposal_id = match self.try_load_active_proposal(storage, signer)? { + Some(existing) => { + // 1.1. check if proposal has already expired + if self.proposal_expired(storage, env, &existing)? { + // 1.2.1. remake the proposal + self.insert_new_active_proposal(storage, env, proposer, signer)? + } else { + // 1.2.2. use the existing proposal + existing.id + } + } + None => self.insert_new_active_proposal(storage, env, proposer, signer)?, + }; + + let vote = (active_proposal_id, proposer); + + // 2. check if this vote already exists + // (technically we could ignore this, but it shouldn't occur anyway) + if self.votes.may_load(storage, vote)?.is_some() { + return Err(NymOfflineSignersContractError::AlreadyVoted { + voter: proposer.clone(), + proposal: active_proposal_id, + target: signer.clone(), + }); + } + + // 3. save the vote + self.votes + .save(storage, vote, &VoteInformation::new(&env.block))?; + + Ok(active_proposal_id) + } + + fn total_votes(&self, storage: &dyn Storage, proposal_id: ProposalId) -> u32 { + self.votes + .prefix(proposal_id) + .range_raw(storage, None, None, Order::Ascending) + .count() as u32 + } + + pub(crate) fn add_proposal_resolution( + &self, + deps: Deps, + env: &Env, + proposal: Proposal, + ) -> Result { + Ok(ProposalWithResolution { + passed: NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE.proposal_passed( + deps, + proposal.id, + None, + )?, + voting_finished: NYM_OFFLINE_SIGNERS_CONTRACT_STORAGE.proposal_expired( + deps.storage, + env, + &proposal, + )?, + proposal, + }) + } + + pub(crate) fn proposal_passed( + &self, + deps: Deps, + proposal_id: ProposalId, + group_contract: Option, + ) -> Result { + let group_contract = match group_contract { + Some(group_contract) => group_contract, + None => { + let dkg_contract_address = self.dkg_contract.load(deps.storage)?; + Cw4Contract::new( + deps.querier + .query_dkg_cw4_contract_address(dkg_contract_address)?, + ) + } + }; + // obtain the total number of group members (i.e. eligible voters) + let eligible_voters = group_members(&deps.querier, &group_contract)?.len() as u32; + + let config = self.config.load(deps.storage)?; + let required_quorum = config.required_quorum; + + // get the vote count and determine the ratio + let votes = self.total_votes(deps.storage, proposal_id); + let vote_ratio = Decimal::from_ratio(votes, eligible_voters); + + // check if we passed quorum + if vote_ratio >= required_quorum { + return Ok(true); + } + + Ok(false) + } + + fn finalize_vote( + &self, + deps: DepsMut, + env: &Env, + proposal_id: ProposalId, + marked_signer: &Addr, + group_contract: Cw4Contract, + ) -> Result { + // check if the signer hasn't already been marked as offline and this is just an additional vote + if self + .offline_signers + .has_signer_information(deps.storage, marked_signer) + { + return Ok(true); + } + + // check if we passed quorum + if self.proposal_passed(deps.as_ref(), proposal_id, Some(group_contract))? { + self.offline_signers.insert_offline_signer_information( + deps.storage, + env, + marked_signer, + &OfflineSignerInformation { + marked_offline_at: env.block.clone(), + associated_proposal: proposal_id, + }, + )?; + return Ok(true); + } + + Ok(false) + } + + pub fn propose_or_vote( + &self, + deps: DepsMut, + env: Env, + proposer: Addr, + signer: Addr, + ) -> Result { + let dkg_contract_address = self.dkg_contract.load(deps.storage)?; + let group_contract = Cw4Contract::new( + deps.querier + .query_dkg_cw4_contract_address(dkg_contract_address)?, + ); + + // 1. check if the proposer is a valid DKG CW4 group member + if group_contract + .is_voting_member(&deps.querier, &proposer, None)? + .is_none() + { + return Err(NymOfflineSignersContractError::NotGroupMember { + address: proposer.clone(), + }); + } + + // 2. check if the proposed signer is a valid DKG CW4 group member + if group_contract + .is_voting_member(&deps.querier, &signer, None)? + .is_none() + { + return Err(NymOfflineSignersContractError::NotGroupMember { + address: signer.clone(), + }); + } + + // 3. check if the signer hasn't recently been marked as online + // (to prevent constant switching between online and offline) + if self.recently_marked_online(deps.storage, &env, &signer)? { + return Err(NymOfflineSignersContractError::RecentlyCameOnline { + address: signer.clone(), + }); + } + + // 4. try to apply the vote + let proposal_id = self.try_vote(deps.storage, &env, &proposer, &signer)?; + + // 5. check if quorum is reached + let reached_quorum = + self.finalize_vote(deps, &env, proposal_id, &signer, group_contract)?; + + Ok(reached_quorum) + } + + pub fn reset_offline_status( + &self, + deps: DepsMut, + env: Env, + sender: Addr, + ) -> Result<(), NymOfflineSignersContractError> { + // 1. check if this sender hasn't been marked offline recently + // (to prevent constant switching between online and offline) + if self.recently_marked_offline(deps.storage, &env, &sender)? { + return Err(NymOfflineSignersContractError::RecentlyCameOffline { address: sender }); + } + + // 2. an offline signer (or a singer in the process of being marked as offline) must have an active + // proposal going against them, if it doesn't exist, return an error + if !self.active_proposals.has(deps.storage, &sender) { + return Err(NymOfflineSignersContractError::NotOffline { address: sender }); + } + + // 3. reset proposal and offline status + self.active_proposals.remove(deps.storage, &sender); + self.offline_signers + .remove_offline_signer_information(deps.storage, &env, &sender)?; + + // 4. update online metadata + self.last_status_reset.save( + deps.storage, + &sender, + &StatusResetInformation { + status_reset_at: env.block.clone(), + }, + )?; + + Ok(()) + } +} + +pub struct OfflineSignersStorage { + // map of all signers marked as offline + pub(crate) information: Map<&'static Addr, OfflineSignerInformation>, + + // list of addresses of signers currently marked as offline + // we need a separate entry to be able to retrieve list of signers marked at particular height. + // given that we won't ever have more than ~20 entries, loading and resaving the whole vec is not a problem + pub(crate) addresses: SnapshotItem>, +} + +impl OfflineSignersStorage { + #[allow(clippy::new_without_default)] + pub(crate) const fn new() -> Self { + OfflineSignersStorage { + information: Map::new(storage_keys::OFFLINE_SIGNERS_INFORMATION), + addresses: SnapshotItem::new( + storage_keys::OFFLINE_SIGNERS, + storage_keys::OFFLINE_SIGNERS_CHECKPOINTS, + storage_keys::OFFLINE_SIGNERS_CHANGELOG, + Strategy::EveryBlock, + ), + } + } + + fn initialise(&self, deps: DepsMut, env: Env) -> Result<(), NymOfflineSignersContractError> { + self.addresses + .save(deps.storage, &Vec::new(), env.block.height)?; + Ok(()) + } + + pub(crate) fn load_signer_information( + &self, + storage: &dyn Storage, + signer: &Addr, + ) -> Result, NymOfflineSignersContractError> { + self.information + .may_load(storage, signer) + .map_err(Into::into) + } + + pub(crate) fn has_signer_information(&self, storage: &dyn Storage, signer: &Addr) -> bool { + self.information.has(storage, signer) + } + + pub(crate) fn insert_offline_signer_information( + &self, + storage: &mut dyn Storage, + env: &Env, + signer: &Addr, + info: &OfflineSignerInformation, + ) -> Result<(), NymOfflineSignersContractError> { + // insert details into the map + self.information.save(storage, signer, info)?; + + // update the snapshot + let mut all_signers = self.addresses.load(storage)?; + all_signers.push(signer.clone()); + self.addresses + .save(storage, &all_signers, env.block.height)?; + Ok(()) + } + + pub(crate) fn remove_offline_signer_information( + &self, + storage: &mut dyn Storage, + env: &Env, + signer: &Addr, + ) -> Result<(), NymOfflineSignersContractError> { + // remove details from the map + self.information.remove(storage, signer); + + // update the snapshot + let mut all_signers = self.addresses.load(storage)?; + if let Some(pos) = all_signers.iter().position(|x| x == signer) { + all_signers.remove(pos); + } + self.addresses + .save(storage, &all_signers, env.block.height)?; + Ok(()) + } +} + +pub mod retrieval_limits { + pub const ACTIVE_PROPOSALS_DEFAULT_LIMIT: u32 = 25; + pub const ACTIVE_PROPOSALS_MAX_LIMIT: u32 = 50; + + pub const PROPOSALS_DEFAULT_LIMIT: u32 = 50; + pub const PROPOSALS_MAX_LIMIT: u32 = 100; + + pub const VOTES_DEFAULT_LIMIT: u32 = 50; + pub const VOTES_MAX_LIMIT: u32 = 100; + + pub const OFFLINE_SIGNERS_DEFAULT_LIMIT: u32 = 50; + pub const OFFLINE_SIGNERS_MAX_LIMIT: u32 = 100; + + pub const LAST_STATUS_RESET_DEFAULT_LIMIT: u32 = 50; + pub const LAST_STATUS_RESET_MAX_LIMIT: u32 = 100; +} + +#[cfg(test)] +mod tests { + use super::*; + use nym_offline_signers_contract_common::InstantiateMsg; + + fn init_with_quorum(required_quorum: Decimal) -> InstantiateMsg { + InstantiateMsg { + dkg_contract_address: "".to_string(), + config: Config { + required_quorum, + ..Default::default() + }, + } + } + + #[cfg(test)] + mod offline_signers_contract_storage { + use super::*; + use crate::testing::{ + init_contract_tester, init_custom_contract_tester, OfflineSignersContractTesterExt, + }; + use cosmwasm_std::testing::mock_env; + use nym_contracts_common_testing::{ChainOpts, ContractOpts}; + + #[cfg(test)] + mod initialisation { + use super::*; + use cosmwasm_std::testing::mock_env; + use nym_contracts_common_testing::mock_dependencies; + + #[test] + fn sets_contract_admin() -> anyhow::Result<()> { + let storage = NymOfflineSignersStorage::new(); + let mut deps = mock_dependencies(); + let env = mock_env(); + let admin1 = deps.api.addr_make("first-admin"); + let admin2 = deps.api.addr_make("second-admin"); + let dummy_dkg_contract = deps.api.addr_make("dkg-contract"); + let config = Config::default(); + + storage.initialise( + deps.as_mut(), + env.clone(), + admin1.clone(), + dummy_dkg_contract.clone(), + config, + )?; + assert!(storage.ensure_is_admin(deps.as_ref(), &admin1).is_ok()); + + let mut deps = mock_dependencies(); + storage.initialise( + deps.as_mut(), + env, + admin2.clone(), + dummy_dkg_contract, + config, + )?; + assert!(storage.ensure_is_admin(deps.as_ref(), &admin2).is_ok()); + + Ok(()) + } + + #[test] + fn sets_dkg_contract_address() -> anyhow::Result<()> { + let storage = NymOfflineSignersStorage::new(); + let mut deps = mock_dependencies(); + let env = mock_env(); + let admin = deps.api.addr_make("admin"); + let dummy_dkg_contract1 = deps.api.addr_make("dkg-contract1"); + let dummy_dkg_contract2 = deps.api.addr_make("dkg-contract2"); + let config = Config::default(); + + storage.initialise( + deps.as_mut(), + env.clone(), + admin.clone(), + dummy_dkg_contract1.clone(), + config, + )?; + assert_eq!( + storage.dkg_contract.load(deps.as_ref().storage)?, + dummy_dkg_contract1 + ); + + let mut deps = mock_dependencies(); + storage.initialise( + deps.as_mut(), + env, + admin, + dummy_dkg_contract2.clone(), + config, + )?; + assert_eq!( + storage.dkg_contract.load(deps.as_ref().storage)?, + dummy_dkg_contract2 + ); + Ok(()) + } + + #[test] + fn forbids_invalid_quorum_value() -> anyhow::Result<()> { + let storage = NymOfflineSignersStorage::new(); + let mut deps = mock_dependencies(); + let env = mock_env(); + let admin = deps.api.addr_make("admin"); + let dummy_dkg_contract = deps.api.addr_make("dkg-contract"); + let bad_config1 = Config { + required_quorum: Decimal::percent(666666), + ..Default::default() + }; + + let bad_config2 = Config { + required_quorum: Decimal::percent(101), + ..Default::default() + }; + + let borderline_good_config = Config { + required_quorum: Decimal::percent(100), + ..Default::default() + }; + + let good_config = Config { + required_quorum: Decimal::percent(69), + ..Default::default() + }; + + let res = storage + .initialise( + deps.as_mut(), + env.clone(), + admin.clone(), + dummy_dkg_contract.clone(), + bad_config1, + ) + .unwrap_err(); + assert_eq!( + res, + NymOfflineSignersContractError::RequiredQuorumBiggerThanOne + ); + + let res = storage + .initialise( + deps.as_mut(), + env.clone(), + admin.clone(), + dummy_dkg_contract.clone(), + bad_config2, + ) + .unwrap_err(); + assert_eq!( + res, + NymOfflineSignersContractError::RequiredQuorumBiggerThanOne + ); + + let res = storage.initialise( + deps.as_mut(), + env.clone(), + admin.clone(), + dummy_dkg_contract.clone(), + borderline_good_config, + ); + assert!(res.is_ok()); + + let mut deps = mock_dependencies(); + let res = storage.initialise( + deps.as_mut(), + env.clone(), + admin.clone(), + dummy_dkg_contract.clone(), + good_config, + ); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn initialises_internal_offline_signers_storage() -> anyhow::Result<()> { + let storage = NymOfflineSignersStorage::new(); + let mut deps = mock_dependencies(); + let env = mock_env(); + let admin = deps.api.addr_make("admin"); + let dummy_dkg_contract = deps.api.addr_make("dkg-contract"); + + storage.initialise( + deps.as_mut(), + env.clone(), + admin.clone(), + dummy_dkg_contract.clone(), + Config::default(), + )?; + + // this checks that the empty vec has actually been saved (as opposed to value not existing at all) + assert!(OfflineSignersStorage::new() + .addresses + .load(deps.as_ref().storage)? + .is_empty()); + + Ok(()) + } + } + + #[test] + #[allow(clippy::panic)] + fn try_load_active_proposal() -> anyhow::Result<()> { + let storage = NymOfflineSignersStorage::new(); + let mut tester = init_contract_tester(); + + let signer = tester.random_group_member(); + let proposer = tester.random_group_member(); + + assert!(storage + .try_load_active_proposal(&tester, &signer)? + .is_none()); + + let env = mock_env(); + storage.propose_or_vote(tester.deps_mut(), env, proposer.clone(), signer.clone())?; + + // this was the first proposal + let Some(proposal) = storage.try_load_active_proposal(&tester, &signer)? else { + panic!("test failure") + }; + + assert_eq!(proposal.id, 1); + assert_eq!(proposal.proposed_offline_signer, signer); + assert_eq!(proposal.proposer, proposer); + + Ok(()) + } + + #[test] + fn recently_marked_online() -> anyhow::Result<()> { + let storage = NymOfflineSignersStorage::new(); + let mut tester = init_contract_tester(); + + let signer = tester.random_group_member(); + + // not even offline + assert!(!storage.recently_marked_online(&tester, &tester.env(), &signer)?); + + tester.insert_offline_signer(&signer); + + // offline + assert!(!storage.recently_marked_online(&tester, &tester.env(), &signer)?); + + // JUST marked online + tester.advance_day_of_blocks(); + tester.reset_offline_status(&signer); + assert!(storage.recently_marked_online(&tester, &tester.env(), &signer)?); + + // few blocks passed (still below threshold); + tester.next_block(); + tester.next_block(); + tester.next_block(); + tester.next_block(); + assert!(storage.recently_marked_online(&tester, &tester.env(), &signer)?); + + // threshold has passed + tester.advance_day_of_blocks(); + assert!(!storage.recently_marked_online(&tester, &tester.env(), &signer)?); + + // offline again + tester.insert_offline_signer(&signer); + assert!(!storage.recently_marked_online(&tester, &tester.env(), &signer)?); + + // and online again + tester.advance_day_of_blocks(); + tester.reset_offline_status(&signer); + assert!(storage.recently_marked_online(&tester, &tester.env(), &signer)?); + + Ok(()) + } + + #[test] + fn recently_marked_offline() -> anyhow::Result<()> { + let storage = NymOfflineSignersStorage::new(); + let mut tester = init_contract_tester(); + + let signer = tester.random_group_member(); + + assert!(!storage.recently_marked_offline(&tester, &tester.env(), &signer)?); + + tester.insert_offline_signer(&signer); + + assert!(storage.recently_marked_offline(&tester, &tester.env(), &signer)?); + + // few blocks passed (still below threshold); + tester.next_block(); + tester.next_block(); + tester.next_block(); + tester.next_block(); + assert!(storage.recently_marked_offline(&tester, &tester.env(), &signer)?); + + // threshold has passed + tester.advance_day_of_blocks(); + assert!(!storage.recently_marked_offline(&tester, &tester.env(), &signer)?); + + // came back online + tester.reset_offline_status(&signer); + assert!(!storage.recently_marked_offline(&tester, &tester.env(), &signer)?); + + tester.advance_day_of_blocks(); + // offline again + tester.insert_offline_signer(&signer); + assert!(storage.recently_marked_offline(&tester, &tester.env(), &signer)?); + + // again threshold has passed + tester.advance_day_of_blocks(); + assert!(!storage.recently_marked_offline(&tester, &tester.env(), &signer)?); + + Ok(()) + } + + #[test] + fn proposal_expired() -> anyhow::Result<()> { + let storage = NymOfflineSignersStorage::new(); + let mut tester = init_contract_tester(); + + let signer = tester.random_group_member(); + + let threshold = storage.config.load(&tester)?.maximum_proposal_lifetime_secs; + + let proposal_id = tester.make_proposal(&signer); + let proposal = storage.proposals.load(&tester, proposal_id)?; + + let initial_time = tester.env().block.time; + + assert!(!storage.proposal_expired(&tester, &tester.env(), &proposal)?); + + tester.next_block(); + assert!(!storage.proposal_expired(&tester, &tester.env(), &proposal)?); + tester.next_block(); + assert!(!storage.proposal_expired(&tester, &tester.env(), &proposal)?); + + tester.set_block_time(initial_time.plus_seconds(threshold - 1)); + assert!(!storage.proposal_expired(&tester, &tester.env(), &proposal)?); + + tester.set_block_time(initial_time.plus_seconds(threshold)); + assert!(storage.proposal_expired(&tester, &tester.env(), &proposal)?); + + tester.advance_day_of_blocks(); + assert!(storage.proposal_expired(&tester, &tester.env(), &proposal)?); + + Ok(()) + } + + #[cfg(test)] + mod try_vote { + use super::*; + use crate::testing::{init_contract_tester, OfflineSignersContractTesterExt}; + use nym_contracts_common_testing::{ChainOpts, ContractOpts, FullReader}; + + #[test] + fn proposal_reuse() -> anyhow::Result<()> { + let storage = NymOfflineSignersStorage::new(); + let mut tester = init_contract_tester(); + + let member1 = tester.random_group_member(); + let member2 = tester.random_group_member(); + let member3 = tester.random_group_member(); + + let voter = tester.random_group_member(); + + // sanity check due to RNG : ) + // if those ever fail, call `init_contract_tester_with_group_members` + // and provide higher than default value there until rngesus smiles at you + assert_ne!(member1, member2); + assert_ne!(member1, member3); + assert_ne!(member2, member3); + + let existing_expired_original = tester.make_proposal(&member1); + // advance blocks so that the proposal would have already expired + tester.advance_day_of_blocks(); + + let existing_not_expired_original = tester.make_proposal(&member2); + + let env = tester.env(); + + // ## TEST SETUP END + + // existing proposal that has already expired + let res_expired = storage.try_vote(tester.storage_mut(), &env, &voter, &member1)?; + + // existing proposal that has NOT yet expired + let res_not_expired = + storage.try_vote(tester.storage_mut(), &env, &voter, &member2)?; + + // no existing proposal + let res_new = storage.try_vote(tester.storage_mut(), &env, &voter, &member3)?; + + // the same proposal has been used + assert_eq!(res_not_expired, existing_not_expired_original); + // new proposal has been created + assert_ne!(res_expired, existing_expired_original); + + let all_proposals = storage.proposals.all_values(&tester)?; + // we expect 4 proposals: + // - the original expired one + // - the non-expired old one + // - the recreated expired one + // - proposal for new signer + assert_eq!(all_proposals.len(), 4); + + // votes are actually saved + assert!(storage + .votes + .has(&tester, (existing_not_expired_original, &voter))); + assert!(storage.votes.has(&tester, (res_expired, &voter))); + assert!(storage.votes.has(&tester, (res_new, &voter))); + assert!(!storage + .votes + .has(&tester, (existing_expired_original, &voter))); + + Ok(()) + } + + #[test] + fn duplicate_votes_are_rejected() -> anyhow::Result<()> { + let storage = NymOfflineSignersStorage::new(); + let mut tester = init_contract_tester(); + + let member1 = tester.random_group_member(); + let member2 = tester.random_group_member(); + let voter1 = tester.random_group_member(); + let voter2 = tester.random_group_member(); + + // sanity check due to RNG : ) + // if those ever fail, call `init_contract_tester_with_group_members` + // and provide higher than default value there until rngesus smiles at you + assert_ne!(member1, member2); + assert_ne!(voter1, voter2); + + assert_ne!(member1, voter1); + assert_ne!(member2, voter1); + + assert_ne!(member1, voter2); + assert_ne!(member2, voter2); + + let env = tester.env(); + + // first vote + assert!(storage + .try_vote(tester.storage_mut(), &env, &voter1, &member1) + .is_ok()); + + // second vote for the same signer is rejected + assert_eq!( + storage + .try_vote(tester.storage_mut(), &env, &voter1, &member1) + .unwrap_err(), + NymOfflineSignersContractError::AlreadyVoted { + voter: voter1.clone(), + proposal: 1, + target: member1.clone(), + } + ); + + // but is fine from another voter + assert!(storage + .try_vote(tester.storage_mut(), &env, &voter2, &member1) + .is_ok()); + + // or towards another signer + assert!(storage + .try_vote(tester.storage_mut(), &env, &voter1, &member2) + .is_ok()); + + // it is also fine after proposal gets implicitly recreated due to expiration + tester.advance_day_of_blocks(); + let env = tester.env(); + assert!(storage + .try_vote(tester.storage_mut(), &env, &voter1, &member1) + .is_ok()); + + Ok(()) + } + } + + #[test] + fn total_votes() -> anyhow::Result<()> { + let storage = NymOfflineSignersStorage::new(); + let mut tester = init_contract_tester(); + + let target1 = tester.random_group_member(); + let target2 = tester.random_group_member(); + assert_ne!(target1, target2); + + let proposal1 = tester.insert_empty_proposal(&target1); + let proposal2 = tester.insert_empty_proposal(&target2); + + let all_voters = tester.group_members(); + for (i, voter) in all_voters.iter().enumerate() { + let env = tester.env(); + + assert_eq!( + storage.total_votes(tester.storage_mut(), proposal1), + i as u32 + ); + storage.try_vote(tester.storage_mut(), &env, voter, &target1)?; + assert_eq!( + storage.total_votes(tester.storage_mut(), proposal1), + (i + 1) as u32 + ); + assert_eq!(storage.total_votes(tester.storage_mut(), proposal2), 0); + } + + for (i, voter) in all_voters.iter().enumerate() { + let env = tester.env(); + + assert_eq!( + storage.total_votes(tester.storage_mut(), proposal2), + i as u32 + ); + storage.try_vote(tester.storage_mut(), &env, voter, &target2)?; + assert_eq!( + storage.total_votes(tester.storage_mut(), proposal2), + (i + 1) as u32 + ); + assert_eq!( + storage.total_votes(tester.storage_mut(), proposal1), + all_voters.len() as u32 + ); + } + + Ok(()) + } + + #[test] + // check requires votes / eligible_voters >= quorum + fn proposal_passed() -> anyhow::Result<()> { + let storage = NymOfflineSignersStorage::new(); + let mut tester_10q = + init_custom_contract_tester(10, init_with_quorum(Decimal::percent(10))); + let mut tester_25q = + init_custom_contract_tester(10, init_with_quorum(Decimal::percent(25))); + let mut tester_100q = + init_custom_contract_tester(10, init_with_quorum(Decimal::percent(100))); + + // check proposal that doesn't exist + assert!(!storage.proposal_passed(tester_10q.deps(), 69, None)?); + + // those values are the same for all testers + let target = tester_10q.random_group_member(); + let all_voters = tester_10q.group_members(); + + // make dummy_proposals + let p_10q = tester_10q.insert_empty_proposal(&target); + let p_25q = tester_25q.insert_empty_proposal(&target); + let p_100q = tester_100q.insert_empty_proposal(&target); + + // initially no proposal has been passed + assert!(!storage.proposal_passed(tester_10q.deps(), p_10q, None)?); + assert!(!storage.proposal_passed(tester_25q.deps(), p_25q, None)?); + assert!(!storage.proposal_passed(tester_100q.deps(), p_100q, None)?); + + // add first vote + tester_10q.add_vote(p_10q, &all_voters[0]); + tester_25q.add_vote(p_25q, &all_voters[0]); + tester_100q.add_vote(p_100q, &all_voters[0]); + + // in the case of the first tester (where quorum is 10%), it should now be passed + assert!(storage.proposal_passed(tester_10q.deps(), p_10q, None)?); + assert!(!storage.proposal_passed(tester_25q.deps(), p_25q, None)?); + assert!(!storage.proposal_passed(tester_100q.deps(), p_100q, None)?); + + // another vote + tester_10q.add_vote(p_10q, &all_voters[1]); + tester_25q.add_vote(p_25q, &all_voters[1]); + tester_100q.add_vote(p_100q, &all_voters[1]); + + // with more votes, first proposal is still marked as passed + assert!(storage.proposal_passed(tester_10q.deps(), p_10q, None)?); + assert!(!storage.proposal_passed(tester_25q.deps(), p_25q, None)?); + assert!(!storage.proposal_passed(tester_100q.deps(), p_100q, None)?); + + // with third vote (30%) second tester has passed its proposal that required 25% quorum) + tester_25q.add_vote(p_25q, &all_voters[2]); + tester_100q.add_vote(p_100q, &all_voters[2]); + + assert!(storage.proposal_passed(tester_25q.deps(), p_25q, None)?); + assert!(!storage.proposal_passed(tester_100q.deps(), p_100q, None)?); + + // last proposal won't be passed until all voters have voted + for voter in all_voters[3..=8].iter() { + tester_100q.add_vote(p_100q, voter); + assert!(!storage.proposal_passed(tester_100q.deps(), p_100q, None)?); + } + tester_100q.add_vote(p_100q, &all_voters[9]); + assert!(storage.proposal_passed(tester_100q.deps(), p_100q, None)?); + + Ok(()) + } + + #[test] + fn finalize_vote() -> anyhow::Result<()> { + fn mock_vote_information() -> VoteInformation { + VoteInformation { + voted_at: mock_env().block, + } + } + + let storage = NymOfflineSignersStorage::new(); + let mut tester = + init_custom_contract_tester(10, init_with_quorum(Decimal::percent(20))); + + let target = tester.random_group_member(); + let all_voters = tester.group_members(); + let proposal = tester.insert_empty_proposal(&target); + let group_contract = tester.group_contract_wrapper(); + let env = mock_env(); + + // first vote (no quorum yet) + storage.votes.save( + tester.storage_mut(), + (proposal, &all_voters[0]), + &mock_vote_information(), + )?; + let got_quorum = storage.finalize_vote( + tester.deps_mut(), + &env, + proposal, + &target, + group_contract.clone(), + )?; + assert!(!storage + .offline_signers + .has_signer_information(&tester, &target)); + assert!(!got_quorum); + + // second vote (reached quorum!) + storage.votes.save( + tester.storage_mut(), + (proposal, &all_voters[1]), + &mock_vote_information(), + )?; + let got_quorum = storage.finalize_vote( + tester.deps_mut(), + &env, + proposal, + &target, + group_contract.clone(), + )?; + assert!(got_quorum); + assert!(storage + .offline_signers + .has_signer_information(&tester, &target)); + + // third vote (already passed quorum before) + storage.votes.save( + tester.storage_mut(), + (proposal, &all_voters[2]), + &mock_vote_information(), + )?; + let got_quorum = storage.finalize_vote( + tester.deps_mut(), + &env, + proposal, + &target, + group_contract, + )?; + assert!(got_quorum); + assert!(storage + .offline_signers + .has_signer_information(&tester, &target)); + + Ok(()) + } + + #[cfg(test)] + mod propose_or_vote { + use super::*; + use itertools::Itertools; + use nym_contracts_common_testing::RandExt; + + #[test] + fn proposer_has_to_be_dkg_group_member() -> anyhow::Result<()> { + let storage = NymOfflineSignersStorage::new(); + let mut tester = init_contract_tester(); + + let signer = tester.random_group_member(); + let bad_proposer = tester.generate_account(); + let good_proposer = tester.random_group_member(); + + let env = tester.env(); + let err = storage + .propose_or_vote(tester.deps_mut(), env, bad_proposer.clone(), signer.clone()) + .unwrap_err(); + assert_eq!( + err, + NymOfflineSignersContractError::NotGroupMember { + address: bad_proposer + } + ); + + let env = tester.env(); + let res = storage.propose_or_vote(tester.deps_mut(), env, good_proposer, signer); + + assert!(res.is_ok()); + Ok(()) + } + + #[test] + fn proposed_signer_has_to_be_dkg_group_member() -> anyhow::Result<()> { + let storage = NymOfflineSignersStorage::new(); + let mut tester = init_contract_tester(); + + let good_signer = tester.random_group_member(); + let bad_signer = tester.generate_account(); + let proposer = tester.random_group_member(); + + let env = tester.env(); + let err = storage + .propose_or_vote(tester.deps_mut(), env, proposer.clone(), bad_signer.clone()) + .unwrap_err(); + assert_eq!( + err, + NymOfflineSignersContractError::NotGroupMember { + address: bad_signer + } + ); + + let env = tester.env(); + let res = storage.propose_or_vote(tester.deps_mut(), env, proposer, good_signer); + + assert!(res.is_ok()); + Ok(()) + } + + #[test] + fn signer_must_have_not_recently_come_back_online() -> anyhow::Result<()> { + let storage = NymOfflineSignersStorage::new(); + let mut tester = init_contract_tester(); + + let signer = tester.random_group_member(); + let proposer = tester.random_group_member(); + + tester.insert_offline_signer(&signer); + tester.advance_day_of_blocks(); + tester.reset_offline_status(&signer); + + let env = tester.env(); + let err = storage + .propose_or_vote(tester.deps_mut(), env, proposer.clone(), signer.clone()) + .unwrap_err(); + assert_eq!( + err, + NymOfflineSignersContractError::RecentlyCameOnline { + address: signer.clone() + } + ); + + tester.advance_day_of_blocks(); + let env = tester.env(); + let res = storage.propose_or_vote(tester.deps_mut(), env, proposer, signer); + + assert!(res.is_ok()); + Ok(()) + } + + #[test] + fn returns_quorum_information() -> anyhow::Result<()> { + let storage = NymOfflineSignersStorage::new(); + let mut tester = + init_custom_contract_tester(10, init_with_quorum(Decimal::percent(30))); + + let voter1 = tester.random_group_member(); + let voter2 = tester.random_group_member(); + let voter3 = tester.random_group_member(); + let signer = tester.random_group_member(); + assert!([&voter1, &voter2, &voter3, &signer] + .iter() + .duplicates() + .next() + .is_none()); + + let env = tester.env(); + assert!(!storage.propose_or_vote( + tester.deps_mut(), + env.clone(), + voter1, + signer.clone() + )?); + assert!(!storage.propose_or_vote( + tester.deps_mut(), + env.clone(), + voter2, + signer.clone() + )?); + assert!(storage.propose_or_vote(tester.deps_mut(), env, voter3, signer.clone())?); + + Ok(()) + } + } + + #[cfg(test)] + mod reset_offline_status { + use super::*; + use nym_contracts_common_testing::ChainOpts; + + #[test] + fn signer_must_have_been_offline_for_threshold_period() -> anyhow::Result<()> { + let storage = NymOfflineSignersStorage::new(); + let mut tester = init_contract_tester(); + + let threshold = storage.config.load(&tester)?.status_change_cooldown_secs; + + let signer = tester.random_group_member(); + tester.insert_offline_signer(&signer); + + // try to reset it immediately + let env = tester.env(); + let err = storage + .reset_offline_status(tester.deps_mut(), env, signer.clone()) + .unwrap_err(); + assert_eq!( + err, + NymOfflineSignersContractError::RecentlyCameOffline { + address: signer.clone() + } + ); + + // wait for the minimum period MINUS one second (so just barely out of it) + tester.advance_time_by(threshold - 1); + let env = tester.env(); + let err = storage + .reset_offline_status(tester.deps_mut(), env, signer.clone()) + .unwrap_err(); + assert_eq!( + err, + NymOfflineSignersContractError::RecentlyCameOffline { + address: signer.clone() + } + ); + + // wait additional second (i.e. exactly minimum period) + tester.advance_time_by(1); + let env = tester.env(); + let res = storage.reset_offline_status(tester.deps_mut(), env, signer.clone()); + assert!(res.is_ok()); + + // another instance, way beyond minimum value + let another_signer = tester.random_group_member(); + tester.insert_offline_signer(&another_signer); + tester.advance_time_by(10 * threshold); + let env = tester.env(); + let res = + storage.reset_offline_status(tester.deps_mut(), env, another_signer.clone()); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn signer_must_be_actually_offline() -> anyhow::Result<()> { + let storage = NymOfflineSignersStorage::new(); + let mut tester = init_contract_tester(); + + let signer = tester.random_group_member(); + + tester.advance_day_of_blocks(); + let env = tester.env(); + let err = storage + .reset_offline_status(tester.deps_mut(), env, signer.clone()) + .unwrap_err(); + assert_eq!( + err, + NymOfflineSignersContractError::NotOffline { + address: signer.clone() + } + ); + + // after marking it, it's fine now + tester.insert_offline_signer(&signer); + tester.advance_day_of_blocks(); + let env = tester.env(); + let res = storage.reset_offline_status(tester.deps_mut(), env, signer.clone()); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn clears_offline_status_and_updates_last_reset() -> 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(); + storage.reset_offline_status(tester.deps_mut(), env, signer.clone())?; + + 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(()) + } + } + } + + #[cfg(test)] + mod offline_signers_storage { + use super::*; + use crate::testing::{init_contract_tester, OfflineSignersContractTesterExt}; + use cosmwasm_std::testing::mock_env; + use nym_contracts_common_testing::{ + mock_dependencies, ChainOpts, ContractOpts, FullReader, RandExt, + }; + + fn mock_offline_signer_info() -> OfflineSignerInformation { + OfflineSignerInformation { + marked_offline_at: mock_env().block, + associated_proposal: 123, + } + } + + #[test] + fn initialisation() -> anyhow::Result<()> { + let storage = OfflineSignersStorage::new(); + + let mut empty_deps = mock_dependencies(); + assert!(storage + .addresses + .may_load(empty_deps.as_mut().storage)? + .is_none()); + let mut tester = init_contract_tester(); + + assert!(storage.addresses.may_load(tester.storage_mut())?.is_some()); + Ok(()) + } + + #[test] + fn checking_for_signer_information() -> anyhow::Result<()> { + let storage = OfflineSignersStorage::new(); + let mut tester = init_contract_tester(); + + let random_signer = tester.random_group_member(); + + // nothing initially + assert!(!storage.has_signer_information(&tester, &random_signer)); + + // after marking it offline it's there + tester.insert_offline_signer(&random_signer); + assert!(storage.has_signer_information(&tester, &random_signer)); + + // and it's gone after the removal + tester.advance_day_of_blocks(); + tester.reset_offline_status(&random_signer); + assert!(!storage.has_signer_information(&tester, &random_signer)); + + Ok(()) + } + + #[test] + fn retrieving_signer_information() -> anyhow::Result<()> { + let storage = OfflineSignersStorage::new(); + let mut tester = init_contract_tester(); + + let random_signer = tester.random_group_member(); + + // nothing initially + assert!(storage + .load_signer_information(&tester, &random_signer)? + .is_none()); + + // after marking it offline it's there + let proposal_id = tester.insert_offline_signer(&random_signer); + let loaded = storage + .load_signer_information(&tester, &random_signer)? + .unwrap(); + assert_eq!(loaded.associated_proposal, proposal_id); + + // and it's gone after the removal + tester.advance_day_of_blocks(); + tester.reset_offline_status(&random_signer); + assert!(storage + .load_signer_information(&tester, &random_signer)? + .is_none()); + + Ok(()) + } + + #[test] + fn insertion_puts_data_in_map_and_item() -> anyhow::Result<()> { + let storage = OfflineSignersStorage::new(); + let mut tester = init_contract_tester(); + + // initial + let initial_env = tester.env(); + assert!(storage.addresses.load(&tester)?.is_empty()); + assert!(storage.information.all_values(&tester)?.is_empty()); + tester.next_block(); + + for i in 0..10 { + let random_signer = tester.generate_account(); + let env = tester.env(); + storage.insert_offline_signer_information( + tester.storage_mut(), + &env, + &random_signer, + &mock_offline_signer_info(), + )?; + tester.next_block(); + + assert_eq!(storage.addresses.load(&tester)?.len(), i + 1); + assert_eq!(storage.information.all_values(&tester)?.len(), i + 1); + } + + // check snapshots + for i in 0..10 { + // add additional block as insertion happened at the beginning of the block + let height = initial_env.block.height + i + 1; + + assert_eq!( + storage + .addresses + .may_load_at_height(&tester, height)? + .unwrap() + .len(), + i as usize + ); + } + + Ok(()) + } + + #[test] + fn removal_removes_data_from_map_and_item() -> anyhow::Result<()> { + let storage = OfflineSignersStorage::new(); + let mut tester = init_contract_tester(); + + let initial_env = tester.env(); + tester.next_block(); + + let mut inserted = Vec::new(); + for _ in 0..10 { + let random_signer = tester.generate_account(); + let env = tester.env(); + storage.insert_offline_signer_information( + tester.storage_mut(), + &env, + &random_signer, + &mock_offline_signer_info(), + )?; + tester.next_block(); + inserted.push(random_signer); + } + + for signer in &inserted { + // before is present in both + let addresses = storage.addresses.load(&tester)?; + assert!(addresses.contains(signer)); + assert!(storage.information.has(&tester, signer)); + + let env = tester.env(); + storage.remove_offline_signer_information(tester.storage_mut(), &env, signer)?; + tester.next_block(); + + // after is gone + let addresses = storage.addresses.load(&tester)?; + assert!(!addresses.contains(signer)); + assert!(!storage.information.has(&tester, signer)); + } + + // check snapshots + for i in 0..10 { + let height = initial_env.block.height + i + 1 + inserted.len() as u64; + + assert_eq!( + inserted.len() + - storage + .addresses + .may_load_at_height(&tester, height)? + .unwrap() + .len(), + i as usize + ); + } + + Ok(()) + } + } +} diff --git a/contracts/offline-signers/src/testing/mod.rs b/contracts/offline-signers/src/testing/mod.rs new file mode 100644 index 00000000000..bef282a3bcf --- /dev/null +++ b/contracts/offline-signers/src/testing/mod.rs @@ -0,0 +1,233 @@ +// Copyright 2025 - Nym Technologies SA +// 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 { + instantiate + } + + fn execute() -> ContractFn { + execute + } + + fn query() -> QueryFn { + query + } + + fn migrate() -> PermissionedFn { + migrate + } + + fn init() -> ContractTester + where + Self: Sized, + { + init_contract_tester_with_group_members(DEFAULT_GROUP_MEMBERS) + } +} + +pub fn init_contract_tester() -> ContractTester { + OfflineSignersContract::init() + .with_common_storage_key(CommonStorageKeys::Admin, storage_keys::CONTRACT_ADMIN) +} + +pub fn init_contract_tester_with_group_members( + members: usize, +) -> ContractTester { + 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 { + // 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::(); + instantiate_msg.dkg_contract_address = dkg_contract_address.to_string(); + + // 5. finally init the offline signers contract + builder + .instantiate::(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 { + 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 { + 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 {} diff --git a/contracts/offline-signers/src/transactions.rs b/contracts/offline-signers/src/transactions.rs new file mode 100644 index 00000000000..945a466b326 --- /dev/null +++ b/contracts/offline-signers/src/transactions.rs @@ -0,0 +1,232 @@ +// Copyright 2025 - Nym Technologies SA +// 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 { + 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 { + 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 { + 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(()) + } +} diff --git a/contracts/performance/schema/nym-performance-contract.json b/contracts/performance/schema/nym-performance-contract.json index 2242aefd752..73952c06383 100644 --- a/contracts/performance/schema/nym-performance-contract.json +++ b/contracts/performance/schema/nym-performance-contract.json @@ -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": { diff --git a/contracts/performance/schema/raw/execute.json b/contracts/performance/schema/raw/execute.json index c4b5c8be041..c34675fd3d4 100644 --- a/contracts/performance/schema/raw/execute.json +++ b/contracts/performance/schema/raw/execute.json @@ -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": { diff --git a/contracts/performance/src/helpers.rs b/contracts/performance/src/helpers.rs index 51ccb4f4b2f..5241403c147 100644 --- a/contracts/performance/src/helpers.rs +++ b/contracts/performance/src/helpers.rs @@ -1,40 +1,16 @@ // Copyright 2025 - Nym Technologies SA // 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( - &self, - address: impl Into, - msg: &nym_mixnet_contract_common::QueryMsg, - ) -> StdResult; - - fn query_mixnet_contract_storage( - &self, - address: impl Into, - key: impl Into, - ) -> StdResult>>; - - fn query_mixnet_contract_storage_value( - &self, - address: impl Into, - key: impl Into, - ) -> StdResult> { - 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) -> StdResult { - 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 MixnetContractQuerier for QuerierWrapper<'_, C> -where - C: CustomQuery, -{ - fn query_mixnet_contract( - &self, - address: impl Into, - msg: &nym_mixnet_contract_common::QueryMsg, - ) -> StdResult { - self.query_wasm_smart(address, msg) - } - - fn query_mixnet_contract_storage( - &self, - address: impl Into, - key: impl Into, - ) -> StdResult>> { - self.query_wasm_raw(address, key) - } -} +impl MixnetContractQuerier for T where T: ContractQuerier {} diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 4603c90e9b9..33b2dc2307c 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -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", diff --git a/nym-wallet/nym-wallet-types/src/network/sandbox.rs b/nym-wallet/nym-wallet-types/src/network/sandbox.rs index f8e66ebe3e9..cf57b9a21d6 100644 --- a/nym-wallet/nym-wallet-types/src/network/sandbox.rs +++ b/nym-wallet/nym-wallet-types/src/network/sandbox.rs @@ -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, diff --git a/tools/internal/testnet-manager/src/manager/contract.rs b/tools/internal/testnet-manager/src/manager/contract.rs index 6328dc0b100..36a94d0b5ba 100644 --- a/tools/internal/testnet-manager/src/manager/contract.rs +++ b/tools/internal/testnet-manager/src/manager/contract.rs @@ -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"), } } } diff --git a/tools/internal/testnet-manager/src/manager/network.rs b/tools/internal/testnet-manager/src/manager/network.rs index 39e04f10cf6..9279ca5be6d 100644 --- a/tools/internal/testnet-manager/src/manager/network.rs +++ b/tools/internal/testnet-manager/src/manager/network.rs @@ -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();