diff --git a/contracts/tg4-engagement/src/contract.rs b/contracts/tg4-engagement/src/contract.rs index a00d3005..80598689 100644 --- a/contracts/tg4-engagement/src/contract.rs +++ b/contracts/tg4-engagement/src/contract.rs @@ -8,8 +8,8 @@ use cw2::set_contract_version; use cw_storage_plus::Bound; use cw_utils::maybe_addr; use tg4::{ - HooksResponse, Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse, - TotalPointsResponse, + HooksResponse, Member, MemberChangedHookMsg, MemberDiff, MemberInfo, MemberListResponse, + MemberResponse, TotalPointsResponse, }; use crate::error::ContractError; @@ -101,7 +101,12 @@ pub fn create( for member in members_list.into_iter() { total += member.points; let member_addr = deps.api.addr_validate(&member.addr)?; - members().save(deps.storage, &member_addr, &member.points, height)?; + members().save( + deps.storage, + &member_addr, + &MemberInfo::new(member.points), + height, + )?; let adjustment = WithdrawAdjustment { shares_correction: 0i128.into(), @@ -163,9 +168,7 @@ pub fn execute_add_points( ADMIN.assert_admin(deps.as_ref(), &info.sender)?; - let old_points = query_member(deps.as_ref(), addr.clone(), None)? - .points - .unwrap_or_default(); + let old_points = query_member(deps.as_ref(), addr.clone(), None)?; // make the local update let diff = update_members( @@ -173,7 +176,8 @@ pub fn execute_add_points( env.block.height, vec![Member { addr, - points: old_points + points, + points: old_points.points.unwrap_or_default() + points, + start_height: old_points.start_height, }], vec![], )?; @@ -466,7 +470,7 @@ pub fn execute_slash( env.block.height, |old| -> StdResult<_> { let old = match old { - Some(old) => Uint128::new(old as _), + Some(old) => Uint128::new(old.points as _), None => Uint128::zero(), }; @@ -475,7 +479,7 @@ pub fn execute_slash( diff = -(slash.u128() as i128); - Ok(new.u128() as _) + Ok(MemberInfo::new(new.u128() as _)) }, )?; apply_points_correction(deps.branch(), &addr, ppw, diff)?; @@ -503,6 +507,7 @@ pub fn withdrawable_rewards( let points: u128 = members() .may_load(deps.storage, owner)? .unwrap_or_default() + .points .into(); let correction: i128 = adjustment.shares_correction.into(); let withdrawn: u128 = adjustment.withdrawn_rewards.into(); @@ -552,13 +557,17 @@ pub fn update_members( let mut diff = 0; let mut insert_funds = false; members().update(deps.storage, &add_addr, height, |old| -> StdResult<_> { - diffs.push(MemberDiff::new(add.addr, old, Some(add.points))); + diffs.push(MemberDiff::new( + add.addr, + old.as_ref().map(|mi| mi.points), + Some(add.points), + )); insert_funds = old.is_none(); let old = old.unwrap_or_default(); - total -= old; + total -= old.points; total += add.points; - diff = add.points as i128 - old as i128; - Ok(add.points) + diff = add.points as i128 - old.points as i128; + Ok(MemberInfo::new(add.points)) })?; apply_points_correction(deps.branch(), &add_addr, ppw, diff)?; } @@ -567,7 +576,7 @@ pub fn update_members( let remove_addr = deps.api.addr_validate(&remove)?; let old = members().may_load(deps.storage, &remove_addr)?; // Only process this if they were actually in the list before - if let Some(points) = old { + if let Some(MemberInfo { points, .. }) = old { diffs.push(MemberDiff::new(remove, Some(points), None)); total -= points; members().remove(deps.storage, &remove_addr, height)?; @@ -645,13 +654,20 @@ fn end_block(mut deps: DepsMut, env: Env) -> Result StdResult> { - let (addr, points) = item?; + let ( + addr, + MemberInfo { + points, + start_height, + }, + ) = item?; if points <= 1 { return Ok(None); } Ok(Some(Member { addr: addr.into(), points, + start_height, })) })() .transpose() @@ -665,8 +681,8 @@ fn end_block(mut deps: DepsMut, env: Env) -> Result( height: Option, ) -> StdResult { let addr = deps.api.addr_validate(&addr)?; - let points = match height { + let mi = match height { Some(h) => members().may_load_at_height(deps.storage, &addr, h), None => members().may_load(deps.storage, &addr), }?; - Ok(MemberResponse { points }) + Ok(mi.into()) } pub fn query_withdrawable_rewards( @@ -844,10 +860,17 @@ fn list_members( .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|item| { - let (addr, points) = item?; + let ( + addr, + MemberInfo { + points, + start_height, + }, + ) = item?; Ok(Member { addr: addr.into(), points, + start_height, }) }) .collect(); @@ -874,10 +897,17 @@ fn list_members_by_points( .range(deps.storage, None, start, Order::Descending) .take(limit) .map(|item| { - let (addr, points) = item?; + let ( + addr, + MemberInfo { + points, + start_height, + }, + ) = item?; Ok(Member { addr: addr.into(), points, + start_height, }) }) .collect(); @@ -926,10 +956,12 @@ mod tests { Member { addr: USER1.into(), points: USER1_POINTS, + start_height: None, }, Member { addr: USER2.into(), points: USER2_POINTS, + start_height: None, }, ], preauths_hooks: 1, @@ -1013,11 +1045,13 @@ mod tests { vec![ Member { addr: USER1.into(), - points: 11 + points: 11, + start_height: None }, Member { addr: USER2.into(), - points: 6 + points: 6, + start_height: None }, ] ); @@ -1030,7 +1064,8 @@ mod tests { members, vec![Member { addr: USER1.into(), - points: 11 + points: 11, + start_height: None },] ); @@ -1045,7 +1080,8 @@ mod tests { members, vec![Member { addr: USER2.into(), - points: 6 + points: 6, + start_height: None },] ); @@ -1072,11 +1108,13 @@ mod tests { vec![ Member { addr: USER1.into(), - points: 11 + points: 11, + start_height: None }, Member { addr: USER2.into(), - points: 6 + points: 6, + start_height: None } ] ); @@ -1091,7 +1129,8 @@ mod tests { members, vec![Member { addr: USER1.into(), - points: 11 + points: 11, + start_height: None },] ); @@ -1106,7 +1145,8 @@ mod tests { members, vec![Member { addr: USER2.into(), - points: 6 + points: 6, + start_height: None },] ); @@ -1153,10 +1193,12 @@ mod tests { Member { addr: USER1.into(), points: USER1_POINTS, + start_height: None, }, Member { addr: USER2.into(), points: USER2_POINTS, + start_height: None, }, ], preauths_hooks: 1, @@ -1233,6 +1275,7 @@ mod tests { let add = vec![Member { addr: USER3.into(), points: 15, + start_height: None, }]; let remove = vec![USER1.into()]; @@ -1274,6 +1317,7 @@ mod tests { let add = vec![Member { addr: USER1.into(), points: 4, + start_height: None, }]; let remove = vec![USER3.into()]; @@ -1296,10 +1340,12 @@ mod tests { Member { addr: USER1.into(), points: 20, + start_height: None, }, Member { addr: USER3.into(), points: 5, + start_height: None, }, ]; let remove = vec![USER1.into()]; @@ -1321,6 +1367,7 @@ mod tests { let add = Member { addr: USER3.into(), points: 15, + start_height: None, }; let env = mock_env(); @@ -1356,6 +1403,7 @@ mod tests { let add = Member { addr: USER2.into(), points: 1, + start_height: None, }; let env = mock_env(); @@ -1489,10 +1537,12 @@ mod tests { Member { addr: USER1.into(), points: 20, + start_height: None, }, Member { addr: USER3.into(), points: 5, + start_height: None, }, ]; let remove = vec![USER2.into()]; @@ -1537,8 +1587,8 @@ mod tests { // get member votes from raw key let member2_raw = deps.storage.get(&member_key(USER2)).unwrap(); - let member2: u64 = from_slice(&member2_raw).unwrap(); - assert_eq!(6, member2); + let member2: MemberInfo = from_slice(&member2_raw).unwrap(); + assert_eq!(6, member2.points); // and execute misses let member3_raw = deps.storage.get(&member_key(USER3)); diff --git a/contracts/tg4-engagement/src/msg.rs b/contracts/tg4-engagement/src/msg.rs index a19fae2f..3bbd721f 100644 --- a/contracts/tg4-engagement/src/msg.rs +++ b/contracts/tg4-engagement/src/msg.rs @@ -193,7 +193,8 @@ mod tests { assert_eq!( SudoMsg::UpdateMember(Member { addr: "xxx".to_string(), - points: 123 + points: 123, + start_height: None }), cosmwasm_std::from_slice::(message.as_bytes()).unwrap() ); diff --git a/contracts/tg4-engagement/src/multitest.rs b/contracts/tg4-engagement/src/multitest.rs index 59ca28b4..52548142 100644 --- a/contracts/tg4-engagement/src/multitest.rs +++ b/contracts/tg4-engagement/src/multitest.rs @@ -11,6 +11,7 @@ fn member(addr: &str, points: u64) -> Member { Member { addr: addr.to_owned(), points, + start_height: None, } } diff --git a/contracts/tg4-engagement/src/multitest/suite.rs b/contracts/tg4-engagement/src/multitest/suite.rs index cad26f5b..40a8afcb 100644 --- a/contracts/tg4-engagement/src/multitest/suite.rs +++ b/contracts/tg4-engagement/src/multitest/suite.rs @@ -26,6 +26,7 @@ pub fn expected_members(members: Vec<(&str, u64)>) -> Vec { .map(|(addr, points)| Member { addr: addr.to_owned(), points, + start_height: None, }) .collect() } @@ -46,6 +47,7 @@ impl SuiteBuilder { self.members.push(Member { addr: addr.to_owned(), points, + start_height: None, }); self } @@ -212,6 +214,7 @@ impl Suite { .map(|(addr, points)| Member { addr: (*addr).to_owned(), points: *points, + start_height: None, }) .collect(); diff --git a/contracts/tg4-group/src/contract.rs b/contracts/tg4-group/src/contract.rs index dcfb9e7d..54a9c4ae 100644 --- a/contracts/tg4-group/src/contract.rs +++ b/contracts/tg4-group/src/contract.rs @@ -175,7 +175,10 @@ fn query_member(deps: Deps, addr: String, height: Option) -> StdResult MEMBERS.may_load_at_height(deps.storage, &addr, h), None => MEMBERS.may_load(deps.storage, &addr), }?; - Ok(MemberResponse { points }) + Ok(MemberResponse { + points, + start_height: None, + }) } // settings for pagination @@ -198,6 +201,7 @@ fn list_members( item.map(|(addr, points)| Member { addr: addr.into(), points, + start_height: None, }) }) .collect::>()?; @@ -225,10 +229,12 @@ mod tests { Member { addr: USER1.into(), points: 11, + start_height: None, }, Member { addr: USER2.into(), points: 6, + start_height: None, }, ], }; @@ -309,6 +315,7 @@ mod tests { let add = vec![Member { addr: USER3.into(), points: 15, + start_height: None, }]; let remove = vec![USER1.into()]; @@ -358,6 +365,7 @@ mod tests { let add = vec![Member { addr: USER1.into(), points: 4, + start_height: None, }]; let remove = vec![USER3.into()]; @@ -385,10 +393,12 @@ mod tests { Member { addr: USER1.into(), points: 20, + start_height: None, }, Member { addr: USER3.into(), points: 5, + start_height: None, }, ]; let remove = vec![USER1.into()]; @@ -504,10 +514,12 @@ mod tests { Member { addr: USER1.into(), points: 20, + start_height: None, }, Member { addr: USER3.into(), points: 5, + start_height: None, }, ]; let remove = vec![USER2.into()]; diff --git a/contracts/tg4-mixer/src/contract.rs b/contracts/tg4-mixer/src/contract.rs index fc9a1be2..85c84fab 100644 --- a/contracts/tg4-mixer/src/contract.rs +++ b/contracts/tg4-mixer/src/contract.rs @@ -11,17 +11,18 @@ use cw_utils::maybe_addr; use tg_bindings::{TgradeMsg, TgradeQuery}; use tg_utils::{ - ensure_from_older_version, members, validate_portion, SlashMsg, HOOKS, PREAUTH_HOOKS, - PREAUTH_SLASHING, SLASHERS, TOTAL, + ensure_from_older_version, validate_portion, SlashMsg, HOOKS, PREAUTH_HOOKS, PREAUTH_SLASHING, + SLASHERS, TOTAL, }; use tg4::{ - HooksResponse, Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse, - Tg4Contract, TotalPointsResponse, + HooksResponse, Member, MemberChangedHookMsg, MemberDiff, MemberInfo, MemberListResponse, + MemberResponse, Tg4Contract, TotalPointsResponse, }; use crate::error::ContractError; use crate::functions::PoEFunction; +use crate::member_indexes::members; use crate::msg::{ ExecuteMsg, GroupsResponse, InstantiateMsg, MixerFunctionResponse, PoEFunctionType, PreauthResponse, QueryMsg, @@ -114,7 +115,12 @@ fn initialize_members( if let Some(right) = other { let points = poe_function.mix(member.points, right)?; total += points; - members().save(deps.storage, &addr, &points, height)?; + members().save( + deps.storage, + &addr, + &MemberInfo::new_with_height(points, height), + height, + )?; } } // and get the next page @@ -212,17 +218,29 @@ pub fn update_members( // update the total with changes. // to calculate this, we need to load the old points before saving the new points let prev_points = mems.may_load(deps.storage, &member_addr)?; - total -= prev_points.unwrap_or_default(); + // convenience unwrap or default + let prev_points_unwrap = prev_points.clone().unwrap_or_default(); + total -= prev_points_unwrap.points; total += new_points.unwrap_or_default(); + let prev_height = prev_points_unwrap.start_height.unwrap_or(height); // store the new value match new_points { - Some(points) => mems.save(deps.storage, &member_addr, &points, height)?, + Some(points) => mems.save( + deps.storage, + &member_addr, + &MemberInfo::new_with_height(points, prev_height), + height, + )?, None => mems.remove(deps.storage, &member_addr, height)?, }; // return the diff - diffs.push(MemberDiff::new(member_addr, prev_points, new_points)); + diffs.push(MemberDiff::new( + member_addr, + prev_points.map(|mi| mi.points), + new_points, + )); } TOTAL.save(deps.storage, &total)?; @@ -404,11 +422,11 @@ fn query_member( height: Option, ) -> StdResult { let addr = deps.api.addr_validate(&addr)?; - let points = match height { + let mi = match height { Some(h) => members().may_load_at_height(deps.storage, &addr, h), None => members().may_load(deps.storage, &addr), }?; - Ok(MemberResponse { points }) + Ok(mi.into()) } // settings for pagination @@ -428,10 +446,17 @@ fn list_members( .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|item| { - let (addr, points) = item?; + let ( + addr, + MemberInfo { + points, + start_height, + }, + ) = item?; Ok(Member { addr: addr.into(), points, + start_height, }) }) .collect(); @@ -446,22 +471,33 @@ fn list_members_by_points( ) -> StdResult { let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; let start = start_after - .map(|m| { - deps.api + .map(|m| match m.start_height { + None => Err(StdError::generic_err( + "The 'start_height' parameter is required for proper pagination", + )), + Some(start_height) => deps + .api .addr_validate(&m.addr) - .map(|addr| Bound::exclusive((m.points, addr))) + .map(|addr| Bound::exclusive(((m.points, -(start_height as i64)), addr))), }) .transpose()?; let members: StdResult> = members() .idx - .points + .points_tie_break .range(deps.storage, None, start, Order::Descending) .take(limit) .map(|item| { - let (addr, points) = item?; + let ( + addr, + MemberInfo { + points, + start_height, + }, + ) = item?; Ok(Member { addr: addr.into(), points, + start_height, }) }) .collect(); @@ -516,6 +552,7 @@ mod tests { Member { addr: addr.into(), points, + start_height: None, } } @@ -689,6 +726,22 @@ mod tests { assert_eq!(points(VOTER5), voter5); } + fn list_members_by_points( + app: &BasicApp, + mixer_addr: &Addr, + start_after: Option, + limit: Option, + ) -> Vec { + let res: MemberListResponse = app + .wrap() + .query_wasm_smart( + mixer_addr, + &QueryMsg::ListMembersByPoints { start_after, limit }, + ) + .unwrap(); + res.members + } + #[test] fn basic_init() { let stakers = vec![ @@ -803,10 +856,12 @@ mod tests { Member { addr: VOTER2.into(), points: 300, + start_height: None, }, Member { addr: VOTER3.into(), points: 1200, + start_height: None, }, ], }; @@ -878,10 +933,12 @@ mod tests { Member { addr: VOTER2.to_owned(), points: 400, + start_height: None, }, Member { addr: VOTER1.to_owned(), points: 8000, + start_height: None, }, ], remove: vec![VOTER3.to_owned()], @@ -978,5 +1035,169 @@ mod tests { ); } + #[test] + fn list_members_by_points_tie_breaking() { + let stakers = vec![ + member(VOTER1, 10000), // 10000 stake, 100 points -> 1000 mixed + member(VOTER3, 7500), // 7500 stake, 300 points -> 1500 mixed + member(VOTER2, 50), // below stake threshold -> None + ]; + + let mut app = AppBuilder::new_custom().build(|router, _, storage| { + router + .bank + .init_balance( + storage, + &Addr::unchecked(RESERVE), + coins(10000, STAKE_DENOM), + ) + .unwrap(); + + for staker in &stakers { + router + .bank + .init_balance( + storage, + &Addr::unchecked(&staker.addr), + coins(staker.points as u128, STAKE_DENOM), + ) + .unwrap(); + } + }); + + let (mixer_addr, _, staker_addr) = setup_test_case(&mut app, stakers); + + // query the membership values + check_membership( + &app, + &mixer_addr, + None, + Some(1000), + None, + Some(1500), + None, + None, + ); + + // list members by points + let members = list_members_by_points(&app, &mixer_addr, None, None); + + assert_eq!( + members, + vec![ + Member { + addr: VOTER3.into(), + points: 1500, + start_height: Some(12347) + }, + Member { + addr: VOTER1.into(), + points: 1000, + start_height: Some(12347) + }, + ] + ); + + // add an extra member for tie-breaking tests + let balance = coins(4950, STAKE_DENOM); // Total equivalent as voter1 + app.execute( + Addr::unchecked(RESERVE), + BankMsg::Send { + to_address: VOTER2.to_owned(), + amount: balance.clone(), + } + .into(), + ) + .unwrap(); + let msg = tg4_stake::msg::ExecuteMsg::Bond { + vesting_tokens: None, + }; + app.execute_contract(Addr::unchecked(VOTER2), staker_addr, &msg, &balance) + .unwrap(); + + // check updated points + check_membership( + &app, + &mixer_addr, + None, + Some(1000), + Some(1000), + Some(1500), + None, + None, + ); + + // list members by points + let members = list_members_by_points(&app, &mixer_addr, None, None); + + // Assert the set is sorted by (descending) points, breaking ties by (ascending) start_height + assert_eq!( + members, + vec![ + Member { + addr: VOTER3.into(), + points: 1500, + start_height: Some(12347) + }, + Member { + addr: VOTER1.into(), + points: 1000, + start_height: Some(12347) + }, + Member { + addr: VOTER2.into(), + points: 1000, + start_height: Some(12348) // VOTER2 should come first, lexicographically (descending order) + }, + ] + ); + + // Test pagination / limits work + let members = list_members_by_points(&app, &mixer_addr, None, Some(1)); + assert_eq!(members.len(), 1); + // Assert the set is proper + assert_eq!( + members, + vec![Member { + addr: VOTER3.into(), + points: 1500, + start_height: Some(12347) + }] + ); + + // Next page + let start_after = Some(members[0].clone()); + let members = list_members_by_points(&app, &mixer_addr, start_after, Some(1)); + assert_eq!(members.len(), 1); + // Assert the set is proper + assert_eq!( + members, + vec![Member { + addr: VOTER1.into(), + points: 1000, + start_height: Some(12347) + },] + ); + + // Next page + let start_after = Some(members[0].clone()); + let members = list_members_by_points(&app, &mixer_addr, start_after, Some(1)); + assert_eq!(members.len(), 1); + // Assert the set is proper + assert_eq!( + members, + vec![Member { + addr: VOTER2.into(), + points: 1000, + start_height: Some(12348) // VOTER2 should come first, lexicographically (descending order) + },] + ); + + // Assert there's no more + let start_after = Some(members[0].clone()); + let members = list_members_by_points(&app, &mixer_addr, start_after, Some(1)); + assert_eq!(members.len(), 0); + } + // TODO: multi-test to init! } diff --git a/contracts/tg4-mixer/src/lib.rs b/contracts/tg4-mixer/src/lib.rs index a1cf2cfc..22c5e315 100644 --- a/contracts/tg4-mixer/src/lib.rs +++ b/contracts/tg4-mixer/src/lib.rs @@ -1,5 +1,6 @@ pub mod contract; pub mod error; pub mod functions; +pub mod member_indexes; pub mod msg; pub mod state; diff --git a/contracts/tg4-mixer/src/member_indexes.rs b/contracts/tg4-mixer/src/member_indexes.rs new file mode 100644 index 00000000..e0ac8834 --- /dev/null +++ b/contracts/tg4-mixer/src/member_indexes.rs @@ -0,0 +1,47 @@ +use cosmwasm_std::Addr; +use cw_storage_plus::{Index, IndexList, IndexedSnapshotMap, MultiIndex, Strategy}; + +use tg4::MemberInfo; + +// Copied from `tg-utils` and re-defined here for the extra tie-break index +pub struct MemberIndexes<'a> { + // Points (multi-)indexes (deserializing the (hidden) pk to Addr) + pub points_tie_break: MultiIndex<'a, (u64, i64), MemberInfo, Addr>, +} + +impl<'a> IndexList for MemberIndexes<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.points_tie_break]; + Box::new(v.into_iter()) + } +} + +/// Indexed snapshot map for members. +/// +/// - The map primary key is `Addr`, and the value is a tuple of `points`, `start_height` values. +/// - The `points` index is a `MultiIndex`, as there can be multiple members with the +/// same points. +/// - The `(points, -start_height)` index is a `MultiIndex`, as there can be multiple members with the +/// same points, added at the same block height. +/// The second tuple element of the tie-breaking index is negative, so that lower heights +/// (older members) are sorted first, as this will be used as a descending index. +/// +/// This allows to query the map members, sorted by points, breaking ties by height, if needed +/// (breaking ties by address in turn). +/// The indexes are not snapshotted; only the current points are indexed at any given time. +pub fn members<'a>() -> IndexedSnapshotMap<'a, &'a Addr, MemberInfo, MemberIndexes<'a>> { + let indexes = MemberIndexes { + points_tie_break: MultiIndex::new( + |mi| (mi.points, mi.start_height.map_or(i64::MIN, |h| -(h as i64))), // Works as long as `start_height <= i64::MAX + 1` + tg4::MEMBERS_KEY, + "members__points_tie_break", + ), + }; + IndexedSnapshotMap::new( + tg4::MEMBERS_KEY, + tg4::MEMBERS_CHECKPOINTS, + tg4::MEMBERS_CHANGELOG, + Strategy::EveryBlock, + indexes, + ) +} diff --git a/contracts/tg4-stake/src/contract.rs b/contracts/tg4-stake/src/contract.rs index 3dbe9bf7..be18d3ef 100644 --- a/contracts/tg4-stake/src/contract.rs +++ b/contracts/tg4-stake/src/contract.rs @@ -11,7 +11,8 @@ use cw2::set_contract_version; use cw_storage_plus::Bound; use cw_utils::maybe_addr; use tg4::{ - HooksResponse, Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse, + HooksResponse, Member, MemberChangedHookMsg, MemberDiff, MemberInfo, MemberListResponse, + MemberResponse, }; use tg_bindings::{ request_privileges, Privilege, PrivilegeChangeMsg, TgradeMsg, TgradeQuery, TgradeSudoMsg, @@ -414,7 +415,7 @@ fn update_membership( ) -> StdResult> { // update their membership points let new = calc_points(new_stake, cfg); - let old = members().may_load(storage, &sender)?; + let old = members().may_load(storage, &sender)?.map(|mi| mi.points); // short-circuit if no change if new == old { @@ -422,7 +423,7 @@ fn update_membership( } // otherwise, record change of points match new.as_ref() { - Some(w) => members().save(storage, &sender, w, height), + Some(&p) => members().save(storage, &sender, &MemberInfo::new(p), height), None => members().remove(storage, &sender, height), }?; @@ -617,11 +618,11 @@ fn query_member( height: Option, ) -> StdResult { let addr = deps.api.addr_validate(&addr)?; - let points = match height { + let mi = match height { Some(h) => members().may_load_at_height(deps.storage, &addr, h), None => members().may_load(deps.storage, &addr), }?; - Ok(MemberResponse { points }) + Ok(mi.into()) } // settings for pagination @@ -641,10 +642,17 @@ fn list_members( .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|item| { - let (addr, points) = item?; + let ( + addr, + MemberInfo { + points, + start_height, + }, + ) = item?; Ok(Member { addr: addr.into(), points, + start_height, }) }) .collect(); @@ -672,10 +680,17 @@ fn list_members_by_points( .range(deps.storage, None, start, Order::Descending) .take(limit) .map(|item| { - let (addr, points) = item?; + let ( + addr, + MemberInfo { + points, + start_height, + }, + ) = item?; Ok(Member { addr: addr.into(), points, + start_height, }) }) .collect(); @@ -1039,11 +1054,13 @@ mod tests { vec![ Member { addr: USER1.into(), - points: 12 + points: 12, + start_height: None }, Member { addr: USER2.into(), - points: 7 + points: 7, + start_height: None }, ] ); @@ -1056,7 +1073,8 @@ mod tests { members, vec![Member { addr: USER1.into(), - points: 12 + points: 12, + start_height: None },] ); @@ -1071,7 +1089,8 @@ mod tests { members, vec![Member { addr: USER2.into(), - points: 7 + points: 7, + start_height: None },] ); @@ -1100,15 +1119,18 @@ mod tests { vec![ Member { addr: USER1.into(), - points: 11 + points: 11, + start_height: None }, Member { addr: USER2.into(), - points: 6 + points: 6, + start_height: None }, Member { addr: USER3.into(), - points: 5 + points: 5, + start_height: None } ] ); @@ -1123,7 +1145,8 @@ mod tests { members, vec![Member { addr: USER1.into(), - points: 11 + points: 11, + start_height: None },] ); @@ -1140,11 +1163,13 @@ mod tests { vec![ Member { addr: USER2.into(), - points: 6 + points: 6, + start_height: None }, Member { addr: USER3.into(), - points: 5 + points: 5, + start_height: None } ] ); @@ -1222,8 +1247,8 @@ mod tests { // get member votes from raw key let member2_raw = deps.storage.get(&member_key(USER2)).unwrap(); - let member2: u64 = from_slice(&member2_raw).unwrap(); - assert_eq!(6, member2); + let member2: MemberInfo = from_slice(&member2_raw).unwrap(); + assert_eq!(6, member2.points); // and execute misses let member3_raw = deps.storage.get(&member_key(USER3)); diff --git a/contracts/tgrade-community-pool/src/multitest/suite.rs b/contracts/tgrade-community-pool/src/multitest/suite.rs index 47e11649..efc9e818 100644 --- a/contracts/tgrade-community-pool/src/multitest/suite.rs +++ b/contracts/tgrade-community-pool/src/multitest/suite.rs @@ -53,6 +53,7 @@ impl SuiteBuilder { self.group_members.push(Member { addr: addr.to_owned(), points, + start_height: None, }); self } @@ -148,6 +149,7 @@ impl SuiteBuilder { add: vec![Member { addr: contract.to_string(), points: self.contract_points, + start_height: None, }], }, &[], diff --git a/contracts/tgrade-validator-voting/src/multitest/suite.rs b/contracts/tgrade-validator-voting/src/multitest/suite.rs index 73b299c1..48fdbdf9 100644 --- a/contracts/tgrade-validator-voting/src/multitest/suite.rs +++ b/contracts/tgrade-validator-voting/src/multitest/suite.rs @@ -61,6 +61,7 @@ impl SuiteBuilder { self.group_members.push(Member { addr: addr.to_owned(), points, + start_height: None, }); self } diff --git a/contracts/tgrade-valset/src/contract.rs b/contracts/tgrade-valset/src/contract.rs index 7e60113d..a26ac71f 100644 --- a/contracts/tgrade-valset/src/contract.rs +++ b/contracts/tgrade-valset/src/contract.rs @@ -896,6 +896,7 @@ fn calculate_diff( let member = Member { addr: vi.operator.to_string(), points: vi.power, + start_height: None, }; (update, member) @@ -1124,6 +1125,7 @@ mod test { .map(|(addr, points)| Member { addr: addr.to_owned(), points, + start_height: None, }) .collect() } diff --git a/contracts/tgrade-valset/src/multitest/suite.rs b/contracts/tgrade-valset/src/multitest/suite.rs index 59f14cc0..815fcaaa 100644 --- a/contracts/tgrade-valset/src/multitest/suite.rs +++ b/contracts/tgrade-valset/src/multitest/suite.rs @@ -187,6 +187,7 @@ impl SuiteBuilder { .map(|(addr, points)| Member { addr: (*addr).to_owned(), points: *points, + start_height: None, }) .collect(), halflife: halflife.into(), @@ -233,7 +234,11 @@ impl SuiteBuilder { let members = members .into_iter() - .map(|(addr, points)| Member { addr, points }) + .map(|(addr, points)| Member { + addr, + points, + start_height: None, + }) .collect(); app.instantiate_contract( diff --git a/packages/tg4/src/helpers.rs b/packages/tg4/src/helpers.rs index f2baa1f2..548ea6fc 100644 --- a/packages/tg4/src/helpers.rs +++ b/packages/tg4/src/helpers.rs @@ -10,7 +10,8 @@ use tg_bindings::TgradeMsg; use crate::msg::Tg4ExecuteMsg; use crate::query::HooksResponse; use crate::{ - member_key, AdminResponse, Member, MemberListResponse, MemberResponse, Tg4QueryMsg, TOTAL_KEY, + member_key, AdminResponse, Member, MemberInfo, MemberListResponse, MemberResponse, Tg4QueryMsg, + TOTAL_KEY, }; pub type SubMsg = cosmwasm_std::SubMsg; @@ -118,7 +119,7 @@ impl Tg4Contract { if value.is_empty() { Ok(None) } else { - from_slice(&value) + Ok(from_slice::(&value)?.points.into()) } } } diff --git a/packages/tg4/src/lib.rs b/packages/tg4/src/lib.rs index f1c70e07..21f5f0fb 100644 --- a/packages/tg4/src/lib.rs +++ b/packages/tg4/src/lib.rs @@ -7,7 +7,7 @@ pub use crate::helpers::Tg4Contract; pub use crate::hook::{MemberChangedHookMsg, MemberDiff}; pub use crate::msg::Tg4ExecuteMsg; pub use crate::query::{ - member_key, AdminResponse, HooksResponse, Member, MemberListResponse, MemberResponse, - Tg4QueryMsg, TotalPointsResponse, MEMBERS_CHANGELOG, MEMBERS_CHECKPOINTS, MEMBERS_KEY, - TOTAL_KEY, + member_key, AdminResponse, HooksResponse, Member, MemberInfo, MemberListResponse, + MemberResponse, Tg4QueryMsg, TotalPointsResponse, MEMBERS_CHANGELOG, MEMBERS_CHECKPOINTS, + MEMBERS_KEY, TOTAL_KEY, }; diff --git a/packages/tg4/src/query.rs b/packages/tg4/src/query.rs index 1de0c5ba..74e30d6d 100644 --- a/packages/tg4/src/query.rs +++ b/packages/tg4/src/query.rs @@ -33,6 +33,28 @@ pub struct AdminResponse { pub admin: Option, } +#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Debug)] +pub struct MemberInfo { + pub points: u64, + pub start_height: Option, +} + +impl MemberInfo { + pub fn new(points: u64) -> Self { + Self { + points, + start_height: None, + } + } + + pub fn new_with_height(points: u64, height: u64) -> Self { + Self { + points, + start_height: Some(height), + } + } +} + /// A group member has some points associated with them. /// This may all be equal, or may have meaning in the app that /// makes use of the group (eg. voting power) @@ -40,6 +62,7 @@ pub struct AdminResponse { pub struct Member { pub addr: String, pub points: u64, + pub start_height: Option, } #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] @@ -50,6 +73,22 @@ pub struct MemberListResponse { #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] pub struct MemberResponse { pub points: Option, + pub start_height: Option, +} + +impl From> for MemberResponse { + fn from(mi: Option) -> Self { + match mi { + None => Self { + points: None, + start_height: None, + }, + Some(mi) => Self { + points: Some(mi.points), + start_height: mi.start_height, + }, + } + } } #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] diff --git a/packages/utils/src/member_indexes.rs b/packages/utils/src/member_indexes.rs index d8d64eff..83ce2ccb 100644 --- a/packages/utils/src/member_indexes.rs +++ b/packages/utils/src/member_indexes.rs @@ -1,8 +1,11 @@ -use crate::{Hooks, Preauth, Slashers}; use cosmwasm_std::Addr; + use cw_controllers::Admin; use cw_storage_plus::{Index, IndexList, IndexedSnapshotMap, Item, MultiIndex, Strategy}; -use tg4::TOTAL_KEY; + +use tg4::{MemberInfo, TOTAL_KEY}; + +use crate::{Hooks, Preauth, Slashers}; pub const ADMIN: Admin = Admin::new("admin"); pub const HOOKS: Hooks = Hooks::new("tg4-hooks"); @@ -13,12 +16,12 @@ pub const TOTAL: Item = Item::new(TOTAL_KEY); pub struct MemberIndexes<'a> { // Points (multi-)index (deserializing the (hidden) pk to Addr) - pub points: MultiIndex<'a, u64, u64, Addr>, + pub points: MultiIndex<'a, u64, MemberInfo, Addr>, } -impl<'a> IndexList for MemberIndexes<'a> { - fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![&self.points]; +impl<'a> IndexList for MemberIndexes<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.points]; Box::new(v.into_iter()) } } @@ -27,9 +30,9 @@ impl<'a> IndexList for MemberIndexes<'a> { /// This allows to query the map members, sorted by points. /// The points index is a `MultiIndex`, as there can be multiple members with the same points. /// The points index is not snapshotted; only the current points are indexed at any given time. -pub fn members<'a>() -> IndexedSnapshotMap<'a, &'a Addr, u64, MemberIndexes<'a>> { +pub fn members<'a>() -> IndexedSnapshotMap<'a, &'a Addr, MemberInfo, MemberIndexes<'a>> { let indexes = MemberIndexes { - points: MultiIndex::new(|&w| w, tg4::MEMBERS_KEY, "members__points"), + points: MultiIndex::new(|mi| mi.points, tg4::MEMBERS_KEY, "members__points"), }; IndexedSnapshotMap::new( tg4::MEMBERS_KEY, diff --git a/packages/voting-contract/src/lib.rs b/packages/voting-contract/src/lib.rs index 76370ef7..29cb211d 100644 --- a/packages/voting-contract/src/lib.rs +++ b/packages/voting-contract/src/lib.rs @@ -419,7 +419,7 @@ pub fn list_voters( .group_contract .list_members(&deps.querier, start_after, limit)? .into_iter() - .map(|Member { addr, points }| VoterDetail { addr, points }) + .map(|Member { addr, points, .. }| VoterDetail { addr, points }) .collect(); Ok(VoterListResponse { voters }) } diff --git a/packages/voting-contract/src/multitest/suite.rs b/packages/voting-contract/src/multitest/suite.rs index a849a68a..ded8b104 100644 --- a/packages/voting-contract/src/multitest/suite.rs +++ b/packages/voting-contract/src/multitest/suite.rs @@ -43,6 +43,7 @@ impl SuiteBuilder { self.members.push(Member { addr: addr.to_owned(), points, + start_height: None, }); self } @@ -127,6 +128,7 @@ impl Suite { .map(|(addr, points)| Member { addr: (*addr).to_owned(), points: *points, + start_height: None, }) .collect();