diff --git a/cosmwasm/contracts/plastic-credit-marketplace/src/execute.rs b/cosmwasm/contracts/plastic-credit-marketplace/src/execute.rs index 2085ac1e..d6d53a98 100644 --- a/cosmwasm/contracts/plastic-credit-marketplace/src/execute.rs +++ b/cosmwasm/contracts/plastic-credit-marketplace/src/execute.rs @@ -2,9 +2,10 @@ use cosmos_sdk_proto::cosmos::authz::v1beta1::MsgExec; use cosmos_sdk_proto::traits::{Message, TypeUrl}; use cosmos_sdk_proto::traits::MessageExt; use cosmwasm_std::{entry_point, Binary, DepsMut, Env, MessageInfo, Response, Uint64, Coin, CosmosMsg, BankMsg, Addr, Decimal, Timestamp, StdError, Uint128}; -use fee_splitter::{edit_fee_split_config, get_fee_split, Share}; +use fee_splitter::{edit_fee_split_config, Share}; use crate::{msg::ExecuteMsg, error::ContractError, state::{LISTINGS, Listing}}; use crate::error::ContractError::FeeSplitError; +use crate::query::get_price_and_fee; use crate::state::{ADMIN, Freeze, freezes}; const MAX_TIMEOUT_SECONDS : u64 = 2419200; // 4 weeks @@ -93,11 +94,11 @@ pub fn execute_buy_credits( return Err(ContractError::NotEnoughCredits {}); } - let total_price = listing.price_per_credit.amount.checked_mul(number_of_credits_to_buy.into()).unwrap(); - if info.funds.len() != 1 || info.funds[0].denom != listing.price_per_credit.denom || info.funds[0].amount < total_price { + let (total_price, fee, fee_split_msgs) = get_price_and_fee(deps.as_ref(), listing.clone(), number_of_credits_to_buy); + if info.funds.len() != 1 || info.funds[0].denom != listing.price_per_credit.denom || info.funds[0].amount < total_price.amount { return Err(ContractError::NotEnoughFunds {}); } - if info.funds[0].amount > total_price { // We can skip the denom check here because it is triggered in the previous if statement + if info.funds[0].amount > total_price.amount { // We can skip the denom check here because it is triggered in the previous if statement return Err(ContractError::TooMuchFunds {}); } @@ -119,11 +120,11 @@ pub fn execute_buy_credits( retiring_entity_additional_data, ); - let funds_before_fee_split = Coin { + let remaining_amount = total_price.amount.checked_sub(fee.amount).unwrap(); + let funds_after_split = Coin { denom: listing.price_per_credit.denom.clone(), - amount: total_price, + amount: remaining_amount, }; - let (fee_split_msgs, funds_after_split, _) = get_fee_split(deps.storage, funds_before_fee_split).unwrap(); let transfer_funds_to_seller_msg = CosmosMsg::Bank(BankMsg::Send { to_address: listing.owner.to_string(), amount: vec![funds_after_split], @@ -401,20 +402,15 @@ fn execute_release_frozen_credits( freezes().save(deps.storage, (Addr::unchecked(owner.clone()), denom.clone(), buyer.clone()), &freeze)?; } - // We need the original_price to calculate the fee split - let original_price = listing.price_per_credit.amount.checked_mul(number_of_credits_to_release.into()).unwrap(); - let original_price_as_coin = Coin { - denom: listing.price_per_credit.denom.clone(), - amount: original_price, - }; - let (fee_split_msgs, _, fee_amount) = get_fee_split(deps.storage, original_price_as_coin).unwrap(); - if fee_amount.amount > Uint128::from(0u128) { - if info.funds.len() != 1 || info.funds[0].denom != listing.price_per_credit.denom || info.funds[0].amount < fee_amount.amount { + let (_, fee, fee_split_msgs) = get_price_and_fee(deps.as_ref(), listing.clone(), number_of_credits_to_release); + + if fee.amount > Uint128::from(0u128) { + if info.funds.len() != 1 || info.funds[0].denom != listing.price_per_credit.denom || info.funds[0].amount < fee.amount { return Err(ContractError::NotEnoughFunds {}); } } - if info.funds.len() > 0 && info.funds[0].amount > fee_amount.amount { // We can skip the denom check here because it is triggered in the previous if statement + if info.funds.len() > 0 && info.funds[0].amount > fee.amount { // We can skip the denom check here because it is triggered in the previous if statement return Err(ContractError::TooMuchFunds {}); } diff --git a/cosmwasm/contracts/plastic-credit-marketplace/src/msg.rs b/cosmwasm/contracts/plastic-credit-marketplace/src/msg.rs index 3f5db303..bbb2111f 100644 --- a/cosmwasm/contracts/plastic-credit-marketplace/src/msg.rs +++ b/cosmwasm/contracts/plastic-credit-marketplace/src/msg.rs @@ -84,6 +84,12 @@ pub enum QueryMsg { }, #[returns(fee_splitter::Config)] FeeSplitConfig {}, + #[returns(PriceResponse)] + Price { + owner: Addr, + denom: String, + number_of_credits_to_buy: u64, + }, } #[cw_serde] @@ -94,4 +100,10 @@ pub struct ListingsResponse { #[cw_serde] pub struct ListingResponse { pub listing: Listing, +} + +#[cw_serde] +pub struct PriceResponse { + pub total_price: Coin, + pub fee: Coin, } \ No newline at end of file diff --git a/cosmwasm/contracts/plastic-credit-marketplace/src/query.rs b/cosmwasm/contracts/plastic-credit-marketplace/src/query.rs index 6f780eaf..b581927d 100644 --- a/cosmwasm/contracts/plastic-credit-marketplace/src/query.rs +++ b/cosmwasm/contracts/plastic-credit-marketplace/src/query.rs @@ -1,7 +1,9 @@ -use cosmwasm_std::{entry_point, Deps, Env, StdResult, Binary, to_binary, Addr}; +use cosmwasm_std::{entry_point, Deps, Env, StdResult, Binary, to_binary, Addr, Coin, CosmosMsg}; use cw_storage_plus::Bound; +use fee_splitter::get_fee_split; use crate::{msg::{QueryMsg, ListingResponse, ListingsResponse}, state::{LISTINGS, Listing}}; +use crate::msg::PriceResponse; pub const DEFAULT_LIMIT: u64 = 30; @@ -11,6 +13,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::Listing {owner, denom} => to_binary(&listing(deps, owner, denom)?), QueryMsg::Listings { limit, start_after } => to_binary(&listings(deps, start_after, limit)?), QueryMsg::FeeSplitConfig {} => to_binary(&fee_splitter::get_config(deps.storage)?), + QueryMsg::Price { owner, denom, number_of_credits_to_buy } => to_binary(&price(deps, owner, denom, number_of_credits_to_buy)?), } } @@ -42,6 +45,30 @@ pub fn listing( Ok(ListingResponse { listing }) } +pub fn price( + deps: Deps, + owner: Addr, + denom: String, + number_of_credits_to_buy: u64, +) -> StdResult { + let listing = LISTINGS.load(deps.storage, (owner, denom.clone()))?; + + let (total_price, fee, _) = get_price_and_fee(deps, listing, number_of_credits_to_buy); + + Ok(PriceResponse { total_price, fee }) +} + +pub fn get_price_and_fee(deps: Deps, listing: Listing, number_of_credits_to_buy: u64) -> (Coin, Coin, Vec) { + let total_price_amount = listing.price_per_credit.amount.checked_mul(number_of_credits_to_buy.into()).unwrap(); + let total_price = Coin { + denom: listing.price_per_credit.denom.clone(), + amount: total_price_amount, + }; + let (fee_split_msgs, fee) = get_fee_split(deps.storage, total_price.clone()).unwrap(); + + (total_price, fee, fee_split_msgs) +} + #[cfg(test)] mod tests { mod query_listings_tests { @@ -49,7 +76,7 @@ mod tests { use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use crate::execute::execute; use crate::{instantiate, query}; - use crate::msg::{ExecuteMsg, ListingsResponse, InstantiateMsg}; + use crate::msg::{ExecuteMsg, ListingsResponse, InstantiateMsg, ListingResponse}; use crate::query::query; #[test] @@ -73,7 +100,7 @@ mod tests { owner: info.sender.clone(), denom: "ptest".to_string(), }).unwrap(); - let res: crate::msg::ListingResponse = from_binary(&res).unwrap(); + let res: ListingResponse = from_binary(&res).unwrap(); assert_eq!(res.listing.denom, "ptest"); assert_eq!(res.listing.number_of_credits, Uint64::from(42u64)); assert_eq!(res.listing.price_per_credit, Coin { @@ -226,4 +253,105 @@ mod tests { assert_eq!(res.shares[0].percentage, Decimal::percent(100)); } } + + mod query_price { + use cosmwasm_std::{Coin, coins, Decimal, from_binary, StdError, Uint128, Uint64}; + use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; + use fee_splitter::Share; + use crate::execute::execute; + use crate::{instantiate, query}; + use crate::msg::{ExecuteMsg, InstantiateMsg, PriceResponse}; + use crate::query::query; + + #[test] + fn test_query_price() { + let mut deps = mock_dependencies(); + let info = mock_info("creator", &coins(2, "token")); + instantiate(deps.as_mut(), mock_env(), info.clone(), InstantiateMsg{ admin: info.sender.to_string(), fee_percentage: Decimal::percent(0), shares: vec![] }).unwrap(); + + let msg = ExecuteMsg::CreateListing { + denom: "ptest".to_string(), + number_of_credits: Uint64::from(42u64), + price_per_credit: Coin { + denom: "token".to_string(), + amount: Uint128::from(1337u128), + }, + operator: None, + }; + execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap(); + + let res = query(deps.as_ref(), mock_env(), query::QueryMsg::Price { + owner: info.sender.clone(), + denom: "ptest".to_string(), + number_of_credits_to_buy: 11, + }).unwrap(); + let res: PriceResponse = from_binary(&res).unwrap(); + assert_eq!(res.total_price, Coin { + denom: "token".to_string(), + amount: Uint128::from(14707u128), + }); + assert_eq!(res.fee, Coin { + denom: "token".to_string(), + amount: Uint128::from(0u128) + }); + } + + #[test] + fn test_query_price_with_fee() { + let mut deps = mock_dependencies(); + let creator_info = mock_info("creator", &coins(2, "token")); + let dev_share = Share { + address: "dev".to_string(), + percentage: Decimal::percent(90), + }; + let user_share = Share { + address: "user".to_string(), + percentage: Decimal::percent(10), + }; + instantiate(deps.as_mut(), mock_env(), creator_info.clone(), InstantiateMsg { admin: creator_info.sender.to_string(), fee_percentage: Decimal::permille(5), shares: vec![dev_share, user_share] }).unwrap(); + + let msg = ExecuteMsg::CreateListing { + denom: "ptest".to_string(), + number_of_credits: Uint64::from(42u64), + price_per_credit: Coin { + denom: "token".to_string(), + amount: Uint128::from(1337u128), + }, + operator: None, + }; + execute(deps.as_mut(), mock_env(), creator_info.clone(), msg.clone()).unwrap(); + + let res = query(deps.as_ref(), mock_env(), query::QueryMsg::Price { + owner: creator_info.sender.clone(), + denom: "ptest".to_string(), + number_of_credits_to_buy: 11, + }).unwrap(); + let res: PriceResponse = from_binary(&res).unwrap(); + assert_eq!(res.total_price, Coin { + denom: "token".to_string(), + amount: Uint128::from(14707u128), + }); + assert_eq!(res.fee, Coin { + denom: "token".to_string(), + amount: Uint128::from(73u128) + }); + } + + #[test] + fn test_query_price_listing_not_found() { + let mut deps = mock_dependencies(); + let creator_info = mock_info("creator", &coins(2, "token")); + instantiate(deps.as_mut(), mock_env(), creator_info.clone(), InstantiateMsg{ admin: creator_info.sender.to_string(), fee_percentage: Decimal::percent(0), shares: vec![] }).unwrap(); + + let res = query(deps.as_ref(), mock_env(), query::QueryMsg::Price { + owner: creator_info.sender.clone(), + denom: "ptest".to_string(), + number_of_credits_to_buy: 11, + }); + match res { + Ok(_) => panic!("Expected error"), + Err(e) => assert_eq!(e, StdError::NotFound { kind: "plastic_credit_marketplace::state::Listing".to_string() }), + } + } + } } \ No newline at end of file diff --git a/cosmwasm/packages/fee-splitter/src/lib.rs b/cosmwasm/packages/fee-splitter/src/lib.rs index e260f21e..1c6c6fae 100644 --- a/cosmwasm/packages/fee-splitter/src/lib.rs +++ b/cosmwasm/packages/fee-splitter/src/lib.rs @@ -61,11 +61,11 @@ pub fn edit_fee_split_config(storage: &mut dyn Storage, fee_percentage: Decimal, } // Returns a tuple of (msgs, remaining_amount_as_coin, fee_amount_as_coin) -pub fn get_fee_split(storage: &dyn Storage, full_price: Coin) -> Result<(Vec, Coin, Coin), FeeSplitterError> { +pub fn get_fee_split(storage: &dyn Storage, full_price: Coin) -> Result<(Vec, Coin), FeeSplitterError> { let config = CONFIG.load(storage).unwrap(); let denom = full_price.denom.clone(); if Decimal::is_zero(&config.fee_percentage) || config.shares.is_empty() { - return Ok((vec![], full_price, Coin { + return Ok((vec![], Coin { denom: denom.clone(), amount: 0u128.into(), })); @@ -89,16 +89,11 @@ pub fn get_fee_split(storage: &dyn Storage, full_price: Coin) -> Result<(Vec Result { @@ -370,13 +365,11 @@ mod tests { instantiate_fee_splitter(deps.as_mut().storage, Decimal::permille(5), vec![dev_share.clone(), stakers_share.clone()]).unwrap(); // 1000 umpwr fee - let (fee_split_msgs, remaining_amount, fee_amount) = get_fee_split(deps.as_mut().storage, Coin { + let (fee_split_msgs, fee_amount) = get_fee_split(deps.as_mut().storage, Coin { denom: "umpwr".to_string(), amount: Uint128::new(1000), }).unwrap(); assert_eq!(fee_split_msgs.len(), 2); - assert_eq!(remaining_amount.denom, "umpwr"); - assert_eq!(remaining_amount.amount, Uint128::new(995)); assert_eq!(fee_amount.denom, "umpwr"); assert_eq!(fee_amount.amount, Uint128::new(5)); @@ -414,13 +407,11 @@ mod tests { // 500 umpwr fee, with 0.5% fee and 80/20 split means that dev gets 2 umpwr and stakers get 0 (rounded down) // and the remaining 498 umpwr is returned - let (fee_split_msgs, remaining_amount, fee_amount) = get_fee_split(deps.as_mut().storage, Coin { + let (fee_split_msgs, fee_amount) = get_fee_split(deps.as_mut().storage, Coin { denom: "umpwr".to_string(), amount: Uint128::new(500), }).unwrap(); assert_eq!(fee_split_msgs.len(), 2); - assert_eq!(remaining_amount.denom, "umpwr"); - assert_eq!(remaining_amount.amount, Uint128::new(498)); assert_eq!(fee_amount.denom, "umpwr"); assert_eq!(fee_amount.amount, Uint128::new(2)); @@ -449,13 +440,11 @@ mod tests { let mut deps = mock_dependencies(); instantiate_fee_splitter(deps.as_mut().storage, Decimal::zero(), vec![]).unwrap(); - let (fee_split_msgs, remaining_amount, fee_amount) = get_fee_split(deps.as_mut().storage, Coin { + let (fee_split_msgs, fee_amount) = get_fee_split(deps.as_mut().storage, Coin { denom: "umpwr".to_string(), amount: Uint128::new(100), }).unwrap(); assert_eq!(fee_split_msgs.len(), 0); - assert_eq!(remaining_amount.denom, "umpwr"); - assert_eq!(remaining_amount.amount, Uint128::new(100)); assert_eq!(fee_amount.denom, "umpwr"); assert_eq!(fee_amount.amount, Uint128::new(0)); }