From 4a58a3a04ac8d1ba71ed43665b2b6c6750e451c1 Mon Sep 17 00:00:00 2001 From: Gabriel Lopez Date: Fri, 16 Sep 2022 20:40:13 -0500 Subject: [PATCH 1/3] Make total a SnapshotItem --- contracts/cw4-group/src/contract.rs | 30 +++++++++++++++++------------ contracts/cw4-group/src/msg.rs | 2 +- contracts/cw4-group/src/state.rs | 20 +++++++++++++------ packages/cw4/src/lib.rs | 2 +- packages/cw4/src/query.rs | 4 +++- 5 files changed, 37 insertions(+), 21 deletions(-) diff --git a/contracts/cw4-group/src/contract.rs b/contracts/cw4-group/src/contract.rs index 913d427f6..63ec3c2e8 100644 --- a/contracts/cw4-group/src/contract.rs +++ b/contracts/cw4-group/src/contract.rs @@ -53,7 +53,7 @@ pub fn create( let member_addr = deps.api.addr_validate(&member.addr)?; MEMBERS.save(deps.storage, &member_addr, &member.weight, height)?; } - TOTAL.save(deps.storage, &total)?; + TOTAL.save(deps.storage, &total, height)?; Ok(()) } @@ -145,7 +145,7 @@ pub fn update_members( } } - TOTAL.save(deps.storage, &total)?; + TOTAL.save(deps.storage, &total, height)?; Ok(MemberChangedHookMsg { diffs }) } @@ -157,20 +157,26 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { at_height: height, } => to_binary(&query_member(deps, addr, height)?), QueryMsg::ListMembers { start_after, limit } => { - to_binary(&list_members(deps, start_after, limit)?) + to_binary(&query_list_members(deps, start_after, limit)?) + } + QueryMsg::TotalWeight { at_height: height } => { + to_binary(&query_total_weight(deps, height)?) } - QueryMsg::TotalWeight {} => to_binary(&query_total_weight(deps)?), QueryMsg::Admin {} => to_binary(&ADMIN.query_admin(deps)?), QueryMsg::Hooks {} => to_binary(&HOOKS.query_hooks(deps)?), } } -fn query_total_weight(deps: Deps) -> StdResult { - let weight = TOTAL.load(deps.storage)?; +pub fn query_total_weight(deps: Deps, height: Option) -> StdResult { + let weight = match height { + Some(h) => TOTAL.may_load_at_height(deps.storage, h), + None => TOTAL.may_load(deps.storage), + }? + .unwrap_or_default(); Ok(TotalWeightResponse { weight }) } -fn query_member(deps: Deps, addr: String, height: Option) -> StdResult { +pub 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), @@ -183,7 +189,7 @@ fn query_member(deps: Deps, addr: String, height: Option) -> StdResult, limit: Option, @@ -246,7 +252,7 @@ mod tests { let res = ADMIN.query_admin(deps.as_ref()).unwrap(); assert_eq!(Some(INIT_ADMIN.into()), res.admin); - let res = query_total_weight(deps.as_ref()).unwrap(); + let res = query_total_weight(deps.as_ref(), None).unwrap(); assert_eq!(17, res.weight); } @@ -264,7 +270,7 @@ mod tests { 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(); + let members = query_list_members(deps.as_ref(), None, None).unwrap(); assert_eq!(members.members.len(), 2); // TODO: assert the set is proper } @@ -293,10 +299,10 @@ mod tests { let count = weights.iter().filter(|x| x.is_some()).count(); // TODO: more detailed compare? - let members = list_members(deps.as_ref(), None, None).unwrap(); + let members = query_list_members(deps.as_ref(), None, None).unwrap(); assert_eq!(count, members.members.len()); - let total = query_total_weight(deps.as_ref()).unwrap(); + let total = query_total_weight(deps.as_ref(), None).unwrap(); assert_eq!(sum, total.weight); // 17 - 11 + 15 = 21 } } diff --git a/contracts/cw4-group/src/msg.rs b/contracts/cw4-group/src/msg.rs index 823359ed2..3f9522749 100644 --- a/contracts/cw4-group/src/msg.rs +++ b/contracts/cw4-group/src/msg.rs @@ -31,7 +31,7 @@ pub enum QueryMsg { #[returns(cw_controllers::AdminResponse)] Admin {}, #[returns(cw4::TotalWeightResponse)] - TotalWeight {}, + TotalWeight { at_height: Option }, #[returns(cw4::MemberListResponse)] ListMembers { start_after: Option, diff --git a/contracts/cw4-group/src/state.rs b/contracts/cw4-group/src/state.rs index 1b5003c98..10497fa99 100644 --- a/contracts/cw4-group/src/state.rs +++ b/contracts/cw4-group/src/state.rs @@ -1,16 +1,24 @@ use cosmwasm_std::Addr; -use cw4::TOTAL_KEY; +use cw4::{ + MEMBERS_CHANGELOG, MEMBERS_CHECKPOINTS, MEMBERS_KEY, TOTAL_KEY, TOTAL_KEY_CHANGELOG, + TOTAL_KEY_CHECKPOINTS, +}; use cw_controllers::{Admin, Hooks}; -use cw_storage_plus::{Item, SnapshotMap, Strategy}; +use cw_storage_plus::{SnapshotItem, SnapshotMap, Strategy}; pub const ADMIN: Admin = Admin::new("admin"); pub const HOOKS: Hooks = Hooks::new("cw4-hooks"); -pub const TOTAL: Item = Item::new(TOTAL_KEY); +pub const TOTAL: SnapshotItem = SnapshotItem::new( + TOTAL_KEY, + TOTAL_KEY_CHECKPOINTS, + TOTAL_KEY_CHANGELOG, + Strategy::EveryBlock, +); pub const MEMBERS: SnapshotMap<&Addr, u64> = SnapshotMap::new( - cw4::MEMBERS_KEY, - cw4::MEMBERS_CHECKPOINTS, - cw4::MEMBERS_CHANGELOG, + MEMBERS_KEY, + MEMBERS_CHECKPOINTS, + MEMBERS_CHANGELOG, Strategy::EveryBlock, ); diff --git a/packages/cw4/src/lib.rs b/packages/cw4/src/lib.rs index 9185b6962..f57fd27d8 100644 --- a/packages/cw4/src/lib.rs +++ b/packages/cw4/src/lib.rs @@ -9,5 +9,5 @@ pub use crate::msg::Cw4ExecuteMsg; pub use crate::query::{ member_key, AdminResponse, Cw4QueryMsg, HooksResponse, Member, MemberListResponse, MemberResponse, TotalWeightResponse, MEMBERS_CHANGELOG, MEMBERS_CHECKPOINTS, MEMBERS_KEY, - TOTAL_KEY, + TOTAL_KEY, TOTAL_KEY_CHANGELOG, TOTAL_KEY_CHECKPOINTS, }; diff --git a/packages/cw4/src/query.rs b/packages/cw4/src/query.rs index 6745d6af1..c23f1fabe 100644 --- a/packages/cw4/src/query.rs +++ b/packages/cw4/src/query.rs @@ -6,7 +6,7 @@ pub enum Cw4QueryMsg { /// Return AdminResponse Admin {}, /// Return TotalWeightResponse - TotalWeight {}, + TotalWeight { at_height: Option }, /// Returns MembersListResponse ListMembers { start_after: Option, @@ -57,6 +57,8 @@ pub struct HooksResponse { /// TOTAL_KEY is meant for raw queries pub const TOTAL_KEY: &str = "total"; +pub const TOTAL_KEY_CHECKPOINTS: &str = "total__checkpoints"; +pub const TOTAL_KEY_CHANGELOG: &str = "total__changelog"; pub const MEMBERS_KEY: &str = "members"; pub const MEMBERS_CHECKPOINTS: &str = "members__checkpoints"; pub const MEMBERS_CHANGELOG: &str = "members__changelog"; From f8a287b6fa308212e4ddf9ad5c4143f5c1a9668e Mon Sep 17 00:00:00 2001 From: Gabriel Lopez Date: Fri, 16 Sep 2022 20:56:07 -0500 Subject: [PATCH 2/3] Safe math --- contracts/cw4-group/src/contract.rs | 21 +++++++++++---------- contracts/cw4-group/src/error.rs | 5 ++++- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/contracts/cw4-group/src/contract.rs b/contracts/cw4-group/src/contract.rs index 63ec3c2e8..bde554ee7 100644 --- a/contracts/cw4-group/src/contract.rs +++ b/contracts/cw4-group/src/contract.rs @@ -2,7 +2,7 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ attr, to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, StdResult, - SubMsg, + SubMsg, Uint64, }; use cw2::set_contract_version; use cw4::{ @@ -47,13 +47,14 @@ pub fn create( .transpose()?; ADMIN.set(deps.branch(), admin_addr)?; - let mut total = 0u64; + let mut total = Uint64::zero(); for member in members.into_iter() { - total += member.weight; + let member_weight = Uint64::from(member.weight); + total = total.checked_add(member_weight)?; let member_addr = deps.api.addr_validate(&member.addr)?; - MEMBERS.save(deps.storage, &member_addr, &member.weight, height)?; + MEMBERS.save(deps.storage, &member_addr, &member_weight.u64(), height)?; } - TOTAL.save(deps.storage, &total, height)?; + TOTAL.save(deps.storage, &total.u64(), height)?; Ok(()) } @@ -120,15 +121,15 @@ pub fn update_members( ) -> Result { ADMIN.assert_admin(deps.as_ref(), &sender)?; - let mut total = TOTAL.load(deps.storage)?; + let mut total = Uint64::from(TOTAL.load(deps.storage)?); let mut diffs: Vec = vec![]; // add all new members and update total for add in to_add.into_iter() { let add_addr = deps.api.addr_validate(&add.addr)?; MEMBERS.update(deps.storage, &add_addr, height, |old| -> StdResult<_> { - total -= old.unwrap_or_default(); - total += add.weight; + total = total.checked_sub(Uint64::from(old.unwrap_or_default()))?; + total = total.checked_add(Uint64::from(add.weight))?; diffs.push(MemberDiff::new(add.addr, old, Some(add.weight))); Ok(add.weight) })?; @@ -140,12 +141,12 @@ pub fn update_members( // Only process this if they were actually in the list before if let Some(weight) = old { diffs.push(MemberDiff::new(remove, Some(weight), None)); - total -= weight; + total = total.checked_sub(Uint64::from(weight))?; MEMBERS.remove(deps.storage, &remove_addr, height)?; } } - TOTAL.save(deps.storage, &total, height)?; + TOTAL.save(deps.storage, &total.u64(), height)?; Ok(MemberChangedHookMsg { diffs }) } diff --git a/contracts/cw4-group/src/error.rs b/contracts/cw4-group/src/error.rs index 82a84fe83..a5d2f57cf 100644 --- a/contracts/cw4-group/src/error.rs +++ b/contracts/cw4-group/src/error.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::StdError; +use cosmwasm_std::{OverflowError, StdError}; use thiserror::Error; use cw_controllers::{AdminError, HookError}; @@ -14,6 +14,9 @@ pub enum ContractError { #[error("{0}")] Admin(#[from] AdminError), + #[error("{0}")] + Overflow(#[from] OverflowError), + #[error("Unauthorized")] Unauthorized {}, } From 95344ee856b97d24aec62d0c1202f33d147b767e Mon Sep 17 00:00:00 2001 From: Gabriel Lopez Date: Sun, 18 Sep 2022 10:16:43 -0500 Subject: [PATCH 3/3] Move tests to proper module and add total_at_height test --- contracts/cw4-group/src/contract.rs | 349 -------------------------- contracts/cw4-group/src/lib.rs | 3 + contracts/cw4-group/src/tests.rs | 368 ++++++++++++++++++++++++++++ 3 files changed, 371 insertions(+), 349 deletions(-) create mode 100644 contracts/cw4-group/src/tests.rs diff --git a/contracts/cw4-group/src/contract.rs b/contracts/cw4-group/src/contract.rs index bde554ee7..fe6270655 100644 --- a/contracts/cw4-group/src/contract.rs +++ b/contracts/cw4-group/src/contract.rs @@ -212,352 +212,3 @@ pub fn query_list_members( Ok(MemberListResponse { members }) } - -#[cfg(test)] -mod tests { - use super::*; - use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::{from_slice, Api, OwnedDeps, Querier, Storage}; - use cw4::{member_key, TOTAL_KEY}; - use cw_controllers::{AdminError, HookError}; - - const INIT_ADMIN: &str = "juan"; - const USER1: &str = "somebody"; - const USER2: &str = "else"; - const USER3: &str = "funny"; - - fn do_instantiate(deps: DepsMut) { - let msg = InstantiateMsg { - admin: Some(INIT_ADMIN.into()), - members: vec![ - Member { - addr: USER1.into(), - weight: 11, - }, - Member { - addr: USER2.into(), - weight: 6, - }, - ], - }; - let info = mock_info("creator", &[]); - instantiate(deps, mock_env(), info, msg).unwrap(); - } - - #[test] - fn proper_instantiation() { - let mut deps = mock_dependencies(); - do_instantiate(deps.as_mut()); - - // it worked, let's query the state - let res = ADMIN.query_admin(deps.as_ref()).unwrap(); - assert_eq!(Some(INIT_ADMIN.into()), res.admin); - - let res = query_total_weight(deps.as_ref(), None).unwrap(); - assert_eq!(17, res.weight); - } - - #[test] - fn try_member_queries() { - let mut deps = mock_dependencies(); - do_instantiate(deps.as_mut()); - - let member1 = query_member(deps.as_ref(), USER1.into(), None).unwrap(); - assert_eq!(member1.weight, Some(11)); - - let member2 = query_member(deps.as_ref(), USER2.into(), None).unwrap(); - assert_eq!(member2.weight, Some(6)); - - let member3 = query_member(deps.as_ref(), USER3.into(), None).unwrap(); - assert_eq!(member3.weight, None); - - let members = query_list_members(deps.as_ref(), None, None).unwrap(); - assert_eq!(members.members.len(), 2); - // TODO: assert the set is proper - } - - fn assert_users( - deps: &OwnedDeps, - user1_weight: Option, - user2_weight: Option, - user3_weight: Option, - height: Option, - ) { - let member1 = query_member(deps.as_ref(), USER1.into(), height).unwrap(); - assert_eq!(member1.weight, user1_weight); - - let member2 = query_member(deps.as_ref(), USER2.into(), height).unwrap(); - assert_eq!(member2.weight, user2_weight); - - let member3 = query_member(deps.as_ref(), USER3.into(), height).unwrap(); - assert_eq!(member3.weight, user3_weight); - - // this is only valid if we are not doing a historical query - if height.is_none() { - // compute expected metrics - let weights = vec![user1_weight, user2_weight, user3_weight]; - let sum: u64 = weights.iter().map(|x| x.unwrap_or_default()).sum(); - let count = weights.iter().filter(|x| x.is_some()).count(); - - // TODO: more detailed compare? - let members = query_list_members(deps.as_ref(), None, None).unwrap(); - assert_eq!(count, members.members.len()); - - let total = query_total_weight(deps.as_ref(), None).unwrap(); - assert_eq!(sum, total.weight); // 17 - 11 + 15 = 21 - } - } - - #[test] - fn add_new_remove_old_member() { - let mut deps = mock_dependencies(); - do_instantiate(deps.as_mut()); - - // add a new one and remove existing one - let add = vec![Member { - addr: USER3.into(), - weight: 15, - }]; - let remove = vec![USER1.into()]; - - // non-admin cannot update - let height = mock_env().block.height; - let err = update_members( - deps.as_mut(), - height + 5, - Addr::unchecked(USER1), - add.clone(), - remove.clone(), - ) - .unwrap_err(); - assert_eq!(err, AdminError::NotAdmin {}.into()); - - // Test the values from instantiate - assert_users(&deps, Some(11), Some(6), None, None); - // Note all values were set at height, the beginning of that block was all None - assert_users(&deps, None, None, None, Some(height)); - // This will get us the values at the start of the block after instantiate (expected initial values) - assert_users(&deps, Some(11), Some(6), None, Some(height + 1)); - - // admin updates properly - update_members( - deps.as_mut(), - height + 10, - Addr::unchecked(INIT_ADMIN), - add, - remove, - ) - .unwrap(); - - // updated properly - assert_users(&deps, None, Some(6), Some(15), None); - - // snapshot still shows old value - assert_users(&deps, Some(11), Some(6), None, Some(height + 1)); - } - - #[test] - fn add_old_remove_new_member() { - // add will over-write and remove have no effect - let mut deps = mock_dependencies(); - do_instantiate(deps.as_mut()); - - // add a new one and remove existing one - let add = vec![Member { - addr: USER1.into(), - weight: 4, - }]; - let remove = vec![USER3.into()]; - - // admin updates properly - let height = mock_env().block.height; - update_members( - deps.as_mut(), - height, - Addr::unchecked(INIT_ADMIN), - add, - remove, - ) - .unwrap(); - assert_users(&deps, Some(4), Some(6), None, None); - } - - #[test] - fn add_and_remove_same_member() { - // add will over-write and remove have no effect - let mut deps = mock_dependencies(); - do_instantiate(deps.as_mut()); - - // USER1 is updated and remove in the same call, we should remove this an add member3 - let add = vec![ - Member { - addr: USER1.into(), - weight: 20, - }, - Member { - addr: USER3.into(), - weight: 5, - }, - ]; - let remove = vec![USER1.into()]; - - // admin updates properly - let height = mock_env().block.height; - update_members( - deps.as_mut(), - height, - Addr::unchecked(INIT_ADMIN), - add, - remove, - ) - .unwrap(); - assert_users(&deps, None, Some(6), Some(5), None); - } - - #[test] - fn add_remove_hooks() { - // add will over-write and remove have no effect - let mut deps = mock_dependencies(); - do_instantiate(deps.as_mut()); - - let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); - assert!(hooks.hooks.is_empty()); - - let contract1 = String::from("hook1"); - let contract2 = String::from("hook2"); - - let add_msg = ExecuteMsg::AddHook { - addr: contract1.clone(), - }; - - // non-admin cannot add hook - let user_info = mock_info(USER1, &[]); - let err = execute( - deps.as_mut(), - mock_env(), - user_info.clone(), - add_msg.clone(), - ) - .unwrap_err(); - assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into()); - - // admin can add it, and it appears in the query - let admin_info = mock_info(INIT_ADMIN, &[]); - let _ = execute( - deps.as_mut(), - mock_env(), - admin_info.clone(), - add_msg.clone(), - ) - .unwrap(); - let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); - assert_eq!(hooks.hooks, vec![contract1.clone()]); - - // cannot remove a non-registered contract - let remove_msg = ExecuteMsg::RemoveHook { - addr: contract2.clone(), - }; - let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), remove_msg).unwrap_err(); - assert_eq!(err, HookError::HookNotRegistered {}.into()); - - // add second contract - let add_msg2 = ExecuteMsg::AddHook { - addr: contract2.clone(), - }; - let _ = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg2).unwrap(); - let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); - assert_eq!(hooks.hooks, vec![contract1.clone(), contract2.clone()]); - - // cannot re-add an existing contract - let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg).unwrap_err(); - assert_eq!(err, HookError::HookAlreadyRegistered {}.into()); - - // non-admin cannot remove - let remove_msg = ExecuteMsg::RemoveHook { addr: contract1 }; - let err = execute(deps.as_mut(), mock_env(), user_info, remove_msg.clone()).unwrap_err(); - assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into()); - - // remove the original - let _ = execute(deps.as_mut(), mock_env(), admin_info, remove_msg).unwrap(); - let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); - assert_eq!(hooks.hooks, vec![contract2]); - } - - #[test] - fn hooks_fire() { - let mut deps = mock_dependencies(); - do_instantiate(deps.as_mut()); - - let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); - assert!(hooks.hooks.is_empty()); - - let contract1 = String::from("hook1"); - let contract2 = String::from("hook2"); - - // register 2 hooks - let admin_info = mock_info(INIT_ADMIN, &[]); - let add_msg = ExecuteMsg::AddHook { - addr: contract1.clone(), - }; - let add_msg2 = ExecuteMsg::AddHook { - addr: contract2.clone(), - }; - for msg in vec![add_msg, add_msg2] { - let _ = execute(deps.as_mut(), mock_env(), admin_info.clone(), msg).unwrap(); - } - - // make some changes - add 3, remove 2, and update 1 - // USER1 is updated and remove in the same call, we should remove this an add member3 - let add = vec![ - Member { - addr: USER1.into(), - weight: 20, - }, - Member { - addr: USER3.into(), - weight: 5, - }, - ]; - let remove = vec![USER2.into()]; - let msg = ExecuteMsg::UpdateMembers { remove, add }; - - // admin updates properly - assert_users(&deps, Some(11), Some(6), None, None); - let res = execute(deps.as_mut(), mock_env(), admin_info, msg).unwrap(); - assert_users(&deps, Some(20), None, Some(5), None); - - // ensure 2 messages for the 2 hooks - assert_eq!(res.messages.len(), 2); - // same order as in the message (adds first, then remove) - let diffs = vec![ - MemberDiff::new(USER1, Some(11), Some(20)), - MemberDiff::new(USER3, None, Some(5)), - MemberDiff::new(USER2, Some(6), None), - ]; - let hook_msg = MemberChangedHookMsg { diffs }; - let msg1 = SubMsg::new(hook_msg.clone().into_cosmos_msg(contract1).unwrap()); - let msg2 = SubMsg::new(hook_msg.into_cosmos_msg(contract2).unwrap()); - assert_eq!(res.messages, vec![msg1, msg2]); - } - - #[test] - fn raw_queries_work() { - // add will over-write and remove have no effect - let mut deps = mock_dependencies(); - do_instantiate(deps.as_mut()); - - // get total from raw key - let total_raw = deps.storage.get(TOTAL_KEY.as_bytes()).unwrap(); - let total: u64 = from_slice(&total_raw).unwrap(); - assert_eq!(17, total); - - // 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); - - // and execute misses - let member3_raw = deps.storage.get(&member_key(USER3)); - assert_eq!(None, member3_raw); - } -} diff --git a/contracts/cw4-group/src/lib.rs b/contracts/cw4-group/src/lib.rs index 98208b782..99fcca8b4 100644 --- a/contracts/cw4-group/src/lib.rs +++ b/contracts/cw4-group/src/lib.rs @@ -5,3 +5,6 @@ pub mod msg; pub mod state; pub use crate::error::ContractError; + +#[cfg(test)] +mod tests; diff --git a/contracts/cw4-group/src/tests.rs b/contracts/cw4-group/src/tests.rs new file mode 100644 index 000000000..ca542efa1 --- /dev/null +++ b/contracts/cw4-group/src/tests.rs @@ -0,0 +1,368 @@ +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::{from_slice, Addr, Api, DepsMut, OwnedDeps, Querier, Storage, SubMsg}; +use cw4::{member_key, Member, MemberChangedHookMsg, MemberDiff, TOTAL_KEY}; +use cw_controllers::{AdminError, HookError}; + +use crate::contract::{ + execute, instantiate, query_list_members, query_member, query_total_weight, update_members, +}; +use crate::msg::{ExecuteMsg, InstantiateMsg}; +use crate::state::{ADMIN, HOOKS}; + +const INIT_ADMIN: &str = "juan"; +const USER1: &str = "somebody"; +const USER2: &str = "else"; +const USER3: &str = "funny"; + +fn do_instantiate(deps: DepsMut) { + let msg = InstantiateMsg { + admin: Some(INIT_ADMIN.into()), + members: vec![ + Member { + addr: USER1.into(), + weight: 11, + }, + Member { + addr: USER2.into(), + weight: 6, + }, + ], + }; + let info = mock_info("creator", &[]); + instantiate(deps, mock_env(), info, msg).unwrap(); +} + +#[test] +fn proper_instantiation() { + let mut deps = mock_dependencies(); + do_instantiate(deps.as_mut()); + + // it worked, let's query the state + let res = ADMIN.query_admin(deps.as_ref()).unwrap(); + assert_eq!(Some(INIT_ADMIN.into()), res.admin); + + let res = query_total_weight(deps.as_ref(), None).unwrap(); + assert_eq!(17, res.weight); +} + +#[test] +fn try_member_queries() { + let mut deps = mock_dependencies(); + do_instantiate(deps.as_mut()); + + let member1 = query_member(deps.as_ref(), USER1.into(), None).unwrap(); + assert_eq!(member1.weight, Some(11)); + + let member2 = query_member(deps.as_ref(), USER2.into(), None).unwrap(); + assert_eq!(member2.weight, Some(6)); + + let member3 = query_member(deps.as_ref(), USER3.into(), None).unwrap(); + assert_eq!(member3.weight, None); + + let members = query_list_members(deps.as_ref(), None, None).unwrap(); + assert_eq!(members.members.len(), 2); + // TODO: assert the set is proper +} + +fn assert_users( + deps: &OwnedDeps, + user1_weight: Option, + user2_weight: Option, + user3_weight: Option, + height: Option, +) { + let member1 = query_member(deps.as_ref(), USER1.into(), height).unwrap(); + assert_eq!(member1.weight, user1_weight); + + let member2 = query_member(deps.as_ref(), USER2.into(), height).unwrap(); + assert_eq!(member2.weight, user2_weight); + + let member3 = query_member(deps.as_ref(), USER3.into(), height).unwrap(); + assert_eq!(member3.weight, user3_weight); + + // this is only valid if we are not doing a historical query + if height.is_none() { + // compute expected metrics + let weights = vec![user1_weight, user2_weight, user3_weight]; + let sum: u64 = weights.iter().map(|x| x.unwrap_or_default()).sum(); + let count = weights.iter().filter(|x| x.is_some()).count(); + + // TODO: more detailed compare? + let members = query_list_members(deps.as_ref(), None, None).unwrap(); + assert_eq!(count, members.members.len()); + + let total = query_total_weight(deps.as_ref(), None).unwrap(); + assert_eq!(sum, total.weight); // 17 - 11 + 15 = 21 + } +} + +#[test] +fn add_new_remove_old_member() { + let mut deps = mock_dependencies(); + do_instantiate(deps.as_mut()); + + // add a new one and remove existing one + let add = vec![Member { + addr: USER3.into(), + weight: 15, + }]; + let remove = vec![USER1.into()]; + + // non-admin cannot update + let height = mock_env().block.height; + let err = update_members( + deps.as_mut(), + height + 5, + Addr::unchecked(USER1), + add.clone(), + remove.clone(), + ) + .unwrap_err(); + assert_eq!(err, AdminError::NotAdmin {}.into()); + + // Test the values from instantiate + assert_users(&deps, Some(11), Some(6), None, None); + // Note all values were set at height, the beginning of that block was all None + assert_users(&deps, None, None, None, Some(height)); + // This will get us the values at the start of the block after instantiate (expected initial values) + assert_users(&deps, Some(11), Some(6), None, Some(height + 1)); + + // admin updates properly + update_members( + deps.as_mut(), + height + 10, + Addr::unchecked(INIT_ADMIN), + add, + remove, + ) + .unwrap(); + + // updated properly + assert_users(&deps, None, Some(6), Some(15), None); + + // snapshot still shows old value + assert_users(&deps, Some(11), Some(6), None, Some(height + 1)); +} + +#[test] +fn add_old_remove_new_member() { + // add will over-write and remove have no effect + let mut deps = mock_dependencies(); + do_instantiate(deps.as_mut()); + + // add a new one and remove existing one + let add = vec![Member { + addr: USER1.into(), + weight: 4, + }]; + let remove = vec![USER3.into()]; + + // admin updates properly + let height = mock_env().block.height; + update_members( + deps.as_mut(), + height, + Addr::unchecked(INIT_ADMIN), + add, + remove, + ) + .unwrap(); + assert_users(&deps, Some(4), Some(6), None, None); +} + +#[test] +fn add_and_remove_same_member() { + // add will over-write and remove have no effect + let mut deps = mock_dependencies(); + do_instantiate(deps.as_mut()); + + // USER1 is updated and remove in the same call, we should remove this an add member3 + let add = vec![ + Member { + addr: USER1.into(), + weight: 20, + }, + Member { + addr: USER3.into(), + weight: 5, + }, + ]; + let remove = vec![USER1.into()]; + + // admin updates properly + let height = mock_env().block.height; + update_members( + deps.as_mut(), + height, + Addr::unchecked(INIT_ADMIN), + add, + remove, + ) + .unwrap(); + assert_users(&deps, None, Some(6), Some(5), None); +} + +#[test] +fn add_remove_hooks() { + // add will over-write and remove have no effect + let mut deps = mock_dependencies(); + do_instantiate(deps.as_mut()); + + let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); + assert!(hooks.hooks.is_empty()); + + let contract1 = String::from("hook1"); + let contract2 = String::from("hook2"); + + let add_msg = ExecuteMsg::AddHook { + addr: contract1.clone(), + }; + + // non-admin cannot add hook + let user_info = mock_info(USER1, &[]); + let err = execute( + deps.as_mut(), + mock_env(), + user_info.clone(), + add_msg.clone(), + ) + .unwrap_err(); + assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into()); + + // admin can add it, and it appears in the query + let admin_info = mock_info(INIT_ADMIN, &[]); + let _ = execute( + deps.as_mut(), + mock_env(), + admin_info.clone(), + add_msg.clone(), + ) + .unwrap(); + let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); + assert_eq!(hooks.hooks, vec![contract1.clone()]); + + // cannot remove a non-registered contract + let remove_msg = ExecuteMsg::RemoveHook { + addr: contract2.clone(), + }; + let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), remove_msg).unwrap_err(); + assert_eq!(err, HookError::HookNotRegistered {}.into()); + + // add second contract + let add_msg2 = ExecuteMsg::AddHook { + addr: contract2.clone(), + }; + let _ = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg2).unwrap(); + let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); + assert_eq!(hooks.hooks, vec![contract1.clone(), contract2.clone()]); + + // cannot re-add an existing contract + let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg).unwrap_err(); + assert_eq!(err, HookError::HookAlreadyRegistered {}.into()); + + // non-admin cannot remove + let remove_msg = ExecuteMsg::RemoveHook { addr: contract1 }; + let err = execute(deps.as_mut(), mock_env(), user_info, remove_msg.clone()).unwrap_err(); + assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into()); + + // remove the original + let _ = execute(deps.as_mut(), mock_env(), admin_info, remove_msg).unwrap(); + let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); + assert_eq!(hooks.hooks, vec![contract2]); +} + +#[test] +fn hooks_fire() { + let mut deps = mock_dependencies(); + do_instantiate(deps.as_mut()); + + let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); + assert!(hooks.hooks.is_empty()); + + let contract1 = String::from("hook1"); + let contract2 = String::from("hook2"); + + // register 2 hooks + let admin_info = mock_info(INIT_ADMIN, &[]); + let add_msg = ExecuteMsg::AddHook { + addr: contract1.clone(), + }; + let add_msg2 = ExecuteMsg::AddHook { + addr: contract2.clone(), + }; + for msg in vec![add_msg, add_msg2] { + let _ = execute(deps.as_mut(), mock_env(), admin_info.clone(), msg).unwrap(); + } + + // make some changes - add 3, remove 2, and update 1 + // USER1 is updated and remove in the same call, we should remove this an add member3 + let add = vec![ + Member { + addr: USER1.into(), + weight: 20, + }, + Member { + addr: USER3.into(), + weight: 5, + }, + ]; + let remove = vec![USER2.into()]; + let msg = ExecuteMsg::UpdateMembers { remove, add }; + + // admin updates properly + assert_users(&deps, Some(11), Some(6), None, None); + let res = execute(deps.as_mut(), mock_env(), admin_info, msg).unwrap(); + assert_users(&deps, Some(20), None, Some(5), None); + + // ensure 2 messages for the 2 hooks + assert_eq!(res.messages.len(), 2); + // same order as in the message (adds first, then remove) + let diffs = vec![ + MemberDiff::new(USER1, Some(11), Some(20)), + MemberDiff::new(USER3, None, Some(5)), + MemberDiff::new(USER2, Some(6), None), + ]; + let hook_msg = MemberChangedHookMsg { diffs }; + let msg1 = SubMsg::new(hook_msg.clone().into_cosmos_msg(contract1).unwrap()); + let msg2 = SubMsg::new(hook_msg.into_cosmos_msg(contract2).unwrap()); + assert_eq!(res.messages, vec![msg1, msg2]); +} + +#[test] +fn raw_queries_work() { + // add will over-write and remove have no effect + let mut deps = mock_dependencies(); + do_instantiate(deps.as_mut()); + + // get total from raw key + let total_raw = deps.storage.get(TOTAL_KEY.as_bytes()).unwrap(); + let total: u64 = from_slice(&total_raw).unwrap(); + assert_eq!(17, total); + + // 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); + + // and execute misses + let member3_raw = deps.storage.get(&member_key(USER3)); + assert_eq!(None, member3_raw); +} + +#[test] +fn total_at_height() { + let mut deps = mock_dependencies(); + do_instantiate(deps.as_mut()); + + let height = mock_env().block.height; + + // Test the values from instantiate + let total = query_total_weight(deps.as_ref(), None).unwrap(); + assert_eq!(17, total.weight); + // Note all values were set at height, the beginning of that block was all None + let total = query_total_weight(deps.as_ref(), Some(height)).unwrap(); + assert_eq!(0, total.weight); + // This will get us the values at the start of the block after instantiate (expected initial values) + let total = query_total_weight(deps.as_ref(), Some(height + 1)).unwrap(); + assert_eq!(17, total.weight); +}