diff --git a/framework/contracts/account/manager/src/commands.rs b/framework/contracts/account/manager/src/commands.rs index 7a0f75cfbf..1074840216 100644 --- a/framework/contracts/account/manager/src/commands.rs +++ b/framework/contracts/account/manager/src/commands.rs @@ -239,7 +239,7 @@ pub fn create_sub_account( assert_admin_right(deps.as_ref(), &msg_info.sender)?; let create_account_msg = &abstract_core::account_factory::ExecuteMsg::CreateAccount { - /// proxy of this manager will be the account owner + // proxy of this manager will be the account owner governance: GovernanceDetails::SubAccount { manager: env.contract.address.into_string(), proxy: ACCOUNT_MODULES.load(deps.storage, PROXY)?.into_string(), diff --git a/framework/packages/abstract-core/src/objects/mod.rs b/framework/packages/abstract-core/src/objects/mod.rs index d3c7bd7373..6bbe9ba05f 100644 --- a/framework/packages/abstract-core/src/objects/mod.rs +++ b/framework/packages/abstract-core/src/objects/mod.rs @@ -27,6 +27,7 @@ pub mod paged_map; pub mod price_source; pub mod time_weighted_average; pub mod validation; +pub mod voting; pub use account::{AccountId, ABSTRACT_ACCOUNT_ID}; pub use ans_asset::AnsAsset; diff --git a/framework/packages/abstract-core/src/objects/voting.rs b/framework/packages/abstract-core/src/objects/voting.rs new file mode 100644 index 0000000000..c59815dd39 --- /dev/null +++ b/framework/packages/abstract-core/src/objects/voting.rs @@ -0,0 +1,1260 @@ +//! # Simple voting +//! Simple voting is a state object to enable voting mechanism on a contract +//! +//! ## Setting up +//! * Create SimpleVoting object in similar way to the cw-storage-plus objects using [`SimpleVoting::new`] method +//! * Inside instantiate contract method use [`SimpleVoting::instantiate`] method +//! * Add [`VoteError`] type to your application errors +//! +//! ## Creating a new proposal +//! To create a new proposal use [`SimpleVoting::new_proposal`] method, it will return ProposalId +//! +//! ## Whitelisting voters +//! Initial whitelist passed during [`SimpleVoting::new_proposal`] method and currently has no way to edit this +//! +//! ## Voting +//! To cast a vote use [`SimpleVoting::cast_vote`] method +//! +//! ## Count voting +//! To count votes use [`SimpleVoting::count_votes`] method during [`ProposalStatus::WaitingForCount`] +//! +//! ## Veto +//! In case your [`VoteConfig`] has veto duration set-up, after proposal.end_timestamp veto period will start +//! * During veto period [`SimpleVoting::veto_proposal`] method could be used to Veto proposal +//! +//! ## Cancel proposal +//! During active voting: +//! * [`SimpleVoting::cancel_proposal`] method could be used to cancel proposal +//! +//! ## Queries +//! * Single-item queries methods allowed by `load_` prefix +//! * List of items queries allowed by `query_` prefix +//! +//! ## Details +//! All methods that modify proposal will return [`ProposalInfo`] to allow logging or checking current status of proposal. +//! +//! Each proposal goes through the following stages: +//! 1. Active: proposal is active and can be voted on. It can also be canceled during this period. +//! 3. VetoPeriod (optional): voting is counted and veto period is active. +//! 2. WaitingForCount: voting period is finished and awaiting counting. +//! 4. Finished: proposal is finished and count is done. The proposal then has one of the following end states: +//! * Passed: proposal passed +//! * Failed: proposal failed +//! * Canceled: proposal was canceled +//! * Vetoed: proposal was vetoed + +use std::{collections::HashSet, fmt::Display}; + +use cosmwasm_std::{ + ensure_eq, Addr, BlockInfo, Decimal, StdError, StdResult, Storage, Timestamp, Uint128, Uint64, +}; +use cw_storage_plus::{Bound, Item, Map}; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum VoteError { + #[error("Std error encountered while handling voting object: {0}")] + Std(#[from] StdError), + + #[error("Tried to add duplicate voter addresses")] + DuplicateAddrs {}, + + #[error("No proposal by proposal id")] + NoProposalById {}, + + #[error("Action allowed only for active proposal")] + ProposalNotActive(ProposalStatus), + + #[error("Threshold error: {0}")] + ThresholdError(String), + + #[error("Veto actions could be done only during veto period, current status: {status}")] + NotVeto { status: ProposalStatus }, + + #[error("Too early to count votes: voting is not over")] + VotingNotOver {}, + + #[error("User is not allowed to vote on this proposal")] + Unauthorized {}, +} + +pub type VoteResult = Result; + +pub const DEFAULT_LIMIT: u64 = 25; +pub type ProposalId = u64; + +/// Simple voting helper +pub struct SimpleVoting<'a> { + next_proposal_id: Item<'a, ProposalId>, + proposals: Map<'a, (ProposalId, &'a Addr), Option>, + proposals_info: Map<'a, ProposalId, ProposalInfo>, + vote_config: Item<'a, VoteConfig>, +} + +impl<'a> SimpleVoting<'a> { + pub const fn new( + proposals_key: &'a str, + id_key: &'a str, + proposals_info_key: &'a str, + vote_config_key: &'a str, + ) -> Self { + Self { + next_proposal_id: Item::new(id_key), + proposals: Map::new(proposals_key), + proposals_info: Map::new(proposals_info_key), + vote_config: Item::new(vote_config_key), + } + } + + /// SimpleVoting setup during instantiation + pub fn instantiate(&self, store: &mut dyn Storage, vote_config: &VoteConfig) -> VoteResult<()> { + vote_config.threshold.validate_percentage()?; + + self.next_proposal_id.save(store, &ProposalId::default())?; + self.vote_config.save(store, vote_config)?; + Ok(()) + } + + pub fn update_vote_config( + &self, + store: &mut dyn Storage, + new_vote_config: &VoteConfig, + ) -> StdResult<()> { + self.vote_config.save(store, new_vote_config) + } + + /// Create new proposal + /// initial_voters is a list of whitelisted to vote + pub fn new_proposal( + &self, + store: &mut dyn Storage, + end: Timestamp, + initial_voters: &[Addr], + ) -> VoteResult { + // Check if addrs unique + let mut unique_addrs = HashSet::with_capacity(initial_voters.len()); + if !initial_voters.iter().all(|x| unique_addrs.insert(x)) { + return Err(VoteError::DuplicateAddrs {}); + } + + let proposal_id = self + .next_proposal_id + .update(store, |id| VoteResult::Ok(id + 1))?; + + let config = self.load_config(store)?; + self.proposals_info.save( + store, + proposal_id, + &ProposalInfo::new(initial_voters.len() as u32, config, end), + )?; + for voter in initial_voters { + self.proposals.save(store, (proposal_id, voter), &None)?; + } + Ok(proposal_id) + } + + /// Assign vote for the voter + pub fn cast_vote( + &self, + store: &mut dyn Storage, + block: &BlockInfo, + proposal_id: ProposalId, + voter: &Addr, + vote: Vote, + ) -> VoteResult { + let mut proposal_info = self.load_proposal(store, block, proposal_id)?; + proposal_info.assert_active_proposal()?; + + self.proposals.update( + store, + (proposal_id, voter), + |previous_vote| match previous_vote { + // We allow re-voting + Some(prev_v) => { + proposal_info.vote_update(prev_v.as_ref(), &vote); + Ok(Some(vote)) + } + None => Err(VoteError::Unauthorized {}), + }, + )?; + + self.proposals_info + .save(store, proposal_id, &proposal_info)?; + Ok(proposal_info) + } + + // Note: this method doesn't check a sender + // Therefore caller of this method should check if he is allowed to cancel vote + /// Cancel proposal + pub fn cancel_proposal( + &self, + store: &mut dyn Storage, + block: &BlockInfo, + proposal_id: ProposalId, + ) -> VoteResult { + let mut proposal_info = self.load_proposal(store, block, proposal_id)?; + proposal_info.assert_active_proposal()?; + + proposal_info.finish_vote(ProposalOutcome::Canceled {}, block); + self.proposals_info + .save(store, proposal_id, &proposal_info)?; + Ok(proposal_info) + } + + /// Count votes and finish or move to the veto period(if configured) for this proposal + pub fn count_votes( + &self, + store: &mut dyn Storage, + block: &BlockInfo, + proposal_id: ProposalId, + ) -> VoteResult<(ProposalInfo, ProposalOutcome)> { + let mut proposal_info = self.load_proposal(store, block, proposal_id)?; + ensure_eq!( + proposal_info.status, + ProposalStatus::WaitingForCount, + VoteError::VotingNotOver {} + ); + + let vote_config = &proposal_info.config; + + // Calculate votes + let threshold = match vote_config.threshold { + // 50% + 1 voter + Threshold::Majority {} => Uint128::from(proposal_info.total_voters / 2 + 1), + Threshold::Percentage(decimal) => decimal * Uint128::from(proposal_info.total_voters), + }; + + let proposal_outcome = if Uint128::from(proposal_info.votes_for) >= threshold { + ProposalOutcome::Passed + } else { + ProposalOutcome::Failed + }; + + // Update vote status + proposal_info.finish_vote(proposal_outcome, block); + self.proposals_info + .save(store, proposal_id, &proposal_info)?; + + Ok((proposal_info, proposal_outcome)) + } + + /// Called by veto admin + /// Finish or Veto this proposal + pub fn veto_proposal( + &self, + store: &mut dyn Storage, + block: &BlockInfo, + proposal_id: ProposalId, + ) -> VoteResult { + let mut proposal_info = self.load_proposal(store, block, proposal_id)?; + + let ProposalStatus::VetoPeriod(_) = proposal_info.status else { + return Err(VoteError::NotVeto { + status: proposal_info.status, + }); + }; + + proposal_info.status = ProposalStatus::Finished(ProposalOutcome::Vetoed); + self.proposals_info + .save(store, proposal_id, &proposal_info)?; + + Ok(proposal_info) + } + + // TODO: It's currently not used, and most likely not desirable to edit voters during active voting + // In case it will get some use: keep in mind that it's untested + + // /// Add new addresses that's allowed to vote + // pub fn add_voters( + // &self, + // store: &mut dyn Storage, + // proposal_id: ProposalId, + // block: &BlockInfo, + // new_voters: &[Addr], + // ) -> VoteResult { + // // Need to check it's existing proposal + // let mut proposal_info = self.load_proposal(store, block, proposal_id)?; + // proposal_info.assert_active_proposal()?; + + // for voter in new_voters { + // // Don't override already existing vote + // self.proposals + // .update(store, (proposal_id, voter), |v| match v { + // Some(_) => Err(VoteError::DuplicateAddrs {}), + // None => { + // proposal_info.total_voters += 1; + // Ok(None) + // } + // })?; + // } + // self.proposals_info + // .save(store, proposal_id, &proposal_info)?; + + // Ok(proposal_info) + // } + + // /// Remove addresses that's allowed to vote + // /// Will re-count votes + // pub fn remove_voters( + // &self, + // store: &mut dyn Storage, + // proposal_id: ProposalId, + // block: &BlockInfo, + // removed_voters: &[Addr], + // ) -> VoteResult { + // let mut proposal_info = self.load_proposal(store, block, proposal_id)?; + // proposal_info.assert_active_proposal()?; + + // for voter in removed_voters { + // if let Some(vote) = self.proposals.may_load(store, (proposal_id, voter))? { + // if let Some(previous_vote) = vote { + // match previous_vote.vote { + // true => proposal_info.votes_for -= 1, + // false => proposal_info.votes_against -= 1, + // } + // } + // proposal_info.total_voters -= 1; + // self.proposals.remove(store, (proposal_id, voter)); + // } + // } + // self.proposals_info + // .save(store, proposal_id, &proposal_info)?; + // Ok(proposal_info) + // } + + /// Load vote by address + pub fn load_vote( + &self, + store: &dyn Storage, + proposal_id: ProposalId, + voter: &Addr, + ) -> VoteResult> { + self.proposals + .load(store, (proposal_id, voter)) + .map_err(Into::into) + } + + /// Load proposal by id with updated status if required + pub fn load_proposal( + &self, + store: &dyn Storage, + block: &BlockInfo, + proposal_id: ProposalId, + ) -> VoteResult { + let mut proposal = self + .proposals_info + .may_load(store, proposal_id)? + .ok_or(VoteError::NoProposalById {})?; + if let ProposalStatus::Active = proposal.status { + let veto_expiration = proposal.end_timestamp.plus_seconds( + proposal + .config + .veto_duration_seconds + .unwrap_or_default() + .u64(), + ); + // Check if veto or count period and update if so + if block.time >= proposal.end_timestamp { + if block.time < veto_expiration { + proposal.status = ProposalStatus::VetoPeriod(veto_expiration) + } else { + proposal.status = ProposalStatus::WaitingForCount + } + } + } + Ok(proposal) + } + + /// Load current vote config + pub fn load_config(&self, store: &dyn Storage) -> StdResult { + self.vote_config.load(store) + } + + /// List of votes by proposal id + pub fn query_by_id( + &self, + store: &dyn Storage, + proposal_id: ProposalId, + start_after: Option<&Addr>, + limit: Option, + ) -> VoteResult)>> { + let min = start_after.map(Bound::exclusive); + let limit = limit.unwrap_or(DEFAULT_LIMIT); + + let votes = self + .proposals + .prefix(proposal_id) + .range(store, min, None, cosmwasm_std::Order::Ascending) + .take(limit as usize) + .collect::>()?; + Ok(votes) + } + + #[allow(clippy::type_complexity)] + pub fn query_list( + &self, + store: &dyn Storage, + start_after: Option<(ProposalId, &Addr)>, + limit: Option, + ) -> VoteResult)>> { + let min = start_after.map(Bound::exclusive); + let limit = limit.unwrap_or(DEFAULT_LIMIT); + + let votes = self + .proposals + .range(store, min, None, cosmwasm_std::Order::Ascending) + .take(limit as usize) + .collect::>()?; + Ok(votes) + } +} + +/// Vote struct +#[cosmwasm_schema::cw_serde] +pub struct Vote { + /// true: Vote for + /// false: Vote against + pub vote: bool, + /// memo for the vote + pub memo: Option, +} + +#[cosmwasm_schema::cw_serde] +pub struct ProposalInfo { + pub total_voters: u32, + pub votes_for: u32, + pub votes_against: u32, + pub status: ProposalStatus, + /// Config it was created with + /// For cases config got changed during voting + pub config: VoteConfig, + pub end_timestamp: Timestamp, +} + +impl ProposalInfo { + pub fn new(initial_voters: u32, config: VoteConfig, end_timestamp: Timestamp) -> Self { + Self { + total_voters: initial_voters, + votes_for: 0, + votes_against: 0, + config, + status: ProposalStatus::Active {}, + end_timestamp, + } + } + + pub fn assert_active_proposal(&self) -> VoteResult<()> { + self.status.assert_is_active() + } + + pub fn vote_update(&mut self, previous_vote: Option<&Vote>, new_vote: &Vote) { + match (previous_vote, new_vote.vote) { + // unchanged vote + (Some(Vote { vote: true, .. }), true) | (Some(Vote { vote: false, .. }), false) => {} + // vote for became vote against + (Some(Vote { vote: true, .. }), false) => { + self.votes_against += 1; + self.votes_for -= 1; + } + // vote against became vote for + (Some(Vote { vote: false, .. }), true) => { + self.votes_for += 1; + self.votes_against -= 1; + } + // new vote for + (None, true) => { + self.votes_for += 1; + } + // new vote against + (None, false) => { + self.votes_against += 1; + } + } + } + + pub fn finish_vote(&mut self, outcome: ProposalOutcome, block: &BlockInfo) { + self.status = ProposalStatus::Finished(outcome); + self.end_timestamp = block.time + } +} + +#[cosmwasm_schema::cw_serde] +pub enum ProposalStatus { + Active, + VetoPeriod(Timestamp), + WaitingForCount, + Finished(ProposalOutcome), +} + +impl Display for ProposalStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ProposalStatus::Active => write!(f, "active"), + ProposalStatus::VetoPeriod(exp) => write!(f, "veto_period until {exp}"), + ProposalStatus::WaitingForCount => write!(f, "waiting_for_count"), + ProposalStatus::Finished(outcome) => write!(f, "finished({outcome})"), + } + } +} + +impl ProposalStatus { + pub fn assert_is_active(&self) -> VoteResult<()> { + match self { + ProposalStatus::Active => Ok(()), + _ => Err(VoteError::ProposalNotActive(self.clone())), + } + } +} + +#[cosmwasm_schema::cw_serde] +#[derive(Copy)] +pub enum ProposalOutcome { + Passed, + Failed, + Canceled, + Vetoed, +} + +impl Display for ProposalOutcome { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ProposalOutcome::Passed => write!(f, "passed"), + ProposalOutcome::Failed => write!(f, "failed"), + ProposalOutcome::Canceled => write!(f, "canceled"), + ProposalOutcome::Vetoed => write!(f, "vetoed"), + } + } +} + +#[cosmwasm_schema::cw_serde] +pub struct VoteConfig { + pub threshold: Threshold, + /// Veto duration after the first vote + /// None disables veto + pub veto_duration_seconds: Option, +} + +#[cosmwasm_schema::cw_serde] +pub enum Threshold { + Majority {}, + Percentage(Decimal), +} + +impl Threshold { + /// Asserts that the 0.0 < percent <= 1.0 + fn validate_percentage(&self) -> VoteResult<()> { + if let Threshold::Percentage(percent) = self { + if percent.is_zero() { + Err(VoteError::ThresholdError("can't be 0%".to_owned())) + } else if *percent > Decimal::one() { + Err(VoteError::ThresholdError( + "not possible to reach >100% votes".to_owned(), + )) + } else { + Ok(()) + } + } else { + Ok(()) + } + } +} + +#[cfg(test)] +mod tests { + use cosmwasm_std::testing::{mock_dependencies, mock_env}; + + use super::*; + const SIMPLE_VOTING: SimpleVoting = + SimpleVoting::new("proposals", "id", "proposal_info", "config"); + + fn setup(storage: &mut dyn Storage, vote_config: &VoteConfig) { + SIMPLE_VOTING.instantiate(storage, vote_config).unwrap(); + } + fn default_setup(storage: &mut dyn Storage) { + setup( + storage, + &VoteConfig { + threshold: Threshold::Majority {}, + veto_duration_seconds: None, + }, + ); + } + + #[test] + fn threshold_validation() { + assert!(Threshold::Majority {}.validate_percentage().is_ok()); + assert!(Threshold::Percentage(Decimal::one()) + .validate_percentage() + .is_ok()); + assert!(Threshold::Percentage(Decimal::percent(1)) + .validate_percentage() + .is_ok()); + + assert_eq!( + Threshold::Percentage(Decimal::percent(101)).validate_percentage(), + Err(VoteError::ThresholdError( + "not possible to reach >100% votes".to_owned() + )) + ); + assert_eq!( + Threshold::Percentage(Decimal::zero()).validate_percentage(), + Err(VoteError::ThresholdError("can't be 0%".to_owned())) + ); + } + + #[test] + fn assert_active_proposal() { + let end_timestamp = Timestamp::from_seconds(100); + + // Normal proposal + let mut proposal = ProposalInfo { + total_voters: 2, + votes_for: 0, + votes_against: 0, + status: ProposalStatus::Active, + config: VoteConfig { + threshold: Threshold::Majority {}, + veto_duration_seconds: Some(Uint64::new(10)), + }, + end_timestamp, + }; + assert!(proposal.assert_active_proposal().is_ok()); + + // Not active + proposal.status = ProposalStatus::VetoPeriod(end_timestamp.plus_seconds(10)); + assert_eq!( + proposal.assert_active_proposal().unwrap_err(), + VoteError::ProposalNotActive(ProposalStatus::VetoPeriod( + end_timestamp.plus_seconds(10) + )) + ); + } + + #[test] + fn create_proposal() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let storage = &mut deps.storage; + default_setup(storage); + + let end_timestamp = env.block.time.plus_seconds(100); + // Create one proposal + let proposal_id = SIMPLE_VOTING + .new_proposal( + storage, + end_timestamp, + &[Addr::unchecked("alice"), Addr::unchecked("bob")], + ) + .unwrap(); + assert_eq!(proposal_id, 1); + + let proposal = SIMPLE_VOTING + .load_proposal(storage, &env.block, proposal_id) + .unwrap(); + assert_eq!( + proposal, + ProposalInfo { + total_voters: 2, + votes_for: 0, + votes_against: 0, + status: ProposalStatus::Active, + config: VoteConfig { + threshold: Threshold::Majority {}, + veto_duration_seconds: None + }, + end_timestamp + } + ); + + // Create another proposal (already expired) + let proposal_id = SIMPLE_VOTING + .new_proposal( + storage, + env.block.time, + &[Addr::unchecked("alice"), Addr::unchecked("bob")], + ) + .unwrap(); + assert_eq!(proposal_id, 2); + + let proposal = SIMPLE_VOTING + .load_proposal(storage, &env.block, proposal_id) + .unwrap(); + assert_eq!( + proposal, + ProposalInfo { + total_voters: 2, + votes_for: 0, + votes_against: 0, + status: ProposalStatus::WaitingForCount, + config: VoteConfig { + threshold: Threshold::Majority {}, + veto_duration_seconds: None + }, + end_timestamp: env.block.time + } + ); + } + + #[test] + fn create_proposal_duplicate_friends() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let storage = &mut deps.storage; + default_setup(storage); + + let end_timestamp = env.block.time.plus_seconds(100); + + let err = SIMPLE_VOTING + .new_proposal( + storage, + end_timestamp, + &[Addr::unchecked("alice"), Addr::unchecked("alice")], + ) + .unwrap_err(); + assert_eq!(err, VoteError::DuplicateAddrs {}); + } + + #[test] + fn cancel_vote() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let storage = &mut deps.storage; + + default_setup(storage); + + let end_timestamp = env.block.time.plus_seconds(100); + // Create one proposal + let proposal_id = SIMPLE_VOTING + .new_proposal( + storage, + end_timestamp, + &[Addr::unchecked("alice"), Addr::unchecked("bob")], + ) + .unwrap(); + + SIMPLE_VOTING + .cancel_proposal(storage, &env.block, proposal_id) + .unwrap(); + + let proposal = SIMPLE_VOTING + .load_proposal(storage, &env.block, proposal_id) + .unwrap(); + assert_eq!( + proposal, + ProposalInfo { + total_voters: 2, + votes_for: 0, + votes_against: 0, + status: ProposalStatus::Finished(ProposalOutcome::Canceled), + config: VoteConfig { + threshold: Threshold::Majority {}, + veto_duration_seconds: None + }, + // Finish time here + end_timestamp: env.block.time + } + ); + + // Can't cancel during non-active + let err = SIMPLE_VOTING + .cancel_proposal(storage, &env.block, proposal_id) + .unwrap_err(); + assert_eq!( + err, + VoteError::ProposalNotActive(ProposalStatus::Finished(ProposalOutcome::Canceled)) + ); + } + + // Check it updates status when required + #[test] + fn load_proposal() { + let mut deps = mock_dependencies(); + let mut env = mock_env(); + let storage = &mut deps.storage; + setup( + storage, + &VoteConfig { + threshold: Threshold::Majority {}, + veto_duration_seconds: Some(Uint64::new(10)), + }, + ); + + let end_timestamp = env.block.time.plus_seconds(100); + let proposal_id = SIMPLE_VOTING + .new_proposal(storage, end_timestamp, &[Addr::unchecked("alice")]) + .unwrap(); + let proposal: ProposalInfo = SIMPLE_VOTING + .load_proposal(storage, &env.block, proposal_id) + .unwrap(); + assert_eq!(proposal.status, ProposalStatus::Active,); + + // Should auto-update to the veto + env.block.time = end_timestamp; + let proposal: ProposalInfo = SIMPLE_VOTING + .load_proposal(storage, &env.block, proposal_id) + .unwrap(); + assert_eq!( + proposal.status, + ProposalStatus::VetoPeriod(end_timestamp.plus_seconds(10)), + ); + + // Should update to the WaitingForCount + env.block.time = end_timestamp.plus_seconds(10); + let proposal: ProposalInfo = SIMPLE_VOTING + .load_proposal(storage, &env.block, proposal_id) + .unwrap(); + assert_eq!(proposal.status, ProposalStatus::WaitingForCount,); + + // Should update to the Finished + SIMPLE_VOTING + .count_votes(storage, &env.block, proposal_id) + .unwrap(); + let proposal: ProposalInfo = SIMPLE_VOTING + .load_proposal(storage, &env.block, proposal_id) + .unwrap(); + assert!(matches!(proposal.status, ProposalStatus::Finished(_))); + + SIMPLE_VOTING + .update_vote_config( + storage, + &VoteConfig { + threshold: Threshold::Majority {}, + veto_duration_seconds: None, + }, + ) + .unwrap(); + + let end_timestamp = env.block.time.plus_seconds(100); + let proposal_id = SIMPLE_VOTING + .new_proposal(storage, end_timestamp, &[Addr::unchecked("alice")]) + .unwrap(); + // Should auto-update to the waiting if not configured veto period + env.block.time = end_timestamp; + let proposal: ProposalInfo = SIMPLE_VOTING + .load_proposal(storage, &env.block, proposal_id) + .unwrap(); + assert_eq!(proposal.status, ProposalStatus::WaitingForCount,); + } + + #[test] + fn cast_vote() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let storage = &mut deps.storage; + default_setup(storage); + + let end_timestamp = env.block.time.plus_seconds(100); + let proposal_id = SIMPLE_VOTING + .new_proposal( + storage, + end_timestamp, + &[Addr::unchecked("alice"), Addr::unchecked("bob")], + ) + .unwrap(); + + // Alice vote + SIMPLE_VOTING + .cast_vote( + deps.as_mut().storage, + &env.block, + proposal_id, + &Addr::unchecked("alice"), + Vote { + vote: false, + memo: None, + }, + ) + .unwrap(); + let vote = SIMPLE_VOTING + .load_vote( + deps.as_ref().storage, + proposal_id, + &Addr::unchecked("alice"), + ) + .unwrap() + .unwrap(); + assert_eq!( + vote, + Vote { + vote: false, + memo: None + } + ); + let proposal = SIMPLE_VOTING + .load_proposal(deps.as_ref().storage, &env.block, proposal_id) + .unwrap(); + assert_eq!( + proposal, + ProposalInfo { + total_voters: 2, + votes_for: 0, + votes_against: 1, + status: ProposalStatus::Active, + config: VoteConfig { + threshold: Threshold::Majority {}, + veto_duration_seconds: None + }, + end_timestamp + } + ); + + // Bob votes + SIMPLE_VOTING + .cast_vote( + deps.as_mut().storage, + &env.block, + proposal_id, + &Addr::unchecked("bob"), + Vote { + vote: false, + memo: Some("memo".to_owned()), + }, + ) + .unwrap(); + let vote = SIMPLE_VOTING + .load_vote(deps.as_ref().storage, proposal_id, &Addr::unchecked("bob")) + .unwrap() + .unwrap(); + assert_eq!( + vote, + Vote { + vote: false, + memo: Some("memo".to_owned()) + } + ); + let proposal = SIMPLE_VOTING + .load_proposal(deps.as_ref().storage, &env.block, proposal_id) + .unwrap(); + assert_eq!( + proposal, + ProposalInfo { + total_voters: 2, + votes_for: 0, + votes_against: 2, + status: ProposalStatus::Active, + config: VoteConfig { + threshold: Threshold::Majority {}, + veto_duration_seconds: None + }, + end_timestamp + } + ); + + // re-cast votes(to the same vote) + SIMPLE_VOTING + .cast_vote( + deps.as_mut().storage, + &env.block, + proposal_id, + &Addr::unchecked("alice"), + Vote { + vote: false, + memo: None, + }, + ) + .unwrap(); + // unchanged + let proposal = SIMPLE_VOTING + .load_proposal(deps.as_ref().storage, &env.block, proposal_id) + .unwrap(); + assert_eq!( + proposal, + ProposalInfo { + total_voters: 2, + votes_for: 0, + votes_against: 2, + status: ProposalStatus::Active, + config: VoteConfig { + threshold: Threshold::Majority {}, + veto_duration_seconds: None + }, + end_timestamp + } + ); + + // re-cast votes(to the opposite vote) + SIMPLE_VOTING + .cast_vote( + deps.as_mut().storage, + &env.block, + proposal_id, + &Addr::unchecked("bob"), + Vote { + vote: true, + memo: None, + }, + ) + .unwrap(); + // unchanged + let proposal = SIMPLE_VOTING + .load_proposal(deps.as_ref().storage, &env.block, proposal_id) + .unwrap(); + assert_eq!( + proposal, + ProposalInfo { + total_voters: 2, + votes_for: 1, + votes_against: 1, + status: ProposalStatus::Active, + config: VoteConfig { + threshold: Threshold::Majority {}, + veto_duration_seconds: None + }, + end_timestamp + } + ); + } + + #[test] + fn invalid_cast_votes() { + let mut deps = mock_dependencies(); + let mut env = mock_env(); + let storage = &mut deps.storage; + setup( + storage, + &VoteConfig { + threshold: Threshold::Majority {}, + veto_duration_seconds: Some(Uint64::new(10)), + }, + ); + + let end_timestamp = env.block.time.plus_seconds(100); + let proposal_id = SIMPLE_VOTING + .new_proposal( + storage, + end_timestamp, + &[Addr::unchecked("alice"), Addr::unchecked("bob")], + ) + .unwrap(); + + // Stranger vote + let err = SIMPLE_VOTING + .cast_vote( + deps.as_mut().storage, + &env.block, + proposal_id, + &Addr::unchecked("stranger"), + Vote { + vote: false, + memo: None, + }, + ) + .unwrap_err(); + assert_eq!(err, VoteError::Unauthorized {}); + + // Vote during veto + env.block.time = end_timestamp; + + // Vote during veto + let err = SIMPLE_VOTING + .cast_vote( + deps.as_mut().storage, + &env.block, + proposal_id, + &Addr::unchecked("alice"), + Vote { + vote: false, + memo: None, + }, + ) + .unwrap_err(); + assert_eq!( + err, + VoteError::ProposalNotActive(ProposalStatus::VetoPeriod( + env.block.time.plus_seconds(10) + )) + ); + + env.block.time = env.block.time.plus_seconds(10); + + // Too late vote + let err = SIMPLE_VOTING + .cast_vote( + deps.as_mut().storage, + &env.block, + proposal_id, + &Addr::unchecked("alice"), + Vote { + vote: false, + memo: None, + }, + ) + .unwrap_err(); + assert_eq!( + err, + VoteError::ProposalNotActive(ProposalStatus::WaitingForCount) + ); + + // Post-finish votes + SIMPLE_VOTING + .count_votes(deps.as_mut().storage, &env.block, proposal_id) + .unwrap(); + let err = SIMPLE_VOTING + .cast_vote( + deps.as_mut().storage, + &env.block, + proposal_id, + &Addr::unchecked("alice"), + Vote { + vote: false, + memo: None, + }, + ) + .unwrap_err(); + assert_eq!( + err, + VoteError::ProposalNotActive(ProposalStatus::Finished(ProposalOutcome::Failed)) + ); + } + + #[test] + fn count_votes() { + let mut deps = mock_dependencies(); + let mut env = mock_env(); + let storage = &mut deps.storage; + default_setup(storage); + + // Failed proposal + let end_timestamp = env.block.time.plus_seconds(100); + let proposal_id = SIMPLE_VOTING + .new_proposal( + storage, + end_timestamp, + &[Addr::unchecked("alice"), Addr::unchecked("bob")], + ) + .unwrap(); + env.block.time = end_timestamp.plus_seconds(10); + SIMPLE_VOTING + .count_votes(storage, &env.block, proposal_id) + .unwrap(); + let proposal = SIMPLE_VOTING + .load_proposal(storage, &env.block, proposal_id) + .unwrap(); + assert_eq!( + proposal, + ProposalInfo { + total_voters: 2, + votes_for: 0, + votes_against: 0, + status: ProposalStatus::Finished(ProposalOutcome::Failed), + config: VoteConfig { + threshold: Threshold::Majority {}, + veto_duration_seconds: None + }, + end_timestamp: end_timestamp.plus_seconds(10) + } + ); + + // Succeeded proposal 2/3 majority + let end_timestamp = env.block.time.plus_seconds(100); + let proposal_id = SIMPLE_VOTING + .new_proposal( + storage, + end_timestamp, + &[ + Addr::unchecked("alice"), + Addr::unchecked("bob"), + Addr::unchecked("afk"), + ], + ) + .unwrap(); + SIMPLE_VOTING + .cast_vote( + storage, + &env.block, + proposal_id, + &Addr::unchecked("alice"), + Vote { + vote: true, + memo: None, + }, + ) + .unwrap(); + SIMPLE_VOTING + .cast_vote( + storage, + &env.block, + proposal_id, + &Addr::unchecked("bob"), + Vote { + vote: true, + memo: None, + }, + ) + .unwrap(); + env.block.time = end_timestamp; + SIMPLE_VOTING + .count_votes(storage, &env.block, proposal_id) + .unwrap(); + let proposal = SIMPLE_VOTING + .load_proposal(storage, &env.block, proposal_id) + .unwrap(); + assert_eq!( + proposal, + ProposalInfo { + total_voters: 3, + votes_for: 2, + votes_against: 0, + status: ProposalStatus::Finished(ProposalOutcome::Passed), + config: VoteConfig { + threshold: Threshold::Majority {}, + veto_duration_seconds: None + }, + end_timestamp + } + ); + + // Succeeded proposal 1/2 50% Decimal + SIMPLE_VOTING + .update_vote_config( + storage, + &VoteConfig { + threshold: Threshold::Percentage(Decimal::percent(50)), + veto_duration_seconds: None, + }, + ) + .unwrap(); + let end_timestamp = env.block.time.plus_seconds(100); + let proposal_id = SIMPLE_VOTING + .new_proposal( + storage, + end_timestamp, + &[Addr::unchecked("alice"), Addr::unchecked("bob")], + ) + .unwrap(); + SIMPLE_VOTING + .cast_vote( + storage, + &env.block, + proposal_id, + &Addr::unchecked("alice"), + Vote { + vote: true, + memo: None, + }, + ) + .unwrap(); + + env.block.time = end_timestamp; + SIMPLE_VOTING + .count_votes(storage, &env.block, proposal_id) + .unwrap(); + let proposal = SIMPLE_VOTING + .load_proposal(storage, &env.block, proposal_id) + .unwrap(); + assert_eq!( + proposal, + ProposalInfo { + total_voters: 2, + votes_for: 1, + votes_against: 0, + status: ProposalStatus::Finished(ProposalOutcome::Passed), + config: VoteConfig { + threshold: Threshold::Percentage(Decimal::percent(50)), + veto_duration_seconds: None + }, + end_timestamp + } + ); + } +} diff --git a/modules/contracts/apps/challenge/.cargo/config b/modules/contracts/apps/challenge/.cargo/config new file mode 100644 index 0000000000..c9a655b443 --- /dev/null +++ b/modules/contracts/apps/challenge/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema --features schema" diff --git a/modules/contracts/apps/challenge/Cargo.toml b/modules/contracts/apps/challenge/Cargo.toml index 6a9ead3c22..118adb7a22 100644 --- a/modules/contracts/apps/challenge/Cargo.toml +++ b/modules/contracts/apps/challenge/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "challenge-app" -version = "0.1.0" +version = "0.2.0" authors = [ "CyberHoward ", "Adair ", @@ -42,14 +42,11 @@ cw-storage-plus = { workspace = true } thiserror = { workspace = true } schemars = { workspace = true } cw-asset = { workspace = true } -cw-utils = { workspace = true } abstract-core = { workspace = true } abstract-app = { workspace = true } abstract-sdk = { workspace = true } -chrono = { workspace = true, default-features = false } - # Dependencies for interface abstract-interface = { workspace = true, optional = true } cw-orch = { workspace = true, optional = true } diff --git a/modules/contracts/apps/challenge/README.md b/modules/contracts/apps/challenge/README.md index e69de29bb2..263dd3fd62 100644 --- a/modules/contracts/apps/challenge/README.md +++ b/modules/contracts/apps/challenge/README.md @@ -0,0 +1,28 @@ +# Challenge App + +The Challenge App Module is used to create challenges with friends and motivate completing challenges by striking admin with chosen asset in case challenge got failed. + +## Features +- Admin of this contract can create challenge + - Name and description of the challenge + - List of friends(either by address or abstract account id) that have a voting power for failing a challenge + - Asset for striking + - Striking strategy: + - Amount per friend (per_friend) + - Split amount between friends (split) + - Challenge duration + - Proposal duration + - Strikes limit (max amount of times admin getting striked for failing this challenge) +- Friends can vote on challenges + - When friend votes on a challenge new proposal will get created + - During proposal period other friends can vote + - After proposal period (and veto period if configured) anyone can execute `count_votes` to count votes and in case votes for punish passed threshold - strike an admin +- During veto period admin can veto this vote +- Between proposals admin can edit list of friends for this challenge + +## Installation + +To use the Challenge App Module in your Rust project, add the following dependency to your `Cargo.toml`: +```toml +challenge-app = { git = "https://github.com/AbstractSDK/abstract.git", tag = "v0.19.0", default-features = false } +``` \ No newline at end of file diff --git a/modules/contracts/apps/challenge/schema/execute_msg.json b/modules/contracts/apps/challenge/schema/execute_msg.json index 7c8695c94a..8ca01944fe 100644 --- a/modules/contracts/apps/challenge/schema/execute_msg.json +++ b/modules/contracts/apps/challenge/schema/execute_msg.json @@ -57,20 +57,56 @@ } ], "definitions": { - "AnsAsset": { + "AccountId": { + "description": "Unique identifier for an account. On each chain this is unique.", "type": "object", "required": [ - "amount", - "name" + "seq", + "trace" ], "properties": { - "amount": { - "$ref": "#/definitions/Uint128" + "seq": { + "description": "Unique identifier for the accounts create on a local chain. Is reused when creating an interchain account.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 }, - "name": { - "$ref": "#/definitions/AssetEntry" + "trace": { + "description": "Sequence of the chain that triggered the IBC account creation `AccountTrace::Local` if the account was created locally Example: Account created on Juno which has an abstract interchain account on Osmosis, which in turn creates an interchain account on Terra -> `AccountTrace::Remote(vec![\"juno\", \"osmosis\"])`", + "allOf": [ + { + "$ref": "#/definitions/AccountTrace" + } + ] + } + }, + "additionalProperties": false + }, + "AccountTrace": { + "description": "The identifier of chain that triggered the account creation", + "oneOf": [ + { + "type": "string", + "enum": [ + "local" + ] + }, + { + "type": "object", + "required": [ + "remote" + ], + "properties": { + "remote": { + "type": "array", + "items": { + "$ref": "#/definitions/ChainName" + } + } + }, + "additionalProperties": false } - } + ] }, "AssetEntry": { "description": "An unchecked ANS asset entry. This is a string that is formatted as `src_chain>[intermediate_chain>]asset_name`", @@ -112,36 +148,20 @@ "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" }, + "ChainName": { + "description": "The name of a chain, aka the chain-id without the post-fix number. ex. `cosmoshub-4` -> `cosmoshub`, `juno-1` -> `juno`", + "type": "string" + }, "ChallengeEntryUpdate": { "description": "Only this struct and these fields are allowed to be updated. The status cannot be externally updated, it is updated by the contract.", "type": "object", "properties": { - "collateral": { - "anyOf": [ - { - "$ref": "#/definitions/AnsAsset" - }, - { - "type": "null" - } - ] - }, "description": { "type": [ "string", "null" ] }, - "end": { - "anyOf": [ - { - "$ref": "#/definitions/Timestamp" - }, - { - "type": "null" - } - ] - }, "name": { "type": [ "string", @@ -152,9 +172,37 @@ "additionalProperties": false }, "ChallengeExecuteMsg": { - "description": "App execute messages", + "description": "Challenge execute messages", "oneOf": [ { + "description": "Update challenge config", + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "required": [ + "new_vote_config" + ], + "properties": { + "new_vote_config": { + "description": "New config for vote", + "allOf": [ + { + "$ref": "#/definitions/VoteConfig" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Create new challenge", "type": "object", "required": [ "create_challenge" @@ -167,7 +215,12 @@ ], "properties": { "challenge_req": { - "$ref": "#/definitions/ChallengeRequest" + "description": "New challenge arguments", + "allOf": [ + { + "$ref": "#/definitions/ChallengeRequest" + } + ] } }, "additionalProperties": false @@ -176,6 +229,7 @@ "additionalProperties": false }, { + "description": "Update existing challenge", "type": "object", "required": [ "update_challenge" @@ -189,9 +243,15 @@ ], "properties": { "challenge": { - "$ref": "#/definitions/ChallengeEntryUpdate" + "description": "Updates to this challenge", + "allOf": [ + { + "$ref": "#/definitions/ChallengeEntryUpdate" + } + ] }, "challenge_id": { + "description": "Id of the challenge to update", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -203,6 +263,7 @@ "additionalProperties": false }, { + "description": "Cancel challenge", "type": "object", "required": [ "cancel_challenge" @@ -215,6 +276,7 @@ ], "properties": { "challenge_id": { + "description": "Challenge Id to cancel", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -226,6 +288,7 @@ "additionalProperties": false }, { + "description": "Update list of friends for challenge", "type": "object", "required": [ "update_friends_for_challenge" @@ -240,18 +303,25 @@ ], "properties": { "challenge_id": { + "description": "Id of the challenge to update", "type": "integer", "format": "uint64", "minimum": 0.0 }, "friends": { + "description": "List of added or removed Friends", "type": "array", "items": { "$ref": "#/definitions/Friend_for_String" } }, "op_kind": { - "$ref": "#/definitions/UpdateFriendsOpKind" + "description": "Kind of operation: add or remove friends", + "allOf": [ + { + "$ref": "#/definitions/UpdateFriendsOpKind" + } + ] } }, "additionalProperties": false @@ -260,27 +330,31 @@ "additionalProperties": false }, { + "description": "Cast vote as a friend", "type": "object", "required": [ - "daily_check_in" + "cast_vote" ], "properties": { - "daily_check_in": { + "cast_vote": { "type": "object", "required": [ - "challenge_id" + "challenge_id", + "vote_to_punish" ], "properties": { "challenge_id": { + "description": "Challenge Id to cast vote on", "type": "integer", "format": "uint64", "minimum": 0.0 }, - "metadata": { - "description": "metadata can be added for extra description of the check-in. For example, if the check-in is a photo, the metadata can be a link to the photo.", - "type": [ - "string", - "null" + "vote_to_punish": { + "description": "Wether voter thinks admin deserves punishment", + "allOf": [ + { + "$ref": "#/definitions/Vote" + } ] } }, @@ -290,30 +364,48 @@ "additionalProperties": false }, { + "description": "Count votes for challenge id", "type": "object", "required": [ - "cast_vote" + "count_votes" ], "properties": { - "cast_vote": { + "count_votes": { "type": "object", "required": [ - "challenge_id", - "vote" + "challenge_id" ], "properties": { "challenge_id": { + "description": "Challenge Id for counting votes", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Veto the last vote", + "type": "object", + "required": [ + "veto" + ], + "properties": { + "veto": { + "type": "object", + "required": [ + "challenge_id" + ], + "properties": { + "challenge_id": { + "description": "Challenge id to do the veto", "type": "integer", "format": "uint64", "minimum": 0.0 - }, - "vote": { - "description": "If the vote.approval is None, we assume the voter approves, and the contract will internally set the approval field to Some(true). This is because we assume that if a friend didn't vote, the friend approves, otherwise the voter would Vote with approval set to Some(false).", - "allOf": [ - { - "$ref": "#/definitions/Vote_for_String" - } - ] } }, "additionalProperties": false @@ -324,44 +416,89 @@ ] }, "ChallengeRequest": { + "description": "Arguments for new challenge", "type": "object", "required": [ - "collateral", - "description", - "end", - "name" + "challenge_duration_seconds", + "init_friends", + "name", + "proposal_duration_seconds", + "strike_asset", + "strike_strategy" ], "properties": { - "collateral": { - "$ref": "#/definitions/AnsAsset" + "challenge_duration_seconds": { + "description": "In what duration challenge should end", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] }, "description": { - "type": "string" + "description": "Description of the challenge", + "type": [ + "string", + "null" + ] }, - "end": { - "$ref": "#/definitions/DurationChoice" + "init_friends": { + "description": "Initial list of friends", + "type": "array", + "items": { + "$ref": "#/definitions/Friend_for_String" + } }, "name": { + "description": "Name of challenge", "type": "string" + }, + "proposal_duration_seconds": { + "description": "Duration set for each proposal Proposals starts after one vote initiated by any of the friends", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "strike_asset": { + "description": "Asset for punishment for failing a challenge", + "allOf": [ + { + "$ref": "#/definitions/AssetEntry" + } + ] + }, + "strike_strategy": { + "description": "How strike will get distributed between friends", + "allOf": [ + { + "$ref": "#/definitions/StrikeStrategy" + } + ] + }, + "strikes_limit": { + "description": "Strike limit, defaults to 1", + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 } }, "additionalProperties": false }, - "DurationChoice": { - "type": "string", - "enum": [ - "week", - "month", - "quarter", - "year", - "one_hundred_years" - ] + "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" }, "Empty": { "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", "type": "object" }, - "Friend_for_String": { + "FriendByAddr_for_String": { + "description": "Friend by address", "type": "object", "required": [ "address", @@ -369,14 +506,47 @@ ], "properties": { "address": { + "description": "Address of the friend", "type": "string" }, "name": { + "description": "Name of the friend", "type": "string" } }, "additionalProperties": false }, + "Friend_for_String": { + "description": "Friend object", + "oneOf": [ + { + "description": "Friend with address and a name", + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "$ref": "#/definitions/FriendByAddr_for_String" + } + }, + "additionalProperties": false + }, + { + "description": "Abstract Account Id of the friend", + "type": "object", + "required": [ + "abstract_account" + ], + "properties": { + "abstract_account": { + "$ref": "#/definitions/AccountId" + } + }, + "additionalProperties": false + } + ] + }, "IbcResponseMsg": { "description": "IbcResponseMsg should be de/serialized under `IbcCallback()` variant in a ExecuteMsg", "type": "object", @@ -424,11 +594,63 @@ } ] }, - "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": [ + "StrikeStrategy": { + "description": "Strategy for striking the admin", + "oneOf": [ { - "$ref": "#/definitions/Uint64" + "description": "Split amount between friends", + "type": "object", + "required": [ + "split" + ], + "properties": { + "split": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + { + "description": "Amount for every friend", + "type": "object", + "required": [ + "per_friend" + ], + "properties": { + "per_friend": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + ] + }, + "Threshold": { + "oneOf": [ + { + "type": "object", + "required": [ + "majority" + ], + "properties": { + "majority": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false } ] }, @@ -441,39 +663,75 @@ "type": "string" }, "UpdateFriendsOpKind": { - "type": "string", - "enum": [ - "add", - "remove" + "oneOf": [ + { + "type": "object", + "required": [ + "add" + ], + "properties": { + "add": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "remove" + ], + "properties": { + "remove": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } ] }, - "Vote_for_String": { + "Vote": { + "description": "Vote struct", "type": "object", "required": [ - "voter" + "vote" ], "properties": { - "approval": { - "description": "The vote result", + "memo": { + "description": "memo for the vote", "type": [ - "boolean", + "string", "null" ] }, - "for_check_in": { - "description": "Correlates to the last_checked_in field of the CheckIn struct.", + "vote": { + "description": "true: Vote for false: Vote against", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "VoteConfig": { + "type": "object", + "required": [ + "threshold" + ], + "properties": { + "threshold": { + "$ref": "#/definitions/Threshold" + }, + "veto_duration_seconds": { + "description": "Veto duration after the first vote None disables veto", "anyOf": [ { - "$ref": "#/definitions/Timestamp" + "$ref": "#/definitions/Uint64" }, { "type": "null" } ] - }, - "voter": { - "description": "The address of the voter", - "type": "string" } }, "additionalProperties": false diff --git a/modules/contracts/apps/challenge/schema/instantiate_msg.json b/modules/contracts/apps/challenge/schema/instantiate_msg.json index f86973b4ec..ca6136dd69 100644 --- a/modules/contracts/apps/challenge/schema/instantiate_msg.json +++ b/modules/contracts/apps/challenge/schema/instantiate_msg.json @@ -19,7 +19,7 @@ "description": "custom instantiate msg", "allOf": [ { - "$ref": "#/definitions/Empty" + "$ref": "#/definitions/ChallengeInstantiateMsg" } ] } @@ -43,9 +43,83 @@ }, "additionalProperties": false }, - "Empty": { - "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", - "type": "object" + "ChallengeInstantiateMsg": { + "description": "Challenge instantiate message", + "type": "object", + "required": [ + "vote_config" + ], + "properties": { + "vote_config": { + "description": "Config for [`SimpleVoting`](abstract_core::objects::voting::SimpleVoting) object", + "allOf": [ + { + "$ref": "#/definitions/VoteConfig" + } + ] + } + }, + "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" + }, + "Threshold": { + "oneOf": [ + { + "type": "object", + "required": [ + "majority" + ], + "properties": { + "majority": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + } + ] + }, + "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" + }, + "VoteConfig": { + "type": "object", + "required": [ + "threshold" + ], + "properties": { + "threshold": { + "$ref": "#/definitions/Threshold" + }, + "veto_duration_seconds": { + "description": "Veto duration after the first vote None disables veto", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false } } } diff --git a/modules/contracts/apps/challenge/schema/module-schema.json b/modules/contracts/apps/challenge/schema/module-schema.json index 535dbac162..5077fe8549 100644 --- a/modules/contracts/apps/challenge/schema/module-schema.json +++ b/modules/contracts/apps/challenge/schema/module-schema.json @@ -1,19 +1,123 @@ { "contract_name": "module-schema", - "contract_version": "0.18.0", + "contract_version": "0.19.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "InstantiateMsg", - "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", - "type": "object" + "description": "Challenge instantiate message", + "type": "object", + "required": [ + "vote_config" + ], + "properties": { + "vote_config": { + "description": "Config for [`SimpleVoting`](abstract_core::objects::voting::SimpleVoting) object", + "allOf": [ + { + "$ref": "#/definitions/VoteConfig" + } + ] + } + }, + "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" + }, + "Threshold": { + "oneOf": [ + { + "type": "object", + "required": [ + "majority" + ], + "properties": { + "majority": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + } + ] + }, + "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" + }, + "VoteConfig": { + "type": "object", + "required": [ + "threshold" + ], + "properties": { + "threshold": { + "$ref": "#/definitions/Threshold" + }, + "veto_duration_seconds": { + "description": "Veto duration after the first vote None disables veto", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } }, "execute": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ExecuteMsg", - "description": "App execute messages", + "description": "Challenge execute messages", "oneOf": [ { + "description": "Update challenge config", + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "required": [ + "new_vote_config" + ], + "properties": { + "new_vote_config": { + "description": "New config for vote", + "allOf": [ + { + "$ref": "#/definitions/VoteConfig" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Create new challenge", "type": "object", "required": [ "create_challenge" @@ -26,7 +130,12 @@ ], "properties": { "challenge_req": { - "$ref": "#/definitions/ChallengeRequest" + "description": "New challenge arguments", + "allOf": [ + { + "$ref": "#/definitions/ChallengeRequest" + } + ] } }, "additionalProperties": false @@ -35,6 +144,7 @@ "additionalProperties": false }, { + "description": "Update existing challenge", "type": "object", "required": [ "update_challenge" @@ -48,9 +158,15 @@ ], "properties": { "challenge": { - "$ref": "#/definitions/ChallengeEntryUpdate" + "description": "Updates to this challenge", + "allOf": [ + { + "$ref": "#/definitions/ChallengeEntryUpdate" + } + ] }, "challenge_id": { + "description": "Id of the challenge to update", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -62,6 +178,7 @@ "additionalProperties": false }, { + "description": "Cancel challenge", "type": "object", "required": [ "cancel_challenge" @@ -74,6 +191,7 @@ ], "properties": { "challenge_id": { + "description": "Challenge Id to cancel", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -85,6 +203,7 @@ "additionalProperties": false }, { + "description": "Update list of friends for challenge", "type": "object", "required": [ "update_friends_for_challenge" @@ -99,18 +218,25 @@ ], "properties": { "challenge_id": { + "description": "Id of the challenge to update", "type": "integer", "format": "uint64", "minimum": 0.0 }, "friends": { + "description": "List of added or removed Friends", "type": "array", "items": { "$ref": "#/definitions/Friend_for_String" } }, "op_kind": { - "$ref": "#/definitions/UpdateFriendsOpKind" + "description": "Kind of operation: add or remove friends", + "allOf": [ + { + "$ref": "#/definitions/UpdateFriendsOpKind" + } + ] } }, "additionalProperties": false @@ -119,27 +245,31 @@ "additionalProperties": false }, { + "description": "Cast vote as a friend", "type": "object", "required": [ - "daily_check_in" + "cast_vote" ], "properties": { - "daily_check_in": { + "cast_vote": { "type": "object", "required": [ - "challenge_id" + "challenge_id", + "vote_to_punish" ], "properties": { "challenge_id": { + "description": "Challenge Id to cast vote on", "type": "integer", "format": "uint64", "minimum": 0.0 }, - "metadata": { - "description": "metadata can be added for extra description of the check-in. For example, if the check-in is a photo, the metadata can be a link to the photo.", - "type": [ - "string", - "null" + "vote_to_punish": { + "description": "Wether voter thinks admin deserves punishment", + "allOf": [ + { + "$ref": "#/definitions/Vote" + } ] } }, @@ -149,30 +279,48 @@ "additionalProperties": false }, { + "description": "Count votes for challenge id", "type": "object", "required": [ - "cast_vote" + "count_votes" ], "properties": { - "cast_vote": { + "count_votes": { "type": "object", "required": [ - "challenge_id", - "vote" + "challenge_id" ], "properties": { "challenge_id": { + "description": "Challenge Id for counting votes", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Veto the last vote", + "type": "object", + "required": [ + "veto" + ], + "properties": { + "veto": { + "type": "object", + "required": [ + "challenge_id" + ], + "properties": { + "challenge_id": { + "description": "Challenge id to do the veto", "type": "integer", "format": "uint64", "minimum": 0.0 - }, - "vote": { - "description": "If the vote.approval is None, we assume the voter approves, and the contract will internally set the approval field to Some(true). This is because we assume that if a friend didn't vote, the friend approves, otherwise the voter would Vote with approval set to Some(false).", - "allOf": [ - { - "$ref": "#/definitions/Vote_for_String" - } - ] } }, "additionalProperties": false @@ -182,55 +330,75 @@ } ], "definitions": { - "AnsAsset": { + "AccountId": { + "description": "Unique identifier for an account. On each chain this is unique.", "type": "object", "required": [ - "amount", - "name" + "seq", + "trace" ], "properties": { - "amount": { - "$ref": "#/definitions/Uint128" + "seq": { + "description": "Unique identifier for the accounts create on a local chain. Is reused when creating an interchain account.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 }, - "name": { - "$ref": "#/definitions/AssetEntry" + "trace": { + "description": "Sequence of the chain that triggered the IBC account creation `AccountTrace::Local` if the account was created locally Example: Account created on Juno which has an abstract interchain account on Osmosis, which in turn creates an interchain account on Terra -> `AccountTrace::Remote(vec![\"juno\", \"osmosis\"])`", + "allOf": [ + { + "$ref": "#/definitions/AccountTrace" + } + ] } - } + }, + "additionalProperties": false + }, + "AccountTrace": { + "description": "The identifier of chain that triggered the account creation", + "oneOf": [ + { + "type": "string", + "enum": [ + "local" + ] + }, + { + "type": "object", + "required": [ + "remote" + ], + "properties": { + "remote": { + "type": "array", + "items": { + "$ref": "#/definitions/ChainName" + } + } + }, + "additionalProperties": false + } + ] }, "AssetEntry": { "description": "An unchecked ANS asset entry. This is a string that is formatted as `src_chain>[intermediate_chain>]asset_name`", "type": "string" }, + "ChainName": { + "description": "The name of a chain, aka the chain-id without the post-fix number. ex. `cosmoshub-4` -> `cosmoshub`, `juno-1` -> `juno`", + "type": "string" + }, "ChallengeEntryUpdate": { "description": "Only this struct and these fields are allowed to be updated. The status cannot be externally updated, it is updated by the contract.", "type": "object", "properties": { - "collateral": { - "anyOf": [ - { - "$ref": "#/definitions/AnsAsset" - }, - { - "type": "null" - } - ] - }, "description": { "type": [ "string", "null" ] }, - "end": { - "anyOf": [ - { - "$ref": "#/definitions/Timestamp" - }, - { - "type": "null" - } - ] - }, "name": { "type": [ "string", @@ -241,40 +409,85 @@ "additionalProperties": false }, "ChallengeRequest": { + "description": "Arguments for new challenge", "type": "object", "required": [ - "collateral", - "description", - "end", - "name" + "challenge_duration_seconds", + "init_friends", + "name", + "proposal_duration_seconds", + "strike_asset", + "strike_strategy" ], "properties": { - "collateral": { - "$ref": "#/definitions/AnsAsset" + "challenge_duration_seconds": { + "description": "In what duration challenge should end", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] }, "description": { - "type": "string" + "description": "Description of the challenge", + "type": [ + "string", + "null" + ] }, - "end": { - "$ref": "#/definitions/DurationChoice" + "init_friends": { + "description": "Initial list of friends", + "type": "array", + "items": { + "$ref": "#/definitions/Friend_for_String" + } }, "name": { + "description": "Name of challenge", "type": "string" + }, + "proposal_duration_seconds": { + "description": "Duration set for each proposal Proposals starts after one vote initiated by any of the friends", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "strike_asset": { + "description": "Asset for punishment for failing a challenge", + "allOf": [ + { + "$ref": "#/definitions/AssetEntry" + } + ] + }, + "strike_strategy": { + "description": "How strike will get distributed between friends", + "allOf": [ + { + "$ref": "#/definitions/StrikeStrategy" + } + ] + }, + "strikes_limit": { + "description": "Strike limit, defaults to 1", + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 } }, "additionalProperties": false }, - "DurationChoice": { - "type": "string", - "enum": [ - "week", - "month", - "quarter", - "year", - "one_hundred_years" - ] + "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" }, - "Friend_for_String": { + "FriendByAddr_for_String": { + "description": "Friend by address", "type": "object", "required": [ "address", @@ -282,19 +495,104 @@ ], "properties": { "address": { + "description": "Address of the friend", "type": "string" }, "name": { + "description": "Name of the friend", "type": "string" } }, "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": [ + "Friend_for_String": { + "description": "Friend object", + "oneOf": [ + { + "description": "Friend with address and a name", + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "$ref": "#/definitions/FriendByAddr_for_String" + } + }, + "additionalProperties": false + }, + { + "description": "Abstract Account Id of the friend", + "type": "object", + "required": [ + "abstract_account" + ], + "properties": { + "abstract_account": { + "$ref": "#/definitions/AccountId" + } + }, + "additionalProperties": false + } + ] + }, + "StrikeStrategy": { + "description": "Strategy for striking the admin", + "oneOf": [ + { + "description": "Split amount between friends", + "type": "object", + "required": [ + "split" + ], + "properties": { + "split": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + { + "description": "Amount for every friend", + "type": "object", + "required": [ + "per_friend" + ], + "properties": { + "per_friend": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + ] + }, + "Threshold": { + "oneOf": [ + { + "type": "object", + "required": [ + "majority" + ], + "properties": { + "majority": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { - "$ref": "#/definitions/Uint64" + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false } ] }, @@ -307,39 +605,75 @@ "type": "string" }, "UpdateFriendsOpKind": { - "type": "string", - "enum": [ - "add", - "remove" - ] - }, - "Vote_for_String": { + "oneOf": [ + { + "type": "object", + "required": [ + "add" + ], + "properties": { + "add": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "remove" + ], + "properties": { + "remove": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Vote": { + "description": "Vote struct", "type": "object", "required": [ - "voter" + "vote" ], "properties": { - "approval": { - "description": "The vote result", + "memo": { + "description": "memo for the vote", "type": [ - "boolean", + "string", "null" ] }, - "for_check_in": { - "description": "Correlates to the last_checked_in field of the CheckIn struct.", + "vote": { + "description": "true: Vote for false: Vote against", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "VoteConfig": { + "type": "object", + "required": [ + "threshold" + ], + "properties": { + "threshold": { + "$ref": "#/definitions/Threshold" + }, + "veto_duration_seconds": { + "description": "Veto duration after the first vote None disables veto", "anyOf": [ { - "$ref": "#/definitions/Timestamp" + "$ref": "#/definitions/Uint64" }, { "type": "null" } ] - }, - "voter": { - "description": "The address of the voter", - "type": "string" } }, "additionalProperties": false @@ -349,8 +683,10 @@ "query": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "QueryMsg", + "description": "Challenge query messages", "oneOf": [ { + "description": "Get challenge info, will return null if there was no challenge by Id", "type": "object", "required": [ "challenge" @@ -363,6 +699,7 @@ ], "properties": { "challenge_id": { + "description": "Id of requested challenge", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -374,6 +711,7 @@ "additionalProperties": false }, { + "description": "Get list of challenges", "type": "object", "required": [ "challenges" @@ -381,18 +719,22 @@ "properties": { "challenges": { "type": "object", - "required": [ - "limit", - "start_after" - ], "properties": { "limit": { - "type": "integer", - "format": "uint32", + "description": "Max amount of challenges in response", + "type": [ + "integer", + "null" + ], + "format": "uint64", "minimum": 0.0 }, "start_after": { - "type": "integer", + "description": "start after challenge Id", + "type": [ + "integer", + "null" + ], "format": "uint64", "minimum": 0.0 } @@ -403,6 +745,7 @@ "additionalProperties": false }, { + "description": "List of friends by Id", "type": "object", "required": [ "friends" @@ -415,6 +758,7 @@ ], "properties": { "challenge_id": { + "description": "Id of requested challenge", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -426,21 +770,37 @@ "additionalProperties": false }, { + "description": "Get vote of friend", "type": "object", "required": [ - "check_ins" + "vote" ], "properties": { - "check_ins": { + "vote": { "type": "object", "required": [ - "challenge_id" + "challenge_id", + "voter_addr" ], "properties": { "challenge_id": { + "description": "Id of requested challenge", "type": "integer", "format": "uint64", "minimum": 0.0 + }, + "proposal_id": { + "description": "Proposal id of previous proposal Providing None requests last proposal results", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "voter_addr": { + "description": "Addr of the friend", + "type": "string" } }, "additionalProperties": false @@ -449,31 +809,95 @@ "additionalProperties": false }, { + "description": "Get votes of challenge", "type": "object", "required": [ - "vote" + "votes" ], "properties": { - "vote": { + "votes": { "type": "object", "required": [ - "challenge_id", - "last_check_in", - "voter_addr" + "challenge_id" ], "properties": { "challenge_id": { + "description": "Id of requested challenge", "type": "integer", "format": "uint64", "minimum": 0.0 }, - "last_check_in": { + "limit": { + "description": "Max amount of challenges in response", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "proposal_id": { + "description": "Proposal id of previous proposal Providing None requests last proposal results", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "start_after": { + "description": "start after Addr", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Get results of previous votes for this challenge", + "type": "object", + "required": [ + "proposals" + ], + "properties": { + "proposals": { + "type": "object", + "required": [ + "challenge_id" + ], + "properties": { + "challenge_id": { + "description": "Challenge Id for previous votes", "type": "integer", "format": "uint64", "minimum": 0.0 }, - "voter_addr": { - "type": "string" + "limit": { + "description": "Max amount of proposals in response", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "start_after": { + "description": "start after ProposalId", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false @@ -481,7 +905,13 @@ }, "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" + } + } }, "migrate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -494,12 +924,14 @@ "challenge": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ChallengeResponse", + "description": "Response for challenge query", "type": "object", "properties": { "challenge": { + "description": "Challenge info, will return null if there was no challenge by Id", "anyOf": [ { - "$ref": "#/definitions/ChallengeEntry" + "$ref": "#/definitions/ChallengeEntryResponse" }, { "type": "null" @@ -509,117 +941,264 @@ }, "additionalProperties": false, "definitions": { - "AnsAsset": { + "AdminStrikes": { "type": "object", "required": [ - "amount", - "name" + "limit", + "num_strikes" ], "properties": { - "amount": { - "$ref": "#/definitions/Uint128" + "limit": { + "description": "When num_strikes reached the limit, the challenge will be cancelled.", + "type": "integer", + "format": "uint8", + "minimum": 0.0 }, - "name": { - "$ref": "#/definitions/AssetEntry" + "num_strikes": { + "description": "The number of strikes the admin has incurred.", + "type": "integer", + "format": "uint8", + "minimum": 0.0 } - } + }, + "additionalProperties": false }, "AssetEntry": { "description": "An unchecked ANS asset entry. This is a string that is formatted as `src_chain>[intermediate_chain>]asset_name`", "type": "string" }, - "ChallengeEntry": { + "ChallengeEntryResponse": { + "description": "Response struct for challenge entry", "type": "object", "required": [ "admin_strikes", - "collateral", + "challenge_id", "description", - "end", + "end_timestamp", "name", - "status", - "total_check_ins" + "proposal_duration_seconds", + "strike_asset", + "strike_strategy" ], "properties": { + "active_proposal": { + "description": "Current active proposal", + "anyOf": [ + { + "$ref": "#/definitions/ProposalInfo" + }, + { + "type": "null" + } + ] + }, "admin_strikes": { - "$ref": "#/definitions/StrikeConfig" + "description": "State of strikes of admin for this challenge", + "allOf": [ + { + "$ref": "#/definitions/AdminStrikes" + } + ] }, - "collateral": { - "$ref": "#/definitions/AnsAsset" + "challenge_id": { + "description": "Id of the challenge,", + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, "description": { + "description": "Description of the challenge", "type": "string" }, - "end": { - "$ref": "#/definitions/Timestamp" + "end_timestamp": { + "description": "When challenge ends", + "allOf": [ + { + "$ref": "#/definitions/Timestamp" + } + ] }, "name": { + "description": "Name of challenge", "type": "string" }, + "proposal_duration_seconds": { + "description": "Proposal duration in seconds", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "strike_asset": { + "description": "Asset for punishment for failing a challenge", + "allOf": [ + { + "$ref": "#/definitions/AssetEntry" + } + ] + }, + "strike_strategy": { + "description": "How strike will get distributed between friends", + "allOf": [ + { + "$ref": "#/definitions/StrikeStrategy" + } + ] + } + }, + "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" + }, + "ProposalInfo": { + "type": "object", + "required": [ + "config", + "end_timestamp", + "status", + "total_voters", + "votes_against", + "votes_for" + ], + "properties": { + "config": { + "description": "Config it was created with For cases config got changed during voting", + "allOf": [ + { + "$ref": "#/definitions/VoteConfig" + } + ] + }, + "end_timestamp": { + "$ref": "#/definitions/Timestamp" + }, "status": { - "$ref": "#/definitions/ChallengeStatus" + "$ref": "#/definitions/ProposalStatus" + }, + "total_voters": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "votes_against": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 }, - "total_check_ins": { + "votes_for": { "type": "integer", - "format": "uint128", + "format": "uint32", "minimum": 0.0 } }, "additionalProperties": false }, - "ChallengeStatus": { - "description": "The status of a challenge. This can be used to trigger an automated Croncat job based on the value of the status", + "ProposalOutcome": { + "type": "string", + "enum": [ + "passed", + "failed", + "canceled", + "vetoed" + ] + }, + "ProposalStatus": { "oneOf": [ { - "description": "The challenge has not been initialized yet. This is the default state.", "type": "string", "enum": [ - "uninitialized" + "active", + "waiting_for_count" ] }, { - "description": "The challenge is active and can be voted on.", - "type": "string", - "enum": [ - "active" - ] + "type": "object", + "required": [ + "veto_period" + ], + "properties": { + "veto_period": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false }, { - "description": "The challenge was cancelled and no collateral was paid out.", - "type": "string", - "enum": [ - "cancelled" - ] + "type": "object", + "required": [ + "finished" + ], + "properties": { + "finished": { + "$ref": "#/definitions/ProposalOutcome" + } + }, + "additionalProperties": false + } + ] + }, + "StrikeStrategy": { + "description": "Strategy for striking the admin", + "oneOf": [ + { + "description": "Split amount between friends", + "type": "object", + "required": [ + "split" + ], + "properties": { + "split": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false }, { - "description": "The challenge has pased the end time.", - "type": "string", - "enum": [ - "over" - ] + "description": "Amount for every friend", + "type": "object", + "required": [ + "per_friend" + ], + "properties": { + "per_friend": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false } ] }, - "StrikeConfig": { - "type": "object", - "required": [ - "limit", - "num_strikes" - ], - "properties": { - "limit": { - "description": "When num_strikes reached the limit, the challenge will be cancelled.", - "type": "integer", - "format": "uint8", - "minimum": 0.0 + "Threshold": { + "oneOf": [ + { + "type": "object", + "required": [ + "majority" + ], + "properties": { + "majority": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false }, - "num_strikes": { - "description": "The number of striked the admin has incurred.", - "type": "integer", - "format": "uint8", - "minimum": 0.0 + { + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false } - }, - "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); ```", @@ -636,128 +1215,309 @@ "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" - } - } - }, - "challenges": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ChallengesResponse", - "type": "array", - "items": { - "$ref": "#/definitions/ChallengeEntry" - }, - "definitions": { - "AnsAsset": { + }, + "VoteConfig": { "type": "object", "required": [ - "amount", - "name" + "threshold" ], "properties": { - "amount": { - "$ref": "#/definitions/Uint128" + "threshold": { + "$ref": "#/definitions/Threshold" }, - "name": { - "$ref": "#/definitions/AssetEntry" + "veto_duration_seconds": { + "description": "Veto duration after the first vote None disables veto", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] } + }, + "additionalProperties": false + } + } + }, + "challenges": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ChallengesResponse", + "description": "Response for challenges query Returns a list of challenges", + "type": "object", + "required": [ + "challenges" + ], + "properties": { + "challenges": { + "description": "List of indexed challenges", + "type": "array", + "items": { + "$ref": "#/definitions/ChallengeEntryResponse" } + } + }, + "additionalProperties": false, + "definitions": { + "AdminStrikes": { + "type": "object", + "required": [ + "limit", + "num_strikes" + ], + "properties": { + "limit": { + "description": "When num_strikes reached the limit, the challenge will be cancelled.", + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "num_strikes": { + "description": "The number of strikes the admin has incurred.", + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "additionalProperties": false }, "AssetEntry": { "description": "An unchecked ANS asset entry. This is a string that is formatted as `src_chain>[intermediate_chain>]asset_name`", "type": "string" }, - "ChallengeEntry": { + "ChallengeEntryResponse": { + "description": "Response struct for challenge entry", "type": "object", "required": [ "admin_strikes", - "collateral", + "challenge_id", "description", - "end", + "end_timestamp", "name", - "status", - "total_check_ins" + "proposal_duration_seconds", + "strike_asset", + "strike_strategy" ], "properties": { + "active_proposal": { + "description": "Current active proposal", + "anyOf": [ + { + "$ref": "#/definitions/ProposalInfo" + }, + { + "type": "null" + } + ] + }, "admin_strikes": { - "$ref": "#/definitions/StrikeConfig" + "description": "State of strikes of admin for this challenge", + "allOf": [ + { + "$ref": "#/definitions/AdminStrikes" + } + ] }, - "collateral": { - "$ref": "#/definitions/AnsAsset" + "challenge_id": { + "description": "Id of the challenge,", + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, "description": { + "description": "Description of the challenge", "type": "string" }, - "end": { - "$ref": "#/definitions/Timestamp" + "end_timestamp": { + "description": "When challenge ends", + "allOf": [ + { + "$ref": "#/definitions/Timestamp" + } + ] }, "name": { + "description": "Name of challenge", "type": "string" }, + "proposal_duration_seconds": { + "description": "Proposal duration in seconds", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "strike_asset": { + "description": "Asset for punishment for failing a challenge", + "allOf": [ + { + "$ref": "#/definitions/AssetEntry" + } + ] + }, + "strike_strategy": { + "description": "How strike will get distributed between friends", + "allOf": [ + { + "$ref": "#/definitions/StrikeStrategy" + } + ] + } + }, + "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" + }, + "ProposalInfo": { + "type": "object", + "required": [ + "config", + "end_timestamp", + "status", + "total_voters", + "votes_against", + "votes_for" + ], + "properties": { + "config": { + "description": "Config it was created with For cases config got changed during voting", + "allOf": [ + { + "$ref": "#/definitions/VoteConfig" + } + ] + }, + "end_timestamp": { + "$ref": "#/definitions/Timestamp" + }, "status": { - "$ref": "#/definitions/ChallengeStatus" + "$ref": "#/definitions/ProposalStatus" + }, + "total_voters": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 }, - "total_check_ins": { + "votes_against": { "type": "integer", - "format": "uint128", + "format": "uint32", + "minimum": 0.0 + }, + "votes_for": { + "type": "integer", + "format": "uint32", "minimum": 0.0 } }, "additionalProperties": false }, - "ChallengeStatus": { - "description": "The status of a challenge. This can be used to trigger an automated Croncat job based on the value of the status", + "ProposalOutcome": { + "type": "string", + "enum": [ + "passed", + "failed", + "canceled", + "vetoed" + ] + }, + "ProposalStatus": { "oneOf": [ { - "description": "The challenge has not been initialized yet. This is the default state.", "type": "string", "enum": [ - "uninitialized" + "active", + "waiting_for_count" ] }, { - "description": "The challenge is active and can be voted on.", - "type": "string", - "enum": [ - "active" - ] + "type": "object", + "required": [ + "veto_period" + ], + "properties": { + "veto_period": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false }, { - "description": "The challenge was cancelled and no collateral was paid out.", - "type": "string", - "enum": [ - "cancelled" - ] + "type": "object", + "required": [ + "finished" + ], + "properties": { + "finished": { + "$ref": "#/definitions/ProposalOutcome" + } + }, + "additionalProperties": false + } + ] + }, + "StrikeStrategy": { + "description": "Strategy for striking the admin", + "oneOf": [ + { + "description": "Split amount between friends", + "type": "object", + "required": [ + "split" + ], + "properties": { + "split": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false }, { - "description": "The challenge has pased the end time.", - "type": "string", - "enum": [ - "over" - ] + "description": "Amount for every friend", + "type": "object", + "required": [ + "per_friend" + ], + "properties": { + "per_friend": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false } ] }, - "StrikeConfig": { - "type": "object", - "required": [ - "limit", - "num_strikes" - ], - "properties": { - "limit": { - "description": "When num_strikes reached the limit, the challenge will be cancelled.", - "type": "integer", - "format": "uint8", - "minimum": 0.0 + "Threshold": { + "oneOf": [ + { + "type": "object", + "required": [ + "majority" + ], + "properties": { + "majority": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false }, - "num_strikes": { - "description": "The number of striked the admin has incurred.", - "type": "integer", - "format": "uint8", - "minimum": 0.0 + { + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false } - }, - "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); ```", @@ -774,103 +1534,314 @@ "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" + }, + "VoteConfig": { + "type": "object", + "required": [ + "threshold" + ], + "properties": { + "threshold": { + "$ref": "#/definitions/Threshold" + }, + "veto_duration_seconds": { + "description": "Veto duration after the first vote None disables veto", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false } } }, - "check_ins": { + "friends": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CheckInsResponse", - "type": "array", - "items": { - "$ref": "#/definitions/CheckIn" + "title": "FriendsResponse", + "description": "Response for friends query Returns a list of friends", + "type": "object", + "required": [ + "friends" + ], + "properties": { + "friends": { + "description": "List of friends on challenge", + "type": "array", + "items": { + "$ref": "#/definitions/Friend_for_Addr" + } + } }, + "additionalProperties": false, "definitions": { - "CheckIn": { - "description": "The check in struct is used to track the admin's check ins. The admin must check in every 24 hours, otherwise they get a strike.", + "AccountId": { + "description": "Unique identifier for an account. On each chain this is unique.", "type": "object", "required": [ - "last", - "next", - "status" + "seq", + "trace" ], "properties": { - "last": { - "description": "The blockheight of the last check in.", + "seq": { + "description": "Unique identifier for the accounts create on a local chain. Is reused when creating an interchain account.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "trace": { + "description": "Sequence of the chain that triggered the IBC account creation `AccountTrace::Local` if the account was created locally Example: Account created on Juno which has an abstract interchain account on Osmosis, which in turn creates an interchain account on Terra -> `AccountTrace::Remote(vec![\"juno\", \"osmosis\"])`", "allOf": [ { - "$ref": "#/definitions/Timestamp" + "$ref": "#/definitions/AccountTrace" } ] - }, - "metadata": { - "description": "Optional metadata for the check in. For example, a link to a tweet.", - "type": [ - "string", - "null" + } + }, + "additionalProperties": false + }, + "AccountTrace": { + "description": "The identifier of chain that triggered the account creation", + "oneOf": [ + { + "type": "string", + "enum": [ + "local" ] }, - "next": { - "description": "The blockheight of the next check in. In the case of a missed check in, this will always be pushed forward internally by the contract.", + { + "type": "object", + "required": [ + "remote" + ], + "properties": { + "remote": { + "type": "array", + "items": { + "$ref": "#/definitions/ChainName" + } + } + }, + "additionalProperties": false + } + ] + }, + "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" + }, + "ChainName": { + "description": "The name of a chain, aka the chain-id without the post-fix number. ex. `cosmoshub-4` -> `cosmoshub`, `juno-1` -> `juno`", + "type": "string" + }, + "FriendByAddr_for_Addr": { + "description": "Friend by address", + "type": "object", + "required": [ + "address", + "name" + ], + "properties": { + "address": { + "description": "Address of the friend", "allOf": [ { - "$ref": "#/definitions/Timestamp" + "$ref": "#/definitions/Addr" } ] }, - "status": { - "description": "The vote status of the CheckIn.", + "name": { + "description": "Name of the friend", + "type": "string" + } + }, + "additionalProperties": false + }, + "Friend_for_Addr": { + "description": "Friend object", + "oneOf": [ + { + "description": "Friend with address and a name", + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "$ref": "#/definitions/FriendByAddr_for_Addr" + } + }, + "additionalProperties": false + }, + { + "description": "Abstract Account Id of the friend", + "type": "object", + "required": [ + "abstract_account" + ], + "properties": { + "abstract_account": { + "$ref": "#/definitions/AccountId" + } + }, + "additionalProperties": false + } + ] + } + } + }, + "proposals": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ProposalsResponse", + "description": "Response for proposals query", + "type": "object", + "required": [ + "proposals" + ], + "properties": { + "proposals": { + "description": "results of proposals", + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + { + "$ref": "#/definitions/ProposalInfo" + } + ], + "maxItems": 2, + "minItems": 2 + } + } + }, + "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" + }, + "ProposalInfo": { + "type": "object", + "required": [ + "config", + "end_timestamp", + "status", + "total_voters", + "votes_against", + "votes_for" + ], + "properties": { + "config": { + "description": "Config it was created with For cases config got changed during voting", "allOf": [ { - "$ref": "#/definitions/CheckInStatus" + "$ref": "#/definitions/VoteConfig" } ] }, - "tally_result": { - "description": "The final result of the votes for this check in.", - "type": [ - "boolean", - "null" - ] + "end_timestamp": { + "$ref": "#/definitions/Timestamp" + }, + "status": { + "$ref": "#/definitions/ProposalStatus" + }, + "total_voters": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "votes_against": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "votes_for": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 } }, "additionalProperties": false }, - "CheckInStatus": { + "ProposalOutcome": { + "type": "string", + "enum": [ + "passed", + "failed", + "canceled", + "vetoed" + ] + }, + "ProposalStatus": { "oneOf": [ { - "description": "The admin has not yet checked in, therefore no voting or tallying has occured for this check in.", "type": "string", "enum": [ - "not_checked_in" + "active", + "waiting_for_count" ] }, { - "description": "The admin has checked in, but all friends have not yet all voted. Some friends may have voted, but not all.", - "type": "string", - "enum": [ - "checked_in_not_yet_voted" - ] + "type": "object", + "required": [ + "veto_period" + ], + "properties": { + "veto_period": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false }, { - "description": "The admin mised their check in and got a strike.", - "type": "string", - "enum": [ - "missed_check_in" - ] - }, + "type": "object", + "required": [ + "finished" + ], + "properties": { + "finished": { + "$ref": "#/definitions/ProposalOutcome" + } + }, + "additionalProperties": false + } + ] + }, + "Threshold": { + "oneOf": [ { - "description": "The admin has checked in and all friends have voted. But the check in has not yet been tallied.", - "type": "string", - "enum": [ - "voted_not_yet_tallied" - ] + "type": "object", + "required": [ + "majority" + ], + "properties": { + "majority": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false }, { - "description": "The check in has been voted and tallied.", - "type": "string", - "enum": [ - "voted_and_tallied" - ] + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false } ] }, @@ -885,33 +1856,26 @@ "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" - } - } - }, - "friends": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "FriendsResponse", - "type": "array", - "items": { - "$ref": "#/definitions/Friend_for_Addr" - }, - "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" }, - "Friend_for_Addr": { + "VoteConfig": { "type": "object", "required": [ - "address", - "name" + "threshold" ], "properties": { - "address": { - "$ref": "#/definitions/Addr" + "threshold": { + "$ref": "#/definitions/Threshold" }, - "name": { - "type": "string" + "veto_duration_seconds": { + "description": "Veto duration after the first vote None disables veto", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false @@ -921,12 +1885,14 @@ "vote": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "VoteResponse", + "description": "Response for vote query", "type": "object", "properties": { "vote": { + "description": "The vote, will return null if there was no vote by this user", "anyOf": [ { - "$ref": "#/definitions/Vote_for_Addr" + "$ref": "#/definitions/Vote" }, { "type": "null" @@ -935,54 +1901,87 @@ } }, "additionalProperties": false, + "definitions": { + "Vote": { + "description": "Vote struct", + "type": "object", + "required": [ + "vote" + ], + "properties": { + "memo": { + "description": "memo for the vote", + "type": [ + "string", + "null" + ] + }, + "vote": { + "description": "true: Vote for false: Vote against", + "type": "boolean" + } + }, + "additionalProperties": false + } + } + }, + "votes": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VotesResponse", + "description": "Response for previous_vote query", + "type": "object", + "required": [ + "votes" + ], + "properties": { + "votes": { + "description": "List of votes by addr", + "type": "array", + "items": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/Addr" + }, + { + "anyOf": [ + { + "$ref": "#/definitions/Vote" + }, + { + "type": "null" + } + ] + } + ], + "maxItems": 2, + "minItems": 2 + } + } + }, + "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" }, - "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" - }, - "Vote_for_Addr": { + "Vote": { + "description": "Vote struct", "type": "object", "required": [ - "voter" + "vote" ], "properties": { - "approval": { - "description": "The vote result", + "memo": { + "description": "memo for the vote", "type": [ - "boolean", + "string", "null" ] }, - "for_check_in": { - "description": "Correlates to the last_checked_in field of the CheckIn struct.", - "anyOf": [ - { - "$ref": "#/definitions/Timestamp" - }, - { - "type": "null" - } - ] - }, - "voter": { - "description": "The address of the voter", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] + "vote": { + "description": "true: Vote for false: Vote against", + "type": "boolean" } }, "additionalProperties": false diff --git a/modules/contracts/apps/challenge/schema/query_msg.json b/modules/contracts/apps/challenge/schema/query_msg.json index df86924f77..ccc89c5aef 100644 --- a/modules/contracts/apps/challenge/schema/query_msg.json +++ b/modules/contracts/apps/challenge/schema/query_msg.json @@ -30,6 +30,10 @@ } ], "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" + }, "BaseQueryMsg": { "oneOf": [ { @@ -77,8 +81,10 @@ ] }, "ChallengeQueryMsg": { + "description": "Challenge query messages", "oneOf": [ { + "description": "Get challenge info, will return null if there was no challenge by Id", "type": "object", "required": [ "challenge" @@ -91,6 +97,7 @@ ], "properties": { "challenge_id": { + "description": "Id of requested challenge", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -102,6 +109,7 @@ "additionalProperties": false }, { + "description": "Get list of challenges", "type": "object", "required": [ "challenges" @@ -109,18 +117,22 @@ "properties": { "challenges": { "type": "object", - "required": [ - "limit", - "start_after" - ], "properties": { "limit": { - "type": "integer", - "format": "uint32", + "description": "Max amount of challenges in response", + "type": [ + "integer", + "null" + ], + "format": "uint64", "minimum": 0.0 }, "start_after": { - "type": "integer", + "description": "start after challenge Id", + "type": [ + "integer", + "null" + ], "format": "uint64", "minimum": 0.0 } @@ -131,6 +143,7 @@ "additionalProperties": false }, { + "description": "List of friends by Id", "type": "object", "required": [ "friends" @@ -143,6 +156,7 @@ ], "properties": { "challenge_id": { + "description": "Id of requested challenge", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -154,21 +168,37 @@ "additionalProperties": false }, { + "description": "Get vote of friend", "type": "object", "required": [ - "check_ins" + "vote" ], "properties": { - "check_ins": { + "vote": { "type": "object", "required": [ - "challenge_id" + "challenge_id", + "voter_addr" ], "properties": { "challenge_id": { + "description": "Id of requested challenge", "type": "integer", "format": "uint64", "minimum": 0.0 + }, + "proposal_id": { + "description": "Proposal id of previous proposal Providing None requests last proposal results", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "voter_addr": { + "description": "Addr of the friend", + "type": "string" } }, "additionalProperties": false @@ -177,31 +207,95 @@ "additionalProperties": false }, { + "description": "Get votes of challenge", "type": "object", "required": [ - "vote" + "votes" ], "properties": { - "vote": { + "votes": { "type": "object", "required": [ - "challenge_id", - "last_check_in", - "voter_addr" + "challenge_id" ], "properties": { "challenge_id": { + "description": "Id of requested challenge", "type": "integer", "format": "uint64", "minimum": 0.0 }, - "last_check_in": { + "limit": { + "description": "Max amount of challenges in response", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "proposal_id": { + "description": "Proposal id of previous proposal Providing None requests last proposal results", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "start_after": { + "description": "start after Addr", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Get results of previous votes for this challenge", + "type": "object", + "required": [ + "proposals" + ], + "properties": { + "proposals": { + "type": "object", + "required": [ + "challenge_id" + ], + "properties": { + "challenge_id": { + "description": "Challenge Id for previous votes", "type": "integer", "format": "uint64", "minimum": 0.0 }, - "voter_addr": { - "type": "string" + "limit": { + "description": "Max amount of proposals in response", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "start_after": { + "description": "start after ProposalId", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false diff --git a/modules/contracts/apps/challenge/schema/raw/execute.json b/modules/contracts/apps/challenge/schema/raw/execute.json index a03292ded8..70d600b9de 100644 --- a/modules/contracts/apps/challenge/schema/raw/execute.json +++ b/modules/contracts/apps/challenge/schema/raw/execute.json @@ -1,9 +1,37 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ExecuteMsg", - "description": "App execute messages", + "description": "Challenge execute messages", "oneOf": [ { + "description": "Update challenge config", + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "required": [ + "new_vote_config" + ], + "properties": { + "new_vote_config": { + "description": "New config for vote", + "allOf": [ + { + "$ref": "#/definitions/VoteConfig" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Create new challenge", "type": "object", "required": [ "create_challenge" @@ -16,7 +44,12 @@ ], "properties": { "challenge_req": { - "$ref": "#/definitions/ChallengeRequest" + "description": "New challenge arguments", + "allOf": [ + { + "$ref": "#/definitions/ChallengeRequest" + } + ] } }, "additionalProperties": false @@ -25,6 +58,7 @@ "additionalProperties": false }, { + "description": "Update existing challenge", "type": "object", "required": [ "update_challenge" @@ -38,9 +72,15 @@ ], "properties": { "challenge": { - "$ref": "#/definitions/ChallengeEntryUpdate" + "description": "Updates to this challenge", + "allOf": [ + { + "$ref": "#/definitions/ChallengeEntryUpdate" + } + ] }, "challenge_id": { + "description": "Id of the challenge to update", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -52,6 +92,7 @@ "additionalProperties": false }, { + "description": "Cancel challenge", "type": "object", "required": [ "cancel_challenge" @@ -64,6 +105,7 @@ ], "properties": { "challenge_id": { + "description": "Challenge Id to cancel", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -75,6 +117,7 @@ "additionalProperties": false }, { + "description": "Update list of friends for challenge", "type": "object", "required": [ "update_friends_for_challenge" @@ -89,18 +132,25 @@ ], "properties": { "challenge_id": { + "description": "Id of the challenge to update", "type": "integer", "format": "uint64", "minimum": 0.0 }, "friends": { + "description": "List of added or removed Friends", "type": "array", "items": { "$ref": "#/definitions/Friend_for_String" } }, "op_kind": { - "$ref": "#/definitions/UpdateFriendsOpKind" + "description": "Kind of operation: add or remove friends", + "allOf": [ + { + "$ref": "#/definitions/UpdateFriendsOpKind" + } + ] } }, "additionalProperties": false @@ -109,27 +159,31 @@ "additionalProperties": false }, { + "description": "Cast vote as a friend", "type": "object", "required": [ - "daily_check_in" + "cast_vote" ], "properties": { - "daily_check_in": { + "cast_vote": { "type": "object", "required": [ - "challenge_id" + "challenge_id", + "vote_to_punish" ], "properties": { "challenge_id": { + "description": "Challenge Id to cast vote on", "type": "integer", "format": "uint64", "minimum": 0.0 }, - "metadata": { - "description": "metadata can be added for extra description of the check-in. For example, if the check-in is a photo, the metadata can be a link to the photo.", - "type": [ - "string", - "null" + "vote_to_punish": { + "description": "Wether voter thinks admin deserves punishment", + "allOf": [ + { + "$ref": "#/definitions/Vote" + } ] } }, @@ -139,30 +193,48 @@ "additionalProperties": false }, { + "description": "Count votes for challenge id", "type": "object", "required": [ - "cast_vote" + "count_votes" ], "properties": { - "cast_vote": { + "count_votes": { "type": "object", "required": [ - "challenge_id", - "vote" + "challenge_id" ], "properties": { "challenge_id": { + "description": "Challenge Id for counting votes", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Veto the last vote", + "type": "object", + "required": [ + "veto" + ], + "properties": { + "veto": { + "type": "object", + "required": [ + "challenge_id" + ], + "properties": { + "challenge_id": { + "description": "Challenge id to do the veto", "type": "integer", "format": "uint64", "minimum": 0.0 - }, - "vote": { - "description": "If the vote.approval is None, we assume the voter approves, and the contract will internally set the approval field to Some(true). This is because we assume that if a friend didn't vote, the friend approves, otherwise the voter would Vote with approval set to Some(false).", - "allOf": [ - { - "$ref": "#/definitions/Vote_for_String" - } - ] } }, "additionalProperties": false @@ -172,55 +244,75 @@ } ], "definitions": { - "AnsAsset": { + "AccountId": { + "description": "Unique identifier for an account. On each chain this is unique.", "type": "object", "required": [ - "amount", - "name" + "seq", + "trace" ], "properties": { - "amount": { - "$ref": "#/definitions/Uint128" + "seq": { + "description": "Unique identifier for the accounts create on a local chain. Is reused when creating an interchain account.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 }, - "name": { - "$ref": "#/definitions/AssetEntry" + "trace": { + "description": "Sequence of the chain that triggered the IBC account creation `AccountTrace::Local` if the account was created locally Example: Account created on Juno which has an abstract interchain account on Osmosis, which in turn creates an interchain account on Terra -> `AccountTrace::Remote(vec![\"juno\", \"osmosis\"])`", + "allOf": [ + { + "$ref": "#/definitions/AccountTrace" + } + ] + } + }, + "additionalProperties": false + }, + "AccountTrace": { + "description": "The identifier of chain that triggered the account creation", + "oneOf": [ + { + "type": "string", + "enum": [ + "local" + ] + }, + { + "type": "object", + "required": [ + "remote" + ], + "properties": { + "remote": { + "type": "array", + "items": { + "$ref": "#/definitions/ChainName" + } + } + }, + "additionalProperties": false } - } + ] }, "AssetEntry": { "description": "An unchecked ANS asset entry. This is a string that is formatted as `src_chain>[intermediate_chain>]asset_name`", "type": "string" }, + "ChainName": { + "description": "The name of a chain, aka the chain-id without the post-fix number. ex. `cosmoshub-4` -> `cosmoshub`, `juno-1` -> `juno`", + "type": "string" + }, "ChallengeEntryUpdate": { "description": "Only this struct and these fields are allowed to be updated. The status cannot be externally updated, it is updated by the contract.", "type": "object", "properties": { - "collateral": { - "anyOf": [ - { - "$ref": "#/definitions/AnsAsset" - }, - { - "type": "null" - } - ] - }, "description": { "type": [ "string", "null" ] }, - "end": { - "anyOf": [ - { - "$ref": "#/definitions/Timestamp" - }, - { - "type": "null" - } - ] - }, "name": { "type": [ "string", @@ -231,40 +323,85 @@ "additionalProperties": false }, "ChallengeRequest": { + "description": "Arguments for new challenge", "type": "object", "required": [ - "collateral", - "description", - "end", - "name" + "challenge_duration_seconds", + "init_friends", + "name", + "proposal_duration_seconds", + "strike_asset", + "strike_strategy" ], "properties": { - "collateral": { - "$ref": "#/definitions/AnsAsset" + "challenge_duration_seconds": { + "description": "In what duration challenge should end", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] }, "description": { - "type": "string" + "description": "Description of the challenge", + "type": [ + "string", + "null" + ] }, - "end": { - "$ref": "#/definitions/DurationChoice" + "init_friends": { + "description": "Initial list of friends", + "type": "array", + "items": { + "$ref": "#/definitions/Friend_for_String" + } }, "name": { + "description": "Name of challenge", "type": "string" + }, + "proposal_duration_seconds": { + "description": "Duration set for each proposal Proposals starts after one vote initiated by any of the friends", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "strike_asset": { + "description": "Asset for punishment for failing a challenge", + "allOf": [ + { + "$ref": "#/definitions/AssetEntry" + } + ] + }, + "strike_strategy": { + "description": "How strike will get distributed between friends", + "allOf": [ + { + "$ref": "#/definitions/StrikeStrategy" + } + ] + }, + "strikes_limit": { + "description": "Strike limit, defaults to 1", + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 } }, "additionalProperties": false }, - "DurationChoice": { - "type": "string", - "enum": [ - "week", - "month", - "quarter", - "year", - "one_hundred_years" - ] + "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" }, - "Friend_for_String": { + "FriendByAddr_for_String": { + "description": "Friend by address", "type": "object", "required": [ "address", @@ -272,19 +409,104 @@ ], "properties": { "address": { + "description": "Address of the friend", "type": "string" }, "name": { + "description": "Name of the friend", "type": "string" } }, "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": [ + "Friend_for_String": { + "description": "Friend object", + "oneOf": [ { - "$ref": "#/definitions/Uint64" + "description": "Friend with address and a name", + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "$ref": "#/definitions/FriendByAddr_for_String" + } + }, + "additionalProperties": false + }, + { + "description": "Abstract Account Id of the friend", + "type": "object", + "required": [ + "abstract_account" + ], + "properties": { + "abstract_account": { + "$ref": "#/definitions/AccountId" + } + }, + "additionalProperties": false + } + ] + }, + "StrikeStrategy": { + "description": "Strategy for striking the admin", + "oneOf": [ + { + "description": "Split amount between friends", + "type": "object", + "required": [ + "split" + ], + "properties": { + "split": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + { + "description": "Amount for every friend", + "type": "object", + "required": [ + "per_friend" + ], + "properties": { + "per_friend": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + ] + }, + "Threshold": { + "oneOf": [ + { + "type": "object", + "required": [ + "majority" + ], + "properties": { + "majority": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false } ] }, @@ -297,39 +519,75 @@ "type": "string" }, "UpdateFriendsOpKind": { - "type": "string", - "enum": [ - "add", - "remove" + "oneOf": [ + { + "type": "object", + "required": [ + "add" + ], + "properties": { + "add": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "remove" + ], + "properties": { + "remove": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } ] }, - "Vote_for_String": { + "Vote": { + "description": "Vote struct", "type": "object", "required": [ - "voter" + "vote" ], "properties": { - "approval": { - "description": "The vote result", + "memo": { + "description": "memo for the vote", "type": [ - "boolean", + "string", "null" ] }, - "for_check_in": { - "description": "Correlates to the last_checked_in field of the CheckIn struct.", + "vote": { + "description": "true: Vote for false: Vote against", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "VoteConfig": { + "type": "object", + "required": [ + "threshold" + ], + "properties": { + "threshold": { + "$ref": "#/definitions/Threshold" + }, + "veto_duration_seconds": { + "description": "Veto duration after the first vote None disables veto", "anyOf": [ { - "$ref": "#/definitions/Timestamp" + "$ref": "#/definitions/Uint64" }, { "type": "null" } ] - }, - "voter": { - "description": "The address of the voter", - "type": "string" } }, "additionalProperties": false diff --git a/modules/contracts/apps/challenge/schema/raw/instantiate.json b/modules/contracts/apps/challenge/schema/raw/instantiate.json index 5f6dfaf43c..5440f761c1 100644 --- a/modules/contracts/apps/challenge/schema/raw/instantiate.json +++ b/modules/contracts/apps/challenge/schema/raw/instantiate.json @@ -1,6 +1,82 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "InstantiateMsg", - "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", - "type": "object" + "description": "Challenge instantiate message", + "type": "object", + "required": [ + "vote_config" + ], + "properties": { + "vote_config": { + "description": "Config for [`SimpleVoting`](abstract_core::objects::voting::SimpleVoting) object", + "allOf": [ + { + "$ref": "#/definitions/VoteConfig" + } + ] + } + }, + "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" + }, + "Threshold": { + "oneOf": [ + { + "type": "object", + "required": [ + "majority" + ], + "properties": { + "majority": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + } + ] + }, + "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" + }, + "VoteConfig": { + "type": "object", + "required": [ + "threshold" + ], + "properties": { + "threshold": { + "$ref": "#/definitions/Threshold" + }, + "veto_duration_seconds": { + "description": "Veto duration after the first vote None disables veto", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } } diff --git a/modules/contracts/apps/challenge/schema/raw/query.json b/modules/contracts/apps/challenge/schema/raw/query.json index 80228f7a98..619cfb49de 100644 --- a/modules/contracts/apps/challenge/schema/raw/query.json +++ b/modules/contracts/apps/challenge/schema/raw/query.json @@ -1,8 +1,10 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "QueryMsg", + "description": "Challenge query messages", "oneOf": [ { + "description": "Get challenge info, will return null if there was no challenge by Id", "type": "object", "required": [ "challenge" @@ -15,6 +17,7 @@ ], "properties": { "challenge_id": { + "description": "Id of requested challenge", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -26,6 +29,7 @@ "additionalProperties": false }, { + "description": "Get list of challenges", "type": "object", "required": [ "challenges" @@ -33,18 +37,22 @@ "properties": { "challenges": { "type": "object", - "required": [ - "limit", - "start_after" - ], "properties": { "limit": { - "type": "integer", - "format": "uint32", + "description": "Max amount of challenges in response", + "type": [ + "integer", + "null" + ], + "format": "uint64", "minimum": 0.0 }, "start_after": { - "type": "integer", + "description": "start after challenge Id", + "type": [ + "integer", + "null" + ], "format": "uint64", "minimum": 0.0 } @@ -55,6 +63,7 @@ "additionalProperties": false }, { + "description": "List of friends by Id", "type": "object", "required": [ "friends" @@ -67,6 +76,7 @@ ], "properties": { "challenge_id": { + "description": "Id of requested challenge", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -78,21 +88,37 @@ "additionalProperties": false }, { + "description": "Get vote of friend", "type": "object", "required": [ - "check_ins" + "vote" ], "properties": { - "check_ins": { + "vote": { "type": "object", "required": [ - "challenge_id" + "challenge_id", + "voter_addr" ], "properties": { "challenge_id": { + "description": "Id of requested challenge", "type": "integer", "format": "uint64", "minimum": 0.0 + }, + "proposal_id": { + "description": "Proposal id of previous proposal Providing None requests last proposal results", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "voter_addr": { + "description": "Addr of the friend", + "type": "string" } }, "additionalProperties": false @@ -101,31 +127,95 @@ "additionalProperties": false }, { + "description": "Get votes of challenge", "type": "object", "required": [ - "vote" + "votes" ], "properties": { - "vote": { + "votes": { "type": "object", "required": [ - "challenge_id", - "last_check_in", - "voter_addr" + "challenge_id" ], "properties": { "challenge_id": { + "description": "Id of requested challenge", "type": "integer", "format": "uint64", "minimum": 0.0 }, - "last_check_in": { + "limit": { + "description": "Max amount of challenges in response", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "proposal_id": { + "description": "Proposal id of previous proposal Providing None requests last proposal results", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "start_after": { + "description": "start after Addr", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Get results of previous votes for this challenge", + "type": "object", + "required": [ + "proposals" + ], + "properties": { + "proposals": { + "type": "object", + "required": [ + "challenge_id" + ], + "properties": { + "challenge_id": { + "description": "Challenge Id for previous votes", "type": "integer", "format": "uint64", "minimum": 0.0 }, - "voter_addr": { - "type": "string" + "limit": { + "description": "Max amount of proposals in response", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "start_after": { + "description": "start after ProposalId", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false @@ -133,5 +223,11 @@ }, "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/modules/contracts/apps/challenge/schema/raw/response_to_challenge.json b/modules/contracts/apps/challenge/schema/raw/response_to_challenge.json index c15fcd0990..b9d9fcd81c 100644 --- a/modules/contracts/apps/challenge/schema/raw/response_to_challenge.json +++ b/modules/contracts/apps/challenge/schema/raw/response_to_challenge.json @@ -1,12 +1,14 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ChallengeResponse", + "description": "Response for challenge query", "type": "object", "properties": { "challenge": { + "description": "Challenge info, will return null if there was no challenge by Id", "anyOf": [ { - "$ref": "#/definitions/ChallengeEntry" + "$ref": "#/definitions/ChallengeEntryResponse" }, { "type": "null" @@ -16,117 +18,264 @@ }, "additionalProperties": false, "definitions": { - "AnsAsset": { + "AdminStrikes": { "type": "object", "required": [ - "amount", - "name" + "limit", + "num_strikes" ], "properties": { - "amount": { - "$ref": "#/definitions/Uint128" + "limit": { + "description": "When num_strikes reached the limit, the challenge will be cancelled.", + "type": "integer", + "format": "uint8", + "minimum": 0.0 }, - "name": { - "$ref": "#/definitions/AssetEntry" + "num_strikes": { + "description": "The number of strikes the admin has incurred.", + "type": "integer", + "format": "uint8", + "minimum": 0.0 } - } + }, + "additionalProperties": false }, "AssetEntry": { "description": "An unchecked ANS asset entry. This is a string that is formatted as `src_chain>[intermediate_chain>]asset_name`", "type": "string" }, - "ChallengeEntry": { + "ChallengeEntryResponse": { + "description": "Response struct for challenge entry", "type": "object", "required": [ "admin_strikes", - "collateral", + "challenge_id", "description", - "end", + "end_timestamp", "name", - "status", - "total_check_ins" + "proposal_duration_seconds", + "strike_asset", + "strike_strategy" ], "properties": { + "active_proposal": { + "description": "Current active proposal", + "anyOf": [ + { + "$ref": "#/definitions/ProposalInfo" + }, + { + "type": "null" + } + ] + }, "admin_strikes": { - "$ref": "#/definitions/StrikeConfig" + "description": "State of strikes of admin for this challenge", + "allOf": [ + { + "$ref": "#/definitions/AdminStrikes" + } + ] }, - "collateral": { - "$ref": "#/definitions/AnsAsset" + "challenge_id": { + "description": "Id of the challenge,", + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, "description": { + "description": "Description of the challenge", "type": "string" }, - "end": { - "$ref": "#/definitions/Timestamp" + "end_timestamp": { + "description": "When challenge ends", + "allOf": [ + { + "$ref": "#/definitions/Timestamp" + } + ] }, "name": { + "description": "Name of challenge", "type": "string" }, + "proposal_duration_seconds": { + "description": "Proposal duration in seconds", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "strike_asset": { + "description": "Asset for punishment for failing a challenge", + "allOf": [ + { + "$ref": "#/definitions/AssetEntry" + } + ] + }, + "strike_strategy": { + "description": "How strike will get distributed between friends", + "allOf": [ + { + "$ref": "#/definitions/StrikeStrategy" + } + ] + } + }, + "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" + }, + "ProposalInfo": { + "type": "object", + "required": [ + "config", + "end_timestamp", + "status", + "total_voters", + "votes_against", + "votes_for" + ], + "properties": { + "config": { + "description": "Config it was created with For cases config got changed during voting", + "allOf": [ + { + "$ref": "#/definitions/VoteConfig" + } + ] + }, + "end_timestamp": { + "$ref": "#/definitions/Timestamp" + }, "status": { - "$ref": "#/definitions/ChallengeStatus" + "$ref": "#/definitions/ProposalStatus" + }, + "total_voters": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "votes_against": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 }, - "total_check_ins": { + "votes_for": { "type": "integer", - "format": "uint128", + "format": "uint32", "minimum": 0.0 } }, "additionalProperties": false }, - "ChallengeStatus": { - "description": "The status of a challenge. This can be used to trigger an automated Croncat job based on the value of the status", + "ProposalOutcome": { + "type": "string", + "enum": [ + "passed", + "failed", + "canceled", + "vetoed" + ] + }, + "ProposalStatus": { "oneOf": [ { - "description": "The challenge has not been initialized yet. This is the default state.", "type": "string", "enum": [ - "uninitialized" + "active", + "waiting_for_count" ] }, { - "description": "The challenge is active and can be voted on.", - "type": "string", - "enum": [ - "active" - ] + "type": "object", + "required": [ + "veto_period" + ], + "properties": { + "veto_period": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false }, { - "description": "The challenge was cancelled and no collateral was paid out.", - "type": "string", - "enum": [ - "cancelled" - ] + "type": "object", + "required": [ + "finished" + ], + "properties": { + "finished": { + "$ref": "#/definitions/ProposalOutcome" + } + }, + "additionalProperties": false + } + ] + }, + "StrikeStrategy": { + "description": "Strategy for striking the admin", + "oneOf": [ + { + "description": "Split amount between friends", + "type": "object", + "required": [ + "split" + ], + "properties": { + "split": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false }, { - "description": "The challenge has pased the end time.", - "type": "string", - "enum": [ - "over" - ] + "description": "Amount for every friend", + "type": "object", + "required": [ + "per_friend" + ], + "properties": { + "per_friend": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false } ] }, - "StrikeConfig": { - "type": "object", - "required": [ - "limit", - "num_strikes" - ], - "properties": { - "limit": { - "description": "When num_strikes reached the limit, the challenge will be cancelled.", - "type": "integer", - "format": "uint8", - "minimum": 0.0 + "Threshold": { + "oneOf": [ + { + "type": "object", + "required": [ + "majority" + ], + "properties": { + "majority": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false }, - "num_strikes": { - "description": "The number of striked the admin has incurred.", - "type": "integer", - "format": "uint8", - "minimum": 0.0 + { + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false } - }, - "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); ```", @@ -143,6 +292,29 @@ "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" + }, + "VoteConfig": { + "type": "object", + "required": [ + "threshold" + ], + "properties": { + "threshold": { + "$ref": "#/definitions/Threshold" + }, + "veto_duration_seconds": { + "description": "Veto duration after the first vote None disables veto", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false } } } diff --git a/modules/contracts/apps/challenge/schema/raw/response_to_challenges.json b/modules/contracts/apps/challenge/schema/raw/response_to_challenges.json index b7d833d086..136148a057 100644 --- a/modules/contracts/apps/challenge/schema/raw/response_to_challenges.json +++ b/modules/contracts/apps/challenge/schema/raw/response_to_challenges.json @@ -1,122 +1,280 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ChallengesResponse", - "type": "array", - "items": { - "$ref": "#/definitions/ChallengeEntry" + "description": "Response for challenges query Returns a list of challenges", + "type": "object", + "required": [ + "challenges" + ], + "properties": { + "challenges": { + "description": "List of indexed challenges", + "type": "array", + "items": { + "$ref": "#/definitions/ChallengeEntryResponse" + } + } }, + "additionalProperties": false, "definitions": { - "AnsAsset": { + "AdminStrikes": { "type": "object", "required": [ - "amount", - "name" + "limit", + "num_strikes" ], "properties": { - "amount": { - "$ref": "#/definitions/Uint128" + "limit": { + "description": "When num_strikes reached the limit, the challenge will be cancelled.", + "type": "integer", + "format": "uint8", + "minimum": 0.0 }, - "name": { - "$ref": "#/definitions/AssetEntry" + "num_strikes": { + "description": "The number of strikes the admin has incurred.", + "type": "integer", + "format": "uint8", + "minimum": 0.0 } - } + }, + "additionalProperties": false }, "AssetEntry": { "description": "An unchecked ANS asset entry. This is a string that is formatted as `src_chain>[intermediate_chain>]asset_name`", "type": "string" }, - "ChallengeEntry": { + "ChallengeEntryResponse": { + "description": "Response struct for challenge entry", "type": "object", "required": [ "admin_strikes", - "collateral", + "challenge_id", "description", - "end", + "end_timestamp", "name", - "status", - "total_check_ins" + "proposal_duration_seconds", + "strike_asset", + "strike_strategy" ], "properties": { + "active_proposal": { + "description": "Current active proposal", + "anyOf": [ + { + "$ref": "#/definitions/ProposalInfo" + }, + { + "type": "null" + } + ] + }, "admin_strikes": { - "$ref": "#/definitions/StrikeConfig" + "description": "State of strikes of admin for this challenge", + "allOf": [ + { + "$ref": "#/definitions/AdminStrikes" + } + ] }, - "collateral": { - "$ref": "#/definitions/AnsAsset" + "challenge_id": { + "description": "Id of the challenge,", + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, "description": { + "description": "Description of the challenge", "type": "string" }, - "end": { - "$ref": "#/definitions/Timestamp" + "end_timestamp": { + "description": "When challenge ends", + "allOf": [ + { + "$ref": "#/definitions/Timestamp" + } + ] }, "name": { + "description": "Name of challenge", "type": "string" }, + "proposal_duration_seconds": { + "description": "Proposal duration in seconds", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "strike_asset": { + "description": "Asset for punishment for failing a challenge", + "allOf": [ + { + "$ref": "#/definitions/AssetEntry" + } + ] + }, + "strike_strategy": { + "description": "How strike will get distributed between friends", + "allOf": [ + { + "$ref": "#/definitions/StrikeStrategy" + } + ] + } + }, + "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" + }, + "ProposalInfo": { + "type": "object", + "required": [ + "config", + "end_timestamp", + "status", + "total_voters", + "votes_against", + "votes_for" + ], + "properties": { + "config": { + "description": "Config it was created with For cases config got changed during voting", + "allOf": [ + { + "$ref": "#/definitions/VoteConfig" + } + ] + }, + "end_timestamp": { + "$ref": "#/definitions/Timestamp" + }, "status": { - "$ref": "#/definitions/ChallengeStatus" + "$ref": "#/definitions/ProposalStatus" + }, + "total_voters": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "votes_against": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 }, - "total_check_ins": { + "votes_for": { "type": "integer", - "format": "uint128", + "format": "uint32", "minimum": 0.0 } }, "additionalProperties": false }, - "ChallengeStatus": { - "description": "The status of a challenge. This can be used to trigger an automated Croncat job based on the value of the status", + "ProposalOutcome": { + "type": "string", + "enum": [ + "passed", + "failed", + "canceled", + "vetoed" + ] + }, + "ProposalStatus": { "oneOf": [ { - "description": "The challenge has not been initialized yet. This is the default state.", "type": "string", "enum": [ - "uninitialized" + "active", + "waiting_for_count" ] }, { - "description": "The challenge is active and can be voted on.", - "type": "string", - "enum": [ - "active" - ] + "type": "object", + "required": [ + "veto_period" + ], + "properties": { + "veto_period": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false }, { - "description": "The challenge was cancelled and no collateral was paid out.", - "type": "string", - "enum": [ - "cancelled" - ] + "type": "object", + "required": [ + "finished" + ], + "properties": { + "finished": { + "$ref": "#/definitions/ProposalOutcome" + } + }, + "additionalProperties": false + } + ] + }, + "StrikeStrategy": { + "description": "Strategy for striking the admin", + "oneOf": [ + { + "description": "Split amount between friends", + "type": "object", + "required": [ + "split" + ], + "properties": { + "split": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false }, { - "description": "The challenge has pased the end time.", - "type": "string", - "enum": [ - "over" - ] + "description": "Amount for every friend", + "type": "object", + "required": [ + "per_friend" + ], + "properties": { + "per_friend": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false } ] }, - "StrikeConfig": { - "type": "object", - "required": [ - "limit", - "num_strikes" - ], - "properties": { - "limit": { - "description": "When num_strikes reached the limit, the challenge will be cancelled.", - "type": "integer", - "format": "uint8", - "minimum": 0.0 + "Threshold": { + "oneOf": [ + { + "type": "object", + "required": [ + "majority" + ], + "properties": { + "majority": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false }, - "num_strikes": { - "description": "The number of striked the admin has incurred.", - "type": "integer", - "format": "uint8", - "minimum": 0.0 + { + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false } - }, - "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); ```", @@ -133,6 +291,29 @@ "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" + }, + "VoteConfig": { + "type": "object", + "required": [ + "threshold" + ], + "properties": { + "threshold": { + "$ref": "#/definitions/Threshold" + }, + "veto_duration_seconds": { + "description": "Veto duration after the first vote None disables veto", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false } } } diff --git a/modules/contracts/apps/challenge/schema/raw/response_to_friends.json b/modules/contracts/apps/challenge/schema/raw/response_to_friends.json index aa6740a848..04b95e44ae 100644 --- a/modules/contracts/apps/challenge/schema/raw/response_to_friends.json +++ b/modules/contracts/apps/challenge/schema/raw/response_to_friends.json @@ -1,16 +1,83 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "FriendsResponse", - "type": "array", - "items": { - "$ref": "#/definitions/Friend_for_Addr" + "description": "Response for friends query Returns a list of friends", + "type": "object", + "required": [ + "friends" + ], + "properties": { + "friends": { + "description": "List of friends on challenge", + "type": "array", + "items": { + "$ref": "#/definitions/Friend_for_Addr" + } + } }, + "additionalProperties": false, "definitions": { + "AccountId": { + "description": "Unique identifier for an account. On each chain this is unique.", + "type": "object", + "required": [ + "seq", + "trace" + ], + "properties": { + "seq": { + "description": "Unique identifier for the accounts create on a local chain. Is reused when creating an interchain account.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "trace": { + "description": "Sequence of the chain that triggered the IBC account creation `AccountTrace::Local` if the account was created locally Example: Account created on Juno which has an abstract interchain account on Osmosis, which in turn creates an interchain account on Terra -> `AccountTrace::Remote(vec![\"juno\", \"osmosis\"])`", + "allOf": [ + { + "$ref": "#/definitions/AccountTrace" + } + ] + } + }, + "additionalProperties": false + }, + "AccountTrace": { + "description": "The identifier of chain that triggered the account creation", + "oneOf": [ + { + "type": "string", + "enum": [ + "local" + ] + }, + { + "type": "object", + "required": [ + "remote" + ], + "properties": { + "remote": { + "type": "array", + "items": { + "$ref": "#/definitions/ChainName" + } + } + }, + "additionalProperties": false + } + ] + }, "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" }, - "Friend_for_Addr": { + "ChainName": { + "description": "The name of a chain, aka the chain-id without the post-fix number. ex. `cosmoshub-4` -> `cosmoshub`, `juno-1` -> `juno`", + "type": "string" + }, + "FriendByAddr_for_Addr": { + "description": "Friend by address", "type": "object", "required": [ "address", @@ -18,13 +85,50 @@ ], "properties": { "address": { - "$ref": "#/definitions/Addr" + "description": "Address of the friend", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] }, "name": { + "description": "Name of the friend", "type": "string" } }, "additionalProperties": false + }, + "Friend_for_Addr": { + "description": "Friend object", + "oneOf": [ + { + "description": "Friend with address and a name", + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "$ref": "#/definitions/FriendByAddr_for_Addr" + } + }, + "additionalProperties": false + }, + { + "description": "Abstract Account Id of the friend", + "type": "object", + "required": [ + "abstract_account" + ], + "properties": { + "abstract_account": { + "$ref": "#/definitions/AccountId" + } + }, + "additionalProperties": false + } + ] } } } diff --git a/modules/contracts/apps/challenge/schema/raw/response_to_previous_proposals.json b/modules/contracts/apps/challenge/schema/raw/response_to_previous_proposals.json new file mode 100644 index 0000000000..32697ec364 --- /dev/null +++ b/modules/contracts/apps/challenge/schema/raw/response_to_previous_proposals.json @@ -0,0 +1,167 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PreviousProposalsResponse", + "description": "Response for previous_vote query", + "type": "object", + "required": [ + "results" + ], + "properties": { + "results": { + "description": "results of previous proposals", + "type": "array", + "items": { + "$ref": "#/definitions/ProposalInfo" + } + } + }, + "additionalProperties": false, + "definitions": { + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "ProposalInfo": { + "type": "object", + "required": [ + "end", + "status", + "total_voters", + "votes_against", + "votes_for" + ], + "properties": { + "end": { + "$ref": "#/definitions/Expiration" + }, + "status": { + "$ref": "#/definitions/ProposalStatus" + }, + "total_voters": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "votes_against": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "votes_for": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "ProposalOutcome": { + "type": "string", + "enum": [ + "passed", + "failed", + "canceled", + "vetoed" + ] + }, + "ProposalStatus": { + "oneOf": [ + { + "type": "string", + "enum": [ + "active" + ] + }, + { + "type": "object", + "required": [ + "veto_period" + ], + "properties": { + "veto_period": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "$ref": "#/definitions/ProposalOutcome" + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "finished" + ], + "properties": { + "finished": { + "$ref": "#/definitions/ProposalOutcome" + } + }, + "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/modules/contracts/apps/challenge/schema/raw/response_to_proposals.json b/modules/contracts/apps/challenge/schema/raw/response_to_proposals.json new file mode 100644 index 0000000000..a330a486de --- /dev/null +++ b/modules/contracts/apps/challenge/schema/raw/response_to_proposals.json @@ -0,0 +1,188 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ProposalsResponse", + "description": "Response for proposals query", + "type": "object", + "required": [ + "proposals" + ], + "properties": { + "proposals": { + "description": "results of proposals", + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + { + "$ref": "#/definitions/ProposalInfo" + } + ], + "maxItems": 2, + "minItems": 2 + } + } + }, + "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" + }, + "ProposalInfo": { + "type": "object", + "required": [ + "config", + "end_timestamp", + "status", + "total_voters", + "votes_against", + "votes_for" + ], + "properties": { + "config": { + "description": "Config it was created with For cases config got changed during voting", + "allOf": [ + { + "$ref": "#/definitions/VoteConfig" + } + ] + }, + "end_timestamp": { + "$ref": "#/definitions/Timestamp" + }, + "status": { + "$ref": "#/definitions/ProposalStatus" + }, + "total_voters": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "votes_against": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "votes_for": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "ProposalOutcome": { + "type": "string", + "enum": [ + "passed", + "failed", + "canceled", + "vetoed" + ] + }, + "ProposalStatus": { + "oneOf": [ + { + "type": "string", + "enum": [ + "active", + "waiting_for_count" + ] + }, + { + "type": "object", + "required": [ + "veto_period" + ], + "properties": { + "veto_period": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "finished" + ], + "properties": { + "finished": { + "$ref": "#/definitions/ProposalOutcome" + } + }, + "additionalProperties": false + } + ] + }, + "Threshold": { + "oneOf": [ + { + "type": "object", + "required": [ + "majority" + ], + "properties": { + "majority": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "$ref": "#/definitions/Decimal" + } + }, + "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" + }, + "VoteConfig": { + "type": "object", + "required": [ + "threshold" + ], + "properties": { + "threshold": { + "$ref": "#/definitions/Threshold" + }, + "veto_duration_seconds": { + "description": "Veto duration after the first vote None disables veto", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } +} diff --git a/modules/contracts/apps/challenge/schema/raw/response_to_vote.json b/modules/contracts/apps/challenge/schema/raw/response_to_vote.json index 7a1c542fca..c5fd391c64 100644 --- a/modules/contracts/apps/challenge/schema/raw/response_to_vote.json +++ b/modules/contracts/apps/challenge/schema/raw/response_to_vote.json @@ -1,12 +1,14 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "VoteResponse", + "description": "Response for vote query", "type": "object", "properties": { "vote": { + "description": "The vote, will return null if there was no vote by this user", "anyOf": [ { - "$ref": "#/definitions/Vote_for_Addr" + "$ref": "#/definitions/Vote" }, { "type": "null" @@ -16,53 +18,23 @@ }, "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" - }, - "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" - }, - "Vote_for_Addr": { + "Vote": { + "description": "Vote struct", "type": "object", "required": [ - "voter" + "vote" ], "properties": { - "approval": { - "description": "The vote result", + "memo": { + "description": "memo for the vote", "type": [ - "boolean", + "string", "null" ] }, - "for_check_in": { - "description": "Correlates to the last_checked_in field of the CheckIn struct.", - "anyOf": [ - { - "$ref": "#/definitions/Timestamp" - }, - { - "type": "null" - } - ] - }, - "voter": { - "description": "The address of the voter", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] + "vote": { + "description": "true: Vote for false: Vote against", + "type": "boolean" } }, "additionalProperties": false diff --git a/modules/contracts/apps/challenge/schema/raw/response_to_votes.json b/modules/contracts/apps/challenge/schema/raw/response_to_votes.json new file mode 100644 index 0000000000..8d626b0688 --- /dev/null +++ b/modules/contracts/apps/challenge/schema/raw/response_to_votes.json @@ -0,0 +1,63 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VotesResponse", + "description": "Response for previous_vote query", + "type": "object", + "required": [ + "votes" + ], + "properties": { + "votes": { + "description": "List of votes by addr", + "type": "array", + "items": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/Addr" + }, + { + "anyOf": [ + { + "$ref": "#/definitions/Vote" + }, + { + "type": "null" + } + ] + } + ], + "maxItems": 2, + "minItems": 2 + } + } + }, + "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" + }, + "Vote": { + "description": "Vote struct", + "type": "object", + "required": [ + "vote" + ], + "properties": { + "memo": { + "description": "memo for the vote", + "type": [ + "string", + "null" + ] + }, + "vote": { + "description": "true: Vote for false: Vote against", + "type": "boolean" + } + }, + "additionalProperties": false + } + } +} diff --git a/modules/contracts/apps/challenge/src/contract.rs b/modules/contracts/apps/challenge/src/contract.rs index cb3c85b523..e0032f159c 100644 --- a/modules/contracts/apps/challenge/src/contract.rs +++ b/modules/contracts/apps/challenge/src/contract.rs @@ -1,7 +1,7 @@ use crate::{ error::AppError, handlers, - msg::{ChallengeExecuteMsg, ChallengeQueryMsg}, + msg::{ChallengeExecuteMsg, ChallengeInstantiateMsg, ChallengeQueryMsg}, }; use abstract_app::AppContract; use cosmwasm_std::{Empty, Response}; @@ -15,7 +15,8 @@ pub const CHALLENGE_APP_ID: &str = "abstract:challenge"; pub type AppResult = Result; /// The type of the app that is used to build your app and access the Abstract SDK features. -pub type ChallengeApp = AppContract; +pub type ChallengeApp = + AppContract; const CHALLENGE_APP: ChallengeApp = ChallengeApp::new(CHALLENGE_APP_ID, CHALLENGE_APP_VERSION, None) diff --git a/modules/contracts/apps/challenge/src/error.rs b/modules/contracts/apps/challenge/src/error.rs index 6ef5d54778..e4968d7ff4 100644 --- a/modules/contracts/apps/challenge/src/error.rs +++ b/modules/contracts/apps/challenge/src/error.rs @@ -1,11 +1,16 @@ use abstract_app::AppError as AbstractAppError; -use abstract_core::AbstractError; +use abstract_core::{ + objects::{validation::ValidationError, voting::VoteError}, + AbstractError, +}; use abstract_sdk::AbstractSdkError; -use cosmwasm_std::StdError; +use cosmwasm_std::{StdError, Timestamp}; use cw_asset::AssetError; use cw_controllers::AdminError; use thiserror::Error; +use crate::state::MAX_AMOUNT_OF_FRIENDS; + #[derive(Error, Debug, PartialEq)] pub enum AppError { #[error("{0}")] @@ -26,24 +31,39 @@ pub enum AppError { #[error("{0}")] DappError(#[from] AbstractAppError), - #[error("Resource not found")] - NotFound {}, - - #[error("Already checked in")] - AlreadyCheckedIn {}, + #[error("{0}")] + VoteError(#[from] VoteError), - #[error("Voter already voted")] - AlreadyVoted {}, + #[error("{0}")] + ValidationError(#[from] ValidationError), - #[error("Friend already vetoed")] - AlreadyAdded {}, + #[error("Challenge not found")] + ChallengeNotFound {}, #[error("Voter not found")] VoterNotFound {}, - #[error("The challenge status is not correct for this action")] - WrongChallengeStatus {}, + #[error("The challenge is not active for the action")] + ChallengeNotActive {}, #[error("The check in status is not correct for this action")] WrongCheckInStatus {}, + + #[error("No friends found for the challenge")] + ZeroFriends {}, + + #[error("Friends limit reached, max: {MAX_AMOUNT_OF_FRIENDS}")] + TooManyFriends {}, + + #[error("Can't have duplicate friends addresses")] + DuplicateFriends {}, + + #[error("Can't edit friends during active proposal: {0}")] + FriendsEditDuringProposal(Timestamp), + + #[error("Challenge expired")] + ChallengeExpired {}, + + #[error("Challenge has no proposals yet")] + ExpectedProposal {}, } diff --git a/modules/contracts/apps/challenge/src/handlers/execute.rs b/modules/contracts/apps/challenge/src/handlers/execute.rs index e14bace620..c4683f1b09 100644 --- a/modules/contracts/apps/challenge/src/handlers/execute.rs +++ b/modules/contracts/apps/challenge/src/handlers/execute.rs @@ -1,16 +1,20 @@ +use std::collections::HashSet; + use crate::error::AppError; +use abstract_core::objects::voting::{ProposalId, ProposalInfo, ProposalOutcome, Vote}; use abstract_dex_adapter::msg::OfferAsset; use abstract_sdk::features::AbstractResponse; -use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, StdError, Timestamp, Uint128}; +use abstract_sdk::{AbstractSdkResult, AccountVerification, Execution, TransferInterface}; +use cosmwasm_std::{ + ensure, Addr, Deps, DepsMut, Empty, Env, MessageInfo, Order, Response, StdResult, Uint128, +}; use crate::contract::{AppResult, ChallengeApp}; -use abstract_sdk::prelude::*; -use crate::msg::{ChallengeExecuteMsg, ChallengeRequest}; +use crate::msg::{ChallengeExecuteMsg, ChallengeRequest, Friend}; use crate::state::{ - ChallengeEntry, ChallengeEntryUpdate, ChallengeStatus, CheckIn, CheckInStatus, Friend, - UpdateFriendsOpKind, Vote, CHALLENGE_FRIENDS, CHALLENGE_LIST, CHALLENGE_VOTES, DAILY_CHECK_INS, - DAY, NEXT_ID, VOTES, + ChallengeEntry, ChallengeEntryUpdate, UpdateFriendsOpKind, CHALLENGES, CHALLENGE_FRIENDS, + CHALLENGE_PROPOSALS, MAX_AMOUNT_OF_FRIENDS, NEXT_ID, SIMPLE_VOTING, }; pub fn execute_handler( @@ -27,21 +31,26 @@ pub fn execute_handler( ChallengeExecuteMsg::UpdateChallenge { challenge_id, challenge, - } => update_challenge(deps, info, app, challenge_id, challenge), + } => update_challenge(deps, env, info, app, challenge_id, challenge), ChallengeExecuteMsg::CancelChallenge { challenge_id } => { - cancel_challenge(deps, info, &app, challenge_id) + cancel_challenge(deps, env, info, &app, challenge_id) } ChallengeExecuteMsg::UpdateFriendsForChallenge { challenge_id, friends, op_kind, - } => update_friends_for_challenge(deps, info, &app, challenge_id, friends, op_kind), - ChallengeExecuteMsg::DailyCheckIn { + } => update_friends_for_challenge(deps, env, info, &app, challenge_id, friends, op_kind), + ChallengeExecuteMsg::CastVote { + vote_to_punish: vote, challenge_id, - metadata, - } => daily_check_in(deps, env, info, &app, challenge_id, metadata), - ChallengeExecuteMsg::CastVote { vote, challenge_id } => { - cast_vote(deps, env, &app, vote, challenge_id) + } => cast_vote(deps, env, info, &app, vote, challenge_id), + ChallengeExecuteMsg::CountVotes { challenge_id } => { + count_votes(deps, env, info, &app, challenge_id) + } + ChallengeExecuteMsg::Veto { challenge_id } => veto(deps, env, info, &app, challenge_id), + ChallengeExecuteMsg::UpdateConfig { new_vote_config } => { + SIMPLE_VOTING.update_vote_config(deps.storage, &new_vote_config)?; + Ok(Response::new()) } } } @@ -56,29 +65,37 @@ fn create_challenge( ) -> AppResult { // Only the admin should be able to create a challenge. app.admin.assert_admin(deps.as_ref(), &info.sender)?; - - let mut challenge = ChallengeEntry::new(challenge_req); - challenge.set_total_check_ins(&env)?; - - //check that the challenge status is ChallengeStatus::Uninitialized - if challenge.status != ChallengeStatus::Uninitialized { - return Err(AppError::WrongChallengeStatus {}); + ensure!( + challenge_req.init_friends.len() < MAX_AMOUNT_OF_FRIENDS as usize, + AppError::TooManyFriends {} + ); + // Validate friend addr and account ids + let friends_validated: Vec<(Addr, Friend)> = challenge_req + .init_friends + .iter() + .cloned() + .map(|human| human.check(deps.as_ref(), &app)) + .collect::>()?; + + let (friend_addrs, friends): (Vec, Vec>) = + friends_validated.into_iter().unzip(); + // Check if addrs unique + let mut unique_addrs = HashSet::with_capacity(friend_addrs.len()); + if !friend_addrs.iter().all(|x| unique_addrs.insert(x)) { + return Err(AppError::DuplicateFriends {}); } - // Generate the challenge id and update the status + // Generate the challenge id let challenge_id = NEXT_ID.update(deps.storage, |id| AppResult::Ok(id + 1))?; - challenge.status = ChallengeStatus::Active; - CHALLENGE_LIST.save(deps.storage, challenge_id, &challenge)?; + CHALLENGE_FRIENDS.save(deps.storage, challenge_id, &friends)?; - // Create the initial check_in entry - DAILY_CHECK_INS.save( - deps.storage, - challenge_id, - &vec![CheckIn::default_from(&env)], - )?; - - // Create the initial challenge_votes entry - CHALLENGE_VOTES.save(deps.storage, challenge_id, &Vec::new())?; + // Create new challenge + let end_timestamp = env + .block + .time + .plus_seconds(challenge_req.challenge_duration_seconds.u64()); + let challenge = ChallengeEntry::new(challenge_req, end_timestamp)?; + CHALLENGES.save(deps.storage, challenge_id, &challenge)?; Ok(app.tag_response( Response::new().add_attribute("challenge_id", challenge_id.to_string()), @@ -88,6 +105,7 @@ fn create_challenge( fn update_challenge( deps: DepsMut, + _env: Env, info: MessageInfo, app: ChallengeApp, challenge_id: u64, @@ -96,14 +114,11 @@ fn update_challenge( app.admin.assert_admin(deps.as_ref(), &info.sender)?; // will return an error if the challenge doesn't exist - let mut loaded_challenge: ChallengeEntry = CHALLENGE_LIST + let mut loaded_challenge: ChallengeEntry = CHALLENGES .may_load(deps.storage, challenge_id)? - .ok_or(AppError::NotFound {})?; - - if loaded_challenge.status != ChallengeStatus::Active { - return Err(AppError::WrongChallengeStatus {}); - } + .ok_or(AppError::ChallengeNotFound {})?; + // TODO: are we ok to edit name/description during proposals? if let Some(name) = new_challenge.name { loaded_challenge.name = name; } @@ -113,7 +128,7 @@ fn update_challenge( } // Save the updated challenge - CHALLENGE_LIST.save(deps.storage, challenge_id, &loaded_challenge)?; + CHALLENGES.save(deps.storage, challenge_id, &loaded_challenge)?; Ok(app.tag_response( Response::new().add_attribute("challenge_id", challenge_id.to_string()), @@ -123,18 +138,27 @@ fn update_challenge( fn cancel_challenge( deps: DepsMut, + env: Env, info: MessageInfo, app: &ChallengeApp, challenge_id: u64, ) -> AppResult { app.admin.assert_admin(deps.as_ref(), &info.sender)?; - let mut challenge = CHALLENGE_LIST.load(deps.storage, challenge_id)?; - if challenge.status != ChallengeStatus::Active { - return Err(AppError::WrongChallengeStatus {}); + let mut challenge = CHALLENGES.load(deps.storage, challenge_id)?; + // Check if this challenge still active + if env.block.time >= challenge.end_timestamp { + return Err(AppError::ChallengeExpired {}); + } + + // If there is active proposal - cancel it + let last_proposal_id = last_proposal(challenge_id, deps.as_ref())?; + if let Some(proposal_id) = last_proposal_id { + SIMPLE_VOTING.cancel_proposal(deps.storage, &env.block, proposal_id)?; } - challenge.status = ChallengeStatus::Cancelled; - CHALLENGE_LIST.save(deps.storage, challenge_id, &challenge)?; + // End it now + challenge.end_timestamp = env.block.time; + CHALLENGES.save(deps.storage, challenge_id, &challenge)?; Ok(app.tag_response( Response::new().add_attribute("challenge_id", challenge_id.to_string()), @@ -144,6 +168,7 @@ fn cancel_challenge( fn update_friends_for_challenge( deps: DepsMut, + env: Env, info: MessageInfo, app: &ChallengeApp, challenge_id: u64, @@ -151,350 +176,253 @@ fn update_friends_for_challenge( op_kind: UpdateFriendsOpKind, ) -> AppResult { app.admin.assert_admin(deps.as_ref(), &info.sender)?; - let challenge = CHALLENGE_LIST.load(deps.storage, challenge_id)?; - if challenge.status != ChallengeStatus::Active { - return Err(AppError::WrongChallengeStatus {}); + // Validate friend addr and account ids + let friends_validated: Vec<(Addr, Friend)> = friends + .iter() + .cloned() + .map(|human| human.check(deps.as_ref(), app)) + .collect::>()?; + + let (voters_addrs, friends): (Vec, Vec>) = + friends_validated.into_iter().unzip(); + + let last_proposal_id = last_proposal(challenge_id, deps.as_ref())?; + + // Don't allow edit friends if last proposal haven't ended yet + if let Some(proposal_id) = last_proposal_id { + let info = SIMPLE_VOTING.load_proposal(deps.storage, &env.block, proposal_id)?; + if env.block.time < info.end_timestamp { + return Err(AppError::FriendsEditDuringProposal(info.end_timestamp)); + } } match op_kind { - UpdateFriendsOpKind::Add => { - add_friends_for_challenge(deps, info, app, challenge_id, friends) + UpdateFriendsOpKind::Add {} => { + let mut current_friends = CHALLENGE_FRIENDS + .may_load(deps.storage, challenge_id)? + .ok_or(AppError::ChallengeNotFound {})?; + + ensure!( + friends.len() + current_friends.len() < MAX_AMOUNT_OF_FRIENDS as usize, + AppError::TooManyFriends {} + ); + + let mut current_friends_addrs: Vec = current_friends + .iter() + .map(|f| f.addr(deps.as_ref(), app)) + .collect::>()?; + current_friends_addrs.extend(voters_addrs); + // Check if addrs unique + let mut unique_addrs = HashSet::with_capacity(current_friends_addrs.len()); + if !current_friends_addrs.iter().all(|x| unique_addrs.insert(x)) { + return Err(AppError::DuplicateFriends {}); + } + + current_friends.extend(friends); + CHALLENGE_FRIENDS.save(deps.storage, challenge_id, ¤t_friends)?; } - UpdateFriendsOpKind::Remove => { - remove_friends_from_challenge(deps, info, app, challenge_id, friends) + UpdateFriendsOpKind::Remove {} => { + CHALLENGE_FRIENDS.update(deps.storage, challenge_id, |current_friends| { + let mut current_friends = current_friends.ok_or(AppError::ZeroFriends {})?; + for rem_friend in friends.iter() { + current_friends.retain(|friend| friend != rem_friend); + } + AppResult::Ok(current_friends) + })?; } } + Ok(app.tag_response( + Response::new().add_attribute("challenge_id", challenge_id.to_string()), + "update_friends", + )) } -fn add_friends_for_challenge( - deps: DepsMut, - info: MessageInfo, - app: &ChallengeApp, +fn get_or_create_active_proposal( + deps: &mut DepsMut, + env: &Env, challenge_id: u64, - friends: Vec>, -) -> AppResult { - // Ensure the caller is an admin - app.admin.assert_admin(deps.as_ref(), &info.sender)?; - - let mut existing_friends = CHALLENGE_FRIENDS - .may_load(deps.storage, challenge_id)? - .unwrap_or_default(); - - for friend in &friends { - if existing_friends.iter().any(|f| f.address == friend.address) { - return Err(AppError::AlreadyAdded {}); + app: &ChallengeApp, +) -> AppResult { + let challenge = CHALLENGES.load(deps.storage, challenge_id)?; + + // Load last proposal and use it if it's active + if let Some(proposal_id) = last_proposal(challenge_id, deps.as_ref())? { + let proposal = SIMPLE_VOTING.load_proposal(deps.storage, &env.block, proposal_id)?; + if proposal.assert_active_proposal().is_ok() { + return Ok(proposal_id); } } - // validate the String addresses and convert them to Addr - // before saving - let friends: Result>, _> = friends - .into_iter() - .map(|friend| friend.check(deps.as_ref())) - .collect(); - - match friends { - Ok(friends) => { - existing_friends.extend(friends); - CHALLENGE_FRIENDS.save(deps.storage, challenge_id, &existing_friends)?; - - Ok(app.tag_response( - Response::new().add_attribute("challenge_id", challenge_id.to_string()), - "add_friends", - )) - } - Err(err) => Err(AppError::Std(StdError::generic_err(format!( - "Error adding friends: {:?}", - err - )))), + // Or create a new one otherwise + if env.block.time >= challenge.end_timestamp { + return Err(AppError::ChallengeExpired {}); } + let friends: Vec = CHALLENGE_FRIENDS + .load(deps.storage, challenge_id)? + .into_iter() + .map(|friend| friend.addr(deps.as_ref(), app)) + .collect::>()?; + let proposal_id = SIMPLE_VOTING.new_proposal( + deps.storage, + env.block + .time + .plus_seconds(challenge.proposal_duration_seconds.u64()), + &friends, + )?; + CHALLENGE_PROPOSALS.save(deps.storage, (challenge_id, proposal_id), &Empty {})?; + + Ok(proposal_id) } -pub fn remove_friends_from_challenge( - deps: DepsMut, +fn cast_vote( + mut deps: DepsMut, + env: Env, info: MessageInfo, app: &ChallengeApp, + vote: Vote, challenge_id: u64, - friend_addresses: Vec>, ) -> AppResult { - // Ensure the caller is an admin - app.admin.assert_admin(deps.as_ref(), &info.sender)?; - - let mut existing_friends = CHALLENGE_FRIENDS - .may_load(deps.storage, challenge_id)? - .unwrap_or_default(); + let proposal_id = get_or_create_active_proposal(&mut deps, &env, challenge_id, app)?; - for friend in &friend_addresses { - if !existing_friends.iter().any(|f| f.address == friend.address) { - return Err(AppError::Std(StdError::generic_err( - "Friend not found for this challenge", - ))); - } - } + let voter = match app + .account_registry(deps.as_ref()) + .assert_proxy(&info.sender) + { + Ok(base) => base.manager, + Err(_) => info.sender, + }; + let proposal_info = + SIMPLE_VOTING.cast_vote(deps.storage, &env.block, proposal_id, &voter, vote)?; - existing_friends.retain(|f| { - !friend_addresses - .iter() - .any(|friend| f.address == friend.address) - }); - CHALLENGE_FRIENDS.save(deps.storage, challenge_id, &existing_friends)?; - Ok(app.tag_response( - Response::new().add_attribute("challenge_id", challenge_id.to_string()), - "remove_friends", - )) + Ok(app + .tag_response(Response::new(), "cast_vote") + .add_attribute("proposal_info", format!("{proposal_info:?}"))) } -fn daily_check_in( +fn count_votes( deps: DepsMut, env: Env, - info: MessageInfo, + _info: MessageInfo, app: &ChallengeApp, challenge_id: u64, - metadata: Option, ) -> AppResult { - app.admin.assert_admin(deps.as_ref(), &info.sender)?; - let mut challenge = CHALLENGE_LIST.load(deps.storage, challenge_id)?; - - // If the challenge has ended, we set the status to Over and return - if env.block.time > challenge.end { - match challenge.status { - ChallengeStatus::Active => { - challenge.status = ChallengeStatus::Over; - CHALLENGE_LIST.save(deps.storage, challenge_id, &challenge)?; - return Ok(app.tag_response( - Response::new().add_attribute( - "message", - "Challenge has ended. You can no longer register a daily check in.", - ), - "daily check in", - )); - } - _ => { - return Err(AppError::Std(StdError::generic_err(format!( - "Challenge has ended. Challenge end_timestamp is {:?} current timestamp is {:?}", - challenge.end.seconds(), - env.block.time.seconds() - )))); - } - } - } - - let mut check_ins = DAILY_CHECK_INS.load(deps.storage, challenge_id)?; - let check_in = check_ins.last().unwrap(); - let next = Timestamp::from_seconds(env.block.time.seconds() + DAY); - - match env.block.time { - now if now == check_in.last => Err(AppError::AlreadyCheckedIn {}), - - // The admin has missed the deadline for checking in, they are given a strike. - // The contract manually sets the next check in time. - now if now >= check_in.next => { - challenge.admin_strikes.num_strikes += 1; - CHALLENGE_LIST.save(deps.storage, challenge_id, &challenge)?; - - // If the admin's strikes reach the limit, cancel the challenge - if challenge.admin_strikes.num_strikes >= challenge.admin_strikes.limit { - return cancel_challenge(deps, info, app, challenge_id); - } - - let check_in = CheckIn { - last: check_in.last, - next, - metadata, - status: CheckInStatus::MissedCheckIn, - tally_result: None, - }; - check_ins.push(check_in); - DAILY_CHECK_INS.save(deps.storage, challenge_id, &check_ins)?; - - Ok(app.tag_response( - Response::new().add_attribute( - "message", - "You missed the daily check in, you have been given a strike", - ), - "daily check in", - )) - } - - // The admin is checking in on time, so we can proceeed. - now if now < check_in.next => { - let check_in = CheckIn { - last: now, - next, - metadata, - status: CheckInStatus::CheckedInNotYetVoted, - tally_result: None, - }; - - check_ins.push(check_in); - DAILY_CHECK_INS.save(deps.storage, challenge_id, &check_ins)?; - - Ok(app.tag_response( - Response::new().add_attribute("action", "check_in"), - "daily check in", - )) - } - _ => Err(AppError::Std(StdError::generic_err( - "Something went wrong with the check in.", - ))), - } + let challenge = CHALLENGES.load(deps.storage, challenge_id)?; + let proposal_id = + last_proposal(challenge_id, deps.as_ref())?.ok_or(AppError::ExpectedProposal {})?; + let (proposal_info, outcome) = + SIMPLE_VOTING.count_votes(deps.storage, &env.block, proposal_id)?; + + try_finish_challenge( + deps, + env, + app, + proposal_info, + outcome, + challenge, + challenge_id, + ) } -fn cast_vote( +fn veto( deps: DepsMut, env: Env, + info: MessageInfo, app: &ChallengeApp, - vote: Vote, challenge_id: u64, ) -> AppResult { - let mut vote = vote.check(deps.as_ref())?.optimistic(); + let proposal_id = + last_proposal(challenge_id, deps.as_ref())?.ok_or(AppError::ExpectedProposal {})?; - let mut check_ins = DAILY_CHECK_INS.load(deps.storage, challenge_id)?; - // We can unwrap because there will always be atleast one element in the vector - let check_in = check_ins.last_mut().unwrap(); - - if check_in.status != CheckInStatus::CheckedInNotYetVoted - && check_in.status != CheckInStatus::VotedNotYetTallied - { - return Err(AppError::Std(StdError::generic_err(format!( - "Wrong check in status {:?} for casting vote", - check_in.status - )))); - } - - // Check if the voter has already voted - if VOTES - .may_load( - deps.storage, - (challenge_id, check_in.last.nanos(), vote.voter.to_owned()), - ) - .map_or(false, |votes| votes.iter().any(|v| v.voter == vote.voter)) - { - return Err(AppError::AlreadyVoted {}); - } - - VOTES.save( - deps.storage, - (challenge_id, check_in.last.nanos(), vote.voter.to_owned()), - &vote, - )?; - - let mut challenge_votes = CHALLENGE_VOTES.load(deps.storage, challenge_id)?; - vote.for_check_in = Some(check_in.last); - challenge_votes.push(vote); - CHALLENGE_VOTES.save(deps.storage, challenge_id, &challenge_votes)?; - - check_in.status = CheckInStatus::VotedNotYetTallied; - DAILY_CHECK_INS.save(deps.storage, challenge_id, &check_ins)?; - - // If all friends have voted, we tally the votes. - if CHALLENGE_VOTES.load(deps.storage, challenge_id)?.len() - == CHALLENGE_FRIENDS.load(deps.storage, challenge_id)?.len() - { - return tally_votes_for_check_in(deps, env, app, challenge_id); - } + app.admin.assert_admin(deps.as_ref(), &info.sender)?; + let proposal_info = SIMPLE_VOTING.veto_proposal(deps.storage, &env.block, proposal_id)?; Ok(app.tag_response( - Response::new().add_attribute("action", "cast_vote"), - "cast_vote", + Response::new().add_attribute("proposal_info", format!("{proposal_info:?}")), + "veto", )) } -fn tally_votes_for_check_in( +fn try_finish_challenge( deps: DepsMut, _env: Env, app: &ChallengeApp, + proposal_info: ProposalInfo, + proposal_outcome: ProposalOutcome, + mut challenge: ChallengeEntry, challenge_id: u64, ) -> AppResult { - let mut check_ins = DAILY_CHECK_INS.load(deps.storage, challenge_id)?; - let check_in = check_ins.last_mut().unwrap(); - if check_in.status != CheckInStatus::VotedNotYetTallied { - return Err(AppError::WrongCheckInStatus {}); - } - - let challenge = CHALLENGE_LIST.load(deps.storage, challenge_id)?; - let votes = CHALLENGE_VOTES.load(deps.storage, challenge_id)?; - - // check for any false votes on check_ins that match the vote timestamps - let any_false_vote = votes - .iter() - .filter(|&vote| { - vote.for_check_in - .map_or(false, |timestamp| timestamp == check_in.last) - }) - .any(|v| v.approval == Some(false)); - - check_in.status = CheckInStatus::VotedAndTallied; - - if any_false_vote { - check_in.tally_result = Some(false); - DAILY_CHECK_INS.save(deps.storage, challenge_id, &check_ins)?; + let friends = CHALLENGE_FRIENDS.load(deps.storage, challenge_id)?; + let challenge_finished = if matches!(proposal_outcome, ProposalOutcome::Passed) { + challenge.admin_strikes.strike() + } else { + false + }; - charge_penalty(deps, app, challenge_id) + // Return here if not required to charge penalty + let res = if !matches!(proposal_outcome, ProposalOutcome::Passed) { + app.tag_response(Response::new(), "finish_vote") } else { - check_in.tally_result = Some(true); - DAILY_CHECK_INS.save(deps.storage, challenge_id, &check_ins)?; - CHALLENGE_LIST.save(deps.storage, challenge_id, &challenge)?; - - Ok(app.tag_response( - Response::new().add_attribute( - "message", - "All votes were positive. ChallengeStatus has been set to OverAndCompleted.", - ), - "tally_votes_for_check_in", - )) - } + charge_penalty(deps, app, challenge, friends)? + }; + Ok(res + .add_attribute("proposal_info", format!("{proposal_info:?}")) + .add_attribute("challenge_finished", challenge_finished.to_string())) } -fn charge_penalty(deps: DepsMut, app: &ChallengeApp, challenge_id: u64) -> AppResult { - let challenge = CHALLENGE_LIST.load(deps.storage, challenge_id)?; - let friends = CHALLENGE_FRIENDS.load(deps.storage, challenge_id)?; - +fn charge_penalty( + deps: DepsMut, + app: &ChallengeApp, + challenge: ChallengeEntry, + friends: Vec>, +) -> Result { let num_friends = friends.len() as u128; if num_friends == 0 { - return Err(AppError::Std(StdError::generic_err( - "No friends found for the challenge.", - ))); + return Err(AppError::ZeroFriends {}); } - - let compute_amount_per_friend = || -> Result { - if challenge.total_check_ins == 0 || num_friends == 0 { - return Err(AppError::Std(StdError::generic_err(format!( - "Cannot compute amount per friend. total_check_ins: {}, num_friends: {}", - challenge.total_check_ins, num_friends - )))); - } - let amount_per_friend = - (challenge.collateral.amount.u128() / challenge.total_check_ins) / num_friends; - Ok(amount_per_friend) + let (amount_per_friend, remainder) = match challenge.strike_strategy { + crate::state::StrikeStrategy::Split(amount) => ( + Uint128::new(amount.u128() / num_friends), + amount.u128() % num_friends, + ), + crate::state::StrikeStrategy::PerFriend(amount) => (amount, 0), }; - let reaminder = challenge.collateral.amount.u128() % num_friends; - let asset_per_friend = OfferAsset { - name: challenge.collateral.name, - amount: Uint128::from(compute_amount_per_friend()?), + name: challenge.strike_asset, + amount: amount_per_friend, }; let bank = app.bank(deps.as_ref()); let executor = app.executor(deps.as_ref()); // Create a transfer action for each friend - let transfer_actions: Result, _> = friends + let transfer_actions = friends .into_iter() - .map(|friend| bank.transfer(vec![asset_per_friend.clone()], &friend.address)) - .collect(); + .map(|friend| { + let recipent = match friend { + Friend::Addr(addr) => addr.address, + Friend::AbstractAccount(account_id) => { + app.account_registry(deps.as_ref()) + .account_base(&account_id)? + .proxy + } + }; + bank.transfer(vec![asset_per_friend.clone()], &recipent) + }) + .collect::, _>>()?; - let transfer_msg = executor.execute(transfer_actions?); + let transfer_msg = executor.execute(transfer_actions)?; Ok(app - .tag_response( - Response::new().add_attribute( - "message", - "All votes were negative. ChallengeStatus has been set to OverAndCompleted.", - ), - "charge_penalty", - ) - .add_messages(transfer_msg) - .add_attribute("remainder was", reaminder.to_string())) + .tag_response(Response::new(), "charge_penalty") + .add_message(transfer_msg) + .add_attribute("remainder", remainder.to_string())) +} + +pub(crate) fn last_proposal(challenge_id: u64, deps: Deps) -> StdResult> { + CHALLENGE_PROPOSALS + .prefix(challenge_id) + .keys(deps.storage, None, None, Order::Descending) + .next() + .transpose() } diff --git a/modules/contracts/apps/challenge/src/handlers/instantiate.rs b/modules/contracts/apps/challenge/src/handlers/instantiate.rs index 144cc0f2e9..28ee381b0f 100644 --- a/modules/contracts/apps/challenge/src/handlers/instantiate.rs +++ b/modules/contracts/apps/challenge/src/handlers/instantiate.rs @@ -1,14 +1,16 @@ use crate::contract::{AppResult, ChallengeApp}; -use crate::state::NEXT_ID; -use cosmwasm_std::{DepsMut, Empty, Env, MessageInfo, Response}; +use crate::msg::ChallengeInstantiateMsg; +use crate::state::{NEXT_ID, SIMPLE_VOTING}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; pub fn instantiate_handler( deps: DepsMut, _env: Env, _info: MessageInfo, _app: ChallengeApp, - _msg: Empty, + msg: ChallengeInstantiateMsg, ) -> AppResult { NEXT_ID.save(deps.storage, &0u64)?; + SIMPLE_VOTING.instantiate(deps.storage, &msg.vote_config)?; Ok(Response::new()) } diff --git a/modules/contracts/apps/challenge/src/handlers/query.rs b/modules/contracts/apps/challenge/src/handlers/query.rs index 4367384412..f40ca791a1 100644 --- a/modules/contracts/apps/challenge/src/handlers/query.rs +++ b/modules/contracts/apps/challenge/src/handlers/query.rs @@ -1,43 +1,66 @@ use crate::contract::{AppResult, ChallengeApp}; use crate::msg::{ - ChallengeQueryMsg, ChallengeResponse, ChallengesResponse, CheckInsResponse, FriendsResponse, - VoteResponse, + ChallengeEntryResponse, ChallengeQueryMsg, ChallengeResponse, ChallengesResponse, + FriendsResponse, ProposalsResponse, VoteResponse, VotesResponse, }; -use crate::state::{ - ChallengeEntry, Vote, CHALLENGE_FRIENDS, CHALLENGE_LIST, DAILY_CHECK_INS, VOTES, -}; -use cosmwasm_std::{to_binary, Binary, Deps, Env, Order, StdResult}; +use crate::state::{CHALLENGES, CHALLENGE_FRIENDS, CHALLENGE_PROPOSALS, SIMPLE_VOTING}; +use abstract_core::objects::voting::{ProposalId, ProposalInfo, VoteResult, DEFAULT_LIMIT}; +use cosmwasm_std::{to_binary, Binary, BlockInfo, Deps, Env, Order, StdResult}; use cw_storage_plus::Bound; +use super::execute::last_proposal; + pub fn query_handler( deps: Deps, - _env: Env, + env: Env, app: &ChallengeApp, msg: ChallengeQueryMsg, ) -> AppResult { match msg { ChallengeQueryMsg::Challenge { challenge_id } => { - to_binary(&query_challenge(deps, app, challenge_id)?) + to_binary(&query_challenge(deps, env, app, challenge_id)?) } ChallengeQueryMsg::Challenges { start_after, limit } => { - to_binary(&query_challenges(deps, start_after, limit)?) + to_binary(&query_challenges(deps, env, start_after, limit)?) } ChallengeQueryMsg::Friends { challenge_id } => { to_binary(&query_friends(deps, app, challenge_id)?) } - ChallengeQueryMsg::CheckIns { challenge_id } => { - to_binary(&query_check_in(deps, app, challenge_id)?) - } ChallengeQueryMsg::Vote { - last_check_in, voter_addr, challenge_id, - } => to_binary(&query_vote_for_check_in( + proposal_id, + } => to_binary(&query_vote( deps, app, voter_addr, - last_check_in, challenge_id, + proposal_id, + )?), + ChallengeQueryMsg::Proposals { + challenge_id, + start_after, + limit, + } => to_binary(&query_proposals( + deps, + env, + app, + challenge_id, + start_after, + limit, + )?), + ChallengeQueryMsg::Votes { + challenge_id, + proposal_id, + start_after, + limit, + } => to_binary(&query_votes( + deps, + app, + challenge_id, + proposal_id, + start_after, + limit, )?), } .map_err(Into::into) @@ -45,53 +68,142 @@ pub fn query_handler( fn query_challenge( deps: Deps, + env: Env, _app: &ChallengeApp, challenge_id: u64, ) -> AppResult { - let challenge = CHALLENGE_LIST.may_load(deps.storage, challenge_id)?; + let challenge = CHALLENGES.may_load(deps.storage, challenge_id)?; + + let proposal = get_proposal_if_active(challenge_id, deps, &env.block)?; + let challenge = + challenge.map(|entry| ChallengeEntryResponse::from_entry(entry, challenge_id, proposal)); Ok(ChallengeResponse { challenge }) } -fn query_challenges(deps: Deps, start: u64, limit: u32) -> AppResult { - let challenges: StdResult> = CHALLENGE_LIST - .range( - deps.storage, - Some(Bound::exclusive(start)), - Some(Bound::inclusive(limit)), - Order::Ascending, - ) - .map(|result| result.map(|(_, entry)| entry)) // strip the keys - .collect::>>(); - Ok(ChallengesResponse(challenges.unwrap_or_default())) +fn get_proposal_if_active( + challenge_id: u64, + deps: Deps, + block: &BlockInfo, +) -> Result, crate::error::AppError> { + let maybe_id = last_proposal(challenge_id, deps)?; + let proposal = maybe_id + .map(|id| { + let proposal = SIMPLE_VOTING.load_proposal(deps.storage, block, id)?; + if proposal.assert_active_proposal().is_ok() { + AppResult::Ok(Some(proposal)) + } else { + AppResult::Ok(None) + } + }) + .transpose()? + .flatten(); + Ok(proposal) +} + +fn query_challenges( + deps: Deps, + env: Env, + start: Option, + limit: Option, +) -> AppResult { + let min = start.map(Bound::exclusive); + let limit = limit.unwrap_or(DEFAULT_LIMIT); + + let challenges = CHALLENGES + .range(deps.storage, min, None, Order::Ascending) + .take(limit as usize) + .map(|result| { + result + .map_err(Into::into) + // Cast result into response + .map(|(challenge_id, entry)| { + let proposal = + get_proposal_if_active(challenge_id, deps, &env.block).unwrap_or_default(); + ChallengeEntryResponse::from_entry(entry, challenge_id, proposal) + }) + }) + .collect::>>()?; + Ok(ChallengesResponse { challenges }) } fn query_friends(deps: Deps, _app: &ChallengeApp, challenge_id: u64) -> AppResult { let friends = CHALLENGE_FRIENDS.may_load(deps.storage, challenge_id)?; - Ok(FriendsResponse(friends.unwrap_or_default())) + Ok(FriendsResponse { + friends: friends.unwrap_or_default(), + }) } -fn query_check_in( +fn query_vote( deps: Deps, _app: &ChallengeApp, + voter_addr: String, challenge_id: u64, -) -> AppResult { - let check_ins = DAILY_CHECK_INS.may_load(deps.storage, challenge_id)?; - Ok(CheckInsResponse(check_ins.unwrap_or_default())) + proposal_id: Option, +) -> AppResult { + let voter = deps.api.addr_validate(&voter_addr)?; + let maybe_proposal_id = if let Some(proposal_id) = proposal_id { + // Only allow loading proposal_id for this challenge + CHALLENGE_PROPOSALS + .may_load(deps.storage, (challenge_id, proposal_id))? + .map(|_| proposal_id) + } else { + last_proposal(challenge_id, deps)? + }; + let vote = if let Some(proposal_id) = maybe_proposal_id { + SIMPLE_VOTING.load_vote(deps.storage, proposal_id, &voter)? + } else { + None + }; + Ok(VoteResponse { vote }) } -fn query_vote_for_check_in( +fn query_proposals( deps: Deps, + env: Env, _app: &ChallengeApp, - voter_addr: String, - last_check_in: u64, challenge_id: u64, -) -> AppResult { - let v = Vote { - voter: voter_addr, - approval: None, - for_check_in: None, + start_after: Option, + limit: Option, +) -> AppResult { + let min = start_after.map(Bound::exclusive); + let limit = limit.unwrap_or(DEFAULT_LIMIT); + let ids: Vec = CHALLENGE_PROPOSALS + .prefix(challenge_id) + .keys(deps.storage, min, None, Order::Ascending) + .take(limit as usize) + .collect::>()?; + let results = ids + .into_iter() + .map(|id| { + SIMPLE_VOTING + .load_proposal(deps.storage, &env.block, id) + .map(|v| (id, v)) + }) + .collect::>>()?; + + Ok(ProposalsResponse { proposals: results }) +} + +fn query_votes( + deps: Deps, + _app: &ChallengeApp, + challenge_id: u64, + proposal_id: Option, + start_after: Option, + limit: Option, +) -> AppResult { + let maybe_proposal_id = if let Some(proposal_id) = proposal_id { + // Only allow loading proposal_id for this challenge + CHALLENGE_PROPOSALS + .may_load(deps.storage, (challenge_id, proposal_id))? + .map(|_| proposal_id) + } else { + last_proposal(challenge_id, deps)? }; - let v = v.check(deps)?; - let vote = VOTES.may_load(deps.storage, (challenge_id, last_check_in, v.voter))?; - Ok(VoteResponse { vote }) + let votes = if let Some(proposal_id) = maybe_proposal_id { + SIMPLE_VOTING.query_by_id(deps.storage, proposal_id, start_after.as_ref(), limit)? + } else { + vec![] + }; + Ok(VotesResponse { votes }) } diff --git a/modules/contracts/apps/challenge/src/msg.rs b/modules/contracts/apps/challenge/src/msg.rs index 9e54e4be78..3b7bc87668 100644 --- a/modules/contracts/apps/challenge/src/msg.rs +++ b/modules/contracts/apps/challenge/src/msg.rs @@ -2,19 +2,38 @@ //! Message types for the challenge app use crate::{ contract::ChallengeApp, - state::{ChallengeEntry, ChallengeEntryUpdate, CheckIn, Friend, UpdateFriendsOpKind, Vote}, + state::{ + AdminStrikes, ChallengeEntry, ChallengeEntryUpdate, StrikeStrategy, UpdateFriendsOpKind, + }, +}; +use abstract_core::objects::{ + voting::{ProposalId, ProposalInfo, Vote, VoteConfig}, + AccountId, AssetEntry, }; -use abstract_dex_adapter::msg::OfferAsset; +use abstract_sdk::{AbstractSdkResult, AccountVerification}; use cosmwasm_schema::QueryResponses; -use cosmwasm_std::Addr; +use cosmwasm_std::{Addr, Deps, StdResult, Timestamp, Uint64}; +use cw_address_like::AddressLike; abstract_app::app_msg_types!(ChallengeApp, ChallengeExecuteMsg, ChallengeQueryMsg); +/// Challenge instantiate message +#[cosmwasm_schema::cw_serde] +pub struct ChallengeInstantiateMsg { + /// Config for [`SimpleVoting`](abstract_core::objects::voting::SimpleVoting) object + pub vote_config: VoteConfig, +} + /// Challenge execute messages #[cosmwasm_schema::cw_serde] #[cfg_attr(feature = "interface", derive(cw_orch::ExecuteFns))] #[cfg_attr(feature = "interface", impl_into(ExecuteMsg))] pub enum ChallengeExecuteMsg { + /// Update challenge config + UpdateConfig { + /// New config for vote + new_vote_config: VoteConfig, + }, /// Create new challenge CreateChallenge { /// New challenge arguments @@ -41,23 +60,22 @@ pub enum ChallengeExecuteMsg { /// Kind of operation: add or remove friends op_kind: UpdateFriendsOpKind, }, - /// Daily check in by the challenge author - DailyCheckIn { - /// Id of the challenge to check in - challenge_id: u64, - /// metadata can be added for extra description of the check-in. - /// For example, if the check-in is a photo, the metadata can be a link to the photo. - metadata: Option, - }, /// Cast vote as a friend CastVote { - /// Id of challenge to cast vote on + /// Challenge Id to cast vote on + challenge_id: u64, + /// Wether voter thinks admin deserves punishment + vote_to_punish: Vote, + }, + /// Count votes for challenge id + CountVotes { + /// Challenge Id for counting votes + challenge_id: u64, + }, + /// Veto the last vote + Veto { + /// Challenge id to do the veto challenge_id: u64, - /// If the vote.approval is None, we assume the voter approves, - /// and the contract will internally set the approval field to Some(true). - /// This is because we assume that if a friend didn't vote, the friend approves, - /// otherwise the voter would Vote with approval set to Some(false). - vote: Vote, }, } @@ -77,9 +95,9 @@ pub enum ChallengeQueryMsg { #[returns(ChallengesResponse)] Challenges { /// start after challenge Id - start_after: u64, + start_after: Option, /// Max amount of challenges in response - limit: u32, + limit: Option, }, /// List of friends by Id #[returns(FriendsResponse)] @@ -87,29 +105,103 @@ pub enum ChallengeQueryMsg { /// Id of requested challenge challenge_id: u64, }, - /// List of check-ins by Id - #[returns(CheckInsResponse)] - CheckIns { - /// Id of requested challenge - challenge_id: u64, - }, - /// Get last vote of friend + /// Get vote of friend #[returns(VoteResponse)] Vote { - /// Block height of last check in - last_check_in: u64, /// Addr of the friend voter_addr: String, /// Id of requested challenge challenge_id: u64, + /// Proposal id of previous proposal + /// Providing None requests last proposal results + proposal_id: Option, }, + /// Get votes of challenge + #[returns(VotesResponse)] + Votes { + /// Id of requested challenge + challenge_id: u64, + /// Proposal id of previous proposal + /// Providing None requests last proposal results + proposal_id: Option, + /// start after Addr + start_after: Option, + /// Max amount of challenges in response + limit: Option, + }, + /// Get results of previous votes for this challenge + #[returns(ProposalsResponse)] + Proposals { + /// Challenge Id for previous votes + challenge_id: u64, + /// start after ProposalId + start_after: Option, + /// Max amount of proposals in response + limit: Option, + }, +} +/// Response for previous_vote query +#[cosmwasm_schema::cw_serde] +pub struct VotesResponse { + /// List of votes by addr + pub votes: Vec<(Addr, Option)>, +} + +/// Response for proposals query +#[cosmwasm_schema::cw_serde] +pub struct ProposalsResponse { + /// results of proposals + pub proposals: Vec<(ProposalId, ProposalInfo)>, } /// Response for challenge query #[cosmwasm_schema::cw_serde] pub struct ChallengeResponse { /// Challenge info, will return null if there was no challenge by Id - pub challenge: Option, + pub challenge: Option, +} + +/// Response struct for challenge entry +#[cosmwasm_schema::cw_serde] +pub struct ChallengeEntryResponse { + /// Id of the challenge, + pub challenge_id: u64, + /// Name of challenge + pub name: String, + /// Asset for punishment for failing a challenge + pub strike_asset: AssetEntry, + /// How strike will get distributed between friends + pub strike_strategy: StrikeStrategy, + /// Description of the challenge + pub description: String, + /// When challenge ends + pub end_timestamp: Timestamp, + /// Proposal duration in seconds + pub proposal_duration_seconds: Uint64, + /// State of strikes of admin for this challenge + pub admin_strikes: AdminStrikes, + /// Current active proposal + pub active_proposal: Option, +} + +impl ChallengeEntryResponse { + pub(crate) fn from_entry( + entry: ChallengeEntry, + challenge_id: u64, + active_proposal: Option, + ) -> Self { + Self { + challenge_id, + name: entry.name, + strike_asset: entry.strike_asset, + strike_strategy: entry.strike_strategy, + description: entry.description, + end_timestamp: entry.end_timestamp, + proposal_duration_seconds: entry.proposal_duration_seconds, + admin_strikes: entry.admin_strikes, + active_proposal, + } + } } /// Arguments for new challenge @@ -117,47 +209,102 @@ pub struct ChallengeResponse { pub struct ChallengeRequest { /// Name of challenge pub name: String, - /// Asset punishment for failing a challenge - pub collateral: OfferAsset, - /// Desciption of the challenge - pub description: String, - /// In what period challenge should end - pub end: DurationChoice, + /// Asset for punishment for failing a challenge + pub strike_asset: AssetEntry, + /// How strike will get distributed between friends + pub strike_strategy: StrikeStrategy, + /// Description of the challenge + pub description: Option, + /// In what duration challenge should end + pub challenge_duration_seconds: Uint64, + /// Duration set for each proposal + /// Proposals starts after one vote initiated by any of the friends + pub proposal_duration_seconds: Uint64, + /// Strike limit, defaults to 1 + pub strikes_limit: Option, + /// Initial list of friends + pub init_friends: Vec>, } -/// Response for check_ins query -/// Returns a list of check ins +/// Friend object #[cosmwasm_schema::cw_serde] -pub struct CheckInsResponse(pub Vec); +pub enum Friend { + /// Friend with address and a name + Addr(FriendByAddr), + /// Abstract Account Id of the friend + AbstractAccount(AccountId), +} + +impl Friend { + pub(crate) fn check( + self, + deps: Deps, + app: &ChallengeApp, + ) -> AbstractSdkResult<(Addr, Friend)> { + let account_registry = app.account_registry(deps); + let checked = match self { + Friend::Addr(human) => { + let checked = human.check(deps)?; + (checked.address.clone(), Friend::Addr(checked)) + } + Friend::AbstractAccount(account_id) => { + let base = account_registry.account_base(&account_id)?; + (base.manager, Friend::AbstractAccount(account_id)) + } + }; + Ok(checked) + } +} + +impl Friend { + pub(crate) fn addr(&self, deps: Deps, app: &ChallengeApp) -> AbstractSdkResult { + Ok(match self { + Friend::Addr(human) => human.address.clone(), + Friend::AbstractAccount(account_id) => { + app.account_registry(deps).account_base(account_id)?.manager + } + }) + } +} -/// Duration for challenge +/// Friend by address #[cosmwasm_schema::cw_serde] -pub enum DurationChoice { - /// One week - Week, - /// One month - Month, - /// Quarter of the year - Quarter, - /// One year - Year, - /// 100 years - OneHundredYears, +pub struct FriendByAddr { + /// Address of the friend + pub address: T, + /// Name of the friend + pub name: String, +} + +impl FriendByAddr { + pub(crate) fn check(self, deps: Deps) -> StdResult> { + let checked = deps.api.addr_validate(&self.address)?; + Ok(FriendByAddr { + address: checked, + name: self.name, + }) + } } /// Response for vote query #[cosmwasm_schema::cw_serde] pub struct VoteResponse { /// The vote, will return null if there was no vote by this user - pub vote: Option>, + pub vote: Option, } /// Response for challenges query /// Returns a list of challenges #[cosmwasm_schema::cw_serde] -pub struct ChallengesResponse(pub Vec); +pub struct ChallengesResponse { + /// List of indexed challenges + pub challenges: Vec, +} /// Response for friends query /// Returns a list of friends #[cosmwasm_schema::cw_serde] -pub struct FriendsResponse(pub Vec>); +pub struct FriendsResponse { + /// List of friends on challenge + pub friends: Vec>, +} diff --git a/modules/contracts/apps/challenge/src/state.rs b/modules/contracts/apps/challenge/src/state.rs index c0883b39f8..d61797dd5d 100644 --- a/modules/contracts/apps/challenge/src/state.rs +++ b/modules/contracts/apps/challenge/src/state.rs @@ -1,12 +1,14 @@ -use abstract_dex_adapter::msg::OfferAsset; -use chrono::Duration; -use cosmwasm_std::{Addr, Deps, Env, StdError, StdResult, Timestamp}; -use cw_address_like::AddressLike; +use abstract_core::objects::{ + validation::{self, ValidationError}, + voting::{ProposalId, SimpleVoting}, + AssetEntry, +}; +use cosmwasm_std::{Addr, Timestamp, Uint128, Uint64}; use cw_storage_plus::{Item, Map}; -use crate::msg::{ChallengeRequest, DurationChoice}; +use crate::msg::{ChallengeRequest, Friend}; -pub const DAY: u64 = 86400; +pub const MAX_AMOUNT_OF_FRIENDS: u64 = 20; #[cosmwasm_schema::cw_serde] pub struct Config { @@ -16,104 +18,69 @@ pub struct Config { #[cosmwasm_schema::cw_serde] pub struct ChallengeEntry { pub name: String, - pub collateral: OfferAsset, + pub strike_asset: AssetEntry, + pub strike_strategy: StrikeStrategy, pub description: String, - pub end: Timestamp, - pub total_check_ins: u128, - pub status: ChallengeStatus, - pub admin_strikes: StrikeConfig, + pub admin_strikes: AdminStrikes, + pub proposal_duration_seconds: Uint64, + pub end_timestamp: Timestamp, } +/// Strategy for striking the admin #[cosmwasm_schema::cw_serde] -pub struct StrikeConfig { - /// The number of striked the admin has incurred. +pub enum StrikeStrategy { + /// Split amount between friends + Split(Uint128), + /// Amount for every friend + PerFriend(Uint128), +} + +#[cosmwasm_schema::cw_serde] +pub struct AdminStrikes { + /// The number of strikes the admin has incurred. pub num_strikes: u8, /// When num_strikes reached the limit, the challenge will be cancelled. pub limit: u8, } -impl Default for StrikeConfig { - fn default() -> Self { - StrikeConfig { +impl AdminStrikes { + fn new(limit: Option) -> Self { + AdminStrikes { num_strikes: 0, - limit: 3, + // One-time strike by default + limit: limit.unwrap_or(1), } } + + pub fn strike(&mut self) -> bool { + self.num_strikes += 1; + // check if it's last strike + self.num_strikes >= self.limit + } } impl ChallengeEntry { /// Creates a new challenge entry with the default status of Uninitialized and no admin strikes. - pub fn new(request: ChallengeRequest) -> Self { - ChallengeEntry { + pub fn new( + request: ChallengeRequest, + end_timestamp: Timestamp, + ) -> Result { + // validate namd and description + validation::validate_name(&request.name)?; + validation::validate_description(request.description.as_deref())?; + + Ok(ChallengeEntry { name: request.name, - collateral: request.collateral, - description: request.description, - end: Self::to_timestamp(request.end), - total_check_ins: 0, - status: ChallengeStatus::default(), - admin_strikes: StrikeConfig::default(), - } - } - - /// Sets the total number of check-ins based on the end time. - pub fn set_total_check_ins(&mut self, env: &Env) -> StdResult<()> { - let now = env.block.time; - - match self.end.seconds().checked_sub(now.seconds()) { - Some(duration_secs) => { - // Calculate the total number of check-ins - self.total_check_ins = duration_secs as u128 / DAY as u128; - Ok(()) - } - None => { - // If the end time is in the past, set the total check ins to 0. - self.total_check_ins = 0; - // If the end time is in the past or there's an overflow in calculation, return an error. - Err(StdError::generic_err(format!( - "Cannot compute. challenge.end time is in the past: {}, now is: {}", - self.end, now - ))) - } - } - } - - pub fn to_timestamp(end: DurationChoice) -> Timestamp { - match end { - DurationChoice::Week => { - Timestamp::from_seconds(Duration::weeks(1).to_std().unwrap().as_secs()) - } - DurationChoice::Month => { - Timestamp::from_seconds(Duration::days(30).to_std().unwrap().as_secs()) - } - DurationChoice::Quarter => { - Timestamp::from_seconds(Duration::days(90).to_std().unwrap().as_secs()) - } - DurationChoice::Year => { - Timestamp::from_seconds(Duration::days(365).to_std().unwrap().as_secs()) - } - DurationChoice::OneHundredYears => { - Timestamp::from_seconds(Duration::days(365 * 100).to_std().unwrap().as_secs()) - } - } + strike_asset: request.strike_asset, + strike_strategy: request.strike_strategy, + description: request.description.unwrap_or_default(), + admin_strikes: AdminStrikes::new(request.strikes_limit), + proposal_duration_seconds: request.proposal_duration_seconds, + end_timestamp, + }) } } -/// The status of a challenge. This can be used to trigger an automated Croncat job -/// based on the value of the status -#[derive(Default)] -#[cosmwasm_schema::cw_serde] -pub enum ChallengeStatus { - /// The challenge has not been initialized yet. This is the default state. - #[default] - Uninitialized, - /// The challenge is active and can be voted on. - Active, - /// The challenge was cancelled and no collateral was paid out. - Cancelled, - /// The challenge has pased the end time. - Over, -} - /// Only this struct and these fields are allowed to be updated. /// The status cannot be externally updated, it is updated by the contract. #[cosmwasm_schema::cw_serde] @@ -124,119 +91,17 @@ pub struct ChallengeEntryUpdate { #[cosmwasm_schema::cw_serde] pub enum UpdateFriendsOpKind { - Add, - Remove, -} - -#[cosmwasm_schema::cw_serde] -pub struct Friend { - pub address: T, - pub name: String, -} - -impl Friend { - /// A helper to convert from a string address to an Addr. - /// Additionally it validates the address. - pub fn check(self, deps: Deps) -> StdResult> { - Ok(Friend { - address: deps.api.addr_validate(&self.address)?, - name: self.name, - }) - } -} - -#[cosmwasm_schema::cw_serde] -pub struct Vote { - /// The address of the voter - pub voter: T, - /// The vote result - pub approval: Option, - /// Correlates to the last_checked_in field of the CheckIn struct. - pub for_check_in: Option, -} - -impl Vote { - /// A helper to convert from a string address to an Addr. - /// Additionally it validates the address. - pub fn check(self, deps: Deps) -> StdResult> { - Ok(Vote { - voter: deps.api.addr_validate(&self.voter)?, - approval: self.approval, - for_check_in: None, - }) - } -} - -impl Vote { - /// If the vote approval field is None, we assume the voter approves, - /// and return a vote with the approval field set to Some(true). - pub fn optimistic(self) -> Vote { - Vote { - voter: self.voter, - approval: Some(self.approval.unwrap_or(true)), - for_check_in: None, - } - } -} - -/// The check in struct is used to track the admin's check ins. -/// The admin must check in every 24 hours, otherwise they get a strike. -#[cosmwasm_schema::cw_serde] -pub struct CheckIn { - /// The blockheight of the last check in. - pub last: Timestamp, - /// The blockheight of the next check in. - /// In the case of a missed check in, this will always be pushed forward - /// internally by the contract. - pub next: Timestamp, - /// Optional metadata for the check in. For example, a link to a tweet. - pub metadata: Option, - /// The vote status of the CheckIn. - pub status: CheckInStatus, - /// The final result of the votes for this check in. - pub tally_result: Option, -} - -#[cosmwasm_schema::cw_serde] -pub enum CheckInStatus { - /// The admin has not yet checked in, therefore no voting or tallying - /// has occured for this check in. - NotCheckedIn, - /// The admin has checked in, but all friends have not yet all voted. - /// Some friends may have voted, but not all. - CheckedInNotYetVoted, - /// The admin mised their check in and got a strike. - MissedCheckIn, - /// The admin has checked in and all friends have voted. - /// But the check in has not yet been tallied. - VotedNotYetTallied, - /// The check in has been voted and tallied. - VotedAndTallied, -} - -impl CheckIn { - pub fn default_from(env: &Env) -> Self { - CheckIn { - last: Timestamp::from_seconds(env.block.time.seconds()), - // set the next check in to be 24 hours from now - next: Timestamp::from_seconds(env.block.time.seconds() + 60 * 60 * 24), - metadata: None, - status: CheckInStatus::NotCheckedIn, - tally_result: None, - } - } + Add {}, + Remove {}, } pub const NEXT_ID: Item = Item::new("next_id"); -pub const CHALLENGE_LIST: Map = Map::new("challenge_list"); -pub const CHALLENGE_FRIENDS: Map>> = Map::new("challenge_friends"); - -/// Key is a tuple of (challenge_id, check_in.last_checked_in, voter_address). -/// By using a composite key, it ensures only one user can vote per check_in. -pub const VOTES: Map<(u64, u64, Addr), Vote> = Map::new("votes"); - -/// For looking up all the votes by challenge_id. This is used to tally the votes. -pub const CHALLENGE_VOTES: Map>> = Map::new("challenge_votes"); - -/// For looking up all the check ins for a challenge_id. -pub const DAILY_CHECK_INS: Map> = Map::new("daily_checkins"); +pub const SIMPLE_VOTING: SimpleVoting = + SimpleVoting::new("votes", "votes_id", "votes_info", "votes_config"); + +pub const CHALLENGES: Map = Map::new("challenges"); +/// Friends list for the challenge +// Reduces gas consumption to load all friends +// Helpful during distributing penalty and re-creation voting +pub const CHALLENGE_FRIENDS: Map>> = Map::new("friends"); +pub const CHALLENGE_PROPOSALS: Map<(u64, ProposalId), cosmwasm_std::Empty> = Map::new("proposals"); diff --git a/modules/contracts/apps/challenge/tests/integrations.rs b/modules/contracts/apps/challenge/tests/integrations.rs index d0e1e24e85..672854c52c 100644 --- a/modules/contracts/apps/challenge/tests/integrations.rs +++ b/modules/contracts/apps/challenge/tests/integrations.rs @@ -4,107 +4,77 @@ use abstract_core::{ objects::{ gov_type::GovernanceDetails, module::{ModuleInfo, ModuleVersion}, + voting::{ProposalInfo, ProposalOutcome, ProposalStatus, Threshold, Vote, VoteConfig}, + AssetEntry, }, }; -use abstract_dex_adapter::msg::OfferAsset; use abstract_interface::{Abstract, AbstractAccount, AppDeployer, *}; use challenge_app::{ contract::{CHALLENGE_APP_ID, CHALLENGE_APP_VERSION}, + error::AppError, msg::{ - ChallengeQueryMsg, ChallengeRequest, ChallengeResponse, ChallengesResponse, - CheckInsResponse, DurationChoice, FriendsResponse, InstantiateMsg, VoteResponse, - }, - state::{ - ChallengeEntryUpdate, ChallengeStatus, CheckInStatus, Friend, UpdateFriendsOpKind, Vote, - DAY, + ChallengeEntryResponse, ChallengeInstantiateMsg, ChallengeQueryMsg, ChallengeRequest, + ChallengeResponse, ChallengesResponse, Friend, FriendByAddr, FriendsResponse, + InstantiateMsg, ProposalsResponse, VoteResponse, }, + state::{AdminStrikes, ChallengeEntryUpdate, StrikeStrategy, UpdateFriendsOpKind}, *, }; -use cosmwasm_std::{coin, Uint128}; +use cosmwasm_std::{coin, Uint128, Uint64}; use cw_asset::AssetInfo; use cw_orch::{anyhow, deploy::Deploy, prelude::*}; use lazy_static::lazy_static; -// consts for testing const ADMIN: &str = "admin"; const DENOM: &str = "TOKEN"; -const CHALLENGE_ID: u64 = 1; +const FIRST_CHALLENGE_ID: u64 = 1; + +const INITIAL_BALANCE: u128 = 50_000_000; lazy_static! { static ref CHALLENGE_REQ: ChallengeRequest = ChallengeRequest { name: "test".to_string(), - collateral: OfferAsset::new("denom", Uint128::new(100_000_000_000)), - description: "Test Challenge".to_string(), - end: DurationChoice::OneHundredYears, + strike_asset: AssetEntry::new("denom"), + strike_strategy: StrikeStrategy::Split(Uint128::new(30_000_000)), + description: Some("Test Challenge".to_string()), + challenge_duration_seconds: Uint64::new(10_000), + proposal_duration_seconds: Uint64::new(1_000), + strikes_limit: None, + init_friends: FRIENDS.clone() }; - static ref ALICE_ADDRESS: String = "alice0x".to_string(); - static ref BOB_ADDRESS: String = "bob0x".to_string(); - static ref CHARLIE_ADDRESS: String = "charlie0x".to_string(); - static ref ALICE_NAME: String = "Alice".to_string(); - static ref BOB_NAME: String = "Bob".to_string(); - static ref CHARLIE_NAME: String = "Charlie".to_string(); + static ref ALICE_ADDRESS: String = "alice".to_string(); + static ref BOB_ADDRESS: String = "bob".to_string(); + static ref CHARLIE_ADDRESS: String = "charlie".to_string(); + static ref ALICE_FRIEND: Friend = Friend::Addr(FriendByAddr { + address: "alice".to_string(), + name: "alice_name".to_string() + }); + static ref BOB_FRIEND: Friend = Friend::Addr(FriendByAddr { + address: "bob".to_string(), + name: "bob_name".to_string() + }); + static ref CHARLIE_FRIEND: Friend = Friend::Addr(FriendByAddr { + address: "charlie".to_string(), + name: "charlie_name".to_string() + }); static ref FRIENDS: Vec> = vec![ - Friend { - address: ALICE_ADDRESS.clone(), - name: ALICE_NAME.clone(), - }, - Friend { - address: BOB_ADDRESS.clone(), - name: BOB_NAME.clone(), - }, - Friend { - address: CHARLIE_ADDRESS.clone(), - name: CHARLIE_NAME.clone(), - }, + ALICE_FRIEND.clone(), + BOB_FRIEND.clone(), + CHARLIE_FRIEND.clone() ]; - static ref ALICE: Friend = Friend { - address: ALICE_ADDRESS.clone(), - name: ALICE_NAME.clone(), - }; - static ref BOB: Friend = Friend { - address: BOB_ADDRESS.clone(), - name: BOB_NAME.clone(), - }; - static ref ALICE_VOTE: Vote = Vote { - voter: ALICE_ADDRESS.clone(), - approval: Some(true), - for_check_in: None, + static ref UNCHECKED_FRIENDS: Vec> = { + FRIENDS + .clone() + .into_iter() + .map(|f| match f { + Friend::Addr(FriendByAddr { address, name }) => Friend::Addr(FriendByAddr { + address: Addr::unchecked(address), + name, + }), + Friend::AbstractAccount(account_id) => Friend::AbstractAccount(account_id), + }) + .collect() }; - static ref VOTES: Vec> = vec![ - Vote { - voter: ALICE_ADDRESS.clone(), - approval: Some(true), - for_check_in: None, - }, - Vote { - voter: BOB_ADDRESS.clone(), - approval: Some(true), - for_check_in: None, - }, - Vote { - voter: CHARLIE_ADDRESS.clone(), - approval: Some(true), - for_check_in: None, - }, - ]; - static ref ALICE_NO_VOTE: Vote = Vote { - voter: ALICE_ADDRESS.clone(), - approval: Some(false), - for_check_in: None, - }; - static ref ONE_NO_VOTE: Vec> = vec![ - ALICE_NO_VOTE.clone(), - Vote { - voter: BOB_ADDRESS.clone(), - approval: Some(true), - for_check_in: None, - }, - Vote { - voter: CHARLIE_ADDRESS.clone(), - approval: Some(true), - for_check_in: None, - }, - ]; } #[allow(unused)] @@ -118,7 +88,7 @@ fn setup() -> anyhow::Result<(Mock, AbstractAccount, Abstract, Deplo let sender = Addr::unchecked(ADMIN); // Create the mock let mock = Mock::new(&sender); - mock.set_balance(&sender, vec![coin(50_000_000, DENOM)])?; + mock.set_balance(&sender, vec![coin(INITIAL_BALANCE, DENOM)])?; let mut challenge_app = ChallengeApp::new(CHALLENGE_APP_ID, mock.clone()); // Deploy Abstract to the mock @@ -163,7 +133,12 @@ fn setup() -> anyhow::Result<(Mock, AbstractAccount, Abstract, Deplo ans_host_address: abstr_deployment.ans_host.addr_str()?, version_control_address: abstr_deployment.version_control.addr_str()?, }, - module: Empty {}, + module: ChallengeInstantiateMsg { + vote_config: VoteConfig { + threshold: Threshold::Majority {}, + veto_duration_seconds: None, + }, + }, }, None, )?; @@ -190,7 +165,9 @@ fn setup() -> anyhow::Result<(Mock, AbstractAccount, Abstract, Deplo fn test_should_successful_install() -> anyhow::Result<()> { let (_mock, _account, _abstr, apps) = setup()?; - let query_res = QueryMsg::from(ChallengeQueryMsg::Challenge { challenge_id: 1 }); + let query_res = QueryMsg::from(ChallengeQueryMsg::Challenge { + challenge_id: FIRST_CHALLENGE_ID, + }); assert_eq!( apps.challenge_app.query::(&query_res)?, ChallengeResponse { challenge: None } @@ -201,402 +178,555 @@ fn test_should_successful_install() -> anyhow::Result<()> { #[test] fn test_should_create_challenge() -> anyhow::Result<()> { - let (_mock, _account, _abstr, apps) = setup()?; + let (mock, _account, _abstr, apps) = setup()?; let challenge_req = CHALLENGE_REQ.clone(); apps.challenge_app.create_challenge(CHALLENGE_REQ.clone())?; - let challenge_query = QueryMsg::from(ChallengeQueryMsg::Challenge { challenge_id: 1 }); + let challenge_query = QueryMsg::from(ChallengeQueryMsg::Challenge { + challenge_id: FIRST_CHALLENGE_ID, + }); - let created = apps + let created_challenge = apps .challenge_app - .query::(&challenge_query)?; - - assert_eq!(created.challenge.as_ref().unwrap().name, "test".to_string()); - assert_eq!( - created.challenge.as_ref().unwrap().collateral, - challenge_req.collateral - ); - assert_eq!( - created.challenge.as_ref().unwrap().description, - challenge_req.description - ); - assert_eq!( - created.challenge.as_ref().unwrap().status, - ChallengeStatus::Active - ); + .query::(&challenge_query)? + .challenge + .unwrap(); + + let expected_response = ChallengeEntryResponse { + challenge_id: FIRST_CHALLENGE_ID, + name: challenge_req.name, + strike_asset: challenge_req.strike_asset, + strike_strategy: challenge_req.strike_strategy, + description: challenge_req.description.unwrap(), + end_timestamp: mock.block_info()?.time.plus_seconds(10_000), + proposal_duration_seconds: Uint64::new(1_000), + admin_strikes: AdminStrikes { + num_strikes: 0, + limit: challenge_req.strikes_limit.unwrap_or(1), + }, + active_proposal: None, + }; + assert_eq!(created_challenge, expected_response); Ok(()) } #[test] -fn test_should_update_challenge() -> anyhow::Result<()> { +fn test_update_challenge() -> anyhow::Result<()> { let (_mock, _account, _abstr, apps) = setup()?; apps.challenge_app.create_challenge(CHALLENGE_REQ.clone())?; - let query = QueryMsg::from(ChallengeQueryMsg::Challenge { challenge_id: 1 }); - apps.challenge_app.query::(&query)?; + let new_name = "update-test".to_string(); + let new_description = "Updated Test Challenge".to_string(); let to_update = ChallengeEntryUpdate { - name: Some("update-test".to_string()), - description: Some("Updated Test Challenge".to_string()), + name: Some(new_name.clone()), + description: Some(new_description.clone()), }; - apps.challenge_app.update_challenge(to_update.clone(), 1)?; - let res = apps.challenge_app.query::(&query)?; + apps.challenge_app + .update_challenge(to_update.clone(), FIRST_CHALLENGE_ID)?; + let res: ChallengeResponse = + apps.challenge_app + .query(&QueryMsg::from(ChallengeQueryMsg::Challenge { + challenge_id: FIRST_CHALLENGE_ID, + }))?; + let challenge = res.challenge.unwrap(); - assert_eq!( - res.challenge.as_ref().unwrap().name, - to_update.name.unwrap() - ); - assert_eq!( - res.challenge.as_ref().unwrap().description, - to_update.description.unwrap(), - ); + assert_eq!(challenge.name, new_name); + assert_eq!(challenge.description, new_description,); Ok(()) } #[test] -fn test_should_cancel_challenge() -> anyhow::Result<()> { - let (_mock, _account, _abstr, apps) = setup()?; +fn test_cancel_challenge() -> anyhow::Result<()> { + let (mock, _account, _abstr, apps) = setup()?; + // Challenge without active proposals apps.challenge_app.create_challenge(CHALLENGE_REQ.clone())?; - let query = QueryMsg::from(ChallengeQueryMsg::Challenge { challenge_id: 1 }); + apps.challenge_app.cancel_challenge(FIRST_CHALLENGE_ID)?; + + let res: ChallengeResponse = + apps.challenge_app + .query(&QueryMsg::from(ChallengeQueryMsg::Challenge { + challenge_id: FIRST_CHALLENGE_ID, + }))?; + let challenge = res.challenge.unwrap(); - apps.challenge_app.query::(&query)?; + assert_eq!(challenge.end_timestamp, mock.block_info()?.time); - apps.challenge_app.cancel_challenge(1)?; + // Challenge with active proposal + apps.challenge_app.create_challenge(CHALLENGE_REQ.clone())?; + apps.challenge_app + .call_as(&Addr::unchecked(ALICE_ADDRESS.as_str())) + .cast_vote( + FIRST_CHALLENGE_ID + 1, + Vote { + vote: true, + memo: None, + }, + )?; - let res = apps.challenge_app.query::(&query)?; + apps.challenge_app + .cancel_challenge(FIRST_CHALLENGE_ID + 1)?; + + let res: ChallengeResponse = + apps.challenge_app + .query(&QueryMsg::from(ChallengeQueryMsg::Challenge { + challenge_id: FIRST_CHALLENGE_ID + 1, + }))?; + let challenge = res.challenge.unwrap(); + assert_eq!(challenge.end_timestamp, mock.block_info()?.time); + let proposals: ProposalsResponse = + apps.challenge_app + .proposals(FIRST_CHALLENGE_ID + 1, None, None)?; - assert_eq!(res.challenge.unwrap().status, ChallengeStatus::Cancelled); + assert_eq!( + proposals.proposals[0].1.status, + ProposalStatus::Finished(ProposalOutcome::Canceled) + ); Ok(()) } #[test] -fn test_should_add_single_friend_for_challenge() -> anyhow::Result<()> { - let (_mock, _account, _abstr, apps) = setup()?; +fn test_add_single_friend_for_challenge() -> anyhow::Result<()> { + let (_mock, _account, abstr, apps) = setup()?; apps.challenge_app.create_challenge(CHALLENGE_REQ.clone())?; - apps.challenge_app - .query::(&QueryMsg::from(ChallengeQueryMsg::Challenge { - challenge_id: 1, - }))?; + let new_account = + abstr + .account_factory + .create_default_account(GovernanceDetails::Monarchy { + monarch: ADMIN.to_string(), + })?; + let new_friend: Friend = Friend::AbstractAccount(new_account.id()?); apps.challenge_app.update_friends_for_challenge( - 1, - vec![ALICE.clone()], - UpdateFriendsOpKind::Add, + FIRST_CHALLENGE_ID, + vec![new_friend.clone()], + UpdateFriendsOpKind::Add {}, )?; - let response = apps - .challenge_app - .query::(&QueryMsg::from(ChallengeQueryMsg::Friends { - challenge_id: 1, - }))?; + let response: FriendsResponse = + apps.challenge_app + .query(&QueryMsg::from(ChallengeQueryMsg::Friends { + challenge_id: FIRST_CHALLENGE_ID, + }))?; + let friends = response.friends; + + let mut expected_friends: Vec> = UNCHECKED_FRIENDS.clone(); + expected_friends.push(Friend::AbstractAccount(new_account.id()?)); + + assert_eq!(friends, expected_friends); - for friend in response.0.iter() { - assert_eq!(friend.address, Addr::unchecked(ALICE_ADDRESS.clone())); - assert_eq!(friend.name, ALICE_NAME.clone()); - } Ok(()) } #[test] -fn test_should_add_friends_for_challenge() -> anyhow::Result<()> { +fn test_add_friends_for_challenge() -> anyhow::Result<()> { let (_mock, _account, _abstr, apps) = setup()?; - apps.challenge_app.create_challenge(CHALLENGE_REQ.clone())?; + let challenge_req_without_friends = ChallengeRequest { + init_friends: vec![], + ..CHALLENGE_REQ.clone() + }; apps.challenge_app - .query::(&QueryMsg::from(ChallengeQueryMsg::Challenge { - challenge_id: 1, - }))?; + .create_challenge(challenge_req_without_friends)?; apps.challenge_app.update_friends_for_challenge( - 1, + FIRST_CHALLENGE_ID, FRIENDS.clone(), - UpdateFriendsOpKind::Add, + UpdateFriendsOpKind::Add {}, )?; - let response = apps - .challenge_app - .query::(&QueryMsg::from(ChallengeQueryMsg::Friends { - challenge_id: 1, - }))?; + let response: FriendsResponse = + apps.challenge_app + .query(&QueryMsg::from(ChallengeQueryMsg::Friends { + challenge_id: FIRST_CHALLENGE_ID, + }))?; + let friends = response.friends; - assert_eq!( - response.0, - vec![ - Friend { - address: Addr::unchecked(ALICE_ADDRESS.clone()), - name: "Alice".to_string(), - }, - Friend { - address: Addr::unchecked(BOB_ADDRESS.clone()), - name: "Bob".to_string(), - }, - Friend { - address: Addr::unchecked(CHARLIE_ADDRESS.clone()), - name: "Charlie".to_string(), - } - ] - ); + let expected_friends: Vec> = UNCHECKED_FRIENDS.clone(); + + assert_eq!(friends, expected_friends); Ok(()) } #[test] -fn test_should_remove_friend_from_challenge() -> anyhow::Result<()> { +fn test_remove_friend_from_challenge() -> anyhow::Result<()> { let (_mock, _account, _abstr, apps) = setup()?; apps.challenge_app.create_challenge(CHALLENGE_REQ.clone())?; - let created = apps - .challenge_app - .query::(&QueryMsg::from(ChallengeQueryMsg::Challenge { - challenge_id: 1, - }))?; - - let challenge = CHALLENGE_REQ.clone(); - assert_eq!(created.challenge.as_ref().unwrap().name, challenge.name); - assert_eq!( - created.challenge.as_ref().unwrap().collateral, - challenge.collateral - ); - assert_eq!( - created.challenge.as_ref().unwrap().description, - challenge.description - ); - assert_eq!( - created.challenge.as_ref().unwrap().status, - ChallengeStatus::Active - ); - - // add friend + // remove friend apps.challenge_app.update_friends_for_challenge( - 1, - vec![ALICE.clone()], - UpdateFriendsOpKind::Add, + FIRST_CHALLENGE_ID, + vec![ALICE_FRIEND.clone()], + UpdateFriendsOpKind::Remove {}, )?; - let friend_query = QueryMsg::from(ChallengeQueryMsg::Friends { challenge_id: 1 }); - - let response = apps.challenge_app.query::(&friend_query)?; + let friends_query = QueryMsg::from(ChallengeQueryMsg::Friends { + challenge_id: FIRST_CHALLENGE_ID, + }); - for friend in response.0.iter() { - assert_eq!(friend.address, Addr::unchecked(ALICE_ADDRESS.clone())); - assert_eq!(friend.name, ALICE_NAME.clone()); - } + let response: FriendsResponse = apps.challenge_app.query(&friends_query)?; + let friends = response.friends; - // remove friend - apps.challenge_app.update_friends_for_challenge( - 1, - vec![ALICE.clone()], - UpdateFriendsOpKind::Remove, - )?; + let mut expected_friends = UNCHECKED_FRIENDS.clone(); + expected_friends.retain(|s| match s { + Friend::Addr(addr) => addr.address != ALICE_ADDRESS.clone(), + Friend::AbstractAccount(_) => todo!(), + }); - let response = apps.challenge_app.query::(&friend_query)?; - assert_eq!(response.0.len(), 0); + assert_eq!(friends, expected_friends); Ok(()) } #[test] -fn test_should_cast_vote() -> anyhow::Result<()> { +fn test_cast_vote() -> anyhow::Result<()> { let (_mock, _account, _abstr, apps) = setup()?; + let vote = Vote { + vote: true, + memo: Some("some memo".to_owned()), + }; apps.challenge_app.create_challenge(CHALLENGE_REQ.clone())?; - apps.challenge_app.daily_check_in(CHALLENGE_ID, None)?; - apps.challenge_app.update_friends_for_challenge( - CHALLENGE_ID, - vec![ALICE.clone(), BOB.clone()], - UpdateFriendsOpKind::Add, - )?; - - let response = apps - .challenge_app - .query::(&QueryMsg::from(ChallengeQueryMsg::CheckIns { - challenge_id: CHALLENGE_ID, - }))?; - - assert_eq!( - response.0.last().unwrap().status, - CheckInStatus::CheckedInNotYetVoted - ); - apps.challenge_app - .cast_vote(CHALLENGE_ID, ALICE_VOTE.clone())?; - - let response = apps - .challenge_app - .query::(&QueryMsg::from(ChallengeQueryMsg::CheckIns { - challenge_id: CHALLENGE_ID, - }))?; + .call_as(&Addr::unchecked(ALICE_ADDRESS.clone())) + .cast_vote(FIRST_CHALLENGE_ID, vote.clone())?; - let response = + let response: VoteResponse = apps.challenge_app - .query::(&QueryMsg::from(ChallengeQueryMsg::Vote { - last_check_in: response.0.last().unwrap().last.nanos(), + .query(&QueryMsg::from(ChallengeQueryMsg::Vote { voter_addr: ALICE_ADDRESS.clone(), - challenge_id: CHALLENGE_ID, + challenge_id: FIRST_CHALLENGE_ID, + proposal_id: None, }))?; - assert_eq!(response.vote.unwrap().approval, Some(true)); + assert_eq!(response.vote, Some(vote)); Ok(()) } #[test] -fn test_should_not_charge_penalty_for_truthy_votes() -> anyhow::Result<()> { - let (mock, account, _abstr, apps) = setup()?; +fn test_update_friends_during_proposal() -> anyhow::Result<()> { + let (mock, _account, _abstr, apps) = setup()?; apps.challenge_app.create_challenge(CHALLENGE_REQ.clone())?; - apps.challenge_app.update_friends_for_challenge( - CHALLENGE_ID, - FRIENDS.clone(), - UpdateFriendsOpKind::Add, - )?; - run_challenge_vote_sequence(&mock, &apps, VOTES.clone())?; + // start proposal + apps.challenge_app + .call_as(&Addr::unchecked(ALICE_ADDRESS.clone())) + .cast_vote( + FIRST_CHALLENGE_ID, + Vote { + vote: true, + memo: None, + }, + )?; - let response = apps + let err: AppError = apps .challenge_app - .query::(&QueryMsg::from(ChallengeQueryMsg::CheckIns { - challenge_id: CHALLENGE_ID, - }))?; - - let vote = - apps.challenge_app - .query::(&QueryMsg::from(ChallengeQueryMsg::Vote { - last_check_in: response.0.last().unwrap().last.nanos(), - voter_addr: ALICE_ADDRESS.clone(), - challenge_id: CHALLENGE_ID, - }))?; - - assert_eq!(vote.vote.unwrap().approval, Some(true)); + .update_friends_for_challenge( + FIRST_CHALLENGE_ID, + vec![ALICE_FRIEND.clone()], + UpdateFriendsOpKind::Remove {}, + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + AppError::FriendsEditDuringProposal(mock.block_info()?.time.plus_seconds(1_000)) + ); - let balance = mock.query_balance(&account.proxy.address()?, DENOM)?; - // if no one voted false, no penalty should be charged, so balance will be 50_000_000 - assert_eq!(balance, Uint128::new(50_000_000)); Ok(()) } #[test] -fn test_should_charge_penalty_for_false_votes() -> anyhow::Result<()> { +fn test_not_charge_penalty_for_voting_false() -> anyhow::Result<()> { let (mock, account, _abstr, apps) = setup()?; apps.challenge_app.create_challenge(CHALLENGE_REQ.clone())?; - let response = apps - .challenge_app - .query::(&QueryMsg::from(ChallengeQueryMsg::Challenge { - challenge_id: CHALLENGE_ID, - }))?; - let challenge = CHALLENGE_REQ.clone(); - assert_eq!(response.challenge.as_ref().unwrap().name, challenge.name); - assert_eq!( - response.challenge.as_ref().unwrap().collateral, - challenge.collateral - ); - assert_eq!( - response.challenge.as_ref().unwrap().description, - challenge.description - ); - assert_eq!( - response.challenge.as_ref().unwrap().status, - ChallengeStatus::Active - ); - - apps.challenge_app.update_friends_for_challenge( - CHALLENGE_ID, - FRIENDS.clone(), - UpdateFriendsOpKind::Add, - )?; + // cast votes + let votes = vec![ + ( + Addr::unchecked(ALICE_ADDRESS.clone()), + Vote { + vote: false, + memo: None, + }, + ), + ( + Addr::unchecked(BOB_ADDRESS.clone()), + Vote { + vote: false, + memo: None, + }, + ), + ( + Addr::unchecked(CHARLIE_ADDRESS.clone()), + Vote { + vote: false, + memo: None, + }, + ), + ]; + run_challenge_vote_sequence(&mock, &apps, votes)?; - println!("Running challenge vote sequence"); - run_challenge_vote_sequence(&mock, &apps, ONE_NO_VOTE.clone())?; - let check_ins_res = apps + let prev_votes_results = apps .challenge_app - .query::(&QueryMsg::from(ChallengeQueryMsg::CheckIns { - challenge_id: CHALLENGE_ID, - }))?; + .proposals(FIRST_CHALLENGE_ID, None, None)? + .proposals; + let expected_end = mock.block_info()?.time; + assert_eq!( + prev_votes_results, + vec![( + 1, + ProposalInfo { + total_voters: 3, + votes_for: 0, + votes_against: 3, + status: ProposalStatus::Finished(ProposalOutcome::Failed), + config: VoteConfig { + threshold: Threshold::Majority {}, + veto_duration_seconds: None, + }, + end_timestamp: expected_end, + } + )] + ); - let response = - apps.challenge_app - .query::(&QueryMsg::from(ChallengeQueryMsg::Vote { - last_check_in: check_ins_res.0.last().unwrap().last.nanos(), - voter_addr: ALICE_ADDRESS.clone(), - challenge_id: CHALLENGE_ID, - }))?; - assert_eq!(response.vote.unwrap().approval, Some(false)); + let balance = mock.query_balance(&account.proxy.address()?, DENOM)?; + // if no one voted true, no penalty should be charged, so balance will be 50_000_000 + assert_eq!(balance, Uint128::new(INITIAL_BALANCE)); + Ok(()) +} - let response = - apps.challenge_app - .query::(&QueryMsg::from(ChallengeQueryMsg::Vote { - last_check_in: check_ins_res.0.last().unwrap().last.nanos(), - voter_addr: BOB_ADDRESS.clone(), - challenge_id: CHALLENGE_ID, - }))?; - assert_eq!(response.vote.unwrap().approval, Some(true)); +#[test] +fn test_charge_penalty_for_voting_true() -> anyhow::Result<()> { + let (mock, account, _abstr, apps) = setup()?; + apps.challenge_app.create_challenge(CHALLENGE_REQ.clone())?; - let response = - apps.challenge_app - .query::(&QueryMsg::from(ChallengeQueryMsg::Vote { - last_check_in: check_ins_res.0.last().unwrap().last.nanos(), - voter_addr: CHARLIE_ADDRESS.clone(), - challenge_id: CHALLENGE_ID, - }))?; - assert_eq!(response.vote.unwrap().approval, Some(true)); + let votes = vec![ + ( + Addr::unchecked(ALICE_ADDRESS.clone()), + Vote { + vote: true, + memo: None, + }, + ), + ( + Addr::unchecked(BOB_ADDRESS.clone()), + Vote { + vote: true, + memo: None, + }, + ), + ( + Addr::unchecked(CHARLIE_ADDRESS.clone()), + Vote { + vote: true, + memo: None, + }, + ), + ]; + run_challenge_vote_sequence(&mock, &apps, votes)?; let balance = mock.query_balance(&account.proxy.address()?, DENOM)?; - assert_eq!(balance, Uint128::new(44537609)); + // Initial balance - strike + assert_eq!(balance, Uint128::new(INITIAL_BALANCE - 30_000_000)); Ok(()) } #[test] -fn test_should_query_challenges_within_range() -> anyhow::Result<()> { +fn test_query_challenges_within_range() -> anyhow::Result<()> { let (_mock, _account, _abstr, apps) = setup()?; for _ in 0..10 { apps.challenge_app.create_challenge(CHALLENGE_REQ.clone())?; } - let response = apps - .challenge_app - .query::(&QueryMsg::from(ChallengeQueryMsg::Challenges { - start_after: 0, - limit: 5, - }))?; + let response: ChallengesResponse = + apps.challenge_app + .query(&QueryMsg::from(ChallengeQueryMsg::Challenges { + start_after: None, + limit: Some(5), + }))?; - assert_eq!(response.0.len(), 5); + assert_eq!(response.challenges.len(), 5); Ok(()) } #[test] -fn test_should_query_challenges_within_different_range() -> anyhow::Result<()> { +fn test_query_challenges_within_different_range() -> anyhow::Result<()> { let (_mock, _account, _abstr, apps) = setup()?; for _ in 0..10 { apps.challenge_app.create_challenge(CHALLENGE_REQ.clone())?; } - let response = apps - .challenge_app - .query::(&QueryMsg::from(ChallengeQueryMsg::Challenges { - start_after: 5, - limit: 8, - }))?; + let response: ChallengesResponse = + apps.challenge_app + .query(&QueryMsg::from(ChallengeQueryMsg::Challenges { + start_after: Some(7), + limit: Some(8), + }))?; - // 10 challenges exist, but we start after 5 and limit to 8, + // 10 challenges exist, but we start after 7 and limit to 8, // so we should get 3 challenges - assert_eq!(response.0.len(), 3); + assert_eq!(response.challenges.len(), 3); + Ok(()) +} + +#[test] +fn test_vetoed() -> anyhow::Result<()> { + let (mock, account, _abstr, apps) = setup()?; + apps.challenge_app.update_config(VoteConfig { + threshold: Threshold::Majority {}, + veto_duration_seconds: Some(Uint64::new(1_000)), + })?; + apps.challenge_app.create_challenge(CHALLENGE_REQ.clone())?; + + let votes = vec![ + ( + Addr::unchecked(ALICE_ADDRESS.clone()), + Vote { + vote: true, + memo: None, + }, + ), + ( + Addr::unchecked(BOB_ADDRESS.clone()), + Vote { + vote: true, + memo: None, + }, + ), + ( + Addr::unchecked(CHARLIE_ADDRESS.clone()), + Vote { + vote: true, + memo: None, + }, + ), + ]; + for (signer, vote) in votes { + apps.challenge_app + .call_as(&signer) + .cast_vote(FIRST_CHALLENGE_ID, vote)?; + } + mock.wait_seconds(1_000)?; + apps.challenge_app.veto(FIRST_CHALLENGE_ID)?; + let prev_proposals: ProposalsResponse = + apps.challenge_app + .proposals(FIRST_CHALLENGE_ID, None, None)?; + let status = prev_proposals.proposals[0].1.status.clone(); + assert_eq!(status, ProposalStatus::Finished(ProposalOutcome::Vetoed)); + + // balance unchanged + let balance = mock.query_balance(&account.proxy.address()?, DENOM)?; + assert_eq!(balance, Uint128::new(INITIAL_BALANCE)); + Ok(()) +} + +#[test] +fn test_veto_expired() -> anyhow::Result<()> { + let (mock, account, _abstr, apps) = setup()?; + apps.challenge_app.update_config(VoteConfig { + threshold: Threshold::Majority {}, + veto_duration_seconds: Some(Uint64::new(1_000)), + })?; + apps.challenge_app.create_challenge(CHALLENGE_REQ.clone())?; + + let votes = vec![ + ( + Addr::unchecked(ALICE_ADDRESS.clone()), + Vote { + vote: true, + memo: None, + }, + ), + ( + Addr::unchecked(BOB_ADDRESS.clone()), + Vote { + vote: true, + memo: None, + }, + ), + ( + Addr::unchecked(CHARLIE_ADDRESS.clone()), + Vote { + vote: true, + memo: None, + }, + ), + ]; + for (signer, vote) in votes { + apps.challenge_app + .call_as(&signer) + .cast_vote(FIRST_CHALLENGE_ID, vote)?; + } + + // wait time to expire veto + mock.wait_seconds(2_000)?; + apps.challenge_app + .call_as(&Addr::unchecked(ALICE_ADDRESS.clone())) + .count_votes(FIRST_CHALLENGE_ID)?; + + let proposals: ProposalsResponse = + apps.challenge_app + .proposals(FIRST_CHALLENGE_ID, None, None)?; + + assert_eq!( + proposals.proposals[0].1.status, + ProposalStatus::Finished(ProposalOutcome::Passed) + ); + + // balance updated + let balance = mock.query_balance(&account.proxy.address()?, DENOM)?; + assert_eq!(balance, Uint128::new(INITIAL_BALANCE - 30_000_000)); + Ok(()) +} + +#[test] +fn test_duplicate_friends() -> anyhow::Result<()> { + let (_mock, _account, _abstr, apps) = setup()?; + // Duplicate initial friends + let err: error::AppError = apps + .challenge_app + .create_challenge(ChallengeRequest { + init_friends: vec![ALICE_FRIEND.clone(), ALICE_FRIEND.clone()], + ..CHALLENGE_REQ.clone() + }) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, error::AppError::DuplicateFriends {}); + + // Add duplicate (Alice already exists) + apps.challenge_app.create_challenge(CHALLENGE_REQ.clone())?; + let err: error::AppError = apps + .challenge_app + .update_friends_for_challenge( + FIRST_CHALLENGE_ID, + vec![ALICE_FRIEND.clone()], + UpdateFriendsOpKind::Add {}, + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, error::AppError::DuplicateFriends {}); Ok(()) } fn run_challenge_vote_sequence( mock: &Mock, apps: &DeployedApps, - votes: Vec>, + votes: Vec<(Addr, Vote)>, ) -> anyhow::Result<()> { - for _ in 0..3 { - mock.wait_seconds(DAY - 100)?; // this ensure we don't miss the check in - apps.challenge_app.daily_check_in(1, None)?; - } - - for vote in votes.clone() { - apps.challenge_app.cast_vote(1, vote)?; + for (signer, vote) in votes { + apps.challenge_app + .call_as(&signer) + .cast_vote(FIRST_CHALLENGE_ID, vote)?; } + mock.wait_seconds(1_000)?; + apps.challenge_app.count_votes(FIRST_CHALLENGE_ID)?; Ok(()) }