1use cosmwasm_schema::cw_serde;
2use cosmwasm_std::{Addr, Coin, StdError, StdResult, Uint128};
3use croncat_sdk_core::types::GasPrice;
4use cw20::Cw20CoinVerified;
5
6use crate::error::SdkError;
7
8#[cw_serde]
9pub struct TaskBalanceResponse {
10 pub balance: Option<TaskBalance>,
11}
12#[cw_serde]
13pub struct TaskBalance {
14 pub native_balance: Uint128,
15 pub cw20_balance: Option<Cw20CoinVerified>,
16 pub ibc_balance: Option<Coin>,
17}
18
19impl TaskBalance {
20 pub fn verify_enough_attached(
21 &self,
22 native_required: Uint128,
23 cw20_required: Option<Cw20CoinVerified>,
24 ibc_required: Option<Coin>,
25 recurring: bool,
26 native_denom: &str,
27 ) -> Result<(), SdkError> {
28 let multiplier = if recurring {
29 Uint128::new(2)
30 } else {
31 Uint128::new(1)
32 };
33 if self.native_balance < native_required * multiplier {
34 return Err(SdkError::NotEnoughNative {
35 denom: native_denom.to_owned(),
36 lack: native_required * multiplier - self.native_balance,
37 });
38 }
39 self.verify_enough_cw20(cw20_required, multiplier)?;
40 match (ibc_required, &self.ibc_balance) {
41 (Some(req), Some(attached)) => {
42 if req.denom != attached.denom {
43 return Err(SdkError::NotEnoughNative {
44 denom: req.denom,
45 lack: req.amount * multiplier,
46 });
47 }
48 if attached.amount < req.amount * multiplier {
49 return Err(SdkError::NotEnoughNative {
50 denom: req.denom,
51 lack: req.amount * multiplier - attached.amount,
52 });
53 }
54 }
55 (Some(req), None) => {
56 return Err(SdkError::NotEnoughNative {
57 denom: req.denom,
58 lack: req.amount * multiplier,
59 })
60 }
61 (None, Some(_)) => {
63 return Err(SdkError::NonRequiredDenom {});
64 }
65 (None, None) => (),
67 }
68 Ok(())
69 }
70
71 pub fn verify_enough_cw20(
72 &self,
73 cw20_required: Option<Cw20CoinVerified>,
74 multiplier: Uint128,
75 ) -> Result<(), SdkError> {
76 match (cw20_required, &self.cw20_balance) {
77 (Some(req), Some(attached)) => {
78 if req.address != attached.address {
79 return Err(SdkError::NotEnoughCw20 {
80 addr: req.address.into_string(),
81 lack: req.amount * multiplier,
82 });
83 }
84 if attached.amount < req.amount * multiplier {
85 return Err(SdkError::NotEnoughCw20 {
86 addr: req.address.into_string(),
87 lack: req.amount * multiplier - attached.amount,
88 });
89 }
90 Ok(())
91 }
92 (Some(req), None) => Err(SdkError::NotEnoughCw20 {
93 addr: req.address.into_string(),
94 lack: req.amount * multiplier,
95 }),
96 (None, Some(_)) => Err(SdkError::NonRequiredDenom {}),
98 (None, None) => Ok(()),
100 }
101 }
102
103 pub fn sub_coin(&mut self, coin: &Coin, native_denom: &str) -> StdResult<()> {
104 if coin.denom == native_denom {
105 self.native_balance = self
106 .native_balance
107 .checked_sub(coin.amount)
108 .map_err(|_| StdError::generic_err("Not enough native balance for operation"))?;
109 } else {
110 match &mut self.ibc_balance {
111 Some(task_coin) if task_coin.denom == coin.denom => {
112 task_coin.amount = task_coin.amount.checked_sub(coin.amount).map_err(|_| {
113 StdError::generic_err("Not enough ibc balance for operation")
114 })?;
115 }
116 _ => {
117 return Err(StdError::generic_err("No balance found for operation"));
118 }
119 }
120 }
121 Ok(())
122 }
123
124 pub fn sub_cw20(&mut self, cw20: &Cw20CoinVerified) -> StdResult<()> {
125 match &mut self.cw20_balance {
126 Some(task_cw20) if task_cw20.address == cw20.address => {
127 task_cw20.amount = task_cw20
129 .amount
130 .checked_sub(cw20.amount)
131 .map_err(|_| StdError::generic_err("Not enough cw20 balance for operation"))?;
132 }
133 _ => {
134 return Err(StdError::GenericErr {
137 msg: "Not enough cw20 balance for operation".to_string(),
138 });
139 }
140 }
141 Ok(())
142 }
143}
144
145#[cw_serde]
146pub struct Config {
147 pub owner_addr: Addr,
149
150 pub pause_admin: Addr,
154
155 pub croncat_factory_addr: Addr,
157
158 pub croncat_tasks_key: (String, [u8; 2]),
160 pub croncat_agents_key: (String, [u8; 2]),
162
163 pub agent_fee: u16,
165 pub treasury_fee: u16,
166 pub gas_price: GasPrice,
167
168 pub treasury_addr: Option<Addr>,
170 pub cw20_whitelist: Vec<Addr>,
171 pub native_denom: String,
172
173 pub limit: u64,
175}
176
177#[cw_serde]
178pub struct UpdateConfig {
179 pub agent_fee: Option<u16>,
180 pub treasury_fee: Option<u16>,
181 pub gas_price: Option<GasPrice>,
182 pub croncat_tasks_key: Option<(String, [u8; 2])>,
183 pub croncat_agents_key: Option<(String, [u8; 2])>,
184 pub treasury_addr: Option<String>,
185 pub cw20_whitelist: Option<Vec<String>>,
188}
189
190#[cfg(test)]
191mod test {
192 use cosmwasm_std::{coin, Addr, Coin, Uint128};
193 use cw20::Cw20CoinVerified;
194
195 use crate::SdkError;
196
197 use super::{GasPrice, TaskBalance};
198
199 #[test]
200 fn gas_price_validation() {
201 assert!(!GasPrice {
202 numerator: 0,
203 denominator: 1,
204 gas_adjustment_numerator: 1
205 }
206 .is_valid());
207
208 assert!(!GasPrice {
209 numerator: 1,
210 denominator: 0,
211 gas_adjustment_numerator: 1
212 }
213 .is_valid());
214
215 assert!(!GasPrice {
216 numerator: 1,
217 denominator: 1,
218 gas_adjustment_numerator: 0
219 }
220 .is_valid());
221
222 assert!(GasPrice {
223 numerator: 1,
224 denominator: 1,
225 gas_adjustment_numerator: 1
226 }
227 .is_valid());
228 }
229
230 #[test]
231 fn gas_price_calculate_test() {
232 let gas_price_wrapper = GasPrice::default();
234 let gas_price = 0.04;
235 let gas_adjustments = 1.5;
236
237 let gas = 200_000;
238 let expected = gas as f64 * gas_adjustments * gas_price;
239 assert_eq!(expected as u128, gas_price_wrapper.calculate(gas).unwrap());
240
241 let gas = 160_000;
242 let expected = gas as f64 * gas_adjustments * gas_price;
243 assert_eq!(expected as u128, gas_price_wrapper.calculate(gas).unwrap());
244
245 let gas = 1_234_000;
246 let expected = gas as f64 * gas_adjustments * gas_price;
247 assert_eq!(expected as u128, gas_price_wrapper.calculate(gas).unwrap());
248
249 let gas_price_wrapper = GasPrice {
251 numerator: 25,
252 denominator: 100,
253 gas_adjustment_numerator: 120,
254 };
255 let gas_price = 0.25;
256 let gas_adjustments = 1.2;
257
258 let gas = 200_000;
259 let expected = gas as f64 * gas_adjustments * gas_price;
260 assert_eq!(expected as u128, gas_price_wrapper.calculate(gas).unwrap());
261
262 let gas = 160_000;
263 let expected = gas as f64 * gas_adjustments * gas_price;
264 assert_eq!(expected as u128, gas_price_wrapper.calculate(gas).unwrap());
265
266 let gas = 1_234_000;
267 let expected = gas as f64 * gas_adjustments * gas_price;
268 assert_eq!(expected as u128, gas_price_wrapper.calculate(gas).unwrap());
269 }
270
271 #[test]
272 fn verify_enough_attached_ok_test() {
273 let native_balance = Uint128::from(100u64);
274 let cw20 = Cw20CoinVerified {
275 address: Addr::unchecked("addr"),
276 amount: Uint128::from(100u64),
277 };
278 let ibc_coin = coin(100, "ibc");
279
280 let task_balance = TaskBalance {
282 native_balance,
283 cw20_balance: None,
284 ibc_balance: None,
285 };
286 assert!(task_balance
287 .verify_enough_attached(Uint128::from(100u64), None, None, false, "denom")
288 .is_ok());
289 assert!(task_balance
290 .verify_enough_attached(Uint128::from(50u64), None, None, true, "denom")
291 .is_ok());
292
293 let task_balance = TaskBalance {
295 native_balance,
296 cw20_balance: Some(cw20.clone()),
297 ibc_balance: Some(ibc_coin.clone()),
298 };
299 assert!(task_balance
301 .verify_enough_attached(Uint128::from(100u64), None, None, false, "denom")
302 .is_err());
303 assert!(task_balance
304 .verify_enough_attached(Uint128::from(50u64), None, None, true, "denom")
305 .is_err());
306 assert!(task_balance
307 .verify_enough_attached(
308 Uint128::from(100u64),
309 Some(cw20),
310 Some(ibc_coin),
311 false,
312 "denom"
313 )
314 .is_ok());
315 assert!(task_balance
316 .verify_enough_attached(
317 Uint128::from(50u64),
318 Some(Cw20CoinVerified {
319 address: Addr::unchecked("addr"),
320 amount: Uint128::from(50u64),
321 }),
322 Some(coin(50, "ibc")),
323 true,
324 "denom"
325 )
326 .is_ok());
327 }
328
329 #[test]
330 fn verify_enough_attached_err_test() {
331 let native_balance = Uint128::from(100u64);
332 let cw20 = Cw20CoinVerified {
333 address: Addr::unchecked("addr"),
334 amount: Uint128::from(100u64),
335 };
336 let ibc_coin = coin(100, "ibc");
337
338 let task_balance = TaskBalance {
340 native_balance,
341 cw20_balance: None,
342 ibc_balance: None,
343 };
344 assert_eq!(
345 task_balance
346 .verify_enough_attached(Uint128::from(101u64), None, None, false, "denom")
347 .unwrap_err(),
348 SdkError::NotEnoughNative {
349 denom: "denom".to_owned(),
350 lack: 1u64.into(),
351 }
352 );
353 assert_eq!(
354 task_balance
355 .verify_enough_attached(native_balance, Some(cw20.clone()), None, false, "denom")
356 .unwrap_err(),
357 SdkError::NotEnoughCw20 {
358 addr: "addr".to_owned(),
359 lack: 100u64.into(),
360 }
361 );
362 assert_eq!(
363 task_balance
364 .verify_enough_attached(
365 native_balance,
366 None,
367 Some(ibc_coin.clone()),
368 false,
369 "denom"
370 )
371 .unwrap_err(),
372 SdkError::NotEnoughNative {
373 denom: "ibc".to_owned(),
374 lack: 100u64.into(),
375 }
376 );
377
378 let task_balance = TaskBalance {
380 native_balance,
381 cw20_balance: Some(cw20.clone()),
382 ibc_balance: Some(ibc_coin.clone()),
383 };
384 assert_eq!(
386 task_balance
387 .verify_enough_attached(
388 Uint128::from(100u64),
389 Some(Cw20CoinVerified {
390 address: Addr::unchecked("addr"),
391 amount: Uint128::from(101u64),
392 }),
393 Some(ibc_coin.clone()),
394 false,
395 "denom"
396 )
397 .unwrap_err(),
398 SdkError::NotEnoughCw20 {
399 addr: "addr".to_owned(),
400 lack: 1u64.into(),
401 }
402 );
403 assert_eq!(
405 task_balance
406 .verify_enough_attached(
407 Uint128::from(100u64),
408 Some(Cw20CoinVerified {
409 address: Addr::unchecked("addr2"),
410 amount: Uint128::from(100u64),
411 }),
412 Some(ibc_coin),
413 false,
414 "denom"
415 )
416 .unwrap_err(),
417 SdkError::NotEnoughCw20 {
418 addr: "addr2".to_owned(),
419 lack: 100u64.into(),
420 }
421 );
422 assert_eq!(
424 task_balance
425 .verify_enough_attached(
426 Uint128::from(100u64),
427 Some(cw20.clone()),
428 Some(coin(101, "ibc")),
429 false,
430 "denom"
431 )
432 .unwrap_err(),
433 SdkError::NotEnoughNative {
434 denom: "ibc".to_owned(),
435 lack: 1u64.into(),
436 }
437 );
438 assert_eq!(
440 task_balance
441 .verify_enough_attached(
442 Uint128::from(100u64),
443 Some(cw20),
444 Some(coin(100, "ibc2")),
445 false,
446 "denom"
447 )
448 .unwrap_err(),
449 SdkError::NotEnoughNative {
450 denom: "ibc2".to_owned(),
451 lack: 100u64.into(),
452 }
453 );
454 }
455
456 #[test]
457 fn sub_coin_test() {
458 let native_balance = Uint128::from(100u64);
459 let ibc_coin = coin(100, "ibc");
460
461 let mut task_balance = TaskBalance {
462 native_balance,
463 cw20_balance: None,
464 ibc_balance: Some(ibc_coin.clone()),
465 };
466
467 task_balance
468 .sub_coin(&coin(10, "native"), "native")
469 .unwrap();
470 assert_eq!(
471 task_balance,
472 TaskBalance {
473 native_balance: Uint128::from(90u64),
474 cw20_balance: None,
475 ibc_balance: Some(ibc_coin),
476 }
477 );
478
479 task_balance.sub_coin(&coin(1, "ibc"), "native").unwrap();
480 assert_eq!(
481 task_balance,
482 TaskBalance {
483 native_balance: Uint128::from(90u64),
484 cw20_balance: None,
485 ibc_balance: Some(coin(99, "ibc")),
486 }
487 );
488
489 assert!(task_balance
490 .sub_coin(&coin(91, "native"), "native")
491 .is_err());
492
493 assert!(task_balance.sub_coin(&coin(100, "ibc"), "native").is_err());
494
495 assert!(task_balance
496 .sub_coin(&coin(100, "wrong"), "native")
497 .is_err());
498 }
499
500 #[test]
501 fn sub_cw20_test() {
502 let native_balance = Uint128::from(100u64);
503 let cw20 = Cw20CoinVerified {
504 address: Addr::unchecked("addr"),
505 amount: Uint128::from(100u64),
506 };
507
508 let mut task_balance = TaskBalance {
509 native_balance,
510 cw20_balance: Some(cw20),
511 ibc_balance: None,
512 };
513
514 task_balance
515 .sub_cw20(&Cw20CoinVerified {
516 address: Addr::unchecked("addr"),
517 amount: Uint128::from(10u64),
518 })
519 .unwrap();
520 assert_eq!(
521 task_balance,
522 TaskBalance {
523 native_balance,
524 cw20_balance: Some(Cw20CoinVerified {
525 address: Addr::unchecked("addr"),
526 amount: Uint128::from(90u64),
527 }),
528 ibc_balance: None,
529 }
530 );
531
532 assert!(task_balance
533 .sub_cw20(&Cw20CoinVerified {
534 address: Addr::unchecked("addr"),
535 amount: Uint128::from(91u64),
536 })
537 .is_err());
538
539 assert!(task_balance
540 .sub_cw20(&Cw20CoinVerified {
541 address: Addr::unchecked("addr2"),
542 amount: Uint128::from(1u64),
543 })
544 .is_err());
545 }
546
547 #[test]
548 fn test_sub_coin_success() {
549 let native_denom = "native";
550 let ibc_denom = "ibc";
551
552 let mut task_balance = TaskBalance {
553 native_balance: Uint128::new(100),
554 cw20_balance: None,
555 ibc_balance: Some(Coin {
556 denom: ibc_denom.to_string(),
557 amount: Uint128::new(200),
558 }),
559 };
560
561 let coin_native = Coin {
562 denom: native_denom.to_string(),
563 amount: Uint128::new(50),
564 };
565
566 let coin_ibc = Coin {
567 denom: ibc_denom.to_string(),
568 amount: Uint128::new(100),
569 };
570
571 task_balance.sub_coin(&coin_native, native_denom).unwrap();
572 task_balance.sub_coin(&coin_ibc, native_denom).unwrap();
573
574 assert_eq!(task_balance.native_balance, Uint128::new(50));
575 assert_eq!(task_balance.ibc_balance.unwrap().amount, Uint128::new(100));
576 }
577
578 #[test]
579 fn test_sub_coin_overflow() {
580 let native_denom = "native";
581 let ibc_denom = "ibc";
582
583 let mut task_balance = TaskBalance {
584 native_balance: Uint128::new(100),
585 cw20_balance: None,
586 ibc_balance: Some(Coin {
587 denom: ibc_denom.to_string(),
588 amount: Uint128::new(200),
589 }),
590 };
591
592 let coin_native_overflow = Coin {
593 denom: native_denom.to_string(),
594 amount: Uint128::new(150),
595 };
596
597 let coin_ibc_overflow = Coin {
598 denom: ibc_denom.to_string(),
599 amount: Uint128::new(300),
600 };
601
602 let coin_nonexistent = Coin {
603 denom: "nonexistent".to_string(),
604 amount: Uint128::new(100),
605 };
606
607 assert!(task_balance
608 .sub_coin(&coin_native_overflow, native_denom)
609 .is_err());
610 assert!(task_balance
611 .sub_coin(&coin_ibc_overflow, native_denom)
612 .is_err());
613 assert!(task_balance
614 .sub_coin(&coin_nonexistent, native_denom)
615 .is_err());
616 }
617
618 #[test]
619 fn test_sub_cw20_success() {
620 let cw20_address = Addr::unchecked("cw20_address");
621 let mut task_balance = TaskBalance {
622 cw20_balance: Some(Cw20CoinVerified {
623 address: cw20_address.clone(),
624 amount: Uint128::from(100u128),
625 }),
626 native_balance: Uint128::zero(),
627 ibc_balance: None,
628 };
629
630 let cw20 = Cw20CoinVerified {
631 address: cw20_address,
632 amount: Uint128::from(50u128),
633 };
634
635 assert!(task_balance.sub_cw20(&cw20).is_ok());
636 assert_eq!(
637 task_balance.cw20_balance.unwrap().amount,
638 Uint128::from(50u128)
639 );
640 }
641
642 #[test]
643 fn test_sub_cw20_insufficient_balance() {
644 let cw20_address = Addr::unchecked("cw20_address");
645 let mut task_balance = TaskBalance {
646 cw20_balance: Some(Cw20CoinVerified {
647 address: cw20_address.clone(),
648 amount: Uint128::from(100u128),
649 }),
650 native_balance: Uint128::zero(),
651 ibc_balance: None,
652 };
653
654 let cw20 = Cw20CoinVerified {
655 address: cw20_address,
656 amount: Uint128::from(200u128),
657 };
658
659 assert!(task_balance.sub_cw20(&cw20).is_err());
660 }
661
662 #[test]
663 fn test_sub_cw20_address_not_found() {
664 let cw20_address = Addr::unchecked("cw20_address");
665 let cw20_address_2 = Addr::unchecked("cw20_address_2");
666 let mut task_balance = TaskBalance {
667 cw20_balance: Some(Cw20CoinVerified {
668 address: cw20_address,
669 amount: Uint128::from(100u128),
670 }),
671 native_balance: Uint128::zero(),
672 ibc_balance: None,
673 };
674
675 let cw20 = Cw20CoinVerified {
676 address: cw20_address_2,
677 amount: Uint128::from(50u128),
678 };
679
680 assert!(task_balance.sub_cw20(&cw20).is_err());
681 }
682}