use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Coin, StdError, StdResult, Uint128};
use croncat_sdk_core::types::GasPrice;
use cw20::Cw20CoinVerified;
use crate::error::SdkError;
#[cw_serde]
pub struct TaskBalanceResponse {
pub balance: Option<TaskBalance>,
}
#[cw_serde]
pub struct TaskBalance {
pub native_balance: Uint128,
pub cw20_balance: Option<Cw20CoinVerified>,
pub ibc_balance: Option<Coin>,
}
impl TaskBalance {
pub fn verify_enough_attached(
&self,
native_required: Uint128,
cw20_required: Option<Cw20CoinVerified>,
ibc_required: Option<Coin>,
recurring: bool,
native_denom: &str,
) -> Result<(), SdkError> {
let multiplier = if recurring {
Uint128::new(2)
} else {
Uint128::new(1)
};
if self.native_balance < native_required * multiplier {
return Err(SdkError::NotEnoughNative {
denom: native_denom.to_owned(),
lack: native_required * multiplier - self.native_balance,
});
}
self.verify_enough_cw20(cw20_required, multiplier)?;
match (ibc_required, &self.ibc_balance) {
(Some(req), Some(attached)) => {
if req.denom != attached.denom {
return Err(SdkError::NotEnoughNative {
denom: req.denom,
lack: req.amount * multiplier,
});
}
if attached.amount < req.amount * multiplier {
return Err(SdkError::NotEnoughNative {
denom: req.denom,
lack: req.amount * multiplier - attached.amount,
});
}
}
(Some(req), None) => {
return Err(SdkError::NotEnoughNative {
denom: req.denom,
lack: req.amount * multiplier,
})
}
(None, Some(_)) => {
return Err(SdkError::NonRequiredDenom {});
}
(None, None) => (),
}
Ok(())
}
pub fn verify_enough_cw20(
&self,
cw20_required: Option<Cw20CoinVerified>,
multiplier: Uint128,
) -> Result<(), SdkError> {
match (cw20_required, &self.cw20_balance) {
(Some(req), Some(attached)) => {
if req.address != attached.address {
return Err(SdkError::NotEnoughCw20 {
addr: req.address.into_string(),
lack: req.amount * multiplier,
});
}
if attached.amount < req.amount * multiplier {
return Err(SdkError::NotEnoughCw20 {
addr: req.address.into_string(),
lack: req.amount * multiplier - attached.amount,
});
}
Ok(())
}
(Some(req), None) => Err(SdkError::NotEnoughCw20 {
addr: req.address.into_string(),
lack: req.amount * multiplier,
}),
(None, Some(_)) => Err(SdkError::NonRequiredDenom {}),
(None, None) => Ok(()),
}
}
pub fn sub_coin(&mut self, coin: &Coin, native_denom: &str) -> StdResult<()> {
if coin.denom == native_denom {
self.native_balance = self
.native_balance
.checked_sub(coin.amount)
.map_err(|_| StdError::generic_err("Not enough native balance for operation"))?;
} else {
match &mut self.ibc_balance {
Some(task_coin) if task_coin.denom == coin.denom => {
task_coin.amount = task_coin.amount.checked_sub(coin.amount).map_err(|_| {
StdError::generic_err("Not enough ibc balance for operation")
})?;
}
_ => {
return Err(StdError::generic_err("No balance found for operation"));
}
}
}
Ok(())
}
pub fn sub_cw20(&mut self, cw20: &Cw20CoinVerified) -> StdResult<()> {
match &mut self.cw20_balance {
Some(task_cw20) if task_cw20.address == cw20.address => {
task_cw20.amount = task_cw20
.amount
.checked_sub(cw20.amount)
.map_err(|_| StdError::generic_err("Not enough cw20 balance for operation"))?;
}
_ => {
return Err(StdError::GenericErr {
msg: "Not enough cw20 balance for operation".to_string(),
});
}
}
Ok(())
}
}
#[cw_serde]
pub struct Config {
pub owner_addr: Addr,
pub pause_admin: Addr,
pub croncat_factory_addr: Addr,
pub croncat_tasks_key: (String, [u8; 2]),
pub croncat_agents_key: (String, [u8; 2]),
pub agent_fee: u16,
pub treasury_fee: u16,
pub gas_price: GasPrice,
pub treasury_addr: Option<Addr>,
pub cw20_whitelist: Vec<Addr>,
pub native_denom: String,
pub limit: u64,
}
#[cw_serde]
pub struct UpdateConfig {
pub agent_fee: Option<u16>,
pub treasury_fee: Option<u16>,
pub gas_price: Option<GasPrice>,
pub croncat_tasks_key: Option<(String, [u8; 2])>,
pub croncat_agents_key: Option<(String, [u8; 2])>,
pub treasury_addr: Option<String>,
pub cw20_whitelist: Option<Vec<String>>,
}
#[cfg(test)]
mod test {
use cosmwasm_std::{coin, Addr, Coin, Uint128};
use cw20::Cw20CoinVerified;
use crate::SdkError;
use super::{GasPrice, TaskBalance};
#[test]
fn gas_price_validation() {
assert!(!GasPrice {
numerator: 0,
denominator: 1,
gas_adjustment_numerator: 1
}
.is_valid());
assert!(!GasPrice {
numerator: 1,
denominator: 0,
gas_adjustment_numerator: 1
}
.is_valid());
assert!(!GasPrice {
numerator: 1,
denominator: 1,
gas_adjustment_numerator: 0
}
.is_valid());
assert!(GasPrice {
numerator: 1,
denominator: 1,
gas_adjustment_numerator: 1
}
.is_valid());
}
#[test]
fn gas_price_calculate_test() {
let gas_price_wrapper = GasPrice::default();
let gas_price = 0.04;
let gas_adjustments = 1.5;
let gas = 200_000;
let expected = gas as f64 * gas_adjustments * gas_price;
assert_eq!(expected as u128, gas_price_wrapper.calculate(gas).unwrap());
let gas = 160_000;
let expected = gas as f64 * gas_adjustments * gas_price;
assert_eq!(expected as u128, gas_price_wrapper.calculate(gas).unwrap());
let gas = 1_234_000;
let expected = gas as f64 * gas_adjustments * gas_price;
assert_eq!(expected as u128, gas_price_wrapper.calculate(gas).unwrap());
let gas_price_wrapper = GasPrice {
numerator: 25,
denominator: 100,
gas_adjustment_numerator: 120,
};
let gas_price = 0.25;
let gas_adjustments = 1.2;
let gas = 200_000;
let expected = gas as f64 * gas_adjustments * gas_price;
assert_eq!(expected as u128, gas_price_wrapper.calculate(gas).unwrap());
let gas = 160_000;
let expected = gas as f64 * gas_adjustments * gas_price;
assert_eq!(expected as u128, gas_price_wrapper.calculate(gas).unwrap());
let gas = 1_234_000;
let expected = gas as f64 * gas_adjustments * gas_price;
assert_eq!(expected as u128, gas_price_wrapper.calculate(gas).unwrap());
}
#[test]
fn verify_enough_attached_ok_test() {
let native_balance = Uint128::from(100u64);
let cw20 = Cw20CoinVerified {
address: Addr::unchecked("addr"),
amount: Uint128::from(100u64),
};
let ibc_coin = coin(100, "ibc");
let task_balance = TaskBalance {
native_balance,
cw20_balance: None,
ibc_balance: None,
};
assert!(task_balance
.verify_enough_attached(Uint128::from(100u64), None, None, false, "denom")
.is_ok());
assert!(task_balance
.verify_enough_attached(Uint128::from(50u64), None, None, true, "denom")
.is_ok());
let task_balance = TaskBalance {
native_balance,
cw20_balance: Some(cw20.clone()),
ibc_balance: Some(ibc_coin.clone()),
};
assert!(task_balance
.verify_enough_attached(Uint128::from(100u64), None, None, false, "denom")
.is_err());
assert!(task_balance
.verify_enough_attached(Uint128::from(50u64), None, None, true, "denom")
.is_err());
assert!(task_balance
.verify_enough_attached(
Uint128::from(100u64),
Some(cw20),
Some(ibc_coin),
false,
"denom"
)
.is_ok());
assert!(task_balance
.verify_enough_attached(
Uint128::from(50u64),
Some(Cw20CoinVerified {
address: Addr::unchecked("addr"),
amount: Uint128::from(50u64),
}),
Some(coin(50, "ibc")),
true,
"denom"
)
.is_ok());
}
#[test]
fn verify_enough_attached_err_test() {
let native_balance = Uint128::from(100u64);
let cw20 = Cw20CoinVerified {
address: Addr::unchecked("addr"),
amount: Uint128::from(100u64),
};
let ibc_coin = coin(100, "ibc");
let task_balance = TaskBalance {
native_balance,
cw20_balance: None,
ibc_balance: None,
};
assert_eq!(
task_balance
.verify_enough_attached(Uint128::from(101u64), None, None, false, "denom")
.unwrap_err(),
SdkError::NotEnoughNative {
denom: "denom".to_owned(),
lack: 1u64.into(),
}
);
assert_eq!(
task_balance
.verify_enough_attached(native_balance, Some(cw20.clone()), None, false, "denom")
.unwrap_err(),
SdkError::NotEnoughCw20 {
addr: "addr".to_owned(),
lack: 100u64.into(),
}
);
assert_eq!(
task_balance
.verify_enough_attached(
native_balance,
None,
Some(ibc_coin.clone()),
false,
"denom"
)
.unwrap_err(),
SdkError::NotEnoughNative {
denom: "ibc".to_owned(),
lack: 100u64.into(),
}
);
let task_balance = TaskBalance {
native_balance,
cw20_balance: Some(cw20.clone()),
ibc_balance: Some(ibc_coin.clone()),
};
assert_eq!(
task_balance
.verify_enough_attached(
Uint128::from(100u64),
Some(Cw20CoinVerified {
address: Addr::unchecked("addr"),
amount: Uint128::from(101u64),
}),
Some(ibc_coin.clone()),
false,
"denom"
)
.unwrap_err(),
SdkError::NotEnoughCw20 {
addr: "addr".to_owned(),
lack: 1u64.into(),
}
);
assert_eq!(
task_balance
.verify_enough_attached(
Uint128::from(100u64),
Some(Cw20CoinVerified {
address: Addr::unchecked("addr2"),
amount: Uint128::from(100u64),
}),
Some(ibc_coin),
false,
"denom"
)
.unwrap_err(),
SdkError::NotEnoughCw20 {
addr: "addr2".to_owned(),
lack: 100u64.into(),
}
);
assert_eq!(
task_balance
.verify_enough_attached(
Uint128::from(100u64),
Some(cw20.clone()),
Some(coin(101, "ibc")),
false,
"denom"
)
.unwrap_err(),
SdkError::NotEnoughNative {
denom: "ibc".to_owned(),
lack: 1u64.into(),
}
);
assert_eq!(
task_balance
.verify_enough_attached(
Uint128::from(100u64),
Some(cw20),
Some(coin(100, "ibc2")),
false,
"denom"
)
.unwrap_err(),
SdkError::NotEnoughNative {
denom: "ibc2".to_owned(),
lack: 100u64.into(),
}
);
}
#[test]
fn sub_coin_test() {
let native_balance = Uint128::from(100u64);
let ibc_coin = coin(100, "ibc");
let mut task_balance = TaskBalance {
native_balance,
cw20_balance: None,
ibc_balance: Some(ibc_coin.clone()),
};
task_balance
.sub_coin(&coin(10, "native"), "native")
.unwrap();
assert_eq!(
task_balance,
TaskBalance {
native_balance: Uint128::from(90u64),
cw20_balance: None,
ibc_balance: Some(ibc_coin),
}
);
task_balance.sub_coin(&coin(1, "ibc"), "native").unwrap();
assert_eq!(
task_balance,
TaskBalance {
native_balance: Uint128::from(90u64),
cw20_balance: None,
ibc_balance: Some(coin(99, "ibc")),
}
);
assert!(task_balance
.sub_coin(&coin(91, "native"), "native")
.is_err());
assert!(task_balance.sub_coin(&coin(100, "ibc"), "native").is_err());
assert!(task_balance
.sub_coin(&coin(100, "wrong"), "native")
.is_err());
}
#[test]
fn sub_cw20_test() {
let native_balance = Uint128::from(100u64);
let cw20 = Cw20CoinVerified {
address: Addr::unchecked("addr"),
amount: Uint128::from(100u64),
};
let mut task_balance = TaskBalance {
native_balance,
cw20_balance: Some(cw20),
ibc_balance: None,
};
task_balance
.sub_cw20(&Cw20CoinVerified {
address: Addr::unchecked("addr"),
amount: Uint128::from(10u64),
})
.unwrap();
assert_eq!(
task_balance,
TaskBalance {
native_balance,
cw20_balance: Some(Cw20CoinVerified {
address: Addr::unchecked("addr"),
amount: Uint128::from(90u64),
}),
ibc_balance: None,
}
);
assert!(task_balance
.sub_cw20(&Cw20CoinVerified {
address: Addr::unchecked("addr"),
amount: Uint128::from(91u64),
})
.is_err());
assert!(task_balance
.sub_cw20(&Cw20CoinVerified {
address: Addr::unchecked("addr2"),
amount: Uint128::from(1u64),
})
.is_err());
}
#[test]
fn test_sub_coin_success() {
let native_denom = "native";
let ibc_denom = "ibc";
let mut task_balance = TaskBalance {
native_balance: Uint128::new(100),
cw20_balance: None,
ibc_balance: Some(Coin {
denom: ibc_denom.to_string(),
amount: Uint128::new(200),
}),
};
let coin_native = Coin {
denom: native_denom.to_string(),
amount: Uint128::new(50),
};
let coin_ibc = Coin {
denom: ibc_denom.to_string(),
amount: Uint128::new(100),
};
task_balance.sub_coin(&coin_native, native_denom).unwrap();
task_balance.sub_coin(&coin_ibc, native_denom).unwrap();
assert_eq!(task_balance.native_balance, Uint128::new(50));
assert_eq!(task_balance.ibc_balance.unwrap().amount, Uint128::new(100));
}
#[test]
fn test_sub_coin_overflow() {
let native_denom = "native";
let ibc_denom = "ibc";
let mut task_balance = TaskBalance {
native_balance: Uint128::new(100),
cw20_balance: None,
ibc_balance: Some(Coin {
denom: ibc_denom.to_string(),
amount: Uint128::new(200),
}),
};
let coin_native_overflow = Coin {
denom: native_denom.to_string(),
amount: Uint128::new(150),
};
let coin_ibc_overflow = Coin {
denom: ibc_denom.to_string(),
amount: Uint128::new(300),
};
let coin_nonexistent = Coin {
denom: "nonexistent".to_string(),
amount: Uint128::new(100),
};
assert!(task_balance
.sub_coin(&coin_native_overflow, native_denom)
.is_err());
assert!(task_balance
.sub_coin(&coin_ibc_overflow, native_denom)
.is_err());
assert!(task_balance
.sub_coin(&coin_nonexistent, native_denom)
.is_err());
}
#[test]
fn test_sub_cw20_success() {
let cw20_address = Addr::unchecked("cw20_address");
let mut task_balance = TaskBalance {
cw20_balance: Some(Cw20CoinVerified {
address: cw20_address.clone(),
amount: Uint128::from(100u128),
}),
native_balance: Uint128::zero(),
ibc_balance: None,
};
let cw20 = Cw20CoinVerified {
address: cw20_address,
amount: Uint128::from(50u128),
};
assert!(task_balance.sub_cw20(&cw20).is_ok());
assert_eq!(
task_balance.cw20_balance.unwrap().amount,
Uint128::from(50u128)
);
}
#[test]
fn test_sub_cw20_insufficient_balance() {
let cw20_address = Addr::unchecked("cw20_address");
let mut task_balance = TaskBalance {
cw20_balance: Some(Cw20CoinVerified {
address: cw20_address.clone(),
amount: Uint128::from(100u128),
}),
native_balance: Uint128::zero(),
ibc_balance: None,
};
let cw20 = Cw20CoinVerified {
address: cw20_address,
amount: Uint128::from(200u128),
};
assert!(task_balance.sub_cw20(&cw20).is_err());
}
#[test]
fn test_sub_cw20_address_not_found() {
let cw20_address = Addr::unchecked("cw20_address");
let cw20_address_2 = Addr::unchecked("cw20_address_2");
let mut task_balance = TaskBalance {
cw20_balance: Some(Cw20CoinVerified {
address: cw20_address,
amount: Uint128::from(100u128),
}),
native_balance: Uint128::zero(),
ibc_balance: None,
};
let cw20 = Cw20CoinVerified {
address: cw20_address_2,
amount: Uint128::from(50u128),
};
assert!(task_balance.sub_cw20(&cw20).is_err());
}
}