diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/schema/dao-pre-propose-approval-single.json b/contracts/pre-propose/dao-pre-propose-approval-single/schema/dao-pre-propose-approval-single.json index c81ffacec..5bf5a2f73 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/schema/dao-pre-propose-approval-single.json +++ b/contracts/pre-propose/dao-pre-propose-approval-single/schema/dao-pre-propose-approval-single.json @@ -1500,6 +1500,30 @@ }, "additionalProperties": false }, + { + "description": "Return whether or not the proposal is pending", + "type": "object", + "required": [ + "is_pending" + ], + "properties": { + "is_pending": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "A pending proposal", "type": "object", @@ -1586,6 +1610,93 @@ } }, "additionalProperties": false + }, + { + "description": "A completed proposal", + "type": "object", + "required": [ + "completed_proposal" + ], + "properties": { + "completed_proposal": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "List of completed proposals", + "type": "object", + "required": [ + "completed_proposals" + ], + "properties": { + "completed_proposals": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "reverse_completed_proposals" + ], + "properties": { + "reverse_completed_proposals": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_before": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] } diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/src/contract.rs b/contracts/pre-propose/dao-pre-propose-approval-single/src/contract.rs index a0ad2b764..616ac3b9d 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/src/contract.rs +++ b/contracts/pre-propose/dao-pre-propose-approval-single/src/contract.rs @@ -16,7 +16,10 @@ use crate::msg::{ ApproverProposeMessage, ExecuteExt, ExecuteMsg, InstantiateExt, InstantiateMsg, ProposeMessage, ProposeMessageInternal, QueryExt, QueryMsg, }; -use crate::state::{advance_approval_id, PendingProposal, APPROVER, PENDING_PROPOSALS}; +use crate::state::{ + advance_approval_id, CompletedProposal, CompletedProposalStatus, PendingProposal, APPROVER, + COMPLETED_PROPOSALS, PENDING_PROPOSALS, +}; pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-pre-propose-approval-single"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -164,14 +167,28 @@ pub fn execute_approve( PrePropose::default().deposits.save( deps.storage, proposal_id, - &(proposal.deposit, proposal.proposer), + &(proposal.deposit.clone(), proposal.proposer.clone()), )?; let propose_messsage = WasmMsg::Execute { contract_addr: proposal_module.into_string(), - msg: to_binary(&ProposeMessageInternal::Propose(proposal.msg))?, + msg: to_binary(&ProposeMessageInternal::Propose(proposal.msg.clone()))?, funds: vec![], }; + + COMPLETED_PROPOSALS.save( + deps.storage, + id, + &CompletedProposal { + status: CompletedProposalStatus::Approved { + created_proposal_id: proposal_id, + }, + approval_id: proposal.approval_id, + proposer: proposal.proposer, + msg: proposal.msg, + deposit: proposal.deposit, + }, + )?; PENDING_PROPOSALS.remove(deps.storage, id); Ok(Response::default() @@ -196,11 +213,25 @@ pub fn execute_reject( } let PendingProposal { - deposit, proposer, .. + approval_id, + proposer, + msg, + deposit, } = PENDING_PROPOSALS .may_load(deps.storage, id)? .ok_or(PreProposeError::ProposalNotFound {})?; + COMPLETED_PROPOSALS.save( + deps.storage, + id, + &CompletedProposal { + status: CompletedProposalStatus::Rejected {}, + approval_id, + proposer: proposer.clone(), + msg: msg.clone(), + deposit: deposit.clone(), + }, + )?; PENDING_PROPOSALS.remove(deps.storage, id); let messages = if let Some(ref deposit_info) = deposit { @@ -297,6 +328,16 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::QueryExtension { msg } => match msg { QueryExt::Approver {} => to_binary(&APPROVER.load(deps.storage)?), + QueryExt::IsPending { id } => { + let pending = PENDING_PROPOSALS.may_load(deps.storage, id)?.is_some(); + // Force load completed proposal if not pending, throwing error + // if not found. + if !pending { + COMPLETED_PROPOSALS.load(deps.storage, id)?; + } + + to_binary(&pending) + } QueryExt::PendingProposal { id } => { to_binary(&PENDING_PROPOSALS.load(deps.storage, id)?) } @@ -317,6 +358,26 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { limit, Order::Ascending, )?), + QueryExt::CompletedProposal { id } => { + to_binary(&COMPLETED_PROPOSALS.load(deps.storage, id)?) + } + QueryExt::CompletedProposals { start_after, limit } => to_binary(&paginate_map_values( + deps, + &COMPLETED_PROPOSALS, + start_after, + limit, + Order::Descending, + )?), + QueryExt::ReverseCompletedProposals { + start_before, + limit, + } => to_binary(&paginate_map_values( + deps, + &COMPLETED_PROPOSALS, + start_before, + limit, + Order::Ascending, + )?), }, _ => PrePropose::default().query(deps, env, msg), } diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/src/msg.rs b/contracts/pre-propose/dao-pre-propose-approval-single/src/msg.rs index 8a4df00df..1e1af65d2 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/src/msg.rs +++ b/contracts/pre-propose/dao-pre-propose-approval-single/src/msg.rs @@ -44,6 +44,9 @@ pub enum QueryExt { /// List the approver address #[returns(cosmwasm_std::Addr)] Approver {}, + /// Return whether or not the proposal is pending + #[returns(bool)] + IsPending { id: u64 }, /// A pending proposal #[returns(crate::state::PendingProposal)] PendingProposal { id: u64 }, @@ -58,6 +61,20 @@ pub enum QueryExt { start_before: Option, limit: Option, }, + /// A completed proposal + #[returns(crate::state::CompletedProposal)] + CompletedProposal { id: u64 }, + /// List of completed proposals + #[returns(Vec)] + CompletedProposals { + start_after: Option, + limit: Option, + }, + #[returns(Vec)] + ReverseCompletedProposals { + start_before: Option, + limit: Option, + }, } pub type InstantiateMsg = InstantiateBase; diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/src/state.rs b/contracts/pre-propose/dao-pre-propose-approval-single/src/state.rs index 5c11aedf9..54fd95440 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/src/state.rs +++ b/contracts/pre-propose/dao-pre-propose-approval-single/src/state.rs @@ -19,8 +19,36 @@ pub struct PendingProposal { pub deposit: Option, } +#[cw_serde] +pub enum CompletedProposalStatus { + /// The proposal has been approved. + Approved { + /// The created proposal ID. + created_proposal_id: u64, + }, + /// The proposal has been rejected. + Rejected {}, +} + +#[cw_serde] +pub struct CompletedProposal { + /// The status of a completed proposal. + pub status: CompletedProposalStatus, + /// The approval ID used to identify this pending proposal. + pub approval_id: u64, + /// The address that created the proposal. + pub proposer: Addr, + /// The propose message that ought to be executed on the proposal + /// message if this proposal is approved. + pub msg: ProposeMsg, + /// Snapshot of the deposit info at the time of proposal + /// submission. + pub deposit: Option, +} + pub const APPROVER: Item = Item::new("approver"); pub const PENDING_PROPOSALS: Map = Map::new("pending_proposals"); +pub const COMPLETED_PROPOSALS: Map = Map::new("completed_proposals"); /// Used internally to track the current approval_id. const CURRENT_ID: Item = Item::new("current_id"); diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs b/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs index cf6eaf5ca..256a1146e 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs @@ -17,6 +17,7 @@ use dao_voting::{ voting::Vote, }; +use crate::state::{CompletedProposal, CompletedProposalStatus}; use crate::{contract::*, msg::*, state::PendingProposal}; fn cw_dao_proposal_single_contract() -> Box> { @@ -189,8 +190,8 @@ fn make_pre_proposal(app: &mut App, pre_propose: Addr, proposer: &str, funds: &[ ) .unwrap(); - // Query for pending proposal and return latest id - let mut pending: Vec = app + // Query for pending proposal and return latest id. Returns descending. + let pending: Vec = app .wrap() .query_wasm_smart( pre_propose, @@ -203,8 +204,8 @@ fn make_pre_proposal(app: &mut App, pre_propose: Addr, proposer: &str, funds: &[ ) .unwrap(); - // Return last item in list, id is first element of tuple - pending.pop().unwrap().approval_id + // Return first item in descending list, id is first element of tuple + pending[0].approval_id } fn mint_natives(app: &mut App, receiver: &str, coins: Vec) { @@ -917,6 +918,120 @@ fn test_pending_proposal_queries() { assert_eq!(reverse_pre_propose_props[0].approval_id, 1); } +#[test] +fn test_completed_proposal_queries() { + let mut app = App::default(); + + let DefaultTestSetup { + core_addr: _, + proposal_single: _, + pre_propose, + } = setup_default_test( + &mut app, + Some(UncheckedDepositInfo { + denom: DepositToken::Token { + denom: UncheckedDenom::Native("ujuno".to_string()), + }, + amount: Uint128::new(10), + refund_policy: DepositRefundPolicy::Always, + }), + false, + ); + + mint_natives(&mut app, "ekez", coins(20, "ujuno")); + let approve_id = make_pre_proposal(&mut app, pre_propose.clone(), "ekez", &coins(10, "ujuno")); + let reject_id = make_pre_proposal(&mut app, pre_propose.clone(), "ekez", &coins(10, "ujuno")); + + let is_pending: bool = app + .wrap() + .query_wasm_smart( + pre_propose.clone(), + &QueryMsg::QueryExtension { + msg: QueryExt::IsPending { id: approve_id }, + }, + ) + .unwrap(); + assert_eq!(is_pending, true); + + let created_approved_id = + approve_proposal(&mut app, pre_propose.clone(), "approver", approve_id); + reject_proposal(&mut app, pre_propose.clone(), "approver", reject_id); + + let is_pending: bool = app + .wrap() + .query_wasm_smart( + pre_propose.clone(), + &QueryMsg::QueryExtension { + msg: QueryExt::IsPending { id: approve_id }, + }, + ) + .unwrap(); + assert_eq!(is_pending, false); + + // Query for individual proposals + let prop1: CompletedProposal = app + .wrap() + .query_wasm_smart( + pre_propose.clone(), + &QueryMsg::QueryExtension { + msg: QueryExt::CompletedProposal { id: approve_id }, + }, + ) + .unwrap(); + assert_eq!( + prop1.status, + CompletedProposalStatus::Approved { + created_proposal_id: created_approved_id + } + ); + + let prop2: CompletedProposal = app + .wrap() + .query_wasm_smart( + pre_propose.clone(), + &QueryMsg::QueryExtension { + msg: QueryExt::CompletedProposal { id: reject_id }, + }, + ) + .unwrap(); + assert_eq!(prop2.status, CompletedProposalStatus::Rejected {}); + + // Query for the pre-propose proposals + let pre_propose_props: Vec = app + .wrap() + .query_wasm_smart( + pre_propose.clone(), + &QueryMsg::QueryExtension { + msg: QueryExt::CompletedProposals { + start_after: None, + limit: None, + }, + }, + ) + .unwrap(); + assert_eq!(pre_propose_props.len(), 2); + assert_eq!(pre_propose_props[0].approval_id, reject_id); + assert_eq!(pre_propose_props[1].approval_id, approve_id); + + // Query props in reverse + let reverse_pre_propose_props: Vec = app + .wrap() + .query_wasm_smart( + pre_propose, + &QueryMsg::QueryExtension { + msg: QueryExt::ReverseCompletedProposals { + start_before: None, + limit: None, + }, + }, + ) + .unwrap(); + + assert_eq!(reverse_pre_propose_props.len(), 2); + assert_eq!(reverse_pre_propose_props[0].approval_id, approve_id); + assert_eq!(reverse_pre_propose_props[1].approval_id, reject_id); +} + #[test] fn test_set_version() { let mut app = App::default();