1pub mod ballots;
2pub mod error;
3pub mod msg;
4#[cfg(test)]
5mod multitest;
6pub mod state;
7
8use serde::de::DeserializeOwned;
9use serde::Serialize;
10
11use ballots::ballots;
12pub use error::ContractError;
13use state::{
14 next_id, proposals, Config, Proposal, ProposalListResponse, ProposalResponse,
15 TextProposalListResponse, Votes, VotingRules, CONFIG, TEXT_PROPOSALS,
16};
17
18use cosmwasm_std::{
19 Addr, BlockInfo, CustomQuery, Deps, DepsMut, Env, MessageInfo, Order, StdResult, Storage,
20};
21use cw_storage_plus::Bound;
22use cw_utils::maybe_addr;
23use tg3::{
24 Status, Vote, VoteInfo, VoteListResponse, VoteResponse, VoterDetail, VoterListResponse,
25 VoterResponse,
26};
27use tg4::{Member, Tg4Contract};
28use tg_bindings::TgradeMsg;
29use tg_utils::Expiration;
30
31type Response = cosmwasm_std::Response<TgradeMsg>;
32
33pub fn instantiate<Q: CustomQuery>(
34 deps: DepsMut<Q>,
35 rules: VotingRules,
36 group_addr: &str,
37) -> Result<Response, ContractError> {
38 let group_contract = Tg4Contract(deps.api.addr_validate(group_addr).map_err(|_| {
39 ContractError::InvalidGroup {
40 addr: group_addr.to_owned(),
41 }
42 })?);
43
44 let cfg = Config {
45 rules,
46 group_contract,
47 };
48
49 cfg.rules.validate()?;
50 CONFIG.save(deps.storage, &cfg)?;
51
52 Ok(Response::default())
53}
54
55pub fn propose<P, Q: CustomQuery>(
56 deps: DepsMut<Q>,
57 env: Env,
58 info: MessageInfo,
59 title: String,
60 description: String,
61 proposal: P,
62) -> Result<Response, ContractError>
63where
64 P: DeserializeOwned + Serialize,
65{
66 let cfg = CONFIG.load(deps.storage)?;
67
68 let vote_power = cfg
71 .group_contract
72 .is_voting_member(&deps.querier, info.sender.as_str())?;
73
74 let expires =
76 Expiration::at_timestamp(env.block.time.plus_seconds(cfg.rules.voting_period_secs()));
77
78 let mut prop = Proposal {
80 title,
81 description,
82 created_by: info.sender.to_string(),
83 start_height: env.block.height,
84 expires,
85 proposal,
86 status: Status::Open,
87 votes: Votes::yes(vote_power),
88 rules: cfg.rules,
89 total_points: cfg.group_contract.total_points(&deps.querier)?,
90 };
91 prop.update_status(&env.block);
92 let id = next_id(deps.storage)?;
93 proposals().save(deps.storage, id, &prop)?;
94
95 ballots().create_ballot(deps.storage, &info.sender, id, vote_power, Vote::Yes)?;
97
98 let resp = msg::ProposalCreationResponse { proposal_id: id };
99
100 Ok(Response::new()
101 .add_attribute("action", "propose")
102 .add_attribute("sender", info.sender)
103 .add_attribute("proposal_id", id.to_string())
104 .add_attribute("status", format!("{:?}", prop.status))
105 .set_data(cosmwasm_std::to_binary(&resp)?))
106}
107
108pub fn vote<P, Q: CustomQuery>(
109 deps: DepsMut<Q>,
110 env: Env,
111 info: MessageInfo,
112 proposal_id: u64,
113 vote: Vote,
114) -> Result<Response, ContractError>
115where
116 P: Serialize + DeserializeOwned,
117{
118 let mut prop = proposals().load(deps.storage, proposal_id)?;
120
121 if ![Status::Open, Status::Passed, Status::Rejected].contains(&prop.status) {
122 return Err(ContractError::NotOpen {});
123 }
124
125 if prop.expires.is_expired(&env.block) {
127 return Err(ContractError::Expired {});
128 }
129
130 let cfg = CONFIG.load(deps.storage)?;
133 let vote_power =
134 cfg.group_contract
135 .was_voting_member(&deps.querier, &info.sender, prop.start_height)?;
136
137 ballots().create_ballot(deps.storage, &info.sender, proposal_id, vote_power, vote)?;
139
140 prop.votes.add_vote(vote, vote_power);
142 prop.update_status(&env.block);
143 proposals::<P>().save(deps.storage, proposal_id, &prop)?;
144
145 Ok(Response::new()
146 .add_attribute("action", "vote")
147 .add_attribute("sender", info.sender)
148 .add_attribute("proposal_id", proposal_id.to_string())
149 .add_attribute("status", format!("{:?}", prop.status)))
150}
151
152pub fn mark_executed<P>(
157 storage: &mut dyn Storage,
158 env: Env,
159 proposal_id: u64,
160) -> Result<Proposal<P>, ContractError>
161where
162 P: Serialize + DeserializeOwned,
163{
164 let mut proposal = proposals::<P>().load(storage, proposal_id)?;
165 proposal.update_status(&env.block);
167 if proposal.current_status(&env.block) != Status::Passed {
170 return Err(ContractError::WrongExecuteStatus {});
171 }
172
173 proposal.status = Status::Executed;
175 proposals::<P>().save(storage, proposal_id, &proposal)?;
176 Ok(proposal)
177}
178
179pub fn execute_text<P, Q: CustomQuery>(
180 deps: DepsMut<Q>,
181 id: u64,
182 proposal: Proposal<P>,
183) -> Result<(), ContractError>
184where
185 P: Serialize + DeserializeOwned,
186{
187 TEXT_PROPOSALS.save(deps.storage, id, &proposal.into())?;
188
189 Ok(())
190}
191
192pub fn close<P, Q: CustomQuery>(
193 deps: DepsMut<Q>,
194 env: Env,
195 info: MessageInfo,
196 proposal_id: u64,
197) -> Result<Response, ContractError>
198where
199 P: Serialize + DeserializeOwned,
200{
201 let mut prop = proposals().load(deps.storage, proposal_id)?;
204
205 if prop.status == Status::Rejected {
206 return Err(ContractError::NotOpen {});
207 }
208
209 prop.update_status(&env.block);
210
211 if [Status::Executed, Status::Passed]
212 .iter()
213 .any(|x| *x == prop.status)
214 {
215 return Err(ContractError::WrongCloseStatus {});
216 }
217 if !prop.expires.is_expired(&env.block) {
218 return Err(ContractError::NotExpired {});
219 }
220
221 prop.status = Status::Rejected;
222 proposals::<P>().save(deps.storage, proposal_id, &prop)?;
223
224 Ok(Response::new()
225 .add_attribute("action", "close")
226 .add_attribute("sender", info.sender)
227 .add_attribute("proposal_id", proposal_id.to_string()))
228}
229
230pub fn query_rules<Q: CustomQuery>(deps: Deps<Q>) -> StdResult<VotingRules> {
231 let cfg = CONFIG.load(deps.storage)?;
232 Ok(cfg.rules)
233}
234
235pub fn query_proposal<P, Q: CustomQuery>(
236 deps: Deps<Q>,
237 env: Env,
238 id: u64,
239) -> StdResult<ProposalResponse<P>>
240where
241 P: Serialize + DeserializeOwned,
242{
243 let prop = proposals().load(deps.storage, id)?;
244 let status = prop.current_status(&env.block);
245 let rules = prop.rules;
246 Ok(ProposalResponse {
247 id,
248 title: prop.title,
249 description: prop.description,
250 proposal: prop.proposal,
251 created_by: prop.created_by,
252 status,
253 expires: prop.expires,
254 rules,
255 total_points: prop.total_points,
256 votes: prop.votes,
257 })
258}
259
260fn map_proposal<P>(
261 block: &BlockInfo,
262 item: StdResult<(u64, Proposal<P>)>,
263) -> StdResult<ProposalResponse<P>> {
264 let (id, prop) = item?;
265 let status = prop.current_status(block);
266 Ok(ProposalResponse {
267 id,
268 title: prop.title,
269 description: prop.description,
270 proposal: prop.proposal,
271 created_by: prop.created_by,
272 status,
273 expires: prop.expires,
274 rules: prop.rules,
275 total_points: prop.total_points,
276 votes: prop.votes,
277 })
278}
279
280pub fn list_proposals<P, Q: CustomQuery>(
281 deps: Deps<Q>,
282 env: Env,
283 start_after: Option<u64>,
284 limit: usize,
285) -> StdResult<ProposalListResponse<P>>
286where
287 P: Serialize + DeserializeOwned,
288{
289 let start = start_after.map(Bound::exclusive);
290 let props: StdResult<Vec<_>> = proposals()
291 .range(deps.storage, start, None, Order::Ascending)
292 .take(limit)
293 .map(|p| map_proposal(&env.block, p))
294 .collect();
295
296 Ok(ProposalListResponse { proposals: props? })
297}
298
299pub fn list_text_proposals<Q: CustomQuery>(
300 deps: Deps<Q>,
301 start_after: Option<u64>,
302 limit: usize,
303) -> StdResult<TextProposalListResponse> {
304 let start = start_after.map(Bound::exclusive);
305 let props: StdResult<Vec<_>> = TEXT_PROPOSALS
306 .range(deps.storage, start, None, Order::Ascending)
307 .take(limit)
308 .map(|r| r.map(|(_, p)| p))
309 .collect();
310
311 Ok(TextProposalListResponse { proposals: props? })
312}
313
314pub fn reverse_proposals<P, Q: CustomQuery>(
315 deps: Deps<Q>,
316 env: Env,
317 start_before: Option<u64>,
318 limit: usize,
319) -> StdResult<ProposalListResponse<P>>
320where
321 P: Serialize + DeserializeOwned,
322{
323 let end = start_before.map(Bound::exclusive);
324 let props: StdResult<Vec<_>> = proposals()
325 .range(deps.storage, None, end, Order::Descending)
326 .take(limit)
327 .map(|p| map_proposal(&env.block, p))
328 .collect();
329
330 Ok(ProposalListResponse { proposals: props? })
331}
332
333pub fn query_vote<Q: CustomQuery>(
334 deps: Deps<Q>,
335 proposal_id: u64,
336 voter: String,
337) -> StdResult<VoteResponse> {
338 let voter_addr = deps.api.addr_validate(&voter)?;
339 let prop = ballots()
340 .ballots
341 .may_load(deps.storage, (proposal_id, &voter_addr))?;
342 let vote = prop.map(|b| VoteInfo {
343 proposal_id,
344 voter,
345 vote: b.vote,
346 points: b.points,
347 });
348 Ok(VoteResponse { vote })
349}
350
351pub fn list_votes<Q: CustomQuery>(
352 deps: Deps<Q>,
353 proposal_id: u64,
354 start_after: Option<String>,
355 limit: usize,
356) -> StdResult<VoteListResponse> {
357 let addr = maybe_addr(deps.api, start_after)?;
358 let start = addr.as_ref().map(Bound::exclusive);
359
360 let votes: StdResult<Vec<_>> = ballots()
361 .ballots
362 .prefix(proposal_id)
363 .range(deps.storage, start, None, Order::Ascending)
364 .take(limit)
365 .map(|item| {
366 let (voter, ballot) = item?;
367 Ok(VoteInfo {
368 proposal_id,
369 voter: voter.into(),
370 vote: ballot.vote,
371 points: ballot.points,
372 })
373 })
374 .collect();
375
376 Ok(VoteListResponse { votes: votes? })
377}
378
379pub fn list_votes_by_voter<Q: CustomQuery>(
380 deps: Deps<Q>,
381 voter: String,
382 start_after: Option<u64>,
383 limit: usize,
384) -> StdResult<VoteListResponse> {
385 let voter_addr = deps.api.addr_validate(&voter)?;
386 let start = start_after.map(|m| Bound::exclusive((m, voter_addr.clone())));
388
389 let votes: StdResult<Vec<_>> = ballots()
390 .ballots
391 .idx
392 .voter
393 .prefix(voter_addr)
394 .range(deps.storage, start, None, Order::Ascending)
395 .take(limit)
396 .map(|item| {
397 let ((proposal_id, _), ballot) = item?;
398 Ok(VoteInfo {
399 proposal_id,
400 voter: ballot.voter.into(),
401 vote: ballot.vote,
402 points: ballot.points,
403 })
404 })
405 .collect();
406
407 Ok(VoteListResponse { votes: votes? })
408}
409
410pub fn query_voter<Q: CustomQuery>(deps: Deps<Q>, voter: String) -> StdResult<VoterResponse> {
411 let cfg = CONFIG.load(deps.storage)?;
412 let voter_addr = deps.api.addr_validate(&voter)?;
413 let points = cfg.group_contract.is_member(&deps.querier, &voter_addr)?;
414
415 Ok(VoterResponse { points })
416}
417
418pub fn list_voters<Q: CustomQuery>(
419 deps: Deps<Q>,
420 start_after: Option<String>,
421 limit: Option<u32>,
422) -> StdResult<VoterListResponse> {
423 let cfg = CONFIG.load(deps.storage)?;
424 let voters = cfg
425 .group_contract
426 .list_members(&deps.querier, start_after, limit)?
427 .into_iter()
428 .map(|Member { addr, points, .. }| VoterDetail { addr, points })
429 .collect();
430 Ok(VoterListResponse { voters })
431}
432
433pub fn query_group_contract<Q: CustomQuery>(deps: Deps<Q>) -> StdResult<Addr> {
434 let cfg = CONFIG.load(deps.storage)?;
435 Ok(cfg.group_contract.addr())
436}