1use cosmwasm_std::{
2 coins, from_binary, to_binary, Addr, BankMsg, Deps, DepsMut, MessageInfo, Order, Response,
3 StdError, StdResult, Storage, Uint128, WasmMsg,
4};
5use croncat_sdk_core::types::GasPrice;
6use croncat_sdk_manager::types::Config;
7use cw20::{Cw20Coin, Cw20CoinVerified, Cw20ExecuteMsg, Cw20ReceiveMsg};
8
9use crate::{
10 helpers::{check_if_sender_is_task_owner, check_ready_for_execution, gas_fee, get_tasks_addr},
11 msg::ReceiveMsg,
12 state::{AGENT_REWARDS, CONFIG, PAUSED, TASKS_BALANCES, TEMP_BALANCES_CW20, TREASURY_BALANCE},
13 ContractError,
14};
15
16pub(crate) fn add_user_cw20(
17 storage: &mut dyn Storage,
18 user_addr: &Addr,
19 cw20: &Cw20CoinVerified,
20) -> StdResult<Uint128> {
21 let new_bal = TEMP_BALANCES_CW20.update(
22 storage,
23 (user_addr, &cw20.address),
24 |bal| -> StdResult<Uint128> {
25 let bal = bal.unwrap_or_default();
26 Ok(bal.checked_add(cw20.amount)?)
27 },
28 )?;
29 Ok(new_bal)
30}
31
32pub(crate) fn sub_user_cw20(
33 storage: &mut dyn Storage,
34 user_addr: &Addr,
35 cw20: &Cw20CoinVerified,
36) -> Result<Uint128, ContractError> {
37 let current_balance = TEMP_BALANCES_CW20.may_load(storage, (user_addr, &cw20.address))?;
38 let new_bal = if let Some(bal) = current_balance {
39 bal.checked_sub(cw20.amount).map_err(StdError::overflow)?
40 } else {
41 return Err(ContractError::EmptyBalance {});
42 };
43
44 if new_bal.is_zero() {
45 TEMP_BALANCES_CW20.remove(storage, (user_addr, &cw20.address));
46 } else {
47 TEMP_BALANCES_CW20.save(storage, (user_addr, &cw20.address), &new_bal)?;
48 }
49 Ok(new_bal)
50}
51
52pub(crate) fn add_fee_rewards(
58 storage: &mut dyn Storage,
59 gas: u64,
60 gas_price: &GasPrice,
61 agent_addr: &Addr,
62 agent_fee: u16,
63 treasury_fee: u16,
64 reimburse_only: bool,
65) -> Result<(), ContractError> {
66 AGENT_REWARDS.update(
67 storage,
68 agent_addr,
69 |agent_balance| -> Result<_, ContractError> {
70 let gas_fee = if reimburse_only {
72 gas
73 } else {
74 gas_fee(gas, agent_fee.into())? + gas
75 };
76 let amount: Uint128 = gas_price.calculate(gas_fee).unwrap().into();
77 Ok(agent_balance.unwrap_or_default().saturating_add(amount))
78 },
79 )?;
80
81 if !reimburse_only {
82 TREASURY_BALANCE.update(storage, |balance| -> Result<_, ContractError> {
83 let gas_fee = gas_fee(gas, treasury_fee.into())?;
84 let amount: Uint128 = gas_price.calculate(gas_fee).unwrap().into();
85 Ok(balance.saturating_add(amount))
86 })?;
87 }
88
89 Ok(())
90}
91
92pub fn execute_receive_cw20(
100 deps: DepsMut,
101 info: MessageInfo,
102 wrapper: Cw20ReceiveMsg,
103) -> Result<Response, ContractError> {
104 let msg: ReceiveMsg = from_binary(&wrapper.msg)?;
105 let paused = PAUSED.load(deps.storage)?;
106 check_ready_for_execution(&info, paused)?;
107 let config = CONFIG.load(deps.storage)?;
108
109 let sender = deps.api.addr_validate(&wrapper.sender)?;
110 let coin_addr = info.sender;
111 if !config.cw20_whitelist.contains(&coin_addr) {
112 return Err(ContractError::NotSupportedCw20 {});
113 }
114
115 let cw20_verified = Cw20CoinVerified {
116 address: coin_addr,
117 amount: wrapper.amount,
118 };
119
120 match msg {
121 ReceiveMsg::RefillTempBalance {} => {
122 let user_cw20_balance = add_user_cw20(deps.storage, &sender, &cw20_verified)?;
123 Ok(Response::new()
124 .add_attribute("action", "receive_cw20")
125 .add_attribute("cw20_received", cw20_verified.to_string())
126 .add_attribute("user_cw20_balance", user_cw20_balance))
127 }
128 ReceiveMsg::RefillTaskBalance { task_hash } => {
129 let tasks_addr = get_tasks_addr(&deps.querier, &config)?;
131 check_if_sender_is_task_owner(&deps.querier, &tasks_addr, &sender, &task_hash)?;
132
133 let mut task_balances = TASKS_BALANCES
134 .may_load(deps.storage, task_hash.as_bytes())?
135 .ok_or(ContractError::NoTaskHash {})?;
136 let mut balance = task_balances
137 .cw20_balance
138 .ok_or(ContractError::InvalidAttachedCoins {})?;
139 if balance.address != cw20_verified.address {
140 return Err(ContractError::InvalidAttachedCoins {});
141 }
142 balance.amount += cw20_verified.amount;
143 task_balances.cw20_balance = Some(balance);
144 TASKS_BALANCES.save(deps.storage, task_hash.as_bytes(), &task_balances)?;
145 Ok(Response::new()
146 .add_attribute("action", "receive_cw20")
147 .add_attribute("cw20_received", cw20_verified.to_string())
148 .add_attribute(
149 "task_cw20_balance",
150 task_balances.cw20_balance.unwrap().amount,
151 ))
152 }
153 }
154}
155
156pub fn execute_refill_task_cw20(
157 deps: DepsMut,
158 info: MessageInfo,
159 task_hash: String,
160 cw20: Cw20Coin,
161) -> Result<Response, ContractError> {
162 let paused = PAUSED.load(deps.storage)?;
163 check_ready_for_execution(&info, paused)?;
164 let config = CONFIG.load(deps.storage)?;
165
166 let tasks_addr = get_tasks_addr(&deps.querier, &config)?;
168 check_if_sender_is_task_owner(&deps.querier, &tasks_addr, &info.sender, &task_hash)?;
169
170 let cw20_verified = Cw20CoinVerified {
171 address: deps.api.addr_validate(&cw20.address)?,
172 amount: cw20.amount,
173 };
174
175 sub_user_cw20(deps.storage, &info.sender, &cw20_verified)?;
176 let mut task_balances = TASKS_BALANCES
177 .may_load(deps.storage, task_hash.as_bytes())?
178 .ok_or(ContractError::NoTaskHash {})?;
179 let mut balance = task_balances
180 .cw20_balance
181 .ok_or(ContractError::InvalidAttachedCoins {})?;
182 if balance.address != cw20_verified.address {
183 return Err(ContractError::InvalidAttachedCoins {});
184 }
185 balance.amount += cw20_verified.amount;
186 task_balances.cw20_balance = Some(balance);
187 TASKS_BALANCES.save(deps.storage, task_hash.as_bytes(), &task_balances)?;
188
189 Ok(Response::new()
190 .add_attribute("action", "refill_task_cw20")
191 .add_attribute("cw20_refilled", cw20_verified.to_string())
192 .add_attribute(
193 "task_cw20_balance",
194 task_balances.cw20_balance.unwrap().to_string(),
195 ))
196}
197
198pub fn execute_user_withdraw(
210 deps: DepsMut,
211 info: MessageInfo,
212 limit: Option<u64>,
213) -> Result<Response, ContractError> {
214 let paused = PAUSED.load(deps.storage)?;
215 check_ready_for_execution(&info, paused)?;
216 let config = CONFIG.load(deps.storage)?;
217 let limit = limit.unwrap_or(config.limit);
218 let user_addr = info.sender;
219 let withdraws: Vec<Cw20CoinVerified> = TEMP_BALANCES_CW20
220 .prefix(&user_addr)
221 .range(deps.storage, None, None, Order::Ascending)
222 .take(limit as usize)
223 .map(|cw20_res| cw20_res.map(|(address, amount)| Cw20CoinVerified { address, amount }))
224 .collect::<StdResult<_>>()?;
225 if withdraws.is_empty() {
226 return Err(ContractError::EmptyBalance {});
227 }
228 for cw20 in withdraws.iter() {
230 sub_user_cw20(deps.storage, &user_addr, cw20)?;
231 }
232
233 let msgs = {
234 let mut msgs = Vec::with_capacity(withdraws.len());
235 for wd in withdraws {
236 msgs.push(WasmMsg::Execute {
237 contract_addr: wd.address.to_string(),
238 msg: to_binary(&Cw20ExecuteMsg::Transfer {
239 recipient: user_addr.to_string(),
240 amount: wd.amount,
241 })?,
242 funds: vec![],
243 });
244 }
245 msgs
246 };
247
248 Ok(Response::new()
249 .add_attribute("action", "user_withdraw")
250 .add_messages(msgs))
251}
252
253pub fn execute_owner_withdraw(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
256 let config = CONFIG.load(deps.storage)?;
257 if info.sender != config.owner_addr {
258 return Err(ContractError::Unauthorized {});
259 }
260 let address = config.treasury_addr.unwrap_or(config.owner_addr);
261
262 let withdraw = TREASURY_BALANCE.load(deps.storage)?;
263 TREASURY_BALANCE.save(deps.storage, &Uint128::zero())?;
264
265 if withdraw.is_zero() {
266 Err(ContractError::EmptyBalance {})
267 } else {
268 let bank_msg = BankMsg::Send {
269 to_address: address.into_string(),
270 amount: coins(withdraw.u128(), config.native_denom),
271 };
272 Ok(Response::new()
273 .add_attribute("action", "owner_withdraw")
274 .add_message(bank_msg))
275 }
276}
277
278pub fn execute_refill_native_balance(
279 deps: DepsMut,
280 info: MessageInfo,
281 task_hash: String,
282) -> Result<Response, ContractError> {
283 if PAUSED.load(deps.storage)? {
284 return Err(ContractError::ContractPaused {});
285 }
286 let config: Config = CONFIG.load(deps.storage)?;
287 let tasks_addr = get_tasks_addr(&deps.querier, &config)?;
289 check_if_sender_is_task_owner(&deps.querier, &tasks_addr, &info.sender, &task_hash)?;
290
291 let mut task_balances = TASKS_BALANCES
292 .may_load(deps.storage, task_hash.as_bytes())?
293 .ok_or(ContractError::NoTaskHash {})?;
294
295 if info.funds.len() > 2 {
296 return Err(ContractError::InvalidAttachedCoins {});
297 }
298 for coin in info.funds {
299 if coin.denom == config.native_denom {
300 task_balances.native_balance += coin.amount
301 } else {
302 let mut ibc = task_balances
303 .ibc_balance
304 .ok_or(ContractError::InvalidAttachedCoins {})?;
305 if ibc.denom != coin.denom {
306 return Err(ContractError::InvalidAttachedCoins {});
307 }
308 ibc.amount += coin.amount;
309 task_balances.ibc_balance = Some(ibc);
310 }
311 }
312 TASKS_BALANCES.save(deps.storage, task_hash.as_bytes(), &task_balances)?;
313 Ok(Response::new().add_attribute("action", "refill_native_balance"))
314}
315
316pub fn query_users_balances(
322 deps: Deps,
323 address: String,
324 from_index: Option<u64>,
325 limit: Option<u64>,
326) -> StdResult<Vec<Cw20CoinVerified>> {
327 let config = CONFIG.load(deps.storage)?;
328 let addr = deps.api.addr_validate(&address)?;
329 let from_index = from_index.unwrap_or_default();
330 let limit = limit.unwrap_or(config.limit);
331
332 let cw20_balance = TEMP_BALANCES_CW20
333 .prefix(&addr)
334 .range(deps.storage, None, None, Order::Ascending)
335 .skip(from_index as usize)
336 .take(limit as usize)
337 .map(|balance_res| {
338 balance_res.map(|(addr, amount)| Cw20CoinVerified {
339 address: addr,
340 amount,
341 })
342 })
343 .collect::<StdResult<_>>()?;
344
345 Ok(cw20_balance)
346}