From 99f1fe487c004393d8bb1da80f284814f04ecbeb Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 23 Feb 2022 23:21:33 +0100 Subject: [PATCH 01/26] Add `MemberInfo` to tg4 --- packages/tg4/src/lib.rs | 6 +++--- packages/tg4/src/query.rs | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) 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..e4c8d3a5 100644 --- a/packages/tg4/src/query.rs +++ b/packages/tg4/src/query.rs @@ -33,6 +33,21 @@ 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, + } + } +} + /// 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) From f4e6ead4ed6c40acec55e4df8218ac4348257871 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 23 Feb 2022 23:22:52 +0100 Subject: [PATCH 02/26] Store members points / start heights using `MemberInfo` --- packages/utils/src/member_indexes.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) 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, From 4e7873002e661f2f3ace14b0836e3a8b4de3c661 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 23 Feb 2022 23:23:22 +0100 Subject: [PATCH 03/26] Adapt raw tg4 member query to new format --- packages/tg4/src/helpers.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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()) } } } From 96d0d437d615362e0ed5db6cd1c4a61c9d2f0b28 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 23 Feb 2022 23:23:55 +0100 Subject: [PATCH 04/26] tg4-engagement: Adapt to new members value format --- contracts/tg4-engagement/src/contract.rs | 47 +++++++++++++++--------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/contracts/tg4-engagement/src/contract.rs b/contracts/tg4-engagement/src/contract.rs index a00d3005..c11ce843 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(), @@ -466,7 +471,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 +480,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 +508,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 +558,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 +577,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,7 +655,7 @@ fn end_block(mut deps: DepsMut, env: Env) -> Result StdResult> { - let (addr, points) = item?; + let (addr, MemberInfo { points, .. }) = item?; if points <= 1 { return Ok(None); } @@ -665,8 +675,8 @@ fn end_block(mut deps: DepsMut, env: Env) -> Result( let points = match height { Some(h) => members().may_load_at_height(deps.storage, &addr, h), None => members().may_load(deps.storage, &addr), - }?; + }? + .map(|mi| mi.points); Ok(MemberResponse { points }) } @@ -844,7 +855,7 @@ fn list_members( .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|item| { - let (addr, points) = item?; + let (addr, MemberInfo { points, .. }) = item?; Ok(Member { addr: addr.into(), points, @@ -874,7 +885,7 @@ fn list_members_by_points( .range(deps.storage, None, start, Order::Descending) .take(limit) .map(|item| { - let (addr, points) = item?; + let (addr, MemberInfo { points, .. }) = item?; Ok(Member { addr: addr.into(), points, @@ -1537,8 +1548,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)); From 22854c365c405c4f085daaf67a4f535b04d3eb2d Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Thu, 24 Feb 2022 08:42:07 +0100 Subject: [PATCH 05/26] tg4-stake: Adapt to new members value format --- contracts/tg4-stake/src/contract.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/contracts/tg4-stake/src/contract.rs b/contracts/tg4-stake/src/contract.rs index 3dbe9bf7..af06091c 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), }?; @@ -620,7 +621,8 @@ fn query_member( let points = match height { Some(h) => members().may_load_at_height(deps.storage, &addr, h), None => members().may_load(deps.storage, &addr), - }?; + }? + .map(|mi| mi.points); Ok(MemberResponse { points }) } @@ -641,7 +643,7 @@ fn list_members( .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|item| { - let (addr, points) = item?; + let (addr, MemberInfo { points, .. }) = item?; Ok(Member { addr: addr.into(), points, @@ -672,7 +674,7 @@ fn list_members_by_points( .range(deps.storage, None, start, Order::Descending) .take(limit) .map(|item| { - let (addr, points) = item?; + let (addr, MemberInfo { points, .. }) = item?; Ok(Member { addr: addr.into(), points, @@ -1222,8 +1224,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)); From cca7d41755aa564817cac191c6645728662d96a4 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Thu, 24 Feb 2022 11:19:22 +0100 Subject: [PATCH 06/26] Redefine members() IndexedMap Add extra points index for tie breaking by start height --- contracts/tg4-mixer/src/lib.rs | 1 + contracts/tg4-mixer/src/member_indexes.rs | 55 +++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 contracts/tg4-mixer/src/member_indexes.rs 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..f1a76bb4 --- /dev/null +++ b/contracts/tg4-mixer/src/member_indexes.rs @@ -0,0 +1,55 @@ +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: MultiIndex<'a, u64, MemberInfo, 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, &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: MultiIndex::new(|mi| mi.points, tg4::MEMBERS_KEY, "members__points"), + points_tie_break: MultiIndex::new( + |mi| { + ( + mi.points, + mi.start_height + .map_or(i64::MIN, |h| (h as i64).wrapping_neg()), + ) + }, // 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, + ) +} From 5f61f0f7ce2b17b7110841bbef6b51b3f7cfdacf Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Thu, 24 Feb 2022 22:00:07 +0100 Subject: [PATCH 07/26] Add MemberInfo::new_with_points constructor --- packages/tg4/src/query.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/tg4/src/query.rs b/packages/tg4/src/query.rs index e4c8d3a5..1bef646c 100644 --- a/packages/tg4/src/query.rs +++ b/packages/tg4/src/query.rs @@ -46,6 +46,13 @@ impl MemberInfo { 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. From 78376a9ca28cf488c2aa10d9b3b806d3777ebfe1 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Thu, 24 Feb 2022 22:24:22 +0100 Subject: [PATCH 08/26] tg4-mixer: Adapt to custom members() Add start_height info to `MemberInfo` --- contracts/tg4-mixer/src/contract.rs | 41 +++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/contracts/tg4-mixer/src/contract.rs b/contracts/tg4-mixer/src/contract.rs index fc9a1be2..1b26e153 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 points = prev_points.clone().unwrap_or_default(); + total -= points.points; total += new_points.unwrap_or_default(); + let prev_height = points.start_height.unwrap_or(i64::MAX as u64 + 1); // Default shouldn't be needed // 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)?; @@ -407,7 +425,8 @@ fn query_member( let points = match height { Some(h) => members().may_load_at_height(deps.storage, &addr, h), None => members().may_load(deps.storage, &addr), - }?; + }? + .map(|mi| mi.points); Ok(MemberResponse { points }) } @@ -428,7 +447,7 @@ fn list_members( .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|item| { - let (addr, points) = item?; + let (addr, MemberInfo { points, .. }) = item?; Ok(Member { addr: addr.into(), points, @@ -458,7 +477,7 @@ fn list_members_by_points( .range(deps.storage, None, start, Order::Descending) .take(limit) .map(|item| { - let (addr, points) = item?; + let (addr, MemberInfo { points, .. }) = item?; Ok(Member { addr: addr.into(), points, From 608b829ef5beffd7f86526d1ca0936fba76f1ec5 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 25 Feb 2022 05:43:38 +0100 Subject: [PATCH 09/26] Add optional start_height to Member and MemberResponse --- packages/tg4/src/query.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/tg4/src/query.rs b/packages/tg4/src/query.rs index 1bef646c..bd5e11b8 100644 --- a/packages/tg4/src/query.rs +++ b/packages/tg4/src/query.rs @@ -62,6 +62,7 @@ impl MemberInfo { pub struct Member { pub addr: String, pub points: u64, + pub start_height: Option, } #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] @@ -72,6 +73,7 @@ pub struct MemberListResponse { #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] pub struct MemberResponse { pub points: Option, + pub start_height: Option, } #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] From 2bb03dcedfc8520e683e7080f1ac9446265238f7 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 25 Feb 2022 05:46:17 +0100 Subject: [PATCH 10/26] tg4-engagement: Adapt to new `Member` / `MemberResponse` value format --- contracts/tg4-engagement/src/contract.rs | 39 +++++++++++++++++++----- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/contracts/tg4-engagement/src/contract.rs b/contracts/tg4-engagement/src/contract.rs index c11ce843..77648032 100644 --- a/contracts/tg4-engagement/src/contract.rs +++ b/contracts/tg4-engagement/src/contract.rs @@ -168,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( @@ -178,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![], )?; @@ -655,13 +654,20 @@ fn end_block(mut deps: DepsMut, env: Env) -> Result StdResult> { - let (addr, MemberInfo { 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() @@ -758,7 +764,10 @@ fn query_member( None => members().may_load(deps.storage, &addr), }? .map(|mi| mi.points); - Ok(MemberResponse { points }) + Ok(MemberResponse { + points, + start_height: None, + }) } pub fn query_withdrawable_rewards( @@ -855,10 +864,17 @@ fn list_members( .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|item| { - let (addr, MemberInfo { points, .. }) = item?; + let ( + addr, + MemberInfo { + points, + start_height, + }, + ) = item?; Ok(Member { addr: addr.into(), points, + start_height, }) }) .collect(); @@ -885,10 +901,17 @@ fn list_members_by_points( .range(deps.storage, None, start, Order::Descending) .take(limit) .map(|item| { - let (addr, MemberInfo { points, .. }) = item?; + let ( + addr, + MemberInfo { + points, + start_height, + }, + ) = item?; Ok(Member { addr: addr.into(), points, + start_height, }) }) .collect(); From 313f0eeb6c9442ee97ea3106f23c9e5ff4de9fe6 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 25 Feb 2022 05:47:38 +0100 Subject: [PATCH 11/26] voting-contract: Adapt to new `Member` value format --- packages/voting-contract/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 }) } From 3bbcb5b09211f91ec28113a35e91b9e5987119b9 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 25 Feb 2022 05:50:27 +0100 Subject: [PATCH 12/26] tg4-group: Adapt to new `Member` value format --- contracts/tg4-group/src/contract.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) 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()]; From 3c3c9f8eb287d2b80436c69ed14db76f51f99a36 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 25 Feb 2022 06:22:48 +0100 Subject: [PATCH 13/26] tg4-stake: Adapt to new `Member` value format --- contracts/tg4-stake/src/contract.rs | 53 ++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/contracts/tg4-stake/src/contract.rs b/contracts/tg4-stake/src/contract.rs index af06091c..d1666a1c 100644 --- a/contracts/tg4-stake/src/contract.rs +++ b/contracts/tg4-stake/src/contract.rs @@ -623,7 +623,10 @@ fn query_member( None => members().may_load(deps.storage, &addr), }? .map(|mi| mi.points); - Ok(MemberResponse { points }) + Ok(MemberResponse { + points, + start_height: None, + }) } // settings for pagination @@ -643,10 +646,17 @@ fn list_members( .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|item| { - let (addr, MemberInfo { points, .. }) = item?; + let ( + addr, + MemberInfo { + points, + start_height, + }, + ) = item?; Ok(Member { addr: addr.into(), points, + start_height, }) }) .collect(); @@ -674,10 +684,17 @@ fn list_members_by_points( .range(deps.storage, None, start, Order::Descending) .take(limit) .map(|item| { - let (addr, MemberInfo { points, .. }) = item?; + let ( + addr, + MemberInfo { + points, + start_height, + }, + ) = item?; Ok(Member { addr: addr.into(), points, + start_height, }) }) .collect(); @@ -1041,11 +1058,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 }, ] ); @@ -1058,7 +1077,8 @@ mod tests { members, vec![Member { addr: USER1.into(), - points: 12 + points: 12, + start_height: None },] ); @@ -1073,7 +1093,8 @@ mod tests { members, vec![Member { addr: USER2.into(), - points: 7 + points: 7, + start_height: None },] ); @@ -1102,15 +1123,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 } ] ); @@ -1125,7 +1149,8 @@ mod tests { members, vec![Member { addr: USER1.into(), - points: 11 + points: 11, + start_height: None },] ); @@ -1142,11 +1167,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 } ] ); From d7564571a2922d65d85fea953bdc2bb6d92870c3 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 25 Feb 2022 07:13:23 +0100 Subject: [PATCH 14/26] tg4-mixer: Use tie-breaking index --- contracts/tg4-mixer/src/contract.rs | 49 +++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/contracts/tg4-mixer/src/contract.rs b/contracts/tg4-mixer/src/contract.rs index 1b26e153..66e5c8d6 100644 --- a/contracts/tg4-mixer/src/contract.rs +++ b/contracts/tg4-mixer/src/contract.rs @@ -422,12 +422,16 @@ 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), - }? - .map(|mi| mi.points); - Ok(MemberResponse { points }) + }?; + let points = mi.as_ref().map(|mi| mi.points); + let start_height = mi.map(|mi| mi.start_height).unwrap_or(None); + Ok(MemberResponse { + points, + start_height, + }) } // settings for pagination @@ -447,10 +451,17 @@ fn list_members( .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|item| { - let (addr, MemberInfo { points, .. }) = item?; + let ( + addr, + MemberInfo { + points, + start_height, + }, + ) = item?; Ok(Member { addr: addr.into(), points, + start_height, }) }) .collect(); @@ -466,21 +477,34 @@ fn list_members_by_points( let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; let start = start_after .map(|m| { - deps.api - .addr_validate(&m.addr) - .map(|addr| Bound::exclusive((m.points, addr))) + deps.api.addr_validate(&m.addr).map(|addr| { + Bound::exclusive(( + ( + m.points, + -(m.start_height.unwrap_or(i64::MAX as u64 + 1) 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, MemberInfo { points, .. }) = item?; + let ( + addr, + MemberInfo { + points, + start_height, + }, + ) = item?; Ok(Member { addr: addr.into(), points, + start_height, }) }) .collect(); @@ -535,6 +559,7 @@ mod tests { Member { addr: addr.into(), points, + start_height: None, } } @@ -822,10 +847,12 @@ mod tests { Member { addr: VOTER2.into(), points: 300, + start_height: None, }, Member { addr: VOTER3.into(), points: 1200, + start_height: None, }, ], }; @@ -897,10 +924,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()], From 26cab6bca8b0264f189f336066591768b48d6f42 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 25 Feb 2022 07:14:46 +0100 Subject: [PATCH 15/26] tg4-mixer: Remove unused points index --- contracts/tg4-mixer/src/member_indexes.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/tg4-mixer/src/member_indexes.rs b/contracts/tg4-mixer/src/member_indexes.rs index f1a76bb4..cf31a014 100644 --- a/contracts/tg4-mixer/src/member_indexes.rs +++ b/contracts/tg4-mixer/src/member_indexes.rs @@ -6,13 +6,12 @@ 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: MultiIndex<'a, u64, MemberInfo, 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, &self.points_tie_break]; + let v: Vec<&dyn Index> = vec![&self.points_tie_break]; Box::new(v.into_iter()) } } @@ -32,7 +31,6 @@ impl<'a> IndexList for MemberIndexes<'a> { /// 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: MultiIndex::new(|mi| mi.points, tg4::MEMBERS_KEY, "members__points"), points_tie_break: MultiIndex::new( |mi| { ( From 63e867258f9ca8247658baf9ce52ff03e08dd24b Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 25 Feb 2022 08:27:15 +0100 Subject: [PATCH 16/26] tg4-valset: Adapt to new `Member` format --- contracts/tgrade-valset/src/contract.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/tgrade-valset/src/contract.rs b/contracts/tgrade-valset/src/contract.rs index 8afacb01..db6b9c5e 100644 --- a/contracts/tgrade-valset/src/contract.rs +++ b/contracts/tgrade-valset/src/contract.rs @@ -841,6 +841,7 @@ fn calculate_diff( let member = Member { addr: vi.operator.to_string(), points: vi.power, + start_height: None, }; (update, member) @@ -1069,6 +1070,7 @@ mod test { .map(|(addr, points)| Member { addr: addr.to_owned(), points, + start_height: None, }) .collect() } From 5bd7f48db4995f76b6c0bb216987af904526bee5 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 25 Feb 2022 08:34:22 +0100 Subject: [PATCH 17/26] Adapt voting-contract tests to new `Member` format --- packages/voting-contract/src/multitest/suite.rs | 2 ++ 1 file changed, 2 insertions(+) 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(); From 229f742d9b6db371780e4c06d5234d09cae8a462 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 25 Feb 2022 08:34:43 +0100 Subject: [PATCH 18/26] Adapt contracts tests to new `Member` format --- contracts/tg4-engagement/src/contract.rs | 36 ++++++++++++++----- contracts/tg4-engagement/src/msg.rs | 3 +- contracts/tg4-engagement/src/multitest.rs | 1 + .../tg4-engagement/src/multitest/suite.rs | 3 ++ .../src/multitest/suite.rs | 2 ++ .../src/multitest/suite.rs | 1 + .../tgrade-valset/src/multitest/suite.rs | 7 +++- 7 files changed, 43 insertions(+), 10 deletions(-) diff --git a/contracts/tg4-engagement/src/contract.rs b/contracts/tg4-engagement/src/contract.rs index 77648032..0c698865 100644 --- a/contracts/tg4-engagement/src/contract.rs +++ b/contracts/tg4-engagement/src/contract.rs @@ -960,10 +960,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, @@ -1047,11 +1049,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 }, ] ); @@ -1064,7 +1068,8 @@ mod tests { members, vec![Member { addr: USER1.into(), - points: 11 + points: 11, + start_height: None },] ); @@ -1079,7 +1084,8 @@ mod tests { members, vec![Member { addr: USER2.into(), - points: 6 + points: 6, + start_height: None },] ); @@ -1106,11 +1112,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 } ] ); @@ -1125,7 +1133,8 @@ mod tests { members, vec![Member { addr: USER1.into(), - points: 11 + points: 11, + start_height: None },] ); @@ -1140,7 +1149,8 @@ mod tests { members, vec![Member { addr: USER2.into(), - points: 6 + points: 6, + start_height: None },] ); @@ -1187,10 +1197,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, @@ -1267,6 +1279,7 @@ mod tests { let add = vec![Member { addr: USER3.into(), points: 15, + start_height: None, }]; let remove = vec![USER1.into()]; @@ -1308,6 +1321,7 @@ mod tests { let add = vec![Member { addr: USER1.into(), points: 4, + start_height: None, }]; let remove = vec![USER3.into()]; @@ -1330,10 +1344,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()]; @@ -1355,6 +1371,7 @@ mod tests { let add = Member { addr: USER3.into(), points: 15, + start_height: None, }; let env = mock_env(); @@ -1390,6 +1407,7 @@ mod tests { let add = Member { addr: USER2.into(), points: 1, + start_height: None, }; let env = mock_env(); @@ -1523,10 +1541,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-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/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/multitest/suite.rs b/contracts/tgrade-valset/src/multitest/suite.rs index b6190f0c..b149f0c2 100644 --- a/contracts/tgrade-valset/src/multitest/suite.rs +++ b/contracts/tgrade-valset/src/multitest/suite.rs @@ -181,6 +181,7 @@ impl SuiteBuilder { .map(|(addr, points)| Member { addr: (*addr).to_owned(), points: *points, + start_height: None, }) .collect(), halflife: halflife.into(), @@ -227,7 +228,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( From f3f8acbda399a30ba7d9b701f25b82c72e7bd669 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 25 Feb 2022 09:42:02 +0100 Subject: [PATCH 19/26] Add members by points tie-breaking tests --- contracts/tg4-mixer/src/contract.rs | 133 ++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/contracts/tg4-mixer/src/contract.rs b/contracts/tg4-mixer/src/contract.rs index 66e5c8d6..cc150e56 100644 --- a/contracts/tg4-mixer/src/contract.rs +++ b/contracts/tg4-mixer/src/contract.rs @@ -733,6 +733,20 @@ mod tests { assert_eq!(points(VOTER5), voter5); } + fn list_members_by_points( + app: &BasicApp, + mixer_addr: &Addr, + start_after: Option, + limit: Option, + ) -> MemberListResponse { + app.wrap() + .query_wasm_smart( + mixer_addr, + &QueryMsg::ListMembersByPoints { start_after, limit }, + ) + .unwrap() + } + #[test] fn basic_init() { let stakers = vec![ @@ -1026,5 +1040,124 @@ 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, + MemberListResponse { + 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 {}; + 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_eq!( + members, + MemberListResponse { + 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(i64::MAX as u64 + 1) + }, + ] + } + ); + } + // TODO: multi-test to init! } From b74060eaec66d5523fa2c7a8b2222f0435edf31e Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 25 Feb 2022 10:40:55 +0100 Subject: [PATCH 20/26] Fix: default start_height When adding member to tg4-mixer first time --- contracts/tg4-mixer/src/contract.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/tg4-mixer/src/contract.rs b/contracts/tg4-mixer/src/contract.rs index cc150e56..81def8a6 100644 --- a/contracts/tg4-mixer/src/contract.rs +++ b/contracts/tg4-mixer/src/contract.rs @@ -219,10 +219,10 @@ pub fn update_members( // 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)?; // convenience unwrap or default - let points = prev_points.clone().unwrap_or_default(); - total -= points.points; + let prev_points_unwrap = prev_points.clone().unwrap_or_default(); + total -= prev_points_unwrap.points; total += new_points.unwrap_or_default(); - let prev_height = points.start_height.unwrap_or(i64::MAX as u64 + 1); // Default shouldn't be needed + let prev_height = prev_points_unwrap.start_height.unwrap_or(height); // store the new value match new_points { @@ -1152,7 +1152,7 @@ mod tests { Member { addr: VOTER2.into(), points: 1000, - start_height: Some(i64::MAX as u64 + 1) + start_height: Some(12348) // VOTER2 should come first, lexicographically (descending order) }, ] } From 097a8e5c1a4312e020a261f72230fa51f4ee297c Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 25 Feb 2022 11:48:34 +0100 Subject: [PATCH 21/26] Add members by points tie breaking pagination tests --- contracts/tg4-mixer/src/contract.rs | 117 +++++++++++++++++++--------- 1 file changed, 81 insertions(+), 36 deletions(-) diff --git a/contracts/tg4-mixer/src/contract.rs b/contracts/tg4-mixer/src/contract.rs index 81def8a6..7e619d16 100644 --- a/contracts/tg4-mixer/src/contract.rs +++ b/contracts/tg4-mixer/src/contract.rs @@ -738,13 +738,15 @@ mod tests { mixer_addr: &Addr, start_after: Option, limit: Option, - ) -> MemberListResponse { - app.wrap() + ) -> Vec { + let res: MemberListResponse = app + .wrap() .query_wasm_smart( mixer_addr, &QueryMsg::ListMembersByPoints { start_after, limit }, ) - .unwrap() + .unwrap(); + res.members } #[test] @@ -1089,20 +1091,18 @@ mod tests { assert_eq!( members, - MemberListResponse { - members: vec![ - Member { - addr: VOTER3.into(), - points: 1500, - start_height: Some(12347) - }, - Member { - addr: VOTER1.into(), - points: 1000, - start_height: Some(12347) - }, - ] - } + 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 @@ -1135,28 +1135,73 @@ mod tests { // 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, - MemberListResponse { - 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) - }, - ] - } + 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! From ecb44ef6ef9a7edda1afdb64b838a2d243b8f148 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 25 Feb 2022 12:44:25 +0100 Subject: [PATCH 22/26] Error if no start_height for pagination --- contracts/tg4-mixer/src/contract.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/contracts/tg4-mixer/src/contract.rs b/contracts/tg4-mixer/src/contract.rs index 7e619d16..1c00aa76 100644 --- a/contracts/tg4-mixer/src/contract.rs +++ b/contracts/tg4-mixer/src/contract.rs @@ -476,16 +476,14 @@ 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.addr_validate(&m.addr).map(|addr| { - Bound::exclusive(( - ( - m.points, - -(m.start_height.unwrap_or(i64::MAX as u64 + 1) as i64), - ), - addr, - )) - }) + .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, -(start_height as i64)), addr))), }) .transpose()?; let members: StdResult> = members() From b225c36bd0201f6e094bcd25232f863d8eba63e4 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 25 Feb 2022 12:45:16 +0100 Subject: [PATCH 23/26] Replace unneeded wrapping_neg() by negation --- contracts/tg4-mixer/src/member_indexes.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/contracts/tg4-mixer/src/member_indexes.rs b/contracts/tg4-mixer/src/member_indexes.rs index cf31a014..e0ac8834 100644 --- a/contracts/tg4-mixer/src/member_indexes.rs +++ b/contracts/tg4-mixer/src/member_indexes.rs @@ -32,13 +32,7 @@ impl<'a> IndexList for MemberIndexes<'a> { 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).wrapping_neg()), - ) - }, // Works as long as `start_height <= i64::MAX + 1` + |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", ), From 906c58034e021fb9d7fc28d3182cbcdbde6462d6 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 25 Feb 2022 14:14:24 +0100 Subject: [PATCH 24/26] Add From helper for MemberResponse --- packages/tg4/src/query.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/tg4/src/query.rs b/packages/tg4/src/query.rs index bd5e11b8..74e30d6d 100644 --- a/packages/tg4/src/query.rs +++ b/packages/tg4/src/query.rs @@ -76,6 +76,21 @@ pub struct MemberResponse { 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)] pub struct TotalPointsResponse { pub points: u64, From 3648c368c474cff7f3a9a1b132cdf91721b51e56 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Fri, 25 Feb 2022 14:17:34 +0100 Subject: [PATCH 25/26] Fix MemberResponse / use From MemberInfo helper --- contracts/tg4-engagement/src/contract.rs | 10 +++------- contracts/tg4-mixer/src/contract.rs | 7 +------ contracts/tg4-stake/src/contract.rs | 10 +++------- 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/contracts/tg4-engagement/src/contract.rs b/contracts/tg4-engagement/src/contract.rs index 0c698865..80598689 100644 --- a/contracts/tg4-engagement/src/contract.rs +++ b/contracts/tg4-engagement/src/contract.rs @@ -759,15 +759,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), - }? - .map(|mi| mi.points); - Ok(MemberResponse { - points, - start_height: None, - }) + }?; + Ok(mi.into()) } pub fn query_withdrawable_rewards( diff --git a/contracts/tg4-mixer/src/contract.rs b/contracts/tg4-mixer/src/contract.rs index 1c00aa76..1f04dc69 100644 --- a/contracts/tg4-mixer/src/contract.rs +++ b/contracts/tg4-mixer/src/contract.rs @@ -426,12 +426,7 @@ fn query_member( Some(h) => members().may_load_at_height(deps.storage, &addr, h), None => members().may_load(deps.storage, &addr), }?; - let points = mi.as_ref().map(|mi| mi.points); - let start_height = mi.map(|mi| mi.start_height).unwrap_or(None); - Ok(MemberResponse { - points, - start_height, - }) + Ok(mi.into()) } // settings for pagination diff --git a/contracts/tg4-stake/src/contract.rs b/contracts/tg4-stake/src/contract.rs index d1666a1c..be18d3ef 100644 --- a/contracts/tg4-stake/src/contract.rs +++ b/contracts/tg4-stake/src/contract.rs @@ -618,15 +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), - }? - .map(|mi| mi.points); - Ok(MemberResponse { - points, - start_height: None, - }) + }?; + Ok(mi.into()) } // settings for pagination From dc2a9e1c1a9201eaef42056c6aabe7f44185f421 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 30 Mar 2022 09:35:33 +0200 Subject: [PATCH 26/26] Adapt to new Bond message format --- contracts/tg4-mixer/src/contract.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/tg4-mixer/src/contract.rs b/contracts/tg4-mixer/src/contract.rs index 1f04dc69..85c84fab 100644 --- a/contracts/tg4-mixer/src/contract.rs +++ b/contracts/tg4-mixer/src/contract.rs @@ -1109,7 +1109,9 @@ mod tests { .into(), ) .unwrap(); - let msg = tg4_stake::msg::ExecuteMsg::Bond {}; + let msg = tg4_stake::msg::ExecuteMsg::Bond { + vesting_tokens: None, + }; app.execute_contract(Addr::unchecked(VOTER2), staker_addr, &msg, &balance) .unwrap();