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..a4e449fb7 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,54 @@ }, "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 proposal, pending or completed.", + "type": "object", + "required": [ + "proposal" + ], + "properties": { + "proposal": { + "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 +1634,117 @@ } }, "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 + }, + { + "description": "The completed approval ID for a created proposal ID.", + "type": "object", + "required": [ + "completed_proposal_id_for_created_proposal_id" + ], + "properties": { + "completed_proposal_id_for_created_proposal_id": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "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..d9245e30b 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, Proposal, ProposalStatus, APPROVER, COMPLETED_PROPOSALS, + CREATED_PROPOSAL_TO_COMPLETED_PROPOSAL, 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"); @@ -123,7 +126,8 @@ pub fn execute_propose( PENDING_PROPOSALS.save( deps.storage, approval_id, - &PendingProposal { + &Proposal { + status: ProposalStatus::Pending {}, approval_id, proposer: info.sender, msg: propose_msg_internal, @@ -164,14 +168,29 @@ 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, + &Proposal { + status: ProposalStatus::Approved { + created_proposal_id: proposal_id, + }, + approval_id: proposal.approval_id, + proposer: proposal.proposer, + msg: proposal.msg, + deposit: proposal.deposit, + }, + )?; + CREATED_PROPOSAL_TO_COMPLETED_PROPOSAL.save(deps.storage, proposal_id, &id)?; PENDING_PROPOSALS.remove(deps.storage, id); Ok(Response::default() @@ -195,12 +214,27 @@ pub fn execute_reject( return Err(PreProposeError::Unauthorized {}); } - let PendingProposal { - deposit, proposer, .. + let Proposal { + approval_id, + proposer, + msg, + deposit, + .. } = PENDING_PROPOSALS .may_load(deps.storage, id)? .ok_or(PreProposeError::ProposalNotFound {})?; + COMPLETED_PROPOSALS.save( + deps.storage, + id, + &Proposal { + status: ProposalStatus::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 +331,25 @@ 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::Proposal { id } => { + if let Some(pending) = PENDING_PROPOSALS.may_load(deps.storage, id)? { + to_binary(&pending) + } else { + // Force load completed proposal if not pending, throwing + // error if not found. + to_binary(&COMPLETED_PROPOSALS.load(deps.storage, id)?) + } + } QueryExt::PendingProposal { id } => { to_binary(&PENDING_PROPOSALS.load(deps.storage, id)?) } @@ -317,6 +370,29 @@ 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, + )?), + QueryExt::CompletedProposalIdForCreatedProposalId { id } => { + to_binary(&CREATED_PROPOSAL_TO_COMPLETED_PROPOSAL.may_load(deps.storage, id)?) + } }, _ => 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..01b83e361 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,20 +44,43 @@ 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 proposal, pending or completed. + #[returns(crate::state::Proposal)] + Proposal { id: u64 }, /// A pending proposal - #[returns(crate::state::PendingProposal)] + #[returns(crate::state::Proposal)] PendingProposal { id: u64 }, /// List of proposals awaiting approval - #[returns(Vec)] + #[returns(Vec)] PendingProposals { start_after: Option, limit: Option, }, - #[returns(Vec)] + #[returns(Vec)] ReversePendingProposals { start_before: Option, limit: Option, }, + /// A completed proposal + #[returns(crate::state::Proposal)] + CompletedProposal { id: u64 }, + /// List of completed proposals + #[returns(Vec)] + CompletedProposals { + start_after: Option, + limit: Option, + }, + #[returns(Vec)] + ReverseCompletedProposals { + start_before: Option, + limit: Option, + }, + /// The completed approval ID for a created proposal ID. + #[returns(::std::option::Option)] + CompletedProposalIdForCreatedProposalId { id: u64 }, } 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..5ceb766dc 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 @@ -6,7 +6,22 @@ use dao_voting::deposit::CheckedDepositInfo; use dao_voting::proposal::SingleChoiceProposeMsg as ProposeMsg; #[cw_serde] -pub struct PendingProposal { +pub enum ProposalStatus { + /// The proposal is pending approval. + Pending {}, + /// The proposal has been approved. + Approved { + /// The created proposal ID. + created_proposal_id: u64, + }, + /// The proposal has been rejected. + Rejected {}, +} + +#[cw_serde] +pub struct Proposal { + /// The status of a completed proposal. + pub status: ProposalStatus, /// The approval ID used to identify this pending proposal. pub approval_id: u64, /// The address that created the proposal. @@ -20,7 +35,10 @@ pub struct PendingProposal { } pub const APPROVER: Item = Item::new("approver"); -pub const PENDING_PROPOSALS: Map = Map::new("pending_proposals"); +pub const PENDING_PROPOSALS: Map = Map::new("pending_proposals"); +pub const COMPLETED_PROPOSALS: Map = Map::new("completed_proposals"); +pub const CREATED_PROPOSAL_TO_COMPLETED_PROPOSAL: Map = + Map::new("created_to_completed_proposal"); /// 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..8375dca9d 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,7 +17,8 @@ use dao_voting::{ voting::Vote, }; -use crate::{contract::*, msg::*, state::PendingProposal}; +use crate::state::{Proposal, ProposalStatus}; +use crate::{contract::*, msg::*}; fn cw_dao_proposal_single_contract() -> Box> { let contract = ContractWrapper::new( @@ -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) { @@ -872,7 +873,7 @@ fn test_pending_proposal_queries() { make_pre_proposal(&mut app, pre_propose.clone(), "ekez", &coins(10, "ujuno")); // Query for individual proposal - let prop1: PendingProposal = app + let prop1: Proposal = app .wrap() .query_wasm_smart( pre_propose.clone(), @@ -882,9 +883,22 @@ fn test_pending_proposal_queries() { ) .unwrap(); assert_eq!(prop1.approval_id, 1); + assert_eq!(prop1.status, ProposalStatus::Pending {}); + + let prop1: Proposal = app + .wrap() + .query_wasm_smart( + pre_propose.clone(), + &QueryMsg::QueryExtension { + msg: QueryExt::Proposal { id: 1 }, + }, + ) + .unwrap(); + assert_eq!(prop1.approval_id, 1); + assert_eq!(prop1.status, ProposalStatus::Pending {}); // Query for the pre-propose proposals - let pre_propose_props: Vec = app + let pre_propose_props: Vec = app .wrap() .query_wasm_smart( pre_propose.clone(), @@ -900,7 +914,7 @@ fn test_pending_proposal_queries() { assert_eq!(pre_propose_props[0].approval_id, 2); // Query props in reverse - let reverse_pre_propose_props: Vec = app + let reverse_pre_propose_props: Vec = app .wrap() .query_wasm_smart( pre_propose, @@ -917,6 +931,148 @@ 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!(is_pending); + + 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!(!is_pending); + + // Query for individual proposals + let prop1: Proposal = app + .wrap() + .query_wasm_smart( + pre_propose.clone(), + &QueryMsg::QueryExtension { + msg: QueryExt::CompletedProposal { id: approve_id }, + }, + ) + .unwrap(); + assert_eq!( + prop1.status, + ProposalStatus::Approved { + created_proposal_id: created_approved_id + } + ); + let prop1: Proposal = app + .wrap() + .query_wasm_smart( + pre_propose.clone(), + &QueryMsg::QueryExtension { + msg: QueryExt::Proposal { id: approve_id }, + }, + ) + .unwrap(); + assert_eq!( + prop1.status, + ProposalStatus::Approved { + created_proposal_id: created_approved_id + } + ); + + let prop1_id: Option = app + .wrap() + .query_wasm_smart( + pre_propose.clone(), + &QueryMsg::QueryExtension { + msg: QueryExt::CompletedProposalIdForCreatedProposalId { + id: created_approved_id, + }, + }, + ) + .unwrap(); + assert_eq!(prop1_id, Some(approve_id)); + + let prop2: Proposal = app + .wrap() + .query_wasm_smart( + pre_propose.clone(), + &QueryMsg::QueryExtension { + msg: QueryExt::CompletedProposal { id: reject_id }, + }, + ) + .unwrap(); + assert_eq!(prop2.status, ProposalStatus::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(); diff --git a/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs b/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs index a52c91042..0ac89646c 100644 --- a/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs @@ -11,7 +11,7 @@ use dao_pre_propose_approval_single::{ msg::{ ExecuteExt, ExecuteMsg, InstantiateExt, InstantiateMsg, ProposeMessage, QueryExt, QueryMsg, }, - state::PendingProposal, + state::Proposal, }; use dao_pre_propose_base::{error::PreProposeError, msg::DepositInfoResponse, state::Config}; use dao_proposal_single as dps; @@ -310,7 +310,7 @@ 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 + let mut pending: Vec = app .wrap() .query_wasm_smart( pre_propose,