Repository: DA0-DA0/dao-contracts (rust)

Issue:
Add missing queries in `cw-govmod-single`.
The module is currently missing the `ReverseProposals` and `Tally` queries. These are reasonably frequently used in the frontend.

Pull request:
Add `ReverseProposals` query to cw-govmod-single.
Resolves #213 

This adds the `ReverseProposals` query to `cw-govmod-single`. It does not add the `Tally` query as in the new design that query is strictly a subset of the `Proposal` query with a little extra work to compute the current turnout percent on the contract side.

Where possible we want to limit the amount of contract side compute happening and limit complexity in the contracts so I'm in favor of just not adding it.

Patch:
diff --git a/contracts/cw-govmod-single/src/contract.rs b/contracts/cw-govmod-single/src/contract.rs
index 02045c9b..5950aaa9 100644
--- a/contracts/cw-govmod-single/src/contract.rs
+++ b/contracts/cw-govmod-single/src/contract.rs
@@ -14,6 +14,7 @@ use crate::{
     error::ContractError,
     msg::{DepositInfo, ExecuteMsg, InstantiateMsg, QueryMsg},
     proposal::{advance_proposal_id, Proposal, Status},
+    query::ProposalListResponse,
     query::{ProposalResponse, VoteInfo, VoteListResponse, VoteResponse},
     state::{
         get_deposit_msg, get_return_deposit_msg, Ballot, Config, BALLOTS, CONFIG, PROPOSALS,
@@ -355,6 +356,10 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
             limit,
         } => query_list_votes(deps, proposal_id, start_after, limit),
         QueryMsg::Info {} => query_info(deps),
+        QueryMsg::ReverseProposals {
+            start_before,
+            limit,
+        } => query_reverse_proposals(deps, env, start_before, limit),
     }
 }
 
@@ -384,7 +389,26 @@ pub fn query_list_proposals(
         .map(|(id, proposal)| proposal.into_response(&env.block, id))
         .collect();
 
-    to_binary(&props)
+    to_binary(&ProposalListResponse { proposals: props })
+}
+
+pub fn query_reverse_proposals(
+    deps: Deps,
+    env: Env,
+    start_before: Option<u64>,
+    limit: Option<u64>,
+) -> StdResult<Binary> {
+    let limit = limit.unwrap_or(DEFAULT_LIMIT);
+    let max = start_before.map(Bound::exclusive);
+    let props: Vec<ProposalResponse> = PROPOSALS
+        .range(deps.storage, None, max, cosmwasm_std::Order::Descending)
+        .take(limit as usize)
+        .collect::<Result<Vec<(u64, Proposal)>, _>>()?
+        .into_iter()
+        .map(|(id, proposal)| proposal.into_response(&env.block, id))
+        .collect();
+
+    to_binary(&ProposalListResponse { proposals: props })
 }
 
 pub fn query_proposal_count(deps: Deps) -> StdResult<Binary> {
diff --git a/contracts/cw-govmod-single/src/msg.rs b/contracts/cw-govmod-single/src/msg.rs
index 9f88935c..b87aefc9 100644
--- a/contracts/cw-govmod-single/src/msg.rs
+++ b/contracts/cw-govmod-single/src/msg.rs
@@ -126,6 +126,10 @@ pub enum QueryMsg {
         start_after: Option<u64>,
         limit: Option<u64>,
     },
+    ReverseProposals {
+        start_before: Option<u64>,
+        limit: Option<u64>,
+    },
     ProposalCount {},
     Vote {
         proposal_id: u64,
diff --git a/contracts/cw-govmod-single/src/query.rs b/contracts/cw-govmod-single/src/query.rs
index 9d20124c..3ac34495 100644
--- a/contracts/cw-govmod-single/src/query.rs
+++ b/contracts/cw-govmod-single/src/query.rs
@@ -36,3 +36,10 @@ pub struct VoteResponse {
 pub struct VoteListResponse {
     pub votes: Vec<VoteInfo>,
 }
+
+/// A list of proposals returned by `ListProposals` and
+/// `ReverseProposals`.
+#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
+pub struct ProposalListResponse {
+    pub proposals: Vec<ProposalResponse>,
+}
diff --git a/contracts/cw-govmod-single/src/threshold.rs b/contracts/cw-govmod-single/src/threshold.rs
index 21e8b12b..3ab017d2 100644
--- a/contracts/cw-govmod-single/src/threshold.rs
+++ b/contracts/cw-govmod-single/src/threshold.rs
@@ -32,6 +32,15 @@ fn validate_percentage(percent: &Decimal) -> Result<(), ContractError> {
     }
 }
 
+/// Asserts that a quorum <= 1. Quorums may be zero.
+fn validate_quorum(quorum: &Decimal) -> Result<(), ContractError> {
+    if *quorum > Decimal::one() {
+        Err(ContractError::UnreachableThreshold {})
+    } else {
+        Ok(())
+    }
+}
+
 impl Threshold {
     /// returns error if this is an unreachable value,
     /// given a total weight of all members in the group
@@ -42,7 +51,7 @@ impl Threshold {
             } => validate_percentage(percentage_needed),
             Threshold::ThresholdQuorum { threshold, quorum } => {
                 validate_percentage(threshold)?;
-                validate_percentage(quorum)
+                validate_quorum(quorum)
             }
         }
     }
diff --git a/contracts/cw20-staked-balance-voting/Cargo.toml b/contracts/cw20-staked-balance-voting/Cargo.toml
index 3b24d980..535a8d97 100644
--- a/contracts/cw20-staked-balance-voting/Cargo.toml
+++ b/contracts/cw20-staked-balance-voting/Cargo.toml
@@ -38,8 +38,8 @@ cw-utils = "0.11"
 schemars = "0.8"
 serde = { version = "1.0", default-features = false, features = ["derive"] }
 thiserror = { version = "1.0" }
-cw-governance-macros = { version = "0.1.0", path = "../cw-governance-macros" }
-cw-governance-interface = { version = "0.1.0", path = "../cw-governance-interface" }
+cw-governance-macros = { version = "0.1.0", path = "../../packages/cw-governance-macros" }
+cw-governance-interface = { version = "0.1.0", path = "../../packages/cw-governance-interface" } 
 stake-cw20 = { path = "../stake-cw20" }
 cw20-base = {  version = "0.12", features = ["library"] }
 
diff --git a/contracts/cw4-voting/Cargo.toml b/contracts/cw4-voting/Cargo.toml
index 015d85d8..acb85a3d 100644
--- a/contracts/cw4-voting/Cargo.toml
+++ b/contracts/cw4-voting/Cargo.toml
@@ -37,8 +37,8 @@ cw-utils = "0.12"
 schemars = "0.8"
 serde = { version = "1.0", default-features = false, features = ["derive"] }
 thiserror = { version = "1.0" }
-cw-governance-macros = { version = "0.1.0", path = "../cw-governance-macros" }
-cw-governance-interface = { version = "0.1.0", path = "../cw-governance-interface" }
+cw-governance-macros = { version = "0.1.0", path = "../../packages/cw-governance-macros" }
+cw-governance-interface = { version = "0.1.0", path = "../../packages/cw-governance-interface" } 
 cw4-group = "0.11"
 cw4 = "0.11"
 


Test patch:
diff --git a/contracts/cw-govmod-single/src/tests.rs b/contracts/cw-govmod-single/src/tests.rs
index 4e72a996..a97bd6b9 100644
--- a/contracts/cw-govmod-single/src/tests.rs
+++ b/contracts/cw-govmod-single/src/tests.rs
@@ -9,7 +9,7 @@ use voting::{Vote, Votes};
 use crate::{
     msg::{DepositInfo, DepositToken, ExecuteMsg, InstantiateMsg, QueryMsg},
     proposal::{Proposal, Status},
-    query::{ProposalResponse, VoteInfo, VoteResponse},
+    query::{ProposalListResponse, ProposalResponse, VoteInfo, VoteResponse},
     state::{CheckedDepositInfo, Config},
     threshold::Threshold,
 };
@@ -1281,3 +1281,148 @@ fn test_no_return_if_no_refunds() {
     // Proposal has been closed but deposit has not been refunded.
     assert_eq!(balance.balance, Uint128::new(9));
 }
+
+#[test]
+fn test_query_list_proposals() {
+    let mut app = App::default();
+    let govmod_id = app.store_code(single_govmod_contract());
+    let gov_addr = instantiate_with_default_governance(
+        &mut app,
+        govmod_id,
+        InstantiateMsg {
+            threshold: Threshold::ThresholdQuorum {
+                threshold: Decimal::percent(50),
+                quorum: Decimal::percent(0),
+            },
+            max_voting_period: cw_utils::Duration::Height(100),
+            only_members_execute: true,
+            deposit_info: None,
+        },
+        Some(vec![Cw20Coin {
+            address: CREATOR_ADDR.to_string(),
+            amount: Uint128::new(100),
+        }]),
+    );
+
+    let gov_modules: Vec<Addr> = app
+        .wrap()
+        .query_wasm_smart(
+            gov_addr,
+            &cw_governance::msg::QueryMsg::GovernanceModules {
+                start_at: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(gov_modules.len(), 1);
+
+    let govmod = gov_modules.into_iter().next().unwrap();
+
+    for i in 1..10 {
+        app.execute_contract(
+            Addr::unchecked(CREATOR_ADDR),
+            govmod.clone(),
+            &ExecuteMsg::Propose {
+                title: format!("Text proposal {}.", i),
+                description: "This is a simple text proposal".to_string(),
+                msgs: vec![],
+                latest: None,
+            },
+            &[],
+        )
+        .unwrap();
+    }
+
+    let proposals_forward: ProposalListResponse = app
+        .wrap()
+        .query_wasm_smart(
+            govmod.clone(),
+            &QueryMsg::ListProposals {
+                start_after: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+    let mut proposals_backward: ProposalListResponse = app
+        .wrap()
+        .query_wasm_smart(
+            govmod.clone(),
+            &QueryMsg::ReverseProposals {
+                start_before: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+
+    proposals_backward.proposals.reverse();
+
+    assert_eq!(proposals_forward.proposals, proposals_backward.proposals);
+
+    let expected = ProposalResponse {
+        id: 1,
+        proposal: Proposal {
+            title: "Text proposal 1.".to_string(),
+            description: "This is a simple text proposal".to_string(),
+            proposer: Addr::unchecked(CREATOR_ADDR),
+            start_height: app.block_info().height,
+            expiration: cw_utils::Expiration::AtHeight(app.block_info().height + 100),
+            threshold: Threshold::ThresholdQuorum {
+                threshold: Decimal::percent(50),
+                quorum: Decimal::percent(0),
+            },
+            total_power: Uint128::new(100),
+            msgs: vec![],
+            status: Status::Open,
+            votes: Votes::zero(),
+            deposit_info: None,
+        },
+    };
+    assert_eq!(proposals_forward.proposals[0], expected);
+
+    // Get proposals (3, 5]
+    let proposals_forward: ProposalListResponse = app
+        .wrap()
+        .query_wasm_smart(
+            govmod.clone(),
+            &QueryMsg::ListProposals {
+                start_after: Some(3),
+                limit: Some(2),
+            },
+        )
+        .unwrap();
+    let mut proposals_backward: ProposalListResponse = app
+        .wrap()
+        .query_wasm_smart(
+            govmod,
+            &QueryMsg::ReverseProposals {
+                start_before: Some(6),
+                limit: Some(2),
+            },
+        )
+        .unwrap();
+
+    let expected = ProposalResponse {
+        id: 4,
+        proposal: Proposal {
+            title: "Text proposal 4.".to_string(),
+            description: "This is a simple text proposal".to_string(),
+            proposer: Addr::unchecked(CREATOR_ADDR),
+            start_height: app.block_info().height,
+            expiration: cw_utils::Expiration::AtHeight(app.block_info().height + 100),
+            threshold: Threshold::ThresholdQuorum {
+                threshold: Decimal::percent(50),
+                quorum: Decimal::percent(0),
+            },
+            total_power: Uint128::new(100),
+            msgs: vec![],
+            status: Status::Open,
+            votes: Votes::zero(),
+            deposit_info: None,
+        },
+    };
+    assert_eq!(proposals_forward.proposals[0], expected);
+    assert_eq!(proposals_backward.proposals[1], expected);
+
+    proposals_backward.proposals.reverse();
+    assert_eq!(proposals_forward.proposals, proposals_backward.proposals);
+}