From 123ca94399c466c9fb59319a9cec4f4163257737 Mon Sep 17 00:00:00 2001 From: Gabriel Lopez Date: Wed, 6 Dec 2023 15:35:00 -0600 Subject: [PATCH] Improve logic for funded/fully funded and keep track of initial due in escrows --- contracts/arena-core/schema/arena-core.json | 2 +- .../arena-escrow/schema/arena-escrow.json | 277 ++++++++++++++---- contracts/arena-escrow/src/contract.rs | 11 +- contracts/arena-escrow/src/error.rs | 4 +- contracts/arena-escrow/src/execute.rs | 35 +-- contracts/arena-escrow/src/msg.rs | 9 +- contracts/arena-escrow/src/query.rs | 44 ++- contracts/arena-escrow/src/state.rs | 16 +- contracts/arena-escrow/src/tests.rs | 64 ++-- .../schema/arena-league-module.json | 2 +- .../schema/arena-wager-module.json | 2 +- packages/cw-balance/src/balance.rs | 107 ++++--- packages/cw-balance/src/lib.rs | 2 +- packages/cw-balance/src/tests/balance.rs | 57 +++- 14 files changed, 451 insertions(+), 181 deletions(-) diff --git a/contracts/arena-core/schema/arena-core.json b/contracts/arena-core/schema/arena-core.json index 354ccdf..2da9aae 100644 --- a/contracts/arena-core/schema/arena-core.json +++ b/contracts/arena-core/schema/arena-core.json @@ -1,6 +1,6 @@ { "contract_name": "arena-core", - "contract_version": "0.9.0", + "contract_version": "0.9.5", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/arena-escrow/schema/arena-escrow.json b/contracts/arena-escrow/schema/arena-escrow.json index 66de7cd..49e01bd 100644 --- a/contracts/arena-escrow/schema/arena-escrow.json +++ b/contracts/arena-escrow/schema/arena-escrow.json @@ -1,6 +1,6 @@ { "contract_name": "arena-escrow", - "contract_version": "0.9.0", + "contract_version": "0.9.5", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -569,6 +569,35 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "initial_dues" + ], + "properties": { + "initial_dues": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -710,39 +739,49 @@ "responses": { "balance": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "BalanceVerified", - "type": "object", - "required": [ - "cw20", - "cw721", - "native" - ], - "properties": { - "cw20": { - "type": "array", - "items": { - "$ref": "#/definitions/Cw20CoinVerified" - } - }, - "cw721": { - "type": "array", - "items": { - "$ref": "#/definitions/Cw721CollectionVerified" - } + "title": "Nullable_BalanceVerified", + "anyOf": [ + { + "$ref": "#/definitions/BalanceVerified" }, - "native": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } + { + "type": "null" } - }, - "additionalProperties": false, + ], "definitions": { "Addr": { "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", "type": "string" }, + "BalanceVerified": { + "type": "object", + "required": [ + "cw20", + "cw721", + "native" + ], + "properties": { + "cw20": { + "type": "array", + "items": { + "$ref": "#/definitions/Cw20CoinVerified" + } + }, + "cw721": { + "type": "array", + "items": { + "$ref": "#/definitions/Cw721CollectionVerified" + } + }, + "native": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + }, + "additionalProperties": false + }, "Coin": { "type": "object", "required": [ @@ -944,39 +983,49 @@ }, "due": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "BalanceVerified", - "type": "object", - "required": [ - "cw20", - "cw721", - "native" - ], - "properties": { - "cw20": { - "type": "array", - "items": { - "$ref": "#/definitions/Cw20CoinVerified" - } - }, - "cw721": { - "type": "array", - "items": { - "$ref": "#/definitions/Cw721CollectionVerified" - } + "title": "Nullable_BalanceVerified", + "anyOf": [ + { + "$ref": "#/definitions/BalanceVerified" }, - "native": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } + { + "type": "null" } - }, - "additionalProperties": false, + ], "definitions": { "Addr": { "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", "type": "string" }, + "BalanceVerified": { + "type": "object", + "required": [ + "cw20", + "cw721", + "native" + ], + "properties": { + "cw20": { + "type": "array", + "items": { + "$ref": "#/definitions/Cw20CoinVerified" + } + }, + "cw721": { + "type": "array", + "items": { + "$ref": "#/definitions/Cw721CollectionVerified" + } + }, + "native": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + }, + "additionalProperties": false + }, "Coin": { "type": "object", "required": [ @@ -1151,20 +1200,29 @@ "title": "DumpStateResponse", "type": "object", "required": [ - "balance", - "dues", "is_locked", "total_balance" ], "properties": { "balance": { - "$ref": "#/definitions/BalanceVerified" + "anyOf": [ + { + "$ref": "#/definitions/BalanceVerified" + }, + { + "type": "null" + } + ] }, - "dues": { - "type": "array", - "items": { - "$ref": "#/definitions/MemberBalanceVerified" - } + "due": { + "anyOf": [ + { + "$ref": "#/definitions/BalanceVerified" + }, + { + "type": "null" + } + ] }, "is_locked": { "type": "boolean" @@ -1174,6 +1232,103 @@ } }, "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "BalanceVerified": { + "type": "object", + "required": [ + "cw20", + "cw721", + "native" + ], + "properties": { + "cw20": { + "type": "array", + "items": { + "$ref": "#/definitions/Cw20CoinVerified" + } + }, + "cw721": { + "type": "array", + "items": { + "$ref": "#/definitions/Cw721CollectionVerified" + } + }, + "native": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + }, + "additionalProperties": false + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Cw20CoinVerified": { + "type": "object", + "required": [ + "address", + "amount" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + "Cw721CollectionVerified": { + "type": "object", + "required": [ + "address", + "token_ids" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "token_ids": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "initial_dues": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_MemberBalanceVerified", + "type": "array", + "items": { + "$ref": "#/definitions/MemberBalanceVerified" + }, "definitions": { "Addr": { "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", diff --git a/contracts/arena-escrow/src/contract.rs b/contracts/arena-escrow/src/contract.rs index 7c5ae14..34a1ac9 100644 --- a/contracts/arena-escrow/src/contract.rs +++ b/contracts/arena-escrow/src/contract.rs @@ -2,7 +2,7 @@ use crate::{ execute, msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, query, - state::{DUE, IS_FUNDED, IS_LOCKED, TOTAL_BALANCE}, + state::{self, DUE, INITIAL_DUE, IS_LOCKED, TOTAL_BALANCE}, ContractError, }; use cosmwasm_std::{ @@ -43,7 +43,7 @@ pub fn instantiate_contract( for member_balance in due { let member_balance = member_balance.to_verified(deps.as_ref())?; - if DUE.has(deps.storage, &member_balance.addr) { + if INITIAL_DUE.has(deps.storage, &member_balance.addr) { return Err(ContractError::StdError( cosmwasm_std::StdError::GenericErr { msg: "Cannot have duplicate addresses due".to_string(), @@ -51,8 +51,8 @@ pub fn instantiate_contract( )); } + INITIAL_DUE.save(deps.storage, &member_balance.addr, &member_balance.balance)?; DUE.save(deps.storage, &member_balance.addr, &member_balance.balance)?; - IS_FUNDED.save(deps.storage, &member_balance.addr, &false)?; } TOTAL_BALANCE.save(deps.storage, &BalanceVerified::new())?; @@ -104,13 +104,16 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::IsLocked {} => to_json_binary(&query::is_locked(deps)), QueryMsg::Distribution { addr } => to_json_binary(&query::distribution(deps, addr)?), QueryMsg::IsFunded { addr } => to_json_binary(&query::is_funded(deps, addr)?), - QueryMsg::IsFullyFunded {} => to_json_binary(&query::is_fully_funded(deps)?), + QueryMsg::IsFullyFunded {} => to_json_binary(&state::is_fully_funded(deps)), QueryMsg::Balances { start_after, limit } => { to_json_binary(&query::balances(deps, start_after, limit)?) } QueryMsg::Dues { start_after, limit } => { to_json_binary(&query::dues(deps, start_after, limit)?) } + QueryMsg::InitialDues { start_after, limit } => { + to_json_binary(&query::initial_dues(deps, start_after, limit)?) + } QueryMsg::Ownership {} => to_json_binary(&cw_ownable::get_ownership(deps.storage)?), QueryMsg::DumpState { addr } => to_json_binary(&query::dump_state(deps, addr)?), } diff --git a/contracts/arena-escrow/src/error.rs b/contracts/arena-escrow/src/error.rs index 846d1a5..1e53950 100644 --- a/contracts/arena-escrow/src/error.rs +++ b/contracts/arena-escrow/src/error.rs @@ -23,8 +23,8 @@ pub enum ContractError { #[error("Locked")] Locked {}, - #[error("NotFunded")] - NotFunded {}, + #[error("NotFullyFunded")] + NotFullyFunded {}, #[error("NoneDue")] NoneDue {}, diff --git a/contracts/arena-escrow/src/execute.rs b/contracts/arena-escrow/src/execute.rs index fff9a80..1d12419 100644 --- a/contracts/arena-escrow/src/execute.rs +++ b/contracts/arena-escrow/src/execute.rs @@ -4,13 +4,13 @@ use cosmwasm_std::{ }; use cw20::{Cw20CoinVerified, Cw20ReceiveMsg}; use cw721::Cw721ReceiveMsg; -use cw_balance::{BalanceError, BalanceVerified, Cw721CollectionVerified, MemberShare}; +use cw_balance::{BalanceVerified, Cw721CollectionVerified, MemberShare}; use cw_ownable::{assert_owner, get_ownership}; use crate::{ query::is_locked, state::{ - is_fully_funded, BALANCE, DUE, IS_FUNDED, IS_LOCKED, PRESET_DISTRIBUTION, TOTAL_BALANCE, + is_fully_funded, BALANCE, DUE, INITIAL_DUE, IS_LOCKED, PRESET_DISTRIBUTION, TOTAL_BALANCE, }, ContractError, }; @@ -57,15 +57,9 @@ fn inner_withdraw( BALANCE.remove(deps.storage, &addr); if !is_processing { total_balance = total_balance.checked_sub(&balance)?; - IS_FUNDED.save(deps.storage, &addr, &false)?; - DUE.update( - deps.storage, - &addr, - |existing_balance| -> Result<_, BalanceError> { - let current_balance = existing_balance.unwrap_or_default(); - Ok(current_balance.checked_add(&balance)?) - }, - )?; + + let initial_due = &INITIAL_DUE.load(deps.storage, &addr)?; + DUE.save(deps.storage, &addr, initial_due)?; } } } @@ -174,29 +168,26 @@ fn receive_balance( addr: Addr, balance: BalanceVerified, ) -> Result { - // Ensure the address has a due balance - if !DUE.has(deps.storage, &addr) { + if !INITIAL_DUE.has(deps.storage, &addr) { return Err(ContractError::NoneDue {}); } // Update the stored balance for the given address let updated_balance = BALANCE.update(deps.storage, &addr, |existing_balance| { - let current_balance = existing_balance.unwrap_or_default(); - balance.checked_add(¤t_balance) + existing_balance.unwrap_or_default().checked_add(&balance) })?; let due_balance = DUE.load(deps.storage, &addr)?; - let remaining_due = due_balance.checked_sub(&updated_balance)?; + let remaining_due = due_balance.difference(&updated_balance)?; let mut msgs: Vec = vec![]; // Handle the case where the due balance is fully paid if remaining_due.is_empty() { DUE.remove(deps.storage, &addr); - IS_FUNDED.save(deps.storage, &addr, &true)?; // Lock if fully funded and send activation message if needed - if is_fully_funded(deps.as_ref())? { + if is_fully_funded(deps.as_ref()) { IS_LOCKED.save(deps.storage, &true)?; if let Some(owner) = get_ownership(deps.storage)?.owner { @@ -232,8 +223,8 @@ pub fn distribute( assert_owner(deps.storage, &info.sender)?; // Ensure the contract is fully funded - if !is_fully_funded(deps.as_ref())? { - return Err(ContractError::NotFunded {}); + if !is_fully_funded(deps.as_ref()) { + return Err(ContractError::NotFullyFunded {}); } if !distribution.is_empty() { @@ -273,10 +264,10 @@ pub fn distribute( } } - // Reset the contract state IS_LOCKED.save(deps.storage, &false)?; + + // Clear the contract state DUE.clear(deps.storage); - IS_FUNDED.clear(deps.storage); PRESET_DISTRIBUTION.clear(deps.storage); // Construct the response and return diff --git a/contracts/arena-escrow/src/msg.rs b/contracts/arena-escrow/src/msg.rs index 59e6dff..9d71d9a 100644 --- a/contracts/arena-escrow/src/msg.rs +++ b/contracts/arena-escrow/src/msg.rs @@ -42,15 +42,20 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, - #[returns(BalanceVerified)] + #[returns(Option)] Balance { addr: String }, - #[returns(BalanceVerified)] + #[returns(Option)] Due { addr: String }, #[returns(Vec)] Dues { start_after: Option, limit: Option, }, + #[returns(Vec)] + InitialDues { + start_after: Option, + limit: Option, + }, #[returns(bool)] IsFunded { addr: String }, #[returns(bool)] diff --git a/contracts/arena-escrow/src/query.rs b/contracts/arena-escrow/src/query.rs index 5ec649d..9e242bc 100644 --- a/contracts/arena-escrow/src/query.rs +++ b/contracts/arena-escrow/src/query.rs @@ -4,24 +4,24 @@ use cw_balance::{BalanceVerified, MemberBalanceVerified, MemberShare}; use cw_storage_plus::Bound; use cw_utils::maybe_addr; -use crate::state::{BALANCE, DUE, IS_FUNDED, IS_LOCKED, PRESET_DISTRIBUTION, TOTAL_BALANCE}; +use crate::state::{BALANCE, DUE, INITIAL_DUE, IS_LOCKED, PRESET_DISTRIBUTION, TOTAL_BALANCE}; #[cw_serde] pub struct DumpStateResponse { - pub dues: Vec, pub is_locked: bool, pub total_balance: BalanceVerified, - pub balance: BalanceVerified, + pub balance: Option, + pub due: Option, } -pub fn balance(deps: Deps, addr: String) -> StdResult { +pub fn balance(deps: Deps, addr: String) -> StdResult> { let addr = deps.api.addr_validate(&addr)?; - Ok(BALANCE.may_load(deps.storage, &addr)?.unwrap_or_default()) + BALANCE.may_load(deps.storage, &addr) } -pub fn due(deps: Deps, addr: String) -> StdResult { +pub fn due(deps: Deps, addr: String) -> StdResult> { let addr = deps.api.addr_validate(&addr)?; - Ok(DUE.may_load(deps.storage, &addr)?.unwrap_or_default()) + DUE.may_load(deps.storage, &addr) } pub fn total_balance(deps: Deps) -> BalanceVerified { @@ -39,11 +39,7 @@ pub fn distribution(deps: Deps, addr: String) -> StdResult pub fn is_funded(deps: Deps, addr: String) -> StdResult { let addr = deps.api.addr_validate(&addr)?; - Ok(IS_FUNDED.load(deps.storage, &addr).unwrap_or_default()) -} - -pub fn is_fully_funded(deps: Deps) -> StdResult { - crate::state::is_fully_funded(deps) + Ok(crate::state::is_funded(deps, &addr)) } pub fn balances( @@ -76,15 +72,35 @@ pub fn dues( }) } +pub fn initial_dues( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult> { + let binding = maybe_addr(deps.api, start_after)?; + let start = binding.as_ref().map(Bound::exclusive); + cw_paginate::paginate_map(&INITIAL_DUE, deps.storage, start, limit, |k, v| { + Ok(MemberBalanceVerified { + addr: k, + balance: v, + }) + }) +} + pub fn dump_state(deps: Deps, addr: Option) -> StdResult { let maybe_addr = maybe_addr(deps.api, addr)?; let balance = maybe_addr + .as_ref() .map(|x| balance(deps, x.to_string())) .transpose()? - .unwrap_or_default(); + .flatten(); + let due = maybe_addr + .map(|x| due(deps, x.to_string())) + .transpose()? + .flatten(); Ok(DumpStateResponse { - dues: dues(deps, None, None)?, + due, is_locked: is_locked(deps), total_balance: total_balance(deps), balance, diff --git a/contracts/arena-escrow/src/state.rs b/contracts/arena-escrow/src/state.rs index f6f87d2..efe2cd9 100644 --- a/contracts/arena-escrow/src/state.rs +++ b/contracts/arena-escrow/src/state.rs @@ -1,20 +1,18 @@ -use cosmwasm_std::{Addr, Deps, StdResult}; +use cosmwasm_std::{Addr, Deps}; use cw_balance::{BalanceVerified, MemberShare}; use cw_storage_plus::{Item, Map}; pub const TOTAL_BALANCE: Item = Item::new("total"); pub const BALANCE: Map<&Addr, BalanceVerified> = Map::new("balance"); +pub const INITIAL_DUE: Map<&Addr, BalanceVerified> = Map::new("initial_due"); pub const DUE: Map<&Addr, BalanceVerified> = Map::new("due"); pub const IS_LOCKED: Item = Item::new("is_locked"); pub const PRESET_DISTRIBUTION: Map<&Addr, Vec>> = Map::new("distribution"); -pub const IS_FUNDED: Map<&Addr, bool> = Map::new("is_funded"); -pub fn is_fully_funded(deps: Deps) -> StdResult { - let all_funded = IS_FUNDED - .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) - .try_fold(true, |acc, result| { - result.map(|(_addr, value)| acc && value) - })?; +pub fn is_fully_funded(deps: Deps) -> bool { + DUE.is_empty(deps.storage) +} - Ok(all_funded) +pub fn is_funded(deps: Deps, addr: &Addr) -> bool { + !DUE.has(deps.storage, addr) } diff --git a/contracts/arena-escrow/src/tests.rs b/contracts/arena-escrow/src/tests.rs index 92c5b7e..03dc578 100644 --- a/contracts/arena-escrow/src/tests.rs +++ b/contracts/arena-escrow/src/tests.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{Addr, Binary, Coin, Empty, Uint128}; -use cw20::Cw20Coin; +use cw20::{Cw20Coin, Cw20CoinVerified}; use cw_balance::{Balance, BalanceVerified, Cw721Collection, MemberBalance, MemberShare}; use cw_multi_test::{App, Executor}; @@ -309,10 +309,17 @@ fn test_deposit_withdraw_and_check_balances() { ) .unwrap(); - assert_eq!( - balance_addr1.get_amount(cw_balance::TokenType::Cw20, context.cw20_addr.as_ref()), - Some(Uint128::from(150u128)) - ); + assert!(balance_addr1 + .difference(&BalanceVerified { + native: vec![], + cw20: vec![Cw20CoinVerified { + address: context.cw20_addr.clone(), + amount: Uint128::from(150u128), + }], + cw721: vec![], + }) + .unwrap() + .is_empty()); let due_addr1: BalanceVerified = context .app .wrap() @@ -323,20 +330,24 @@ fn test_deposit_withdraw_and_check_balances() { }, ) .unwrap(); - assert_eq!( - due_addr1.get_amount(cw_balance::TokenType::Cw20, context.cw20_addr.as_ref()), - None - ); + assert!(due_addr1.cw20.is_empty()); + let balance_total: BalanceVerified = context .app .wrap() .query_wasm_smart(context.escrow_addr.clone(), &QueryMsg::TotalBalance {}) .unwrap(); - - assert_eq!( - balance_total.get_amount(cw_balance::TokenType::Cw20, context.cw20_addr.as_ref()), - Some(Uint128::from(150u128)) - ); + assert!(balance_total + .difference(&BalanceVerified { + native: vec![], + cw20: vec![Cw20CoinVerified { + address: context.cw20_addr.clone(), + amount: Uint128::from(150u128), + }], + cw721: vec![], + }) + .unwrap() + .is_empty()); // Withdraw context @@ -378,16 +389,17 @@ fn test_deposit_withdraw_and_check_balances() { }, ) .unwrap(); - assert_eq!( - due_addr1.get_amount(cw_balance::TokenType::Cw20, context.cw20_addr.as_ref()), - Some(Uint128::from(150u128)) - ); - assert_eq!( - balance_addr1.get_amount(cw_balance::TokenType::Cw20, context.cw20_addr.as_ref()), - None - ); - assert_eq!( - balance_total.get_amount(cw_balance::TokenType::Cw20, context.cw20_addr.as_ref()), - None - ); + assert!(due_addr1 + .difference(&BalanceVerified { + native: vec![], + cw20: vec![Cw20CoinVerified { + address: context.cw20_addr.clone(), + amount: Uint128::from(150u128), + }], + cw721: vec![], + }) + .unwrap() + .is_empty()); + assert!(balance_addr1.is_empty()); + assert!(balance_total.is_empty()); } diff --git a/contracts/arena-league-module/schema/arena-league-module.json b/contracts/arena-league-module/schema/arena-league-module.json index 693063c..301124b 100644 --- a/contracts/arena-league-module/schema/arena-league-module.json +++ b/contracts/arena-league-module/schema/arena-league-module.json @@ -1,6 +1,6 @@ { "contract_name": "arena-league-module", - "contract_version": "0.9.0", + "contract_version": "0.9.5", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/arena-wager-module/schema/arena-wager-module.json b/contracts/arena-wager-module/schema/arena-wager-module.json index 5b7b0c3..f53adf6 100644 --- a/contracts/arena-wager-module/schema/arena-wager-module.json +++ b/contracts/arena-wager-module/schema/arena-wager-module.json @@ -1,6 +1,6 @@ { "contract_name": "arena-wager-module", - "contract_version": "0.9.0", + "contract_version": "0.9.5", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/packages/cw-balance/src/balance.rs b/packages/cw-balance/src/balance.rs index 5bfa4df..a898b97 100644 --- a/packages/cw-balance/src/balance.rs +++ b/packages/cw-balance/src/balance.rs @@ -6,7 +6,7 @@ use cosmwasm_std::{ use cw20::{Cw20Coin, Cw20CoinVerified, Cw20ExecuteMsg}; use cw721::Cw721ExecuteMsg; use std::collections::btree_map::Entry; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::fmt::{Display, Formatter, Result as FmtResult}; use crate::{is_contract, BalanceError, Cw721Collection, Cw721CollectionVerified, MemberShare}; @@ -43,14 +43,6 @@ pub struct Balance { pub cw721: Vec, } -// Enum to represent the type of token -#[cw_serde] -pub enum TokenType { - Native, - Cw20, - Cw721, -} - // Method to convert Balance to BalanceVerified impl Balance { pub fn to_verified(self, deps: Deps) -> StdResult { @@ -118,37 +110,80 @@ impl BalanceVerified { Self::default() } - // Method to check if BalanceVerified is empty - pub fn is_empty(&self) -> bool { - self.native.is_empty() && self.cw20.is_empty() && self.cw721.is_empty() - } + // Method to calculate the amounts needed to reach other balance - // Method to get the amount of a specific token - pub fn get_amount(&self, token_type: TokenType, identifier: &str) -> Option { - match token_type { - TokenType::Native => self - .native - .iter() - .find(|coin| coin.denom == identifier) - .map(|coin| coin.amount), - TokenType::Cw20 => self - .cw20 - .iter() - .find(|cw20_coin| cw20_coin.address == identifier) - .map(|cw20_coin| cw20_coin.amount), - TokenType::Cw721 => { - if self.cw721.iter().any(|cw721_tokens| { - cw721_tokens - .token_ids - .iter() - .any(|token| cw721_tokens.address.to_string() + token == identifier) - }) { - Some(Uint128::one()) - } else { - None + pub fn difference(&self, other: &BalanceVerified) -> StdResult { + let mut diff = BalanceVerified::new(); + + let native_map: HashMap<&String, Uint128> = self + .native + .iter() + .map(|coin| (&coin.denom, coin.amount)) + .collect(); + + for coin in &other.native { + match native_map.get(&coin.denom) { + Some(&amount) if amount < coin.amount => { + diff.native.push(Coin { + denom: coin.denom.clone(), + amount: coin.amount.checked_sub(amount)?, + }); + } + None => diff.native.push(coin.clone()), + _ => (), + } + } + + let cw20_map: HashMap<&Addr, Uint128> = self + .cw20 + .iter() + .map(|coin| (&coin.address, coin.amount)) + .collect(); + + for coin in &other.cw20 { + match cw20_map.get(&coin.address) { + Some(&amount) if amount < coin.amount => { + diff.cw20.push(Cw20CoinVerified { + address: coin.address.clone(), + amount: coin.amount.checked_sub(amount)?, + }); } + None => diff.cw20.push(coin.clone()), + _ => (), } } + + let cw721_map: HashMap<&Addr, BTreeSet<&String>> = self + .cw721 + .iter() + .map(|token| (&token.address, token.token_ids.iter().collect())) + .collect(); + + for token in &other.cw721 { + let token_ids_set: BTreeSet<&String> = token.token_ids.iter().collect(); + match cw721_map.get(&token.address) { + Some(token_ids) if !token_ids.is_superset(&token_ids_set) => { + let diff_token_ids: Vec = token_ids_set + .difference(token_ids) + .cloned() + .map(|s| s.to_owned()) + .collect(); + diff.cw721.push(Cw721CollectionVerified { + address: token.address.clone(), + token_ids: diff_token_ids, + }); + } + None => diff.cw721.push(token.clone()), + _ => (), + } + } + + Ok(diff) + } + + // Method to check if BalanceVerified is empty + pub fn is_empty(&self) -> bool { + self.native.is_empty() && self.cw20.is_empty() && self.cw721.is_empty() } // Method to add two BalanceVerified together diff --git a/packages/cw-balance/src/lib.rs b/packages/cw-balance/src/lib.rs index 79557d0..3be57b8 100644 --- a/packages/cw-balance/src/lib.rs +++ b/packages/cw-balance/src/lib.rs @@ -4,7 +4,7 @@ mod shares; mod tokens; mod util; -pub use balance::{Balance, BalanceVerified, MemberBalance, MemberBalanceVerified, TokenType}; +pub use balance::{Balance, BalanceVerified, MemberBalance, MemberBalanceVerified}; pub use error::BalanceError; pub use shares::MemberShare; pub use tokens::{Cw721Collection, Cw721CollectionVerified}; diff --git a/packages/cw-balance/src/tests/balance.rs b/packages/cw-balance/src/tests/balance.rs index 90daac6..3a0f57a 100644 --- a/packages/cw-balance/src/tests/balance.rs +++ b/packages/cw-balance/src/tests/balance.rs @@ -1,4 +1,7 @@ -use crate::BalanceVerified; +use cosmwasm_std::{Addr, Coin, Uint128}; +use cw20::Cw20CoinVerified; + +use crate::{BalanceVerified, Cw721CollectionVerified}; #[test] fn test_add_empty_balances() { @@ -17,3 +20,55 @@ fn test_subtract_empty_balances() { let new_balance = balance_a.checked_sub(&balance_b).unwrap(); assert!(new_balance.is_empty()); } + +#[test] +fn test_difference_balances() { + let balance_a = BalanceVerified { + native: vec![ + Coin { + denom: "token1".to_string(), + amount: Uint128::from(100u64), + }, + Coin { + denom: "token2".to_string(), + amount: Uint128::from(100u64), + }, + ], + cw20: vec![Cw20CoinVerified { + address: Addr::unchecked("address1"), + amount: Uint128::from(200u64), + }], + cw721: vec![Cw721CollectionVerified { + address: Addr::unchecked("address2"), + token_ids: vec!["tokenid1".to_string(), "tokenid2".to_string()], + }], + }; + let balance_b = BalanceVerified { + native: vec![Coin { + denom: "token1".to_string(), + amount: Uint128::from(50u64), + }], + cw20: vec![Cw20CoinVerified { + address: Addr::unchecked("address1"), + amount: Uint128::from(100u64), + }], + cw721: vec![Cw721CollectionVerified { + address: Addr::unchecked("address2"), + token_ids: vec!["tokenid1".to_string()], + }], + }; + + // Check a valid difference of balance b to balance a + let diff_balance = balance_b.difference(&balance_a).unwrap(); + assert_eq!(diff_balance.native[0].amount, Uint128::from(50u64)); + assert_eq!(diff_balance.native[1].amount, Uint128::from(100u64)); + assert_eq!(diff_balance.cw20[0].amount, Uint128::from(100u64)); + assert_eq!( + diff_balance.cw721[0].token_ids, + vec!["tokenid2".to_string()] + ); + + // Assert no difference from balance a to balance b + let diff_balance = balance_a.difference(&balance_b).unwrap(); + assert!(diff_balance.is_empty()); +}