1use andromeda_std::{
2 amp::recipient::Recipient,
3 andr_exec, andr_instantiate, andr_instantiate_modules, andr_query,
4 common::{merge_coins, MillisecondsExpiration},
5 error::ContractError,
6};
7use cosmwasm_schema::{cw_serde, QueryResponses};
8use cosmwasm_std::{ensure, Api, BlockInfo, Coin};
9
10#[cw_serde]
11pub enum EscrowCondition {
13 Expiration(MillisecondsExpiration),
15 MinimumFunds(Vec<Coin>),
17}
18
19#[cw_serde]
20pub struct Escrow {
22 pub coins: Vec<Coin>,
24 pub condition: Option<EscrowCondition>,
26 pub recipient: Recipient,
28 pub recipient_addr: String,
30}
31
32impl Escrow {
33 pub fn validate(&self, api: &dyn Api, block: &BlockInfo) -> Result<(), ContractError> {
39 ensure!(
40 !self.coins.is_empty(),
41 ContractError::InvalidFunds {
42 msg: "At least one coin should be sent".to_string(),
43 }
44 );
45 ensure!(
46 api.addr_validate(&self.recipient_addr).is_ok(),
47 ContractError::InvalidAddress {}
48 );
49
50 if let Some(EscrowCondition::MinimumFunds(funds)) = &self.condition {
51 ensure!(
52 !funds.is_empty(),
53 ContractError::InvalidFunds {
54 msg: "Minumum funds must not be empty".to_string(),
55 }
56 );
57 let mut funds: Vec<Coin> = funds.clone();
58 funds.sort_by(|a, b| a.denom.cmp(&b.denom));
59 for i in 0..funds.len() - 1 {
60 ensure!(
61 funds[i].denom != funds[i + 1].denom,
62 ContractError::DuplicateCoinDenoms {}
63 );
64 }
65 return Ok(());
69 }
70
71 ensure!(
72 self.is_locked(block)? || self.condition.is_none(),
73 ContractError::ExpirationInPast {}
74 );
75 Ok(())
76 }
77
78 pub fn is_locked(&self, block: &BlockInfo) -> Result<bool, ContractError> {
80 match &self.condition {
81 None => Ok(false),
82 Some(condition) => match condition {
83 EscrowCondition::Expiration(expiration) => Ok(!expiration.is_in_past(block)),
84 EscrowCondition::MinimumFunds(funds) => {
85 Ok(!self.min_funds_deposited(funds.clone()))
86 }
87 },
88 }
89 }
90
91 fn min_funds_deposited(&self, required_funds: Vec<Coin>) -> bool {
94 required_funds.iter().all(|required_coin| {
95 self.coins.iter().any(|deposited_coin| {
96 deposited_coin.denom == required_coin.denom
97 && required_coin.amount <= deposited_coin.amount
98 })
99 })
100 }
101
102 pub fn add_funds(&mut self, coins_to_add: Vec<Coin>) {
112 self.coins = merge_coins(self.coins.to_vec(), coins_to_add);
113 }
114}
115
116#[andr_instantiate]
117#[andr_instantiate_modules]
118#[cw_serde]
119pub struct InstantiateMsg {}
120
121#[andr_exec]
122#[cw_serde]
123pub enum ExecuteMsg {
124 HoldFunds {
126 condition: Option<EscrowCondition>,
127 recipient: Option<Recipient>,
128 },
129 ReleaseFunds {
131 recipient_addr: Option<String>,
132 start_after: Option<String>,
133 limit: Option<u32>,
134 },
135 ReleaseSpecificFunds {
136 owner: String,
137 recipient_addr: Option<String>,
138 },
139}
140#[andr_query]
141#[cw_serde]
142#[derive(QueryResponses)]
143pub enum QueryMsg {
144 #[returns(GetLockedFundsResponse)]
146 GetLockedFunds { owner: String, recipient: String },
147 #[returns(GetLockedFundsForRecipientResponse)]
149 GetLockedFundsForRecipient {
150 recipient: String,
151 start_after: Option<String>,
152 limit: Option<u32>,
153 },
154}
155
156#[cw_serde]
157#[serde(rename_all = "snake_case")]
158pub struct GetLockedFundsResponse {
159 pub funds: Option<Escrow>,
160}
161
162#[cw_serde]
163#[serde(rename_all = "snake_case")]
164pub struct GetLockedFundsForRecipientResponse {
165 pub funds: Vec<Escrow>,
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use andromeda_std::common::Milliseconds;
172 use cosmwasm_std::testing::mock_dependencies;
173 use cosmwasm_std::{coin, Timestamp};
174
175 #[test]
176 fn test_validate() {
177 let deps = mock_dependencies();
178 let condition = EscrowCondition::Expiration(Milliseconds::from_seconds(101));
179 let coins = vec![coin(100u128, "uluna")];
180 let recipient = Recipient::from_string("owner");
181
182 let valid_escrow = Escrow {
183 recipient: recipient.clone(),
184 coins: coins.clone(),
185 condition: Some(condition.clone()),
186 recipient_addr: "owner".to_string(),
187 };
188 let block = BlockInfo {
189 height: 1000,
190 time: Timestamp::from_seconds(100),
191 chain_id: "foo".to_string(),
192 };
193 valid_escrow.validate(deps.as_ref().api, &block).unwrap();
194
195 let valid_escrow = Escrow {
196 recipient: recipient.clone(),
197 coins: coins.clone(),
198 condition: None,
199 recipient_addr: "owner".to_string(),
200 };
201 let block = BlockInfo {
202 height: 1000,
203 time: Timestamp::from_seconds(3333),
204 chain_id: "foo".to_string(),
205 };
206 valid_escrow.validate(deps.as_ref().api, &block).unwrap();
207
208 let invalid_recipient_escrow = Escrow {
209 recipient: Recipient::from_string(String::default()),
210 coins: coins.clone(),
211 condition: Some(condition.clone()),
212 recipient_addr: String::default(),
213 };
214
215 let resp = invalid_recipient_escrow
216 .validate(deps.as_ref().api, &block)
217 .unwrap_err();
218 assert_eq!(ContractError::InvalidAddress {}, resp);
219
220 let invalid_coins_escrow = Escrow {
221 recipient: recipient.clone(),
222 coins: vec![],
223 condition: Some(condition),
224 recipient_addr: "owner".to_string(),
225 };
226
227 let resp = invalid_coins_escrow
228 .validate(deps.as_ref().api, &block)
229 .unwrap_err();
230 assert_eq!(
231 ContractError::InvalidFunds {
232 msg: "At least one coin should be sent".to_string()
233 },
234 resp
235 );
236
237 let invalid_time_escrow = Escrow {
238 recipient,
239 coins,
240 condition: Some(EscrowCondition::Expiration(Milliseconds::from_seconds(0))),
241 recipient_addr: "owner".to_string(),
242 };
243 let block = BlockInfo {
244 height: 1000,
245 time: Timestamp::from_seconds(1),
246 chain_id: "foo".to_string(),
247 };
248 assert_eq!(
249 ContractError::ExpirationInPast {},
250 invalid_time_escrow
251 .validate(deps.as_ref().api, &block)
252 .unwrap_err()
253 );
254 }
255
256 #[test]
257 fn test_validate_funds_condition() {
258 let deps = mock_dependencies();
259 let recipient = Recipient::from_string("owner");
260
261 let valid_escrow = Escrow {
262 recipient: recipient.clone(),
263 coins: vec![coin(100, "uluna")],
264 condition: Some(EscrowCondition::MinimumFunds(vec![
265 coin(100, "uusd"),
266 coin(100, "uluna"),
267 ])),
268 recipient_addr: "owner".to_string(),
269 };
270 let block = BlockInfo {
271 height: 1000,
272 time: Timestamp::from_seconds(4444),
273 chain_id: "foo".to_string(),
274 };
275 valid_escrow.validate(deps.as_ref().api, &block).unwrap();
276
277 let valid_escrow = Escrow {
279 recipient: recipient.clone(),
280 coins: vec![coin(200, "uluna")],
281 condition: Some(EscrowCondition::MinimumFunds(vec![coin(100, "uluna")])),
282 recipient_addr: "owner".to_string(),
283 };
284 valid_escrow.validate(deps.as_ref().api, &block).unwrap();
285
286 let invalid_escrow = Escrow {
288 recipient: recipient.clone(),
289 coins: vec![coin(100, "uluna")],
290 condition: Some(EscrowCondition::MinimumFunds(vec![])),
291 recipient_addr: "owner".to_string(),
292 };
293 assert_eq!(
294 ContractError::InvalidFunds {
295 msg: "Minumum funds must not be empty".to_string(),
296 },
297 invalid_escrow
298 .validate(deps.as_ref().api, &block)
299 .unwrap_err()
300 );
301
302 let invalid_escrow = Escrow {
304 recipient,
305 coins: vec![coin(100, "uluna")],
306 condition: Some(EscrowCondition::MinimumFunds(vec![
307 coin(100, "uusd"),
308 coin(100, "uluna"),
309 coin(200, "uusd"),
310 ])),
311 recipient_addr: "owner".to_string(),
312 };
313 assert_eq!(
314 ContractError::DuplicateCoinDenoms {},
315 invalid_escrow
316 .validate(deps.as_ref().api, &block)
317 .unwrap_err()
318 );
319 }
320
321 #[test]
322 fn test_min_funds_deposited() {
323 let recipient = Recipient::from_string("owner");
324 let escrow = Escrow {
325 recipient: recipient.clone(),
326 coins: vec![coin(100, "uluna")],
327 condition: None,
328 recipient_addr: "owner".to_string(),
329 };
330 assert!(!escrow.min_funds_deposited(vec![coin(100, "uusd")]));
331
332 let escrow = Escrow {
333 recipient: recipient.clone(),
334 coins: vec![coin(100, "uluna")],
335 condition: None,
336 recipient_addr: "owner".to_string(),
337 };
338 assert!(!escrow.min_funds_deposited(vec![coin(100, "uusd"), coin(100, "uluna")]));
339
340 let escrow = Escrow {
341 recipient: recipient.clone(),
342 coins: vec![coin(100, "uluna")],
343 condition: None,
344 recipient_addr: "owner".to_string(),
345 };
346 assert!(escrow.min_funds_deposited(vec![coin(100, "uluna")]));
347
348 let escrow = Escrow {
349 recipient,
350 coins: vec![coin(200, "uluna")],
351 condition: None,
352 recipient_addr: "owner".to_string(),
353 };
354 assert!(escrow.min_funds_deposited(vec![coin(100, "uluna")]));
355 }
356
357 #[test]
358 fn test_add_funds() {
359 let mut escrow = Escrow {
360 coins: vec![coin(100, "uusd"), coin(100, "uluna")],
361 condition: None,
362 recipient: Recipient::from_string(""),
363 recipient_addr: "".to_string(),
364 };
365 let funds_to_add = vec![coin(25, "uluna"), coin(50, "uusd"), coin(100, "ucad")];
366
367 escrow.add_funds(funds_to_add);
368 assert_eq!(
369 vec![coin(150, "uusd"), coin(125, "uluna"), coin(100, "ucad")],
370 escrow.coins
371 );
372 }
373}