From d5a217a1ccb4566c6cdf96570dad8a974b1f2ac3 Mon Sep 17 00:00:00 2001 From: George Ornbo Date: Wed, 4 Oct 2023 09:32:37 +0100 Subject: [PATCH 1/4] Add must_pay_two_coins This requires that two and only two of the specified denoms are sent --- src/lib.rs | 2 +- src/payment.rs | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index d88e6592..80114d6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,7 @@ pub use parse_reply::{ parse_reply_instantiate_data, MsgExecuteContractResponse, MsgInstantiateContractResponse, ParseReplyError, }; -pub use payment::{may_pay, must_pay, nonpayable, one_coin, PaymentError}; +pub use payment::{may_pay, must_pay, must_pay_two_coins, nonpayable, one_coin, PaymentError}; pub use threshold::{Threshold, ThresholdError, ThresholdResponse}; pub use crate::balance::NativeBalance; diff --git a/src/payment.rs b/src/payment.rs index f6ccdd20..e24de265 100644 --- a/src/payment.rs +++ b/src/payment.rs @@ -52,6 +52,37 @@ pub fn may_pay(info: &MessageInfo, denom: &str) -> Result } } +pub fn must_pay_two_coins( + info: &MessageInfo, + first_denom: &str, + second_denom: &str, +) -> Result<(Uint128, Uint128), PaymentError> { + if info.funds.is_empty() { + Err(PaymentError::NoFunds {}) + } else if info.funds.len() == 1 && info.funds[0].denom == first_denom { + Err(PaymentError::MissingDenom(second_denom.to_string())) + } else if info.funds.len() == 1 && info.funds[0].denom == second_denom { + Err(PaymentError::MissingDenom(first_denom.to_string())) + } else if info.funds.len() == 2 { + let first_coin = match info.funds.iter().find(|c| c.denom == first_denom) { + Some(c) => c, + None => return Err(PaymentError::MissingDenom(first_denom.to_string())), + }; + let second_coin = match info.funds.iter().find(|c| c.denom == second_denom) { + Some(c) => c, + None => return Err(PaymentError::MissingDenom(second_denom.to_string())), + }; + Ok((first_coin.amount, second_coin.amount)) + } else { + let wrong = info + .funds + .iter() + .find(|c| c.denom != first_denom && c.denom != second_denom) + .unwrap(); + Err(PaymentError::ExtraDenom(wrong.denom.to_string())) + } +} + #[derive(Error, Debug, PartialEq, Eq)] pub enum PaymentError { #[error("Must send reserve token '{0}'")] @@ -133,4 +164,44 @@ mod test { let err = must_pay(&mixed_payment, atom).unwrap_err(); assert_eq!(err, PaymentError::MultipleDenoms {}); } + + #[test] + fn must_pay_two_coins_works() { + let atom: &str = "uatom"; + let osmo: &str = "uosmo"; + let eth: &str = "eth"; + let no_payment = mock_info(SENDER, &[]); + let atom_payment = mock_info(SENDER, &coins(100, atom)); + let osmo_payment = mock_info(SENDER, &coins(100, osmo)); + let two_coin_payment = mock_info(SENDER, &[coin(50, atom), coin(120, eth)]); + let duplicate_coins_payment = mock_info(SENDER, &[coin(50, atom), coin(120, atom)]); + let three_coin_payment = + mock_info(SENDER, &[coin(50, atom), coin(120, eth), coin(120, osmo)]); + + let (coin_one_amount, coin_two_amount) = + must_pay_two_coins(&two_coin_payment, atom, eth).unwrap(); + assert_eq!(coin_one_amount, Uint128::new(50)); + assert_eq!(coin_two_amount, Uint128::new(120)); + + let err = must_pay_two_coins(&duplicate_coins_payment, atom, osmo).unwrap_err(); + assert_eq!(err, PaymentError::MissingDenom(osmo.to_string())); + + let err = must_pay_two_coins(&no_payment, atom, osmo).unwrap_err(); + assert_eq!(err, PaymentError::NoFunds {}); + + let err = must_pay_two_coins(&atom_payment, atom, osmo).unwrap_err(); + assert_eq!(err, PaymentError::MissingDenom(osmo.to_string())); + + let err = must_pay_two_coins(&osmo_payment, atom, osmo).unwrap_err(); + assert_eq!(err, PaymentError::MissingDenom(atom.to_string())); + + let err = must_pay_two_coins(&two_coin_payment, atom, osmo).unwrap_err(); + assert_eq!(err, PaymentError::MissingDenom(osmo.to_string())); + + let err = must_pay_two_coins(&three_coin_payment, osmo, atom).unwrap_err(); + assert_eq!(err, PaymentError::ExtraDenom(eth.to_string())); + + let err = must_pay_two_coins(&two_coin_payment, osmo, atom).unwrap_err(); + assert_eq!(err, PaymentError::MissingDenom(osmo.to_string())); + } } From 8817d71545bba40fab4b63c680741fe5a4be3162 Mon Sep 17 00:00:00 2001 From: George Ornbo Date: Wed, 4 Oct 2023 15:00:01 +0100 Subject: [PATCH 2/4] Change function signature to must_pay_many(&[&str]) --- src/lib.rs | 2 +- src/payment.rs | 50 ++++++++++++++++++++++++-------------------------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 80114d6a..736173ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,7 @@ pub use parse_reply::{ parse_reply_instantiate_data, MsgExecuteContractResponse, MsgInstantiateContractResponse, ParseReplyError, }; -pub use payment::{may_pay, must_pay, must_pay_two_coins, nonpayable, one_coin, PaymentError}; +pub use payment::{may_pay, must_pay, must_pay_many, nonpayable, one_coin, PaymentError}; pub use threshold::{Threshold, ThresholdError, ThresholdResponse}; pub use crate::balance::NativeBalance; diff --git a/src/payment.rs b/src/payment.rs index e24de265..be4e4e69 100644 --- a/src/payment.rs +++ b/src/payment.rs @@ -52,35 +52,33 @@ pub fn may_pay(info: &MessageInfo, denom: &str) -> Result } } -pub fn must_pay_two_coins( +pub fn must_pay_many( info: &MessageInfo, - first_denom: &str, - second_denom: &str, -) -> Result<(Uint128, Uint128), PaymentError> { + required_denoms: &[&str], +) -> Result, PaymentError> { if info.funds.is_empty() { - Err(PaymentError::NoFunds {}) - } else if info.funds.len() == 1 && info.funds[0].denom == first_denom { - Err(PaymentError::MissingDenom(second_denom.to_string())) - } else if info.funds.len() == 1 && info.funds[0].denom == second_denom { - Err(PaymentError::MissingDenom(first_denom.to_string())) - } else if info.funds.len() == 2 { - let first_coin = match info.funds.iter().find(|c| c.denom == first_denom) { - Some(c) => c, - None => return Err(PaymentError::MissingDenom(first_denom.to_string())), - }; - let second_coin = match info.funds.iter().find(|c| c.denom == second_denom) { - Some(c) => c, - None => return Err(PaymentError::MissingDenom(second_denom.to_string())), - }; - Ok((first_coin.amount, second_coin.amount)) - } else { - let wrong = info - .funds - .iter() - .find(|c| c.denom != first_denom && c.denom != second_denom) - .unwrap(); - Err(PaymentError::ExtraDenom(wrong.denom.to_string())) + return Err(PaymentError::NoFunds {}); + } + + if info.funds.len() != required_denoms.len() { + return Err(PaymentError::IncorrectNumberOfDenoms {}); + } + + let mut amounts: Vec = Vec::new(); + + for denom in required_denoms.iter() { + match info.funds.iter().find(|c| &c.denom == denom) { + Some(coin) => { + if coin.amount == Uint128::zero() { + return Err(PaymentError::NoFunds {}); + } + amounts.push(coin.amount); + } + None => return Err(PaymentError::MissingDenom(denom.to_string())), + } } + + Ok(amounts) } #[derive(Error, Debug, PartialEq, Eq)] From 9863bd07ec14a1fde4bcdedb6f7d1207d3f5d6f7 Mon Sep 17 00:00:00 2001 From: George Ornbo Date: Wed, 4 Oct 2023 15:07:44 +0100 Subject: [PATCH 3/4] Add missing variant and tests --- src/payment.rs | 72 ++++++++++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 40 deletions(-) diff --git a/src/payment.rs b/src/payment.rs index be4e4e69..dbf3d5d9 100644 --- a/src/payment.rs +++ b/src/payment.rs @@ -97,6 +97,9 @@ pub enum PaymentError { #[error("This message does no accept funds")] NonPayable {}, + + #[error("Must send required denoms")] + IncorrectNumberOfDenoms {}, } #[cfg(test)] @@ -137,6 +140,35 @@ mod test { let err = may_pay(&mixed_payment, atom).unwrap_err(); assert_eq!(err, PaymentError::ExtraDenom("wei".to_string())); } + #[test] + fn test_must_pay_many_works() { + let atom: &str = "uatom"; + let eth: &str = "wei"; + + let no_payment = mock_info(SENDER, &[]); + let atom_payment = mock_info(SENDER, &coins(100, atom)); + let eth_payment = mock_info(SENDER, &coins(100, eth)); + let mixed_payment = mock_info(SENDER, &[coin(50, atom), coin(120, eth)]); + + let err = must_pay_many(&no_payment, &[atom, eth]).unwrap_err(); + assert_eq!(err, PaymentError::NoFunds {}); + + let err = must_pay_many(&atom_payment, &[atom, eth]).unwrap_err(); + assert_eq!(err, PaymentError::IncorrectNumberOfDenoms {}); + + let err = must_pay_many(ð_payment, &[atom]).unwrap_err(); + assert_eq!(err, PaymentError::MissingDenom(atom.to_string())); + + let err = must_pay_many(&mixed_payment, &[atom]).unwrap_err(); + assert_eq!(err, PaymentError::IncorrectNumberOfDenoms {}); + + let res = must_pay_many(&mixed_payment, &[atom, eth]).unwrap(); + assert_eq!(res, vec![Uint128::new(50), Uint128::new(120)]); + + let zero_atom_payment = mock_info(SENDER, &coins(0, atom)); + let err = must_pay_many(&zero_atom_payment, &[atom]).unwrap_err(); + assert_eq!(err, PaymentError::NoFunds {}); + } #[test] fn must_pay_works() { @@ -162,44 +194,4 @@ mod test { let err = must_pay(&mixed_payment, atom).unwrap_err(); assert_eq!(err, PaymentError::MultipleDenoms {}); } - - #[test] - fn must_pay_two_coins_works() { - let atom: &str = "uatom"; - let osmo: &str = "uosmo"; - let eth: &str = "eth"; - let no_payment = mock_info(SENDER, &[]); - let atom_payment = mock_info(SENDER, &coins(100, atom)); - let osmo_payment = mock_info(SENDER, &coins(100, osmo)); - let two_coin_payment = mock_info(SENDER, &[coin(50, atom), coin(120, eth)]); - let duplicate_coins_payment = mock_info(SENDER, &[coin(50, atom), coin(120, atom)]); - let three_coin_payment = - mock_info(SENDER, &[coin(50, atom), coin(120, eth), coin(120, osmo)]); - - let (coin_one_amount, coin_two_amount) = - must_pay_two_coins(&two_coin_payment, atom, eth).unwrap(); - assert_eq!(coin_one_amount, Uint128::new(50)); - assert_eq!(coin_two_amount, Uint128::new(120)); - - let err = must_pay_two_coins(&duplicate_coins_payment, atom, osmo).unwrap_err(); - assert_eq!(err, PaymentError::MissingDenom(osmo.to_string())); - - let err = must_pay_two_coins(&no_payment, atom, osmo).unwrap_err(); - assert_eq!(err, PaymentError::NoFunds {}); - - let err = must_pay_two_coins(&atom_payment, atom, osmo).unwrap_err(); - assert_eq!(err, PaymentError::MissingDenom(osmo.to_string())); - - let err = must_pay_two_coins(&osmo_payment, atom, osmo).unwrap_err(); - assert_eq!(err, PaymentError::MissingDenom(atom.to_string())); - - let err = must_pay_two_coins(&two_coin_payment, atom, osmo).unwrap_err(); - assert_eq!(err, PaymentError::MissingDenom(osmo.to_string())); - - let err = must_pay_two_coins(&three_coin_payment, osmo, atom).unwrap_err(); - assert_eq!(err, PaymentError::ExtraDenom(eth.to_string())); - - let err = must_pay_two_coins(&two_coin_payment, osmo, atom).unwrap_err(); - assert_eq!(err, PaymentError::MissingDenom(osmo.to_string())); - } } From 712d414b9fc9d0f69b28da91b491ad8d22270cbf Mon Sep 17 00:00:00 2001 From: George Ornbo Date: Sun, 8 Oct 2023 19:29:52 +0100 Subject: [PATCH 4/4] Update function signature and tests --- src/payment.rs | 54 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/src/payment.rs b/src/payment.rs index dbf3d5d9..80993cc3 100644 --- a/src/payment.rs +++ b/src/payment.rs @@ -1,4 +1,5 @@ use cosmwasm_std::{Coin, MessageInfo, Uint128}; +use std::collections::HashMap; use thiserror::Error; /// returns an error if any coins were sent @@ -55,30 +56,34 @@ pub fn may_pay(info: &MessageInfo, denom: &str) -> Result pub fn must_pay_many( info: &MessageInfo, required_denoms: &[&str], -) -> Result, PaymentError> { +) -> Result, PaymentError> { if info.funds.is_empty() { return Err(PaymentError::NoFunds {}); } - if info.funds.len() != required_denoms.len() { - return Err(PaymentError::IncorrectNumberOfDenoms {}); + return Err(PaymentError::IncorrectNumberOfDenoms(required_denoms.len())); } - let mut amounts: Vec = Vec::new(); + let mut funds_map = HashMap::new(); + for coin in &info.funds { + if funds_map.insert(coin.denom.as_str(), coin).is_some() { + return Err(PaymentError::DuplicateDenom(coin.denom.clone())); + } + } - for denom in required_denoms.iter() { - match info.funds.iter().find(|c| &c.denom == denom) { + let mut matched_coins: Vec = Vec::new(); + for &denom in required_denoms { + match funds_map.get(denom) { Some(coin) => { if coin.amount == Uint128::zero() { - return Err(PaymentError::NoFunds {}); + return Err(PaymentError::ZeroAmountDenom(denom.to_string())); } - amounts.push(coin.amount); + matched_coins.push((**coin).clone()); } None => return Err(PaymentError::MissingDenom(denom.to_string())), } } - - Ok(amounts) + Ok(matched_coins) } #[derive(Error, Debug, PartialEq, Eq)] @@ -98,8 +103,14 @@ pub enum PaymentError { #[error("This message does no accept funds")] NonPayable {}, - #[error("Must send required denoms")] - IncorrectNumberOfDenoms {}, + #[error("Must send '{0}' denoms")] + IncorrectNumberOfDenoms(usize), + + #[error("Zero amound sent for '{0}'")] + ZeroAmountDenom(String), + + #[error("Received duplicate denom '{0}'")] + DuplicateDenom(String), } #[cfg(test)] @@ -147,6 +158,8 @@ mod test { let no_payment = mock_info(SENDER, &[]); let atom_payment = mock_info(SENDER, &coins(100, atom)); + let zero_amount_payment = mock_info(SENDER, &[coin(0, atom), coin(120, eth)]); + let duplicate_atom_payment = mock_info(SENDER, &[coin(50, atom), coin(120, atom)]); let eth_payment = mock_info(SENDER, &coins(100, eth)); let mixed_payment = mock_info(SENDER, &[coin(50, atom), coin(120, eth)]); @@ -154,20 +167,19 @@ mod test { assert_eq!(err, PaymentError::NoFunds {}); let err = must_pay_many(&atom_payment, &[atom, eth]).unwrap_err(); - assert_eq!(err, PaymentError::IncorrectNumberOfDenoms {}); + assert_eq!(err, PaymentError::IncorrectNumberOfDenoms(2)); + + let err = must_pay_many(&duplicate_atom_payment, &[atom, eth]).unwrap_err(); + assert_eq!(err, PaymentError::DuplicateDenom(atom.to_string())); + + let err = must_pay_many(&zero_amount_payment, &[atom, eth]).unwrap_err(); + assert_eq!(err, PaymentError::ZeroAmountDenom(atom.to_string())); let err = must_pay_many(ð_payment, &[atom]).unwrap_err(); assert_eq!(err, PaymentError::MissingDenom(atom.to_string())); - let err = must_pay_many(&mixed_payment, &[atom]).unwrap_err(); - assert_eq!(err, PaymentError::IncorrectNumberOfDenoms {}); - let res = must_pay_many(&mixed_payment, &[atom, eth]).unwrap(); - assert_eq!(res, vec![Uint128::new(50), Uint128::new(120)]); - - let zero_atom_payment = mock_info(SENDER, &coins(0, atom)); - let err = must_pay_many(&zero_atom_payment, &[atom]).unwrap_err(); - assert_eq!(err, PaymentError::NoFunds {}); + assert_eq!(res, vec![coin(50, atom), coin(120, eth)]); } #[test]