From 9920f3482f7f1c2d36a4616ad150e8d4ddbcb9a4 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 16 Apr 2021 13:43:17 +0200 Subject: [PATCH 1/6] Add members() IndexedSnapshotMap to cw4-stake --- contracts/cw4-stake/src/state.rs | 34 +++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/contracts/cw4-stake/src/state.rs b/contracts/cw4-stake/src/state.rs index 224c52aae..1868faef9 100644 --- a/contracts/cw4-stake/src/state.rs +++ b/contracts/cw4-stake/src/state.rs @@ -6,7 +6,10 @@ use cw0::Duration; use cw20::Denom; use cw4::TOTAL_KEY; use cw_controllers::{Admin, Claims, Hooks}; -use cw_storage_plus::{Item, Map, SnapshotMap, Strategy}; +use cw_storage_plus::{ + Index, IndexList, IndexedSnapshotMap, Item, Map, MultiIndex, PkOwned, SnapshotMap, Strategy, + U64Key, +}; pub const CLAIMS: Claims = Claims::new("claims"); @@ -31,4 +34,33 @@ pub const MEMBERS: SnapshotMap<&Addr, u64> = SnapshotMap::new( Strategy::EveryBlock, ); +pub struct MemberIndexes<'a> { + // pk goes to second tuple element + pub weight: MultiIndex<'a, (U64Key, PkOwned), u64>, +} + +impl<'a> IndexList for MemberIndexes<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.weight]; + Box::new(v.into_iter()) + } +} + +pub fn members<'a>() -> IndexedSnapshotMap<'a, &'a Addr, u64, MemberIndexes<'a>> { + let indexes = MemberIndexes { + weight: MultiIndex::new( + |&w, k| (U64Key::new(w), PkOwned(k)), + cw4::MEMBERS_KEY, + "members__weight", + ), + }; + IndexedSnapshotMap::new( + cw4::MEMBERS_KEY, + cw4::MEMBERS_CHECKPOINTS, + cw4::MEMBERS_CHANGELOG, + Strategy::EveryBlock, + indexes, + ) +} + pub const STAKE: Map<&Addr, Uint128> = Map::new("stake"); From f5d4a5f014aed07f7b1cbf030e86c7a57ec2fade Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 16 Apr 2021 14:40:16 +0200 Subject: [PATCH 2/6] Add list_members_by_weight method / msg to cw4-stake --- contracts/cw4-stake/src/contract.rs | 50 ++++++++++++++++++++++++----- contracts/cw4-stake/src/msg.rs | 5 +++ 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/contracts/cw4-stake/src/contract.rs b/contracts/cw4-stake/src/contract.rs index 912008a7c..571ee87bc 100644 --- a/contracts/cw4-stake/src/contract.rs +++ b/contracts/cw4-stake/src/contract.rs @@ -12,11 +12,11 @@ use cw4::{ Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse, TotalWeightResponse, }; -use cw_storage_plus::Bound; +use cw_storage_plus::{Bound, PrimaryKey, U64Key}; use crate::error::ContractError; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, StakedResponse}; -use crate::state::{Config, ADMIN, CLAIMS, CONFIG, HOOKS, MEMBERS, STAKE, TOTAL}; +use crate::state::{members, Config, ADMIN, CLAIMS, CONFIG, HOOKS, STAKE, TOTAL}; // version info for migration info const CONTRACT_NAME: &str = "crates.io:cw4-stake"; @@ -195,7 +195,7 @@ fn update_membership( ) -> StdResult> { // update their membership weight let new = calc_weight(new_stake, cfg); - let old = MEMBERS.may_load(storage, &sender)?; + let old = members().may_load(storage, &sender)?; // short-circuit if no change if new == old { @@ -203,8 +203,8 @@ fn update_membership( } // otherwise, record change of weight match new.as_ref() { - Some(w) => MEMBERS.save(storage, &sender, w, height), - None => MEMBERS.remove(storage, &sender, height), + Some(w) => members().save(storage, &sender, w, height), + None => members().remove(storage, &sender, height), }?; // update total @@ -286,6 +286,9 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::ListMembers { start_after, limit } => { to_binary(&list_members(deps, start_after, limit)?) } + QueryMsg::ListMembersByWeight { start_after, limit } => { + to_binary(&list_members_by_weight(deps, start_after, limit)?) + } QueryMsg::TotalWeight {} => to_binary(&query_total_weight(deps)?), QueryMsg::Claims { address } => { to_binary(&CLAIMS.query_claims(deps, &deps.api.addr_validate(&address)?)?) @@ -320,8 +323,8 @@ pub fn query_staked(deps: Deps, addr: String) -> StdResult { fn query_member(deps: Deps, addr: String, height: Option) -> StdResult { let addr = deps.api.addr_validate(&addr)?; let weight = match height { - Some(h) => MEMBERS.may_load_at_height(deps.storage, &addr, h), - None => MEMBERS.may_load(deps.storage, &addr), + Some(h) => members().primary.may_load_at_height(deps.storage, &addr, h), + None => members().may_load(deps.storage, &addr), }?; Ok(MemberResponse { weight }) } @@ -339,7 +342,7 @@ fn list_members( let addr = maybe_addr(deps.api, start_after)?; let start = addr.map(|addr| Bound::exclusive(addr.as_ref())); - let members: StdResult> = MEMBERS + let members: StdResult> = members() .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|item| { @@ -354,6 +357,32 @@ fn list_members( Ok(MemberListResponse { members: members? }) } +fn list_members_by_weight( + deps: Deps, + start_after: Option<(u64, String)>, + limit: Option, +) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = + start_after.map(|(w, a)| Bound::exclusive((U64Key::from(w), a.as_str()).joined_key())); + + let members: StdResult> = members() + .idx + .weight + .range(deps.storage, None, start, Order::Descending) + .take(limit) + .map(|item| { + let (key, weight) = item?; + Ok(Member { + addr: String::from_utf8(key)?, + weight, + }) + }) + .collect(); + + Ok(MemberListResponse { members: members? }) +} + #[cfg(test)] mod tests { use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; @@ -449,6 +478,11 @@ mod tests { res.weight } + // TODO: Test list_members + + // TODO: Test list_members_by_weight + // Test pagination / limits + // this tests the member queries fn assert_users( deps: Deps, diff --git a/contracts/cw4-stake/src/msg.rs b/contracts/cw4-stake/src/msg.rs index eacb14a70..0648a6381 100644 --- a/contracts/cw4-stake/src/msg.rs +++ b/contracts/cw4-stake/src/msg.rs @@ -60,6 +60,11 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, + /// Returns MembersListResponse, sorted by weight descending. + ListMembersByWeight { + start_after: Option<(u64, String)>, + limit: Option, + }, /// Returns MemberResponse Member { addr: String, From ff2e61e74d9d4ac3d9d881dbd6dc445be790298f Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Sat, 17 Apr 2021 10:42:32 +0200 Subject: [PATCH 3/6] Update schema --- contracts/cw4-stake/schema/query_msg.json | 41 +++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/contracts/cw4-stake/schema/query_msg.json b/contracts/cw4-stake/schema/query_msg.json index a72a836d8..787d52bfd 100644 --- a/contracts/cw4-stake/schema/query_msg.json +++ b/contracts/cw4-stake/schema/query_msg.json @@ -98,6 +98,47 @@ }, "additionalProperties": false }, + { + "description": "Returns MembersListResponse, sorted by weight descending.", + "type": "object", + "required": [ + "list_members_by_weight" + ], + "properties": { + "list_members_by_weight": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + { + "type": "string" + } + ], + "maxItems": 2, + "minItems": 2 + } + } + } + }, + "additionalProperties": false + }, { "description": "Returns MemberResponse", "type": "object", From 855b1d8e24f904346b5f2f0c95cbab93801601fc Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Sat, 17 Apr 2021 10:58:07 +0200 Subject: [PATCH 4/6] Add query membership tests --- contracts/cw4-stake/src/contract.rs | 76 +++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 5 deletions(-) diff --git a/contracts/cw4-stake/src/contract.rs b/contracts/cw4-stake/src/contract.rs index 571ee87bc..bddf5b218 100644 --- a/contracts/cw4-stake/src/contract.rs +++ b/contracts/cw4-stake/src/contract.rs @@ -478,11 +478,6 @@ mod tests { res.weight } - // TODO: Test list_members - - // TODO: Test list_members_by_weight - // Test pagination / limits - // this tests the member queries fn assert_users( deps: Deps, @@ -564,6 +559,77 @@ mod tests { // after second stake } + #[test] + fn try_member_queries() { + let mut deps = mock_dependencies(&[]); + default_instantiate(deps.as_mut()); + + bond(deps.as_mut(), 12_000, 7_500, 4_000, 1); + + let member1 = query_member(deps.as_ref(), USER1.into(), None).unwrap(); + assert_eq!(member1.weight, Some(12)); + + let member2 = query_member(deps.as_ref(), USER2.into(), None).unwrap(); + assert_eq!(member2.weight, Some(7)); + + let member3 = query_member(deps.as_ref(), USER3.into(), None).unwrap(); + assert_eq!(member3.weight, None); + + let members = list_members(deps.as_ref(), None, None).unwrap().members; + assert_eq!(members.len(), 2); + // Assert the set is proper + assert_eq!( + members, + vec![ + Member { + addr: USER2.into(), + weight: 7 + }, + Member { + addr: USER1.into(), + weight: 12 + }, + ] + ); + + // Test pagination / limits + let members = list_members(deps.as_ref(), None, Some(1)).unwrap().members; + assert_eq!(members.len(), 1); + // Assert the set is proper + assert_eq!( + members, + vec![Member { + addr: USER2.into(), + weight: 7 + },] + ); + + // Next page + let start_after = Some(members[0].addr.clone()); + let members = list_members(deps.as_ref(), start_after, Some(1)) + .unwrap() + .members; + assert_eq!(members.len(), 1); + // Assert the set is proper + assert_eq!( + members, + vec![Member { + addr: USER1.into(), + weight: 12 + },] + ); + + // Assert there's no more + let start_after = Some(members[0].addr.clone()); + let members = list_members(deps.as_ref(), start_after, Some(1)) + .unwrap() + .members; + assert_eq!(members.len(), 0); + } + + // TODO: Test list_members_by_weight + // Test pagination / limits + #[test] fn unbond_stake_update_membership() { let mut deps = mock_dependencies(&[]); From 9bee38e70df2d24d733ac72bb9b85a26f41ff845 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Sat, 17 Apr 2021 11:24:23 +0200 Subject: [PATCH 5/6] Add list_members_by_weight() tests --- contracts/cw4-stake/src/contract.rs | 76 ++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/contracts/cw4-stake/src/contract.rs b/contracts/cw4-stake/src/contract.rs index bddf5b218..ee67afd9a 100644 --- a/contracts/cw4-stake/src/contract.rs +++ b/contracts/cw4-stake/src/contract.rs @@ -627,8 +627,80 @@ mod tests { assert_eq!(members.len(), 0); } - // TODO: Test list_members_by_weight - // Test pagination / limits + #[test] + fn try_list_members_by_weight() { + let mut deps = mock_dependencies(&[]); + default_instantiate(deps.as_mut()); + + bond(deps.as_mut(), 11_000, 6_500, 5_000, 1); + + let members = list_members_by_weight(deps.as_ref(), None, None) + .unwrap() + .members; + assert_eq!(members.len(), 3); + // Assert the set is sorted by (descending) weight + assert_eq!( + members, + vec![ + Member { + addr: USER1.into(), + weight: 11 + }, + Member { + addr: USER2.into(), + weight: 6 + }, + Member { + addr: USER3.into(), + weight: 5 + } + ] + ); + + // Test pagination / limits + let members = list_members_by_weight(deps.as_ref(), None, Some(1)) + .unwrap() + .members; + assert_eq!(members.len(), 1); + // Assert the set is proper + assert_eq!( + members, + vec![Member { + addr: USER1.into(), + weight: 11 + },] + ); + + // Next page + let last = members.last().unwrap(); + let start_after = Some((last.weight, last.addr.clone())); + let members = list_members_by_weight(deps.as_ref(), start_after, None) + .unwrap() + .members; + assert_eq!(members.len(), 2); + // Assert the set is proper + assert_eq!( + members, + vec![ + Member { + addr: USER2.into(), + weight: 6 + }, + Member { + addr: USER3.into(), + weight: 5 + } + ] + ); + + // Assert there's no more + let last = members.last().unwrap(); + let start_after = Some((last.weight, last.addr.clone())); + let members = list_members_by_weight(deps.as_ref(), start_after, Some(1)) + .unwrap() + .members; + assert_eq!(members.len(), 0); + } #[test] fn unbond_stake_update_membership() { From dd1d40ce0ec8bd3c8aa85cd86e842499aabdd1bf Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Mon, 19 Apr 2021 12:04:47 +0200 Subject: [PATCH 6/6] Use `may_load_at_height` wrapper in cw4-stake --- contracts/cw4-stake/src/contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/cw4-stake/src/contract.rs b/contracts/cw4-stake/src/contract.rs index ee67afd9a..21ada7850 100644 --- a/contracts/cw4-stake/src/contract.rs +++ b/contracts/cw4-stake/src/contract.rs @@ -323,7 +323,7 @@ pub fn query_staked(deps: Deps, addr: String) -> StdResult { fn query_member(deps: Deps, addr: String, height: Option) -> StdResult { let addr = deps.api.addr_validate(&addr)?; let weight = match height { - Some(h) => members().primary.may_load_at_height(deps.storage, &addr, h), + Some(h) => members().may_load_at_height(deps.storage, &addr, h), None => members().may_load(deps.storage, &addr), }?; Ok(MemberResponse { weight })