1use std::cmp::min;
4use std::collections::HashMap;
5use std::convert::TryInto;
6
7use ergotree_ir::chain::ergo_box::box_value::BoxValue;
8use ergotree_ir::chain::ergo_box::BoxTokens;
9use ergotree_ir::chain::ergo_box::ErgoBox;
10use ergotree_ir::chain::token::Token;
11use ergotree_ir::chain::token::TokenAmount;
12use ergotree_ir::chain::token::TokenAmountError;
13use ergotree_ir::chain::token::TokenId;
14use thiserror::Error;
15
16use crate::wallet::box_selector::sum_tokens;
17use crate::wallet::box_selector::sum_tokens_from_boxes;
18use crate::wallet::box_selector::sum_value;
19use crate::wallet::box_selector::ErgoBoxAssetsData;
20
21use super::sum_tokens_from_hashmaps;
22use super::BoxSelectorError;
23use super::ErgoBoxAssets;
24use super::{BoxSelection, BoxSelector};
25
26#[derive(Default)]
28pub struct SimpleBoxSelector {}
29
30impl SimpleBoxSelector {
31 pub fn new() -> Self {
33 SimpleBoxSelector {}
34 }
35}
36
37impl<T: ErgoBoxAssets + Clone> BoxSelector<T> for SimpleBoxSelector {
38 fn select(
44 &self,
45 inputs: Vec<T>,
46 target_balance: BoxValue,
47 target_tokens: &[Token],
48 ) -> Result<BoxSelection<T>, BoxSelectorError> {
49 let mut selected_inputs: Vec<T> = vec![];
50 let mut selected_boxes_value: u64 = 0;
51 let target_balance_original = target_balance;
52 let target_balance: u64 = target_balance.into();
53 let mut target_tokens_left: HashMap<TokenId, TokenAmount> =
55 sum_tokens(Some(target_tokens))?;
56 let mut has_value_change = false;
57 let mut has_token_change = false;
58 let mut sorted_inputs = inputs;
59 sorted_inputs.sort_by(|a, b| {
60 let a_target_tokens_count = a
61 .tokens()
62 .into_iter()
63 .flatten()
64 .filter(|t| target_tokens_left.contains_key(&t.token_id))
65 .count();
66 let b_target_tokens_count = b
67 .tokens()
68 .into_iter()
69 .flatten()
70 .filter(|t| target_tokens_left.contains_key(&t.token_id))
71 .count();
72 a_target_tokens_count.cmp(&b_target_tokens_count)
73 });
74 sorted_inputs.reverse();
76 for b in sorted_inputs {
77 let value_change_amt: u64 = if target_balance > selected_boxes_value {
78 0
79 } else {
80 selected_boxes_value - target_balance
81 };
82 if target_balance > selected_boxes_value
83 || (has_value_change || has_token_change)
84 && (value_change_amt < *BoxValue::SAFE_USER_MIN.as_u64())
85 || (!target_tokens_left.is_empty()
86 && b.tokens()
87 .into_iter()
88 .flatten()
89 .any(|t| target_tokens_left.contains_key(&t.token_id)))
90 {
91 selected_boxes_value += u64::from(b.value());
92 if selected_boxes_value > target_balance {
93 has_value_change = true;
94 }
95 let mut selected_tokens_from_this_box: HashMap<TokenId, TokenAmount> =
96 HashMap::new();
97 b.tokens()
98 .into_iter()
99 .flatten()
100 .try_for_each::<_, Result<(), BoxSelectorError>>(|t| {
101 if let Some(token_amount_left_to_select) =
102 target_tokens_left.get(&t.token_id).cloned()
103 {
104 let token_amount_in_box = t.amount;
105 if token_amount_left_to_select <= token_amount_in_box {
106 target_tokens_left.remove(&t.token_id);
107 } else if let Some(amt) = target_tokens_left.get_mut(&t.token_id) {
108 *amt = amt.checked_sub(&token_amount_in_box)?;
109 }
110
111 let selected_token_amt =
112 min(token_amount_in_box, token_amount_left_to_select);
113 if let Some(amt) = selected_tokens_from_this_box.get_mut(&t.token_id) {
114 *amt = amt.checked_add(&selected_token_amt)?;
115 } else {
116 selected_tokens_from_this_box
117 .insert(t.token_id, selected_token_amt);
118 }
119 }
120 Ok(())
121 })?;
122 if sum_tokens(b.tokens().as_ref().map(BoxTokens::as_ref))?
123 != selected_tokens_from_this_box
124 {
125 has_token_change = true;
126 };
127 selected_inputs.push(b);
128 };
129 }
130 if selected_boxes_value < target_balance {
131 return Err(BoxSelectorError::NotEnoughCoins(
132 target_balance - selected_boxes_value,
133 ));
134 }
135 if !target_tokens.is_empty() && !target_tokens_left.is_empty() {
136 return Err(BoxSelectorError::NotEnoughTokens(
137 target_tokens_left.into_iter().map(Token::from).collect(),
138 ));
139 }
140 let change_boxes: Vec<ErgoBoxAssetsData> = if !has_value_change && !has_token_change {
141 vec![]
142 } else {
143 let change_value: BoxValue = (selected_boxes_value - target_balance)
144 .try_into()
145 .map_err(|e| {
146 NotEnoughCoinsForChangeBox(format!(
147 "change box value {} is too small, error: {} ",
148 selected_boxes_value - target_balance,
149 e
150 ))
151 })?;
152 let mut change_tokens = sum_tokens_from_boxes(selected_inputs.as_slice())?;
153 target_tokens.iter().try_for_each(|t| {
154 match change_tokens.get(&t.token_id).cloned() {
155 Some(selected_boxes_t_amt) if selected_boxes_t_amt == t.amount => {
156 change_tokens.remove(&t.token_id);
157 Ok(())
158 }
159 Some(selected_boxes_t_amt) if selected_boxes_t_amt > t.amount => {
160 change_tokens
161 .insert(t.token_id, selected_boxes_t_amt.checked_sub(&t.amount)?);
162 Ok(())
163 }
164 _ => Err(BoxSelectorError::NotEnoughTokens(vec![t.clone()])),
165 }
166 })?;
167 make_change_boxes(change_value, change_tokens)?
168 };
169 check_input_preservation(
170 selected_inputs.as_slice(),
171 change_boxes.as_slice(),
172 target_balance_original,
173 target_tokens,
174 )?;
175 let selected_inputs_len = selected_inputs.len();
176 Ok(BoxSelection {
177 boxes: selected_inputs
178 .try_into()
179 .map_err(|_| BoxSelectorError::SelectedInputsOutOfBounds(selected_inputs_len))?,
180 change_boxes,
181 })
182 }
183}
184
185#[derive(Clone, Debug, PartialEq, Eq, Error)]
187#[error("Error on checking of the inputs preservation in box selection")]
188pub struct CheckPreservationError(String);
189
190impl From<TokenAmountError> for CheckPreservationError {
191 fn from(e: TokenAmountError) -> Self {
192 CheckPreservationError(format!("TokenAmountError: {}", e))
193 }
194}
195
196fn check_input_preservation<T: ErgoBoxAssets>(
198 selected_inputs: &[T],
199 change_boxes: &[ErgoBoxAssetsData],
200 target_balance: BoxValue,
201 target_tokens: &[Token],
202) -> Result<(), CheckPreservationError> {
203 let sum_selected_inputs = sum_value(selected_inputs);
204 let sum_change_boxes = sum_value(change_boxes);
205 if sum_selected_inputs != sum_change_boxes + target_balance.as_u64() {
206 return Err(CheckPreservationError(
207 format!("total value of the selected boxes {:?} should equal target balance {:?} + total value in change boxes {:?}", sum_selected_inputs, target_balance.as_u64(), sum_change_boxes)
208 ));
209 }
210
211 let sum_tokens_selected_inputs = sum_tokens_from_boxes(selected_inputs)?;
212 let sum_tokens_change_boxes = sum_tokens_from_boxes(change_boxes)?;
213 let sum_tokens_target = sum_tokens(Some(target_tokens))?;
214 if sum_tokens_selected_inputs
215 != sum_tokens_from_hashmaps(sum_tokens_change_boxes.clone(), sum_tokens_target.clone())?
216 {
217 return Err(CheckPreservationError(
218 format!("all tokens from selected boxes {:?} should equal all tokens from the change boxes {:?} + target tokens {:?}", sum_tokens_selected_inputs, sum_tokens_change_boxes, sum_tokens_target)
219 ));
220 }
221 Ok(())
222}
223
224#[derive(Error, PartialEq, Eq, Debug, Clone)]
226#[error("Not enough coins for change box(es)")]
227pub struct NotEnoughCoinsForChangeBox(String);
228
229fn make_change_boxes(
231 change_value: BoxValue,
232 change_tokens: HashMap<TokenId, TokenAmount>,
233) -> Result<Vec<ErgoBoxAssetsData>, NotEnoughCoinsForChangeBox> {
234 if change_tokens.is_empty() {
235 Ok(vec![ErgoBoxAssetsData {
236 value: change_value,
237 tokens: None,
238 }])
239 } else if change_tokens.len() <= ErgoBox::MAX_TOKENS_COUNT {
240 #[allow(clippy::unwrap_used)]
241 Ok(vec![ErgoBoxAssetsData {
243 value: change_value,
244 tokens: Some(
245 BoxTokens::from_vec(change_tokens.into_iter().map(Token::from).collect()).unwrap(),
246 ),
247 }])
248 } else {
249 let mut change_boxes = vec![];
250 let mut change_tokens_left: Vec<Token> =
251 change_tokens.into_iter().map(Token::from).collect();
252 let mut change_value_left = change_value;
253 while !change_tokens_left.is_empty() {
254 if change_tokens_left.len() <= ErgoBox::MAX_TOKENS_COUNT {
255 #[allow(clippy::unwrap_used)]
256 let change_box = ErgoBoxAssetsData {
258 value: change_value_left,
259 tokens: Some(BoxTokens::from_vec(change_tokens_left).unwrap()),
260 };
261 change_boxes.push(change_box);
262 break;
263 } else {
264 #[allow(clippy::unwrap_used)] let value = BoxValue::SAFE_USER_MIN.checked_mul_u32(2).unwrap();
267 let tokens_to_drain = ErgoBox::MAX_TOKENS_COUNT;
268 let drained_tokens: Vec<Token> =
269 change_tokens_left.drain(..tokens_to_drain).collect();
270 #[allow(clippy::unwrap_used)]
271 let change_box = ErgoBoxAssetsData {
273 value,
274 tokens: Some(BoxTokens::from_vec(drained_tokens).unwrap()),
275 };
276 change_boxes.push(change_box);
277 change_value_left = change_value_left.checked_sub(&value).map_err(|e| {
278 NotEnoughCoinsForChangeBox(format!(
279 "Not enough coins left ({:?}) for change box {:?}, error: {}",
280 change_value_left, value, e
281 ))
282 })?;
283 }
284 }
285 Ok(change_boxes)
286 }
287}
288
289#[cfg(test)]
290#[allow(clippy::unwrap_used, clippy::panic)]
291mod tests {
292
293 use std::convert::TryFrom;
294
295 use ergotree_ir::chain::{
296 address::{AddressEncoder, NetworkPrefix},
297 ergo_box::{box_value::checked_sum, ErgoBox, ErgoBoxCandidate},
298 token::arbitrary::ArbTokenIdParam,
299 };
300 use proptest::{collection::vec, prelude::*};
301
302 use crate::{
303 chain::ergo_box::box_builder::{ErgoBoxCandidateBuilder, ErgoBoxCandidateBuilderError},
304 wallet::box_selector::{
305 arbitrary::{ArbErgoBoxAssetsDataParam, ArbTokensParam},
306 sum_value,
307 },
308 };
309
310 use super::*;
311
312 #[test]
313 fn test_empty_inputs() {
314 let s = SimpleBoxSelector::new();
315 let inputs: Vec<ErgoBox> = vec![];
316 let r = s.select(inputs, BoxValue::SAFE_USER_MIN, vec![].as_slice());
317 assert!(r.is_err());
318 }
319
320 proptest! {
321
322 #[test]
323 fn test_select_not_enough_value(inputs in
324 vec(any_with::<ErgoBoxAssetsData>(
325 (BoxValue::MIN_RAW * 1000 .. BoxValue::MIN_RAW * 10000).into()), 1..10)) {
326 let s = SimpleBoxSelector::new();
327 let all_inputs_val = checked_sum(inputs.iter().map(|b| b.value)).unwrap();
328
329 let balance_too_much = all_inputs_val.checked_add(&BoxValue::SAFE_USER_MIN).unwrap();
330 prop_assert!(s.select(inputs, balance_too_much, vec![].as_slice()).is_err());
331 }
332
333 #[test]
334 fn test_select_value(inputs in
335 vec(any_with::<ErgoBoxAssetsData>(
336 (BoxValue::MIN_RAW * 1000 .. BoxValue::MIN_RAW * 10000).into()), 2..10)) {
337 let all_inputs_val = checked_sum(inputs.iter().map(|b| b.value)).unwrap();
338 let s = SimpleBoxSelector::new();
339 let target_balance = all_inputs_val.checked_sub(&(all_inputs_val.as_u64()/2).try_into().unwrap()).unwrap();
340 let target_tokens = vec![];
341 let selection = s.select(inputs, target_balance, target_tokens.as_slice()).unwrap();
342 let out_box = ErgoBoxAssetsData {value: target_balance, tokens: target_tokens.try_into().ok()};
343 let mut change_boxes_plus_out = vec![out_box];
344 change_boxes_plus_out.append(&mut selection.change_boxes.clone());
345 prop_assert_eq!(sum_value(selection.boxes.as_slice()),
346 sum_value(change_boxes_plus_out.as_slice()),
347 "total value of the selected boxes should equal target balance + total value in change boxes");
348 prop_assert_eq!(sum_tokens_from_boxes(selection.boxes.as_slice()).unwrap(),
349 sum_tokens_from_boxes(change_boxes_plus_out.as_slice()).unwrap(),
350 "all tokens from selected boxes should equal all tokens from the change boxes + target tokens")
351 }
352
353 #[test]
354 fn test_select_change_value_is_too_small(inputs in
355 vec(any_with::<ErgoBoxAssetsData>(
356 (BoxValue::MIN_RAW * 1000 .. BoxValue::MIN_RAW * 10000).into()), 2..10)) {
357 let first_input_box = inputs.first().unwrap().clone();
358 let s = SimpleBoxSelector::new();
359 let target_balance = BoxValue::try_from(first_input_box.value().as_u64() - 1).unwrap();
360 let selection = s.select(inputs, target_balance, vec![].as_slice()).unwrap();
361 prop_assert!(!selection.change_boxes.is_empty());
362 let out_box = ErgoBoxAssetsData {value: target_balance, tokens: None};
363 let mut change_boxes_plus_out = vec![out_box];
364 change_boxes_plus_out.append(&mut selection.change_boxes.clone());
365 prop_assert_eq!(sum_value(selection.boxes.as_slice()),
366 sum_value(change_boxes_plus_out.as_slice()),
367 "total value of the selected boxes should equal target balance + total value in change boxes");
368 prop_assert_eq!(sum_tokens_from_boxes(selection.boxes.as_slice()).unwrap(),
369 sum_tokens_from_boxes(change_boxes_plus_out.as_slice()).unwrap(),
370 "all tokens from selected boxes should equal all tokens from the change boxes + target tokens")
371 }
372
373 #[test]
374 fn test_select_value_change_and_tokens(inputs in
375 vec(any_with::<ErgoBoxAssetsData>(
376 (BoxValue::MIN_RAW * 1000 .. BoxValue::MIN_RAW * 10000).into()), 2..10),
377 target_balance in
378 any_with::<BoxValue>((BoxValue::MIN_RAW * 100 .. BoxValue::MIN_RAW * 1500).into())) {
379 let first_input_box = inputs.first().unwrap().clone();
380 prop_assume!(first_input_box.tokens.is_some());
381 let first_input_box_token = first_input_box.tokens.as_ref().unwrap().first();
382 let first_input_box_token_amount = u64::from(first_input_box_token.amount);
383 prop_assume!(first_input_box_token_amount > 1);
384 let s = SimpleBoxSelector::new();
385 let target_token_amount = first_input_box_token_amount / 2;
386 let target_token_id = first_input_box_token.token_id;
387 let target_token = Token {token_id: target_token_id,
388 amount: target_token_amount.try_into().unwrap()};
389 let selection = s.select(inputs, target_balance, vec![target_token.clone()].as_slice()).unwrap();
390 prop_assert!(!selection.change_boxes.is_empty());
391 let out_box = ErgoBoxAssetsData {value: target_balance, tokens: vec![target_token].try_into().ok()};
392 let mut change_boxes_plus_out = vec![out_box];
393 change_boxes_plus_out.append(&mut selection.change_boxes.clone());
394 prop_assert_eq!(sum_value(selection.boxes.as_slice()),
395 sum_value(change_boxes_plus_out.as_slice()),
396 "total value of the selected boxes should equal target balance + total value in change boxes");
397 prop_assert_eq!(sum_tokens_from_boxes(selection.boxes.as_slice()).unwrap(),
398 sum_tokens_from_boxes(change_boxes_plus_out.as_slice()).unwrap(),
399 "all tokens from selected boxes should equal all tokens from the change boxes + target tokens");
400 }
401
402 #[test]
403 fn test_select_all_value(inputs in
404 vec(any_with::<ErgoBoxAssetsData>(
405 (BoxValue::MIN_RAW * 1000 .. BoxValue::MIN_RAW * 10000).into()), 1..10)) {
406 let s = SimpleBoxSelector::new();
407 let all_inputs_val = checked_sum(inputs.iter().map(|b| b.value)).unwrap();
408 let balance_less = all_inputs_val.checked_sub(&BoxValue::SAFE_USER_MIN).unwrap();
409 let selection = s.select(inputs, balance_less, vec![].as_slice()).unwrap();
410 let out_box = ErgoBoxAssetsData {value: balance_less, tokens: None};
411 let mut change_boxes_plus_out = vec![out_box];
412 change_boxes_plus_out.append(&mut selection.change_boxes.clone());
413 prop_assert_eq!(sum_value(selection.boxes.as_slice()),
414 sum_value(change_boxes_plus_out.as_slice()),
415 "total value of the selected boxes should equal target balance + total value in change boxes");
416 prop_assert_eq!(sum_tokens_from_boxes(selection.boxes.as_slice()).unwrap(),
417 sum_tokens_from_boxes(change_boxes_plus_out.as_slice()).unwrap(),
418 "all tokens from selected boxes should equal all tokens from the change boxes + target tokens")
419 }
420
421 #[test]
422 fn test_select_single_token(inputs in
423 vec(any_with::<ErgoBoxAssetsData>(
424 (BoxValue::MIN_RAW * 1000 .. BoxValue::MIN_RAW * 10000).into()), 1..10),
425 target_balance in
426 any_with::<BoxValue>((BoxValue::MIN_RAW * 100 .. BoxValue::MIN_RAW * 800).into()),
427 target_token_amount in 1..100u64) {
428 let s = SimpleBoxSelector::new();
429 let all_input_tokens = sum_tokens_from_boxes(inputs.as_slice()).unwrap();
430 prop_assume!(!all_input_tokens.is_empty());
431 let target_token_id = all_input_tokens.keys().collect::<Vec<&TokenId>>().get((all_input_tokens.len() - 1) / 2)
432 .cloned().unwrap();
433 let input_token_amount = *all_input_tokens.get(target_token_id).unwrap();
434 let target_token_amount: TokenAmount = target_token_amount.try_into().unwrap();
435 prop_assume!(input_token_amount >= target_token_amount);
436 let target_token = Token {token_id: *target_token_id, amount: target_token_amount};
437 let selection = s.select(inputs, target_balance, vec![target_token.clone()].as_slice()).unwrap();
438 let out_box = ErgoBoxAssetsData {value: target_balance, tokens: vec![target_token].try_into().ok()};
439 let mut change_boxes_plus_out = vec![out_box];
440 change_boxes_plus_out.append(&mut selection.change_boxes.clone());
441 prop_assert_eq!(sum_value(selection.boxes.as_slice()),
442 sum_value(change_boxes_plus_out.as_slice()),
443 "total value of the selected boxes should equal target balance + total value in change boxes");
444 prop_assert_eq!(sum_tokens_from_boxes(selection.boxes.as_slice()).unwrap(),
445 sum_tokens_from_boxes(change_boxes_plus_out.as_slice()).unwrap(),
446 "all tokens from selected boxes should equal all tokens from the change boxes + target tokens");
447 prop_assert!(
448 selection.boxes.iter()
449 .all(|b| b.tokens().into_iter().flatten().any(|t| t.token_id == *target_token_id)),
450 "only boxes that have target token should be selected, got: {0:?}", selection.boxes
451 );
452 }
453
454 #[test]
455 fn test_select_single_token_all_amount(inputs in
456 vec(any_with::<ErgoBoxAssetsData>(
457 (BoxValue::MIN_RAW * 1000 .. BoxValue::MIN_RAW * 10000).into()), 1..10),
458 target_balance in
459 any_with::<BoxValue>((BoxValue::MIN_RAW * 100 .. BoxValue::MIN_RAW * 500).into())) {
460 let s = SimpleBoxSelector::new();
461 let all_input_tokens = sum_tokens_from_boxes(inputs.as_slice()).unwrap();
462 prop_assume!(!all_input_tokens.is_empty());
463 let target_token_id = all_input_tokens.keys().collect::<Vec<&TokenId>>().get((all_input_tokens.len() - 1) / 2)
464 .cloned().unwrap();
465 let input_token_amount = *all_input_tokens.get(target_token_id).unwrap();
466 let target_token = Token {token_id: *target_token_id, amount: input_token_amount};
467 let selection = s.select(inputs, target_balance, vec![target_token.clone()].as_slice()).unwrap();
468 let out_box = ErgoBoxAssetsData {value: target_balance, tokens: vec![target_token].try_into().ok()};
469 let mut change_boxes_plus_out = vec![out_box];
470 change_boxes_plus_out.append(&mut selection.change_boxes.clone());
471 prop_assert_eq!(sum_value(selection.boxes.as_slice()),
472 sum_value(change_boxes_plus_out.as_slice()),
473 "total value of the selected boxes should equal target balance + total value in change boxes");
474 prop_assert_eq!(sum_tokens_from_boxes(selection.boxes.as_slice()).unwrap(),
475 sum_tokens_from_boxes(change_boxes_plus_out.as_slice()).unwrap(),
476 "all tokens from selected boxes should equal all tokens from the change boxes + target tokens");
477 prop_assert!(
478 selection.boxes.iter()
479 .all(|b| b.tokens().into_iter().flatten().any(|t| t.token_id == *target_token_id)),
480 "only boxes that have target token should be selected, got: {0:?}", selection.boxes
481 );
482 }
483
484 #[test]
485 fn test_select_multiple_tokens(inputs in
486 vec(any_with::<ErgoBoxAssetsData>(
487 (BoxValue::MIN_RAW * 1000 .. BoxValue::MIN_RAW * 10000).into()), 1..10),
488 target_balance in
489 any_with::<BoxValue>((BoxValue::MIN_RAW * 100 .. BoxValue::MIN_RAW * 500).into()),
490 target_token1_amount in 1..100u64,
491 target_token2_amount in 2..100u64) {
492 let s = SimpleBoxSelector::new();
493 let all_input_tokens = sum_tokens_from_boxes(inputs.as_slice()).unwrap();
494 prop_assume!(all_input_tokens.len() >= 2);
495 let all_input_tokens_keys = all_input_tokens.keys().collect::<Vec<&TokenId>>();
496 let target_token1_id = all_input_tokens_keys.first().cloned().unwrap();
497 let target_token2_id = all_input_tokens_keys.last().cloned().unwrap();
498 let input_token1_amount = *all_input_tokens.get(target_token1_id).unwrap();
499 let input_token2_amount = *all_input_tokens.get(target_token2_id).unwrap();
500 prop_assume!(u64::from(input_token1_amount) >= target_token1_amount);
501 prop_assume!(u64::from(input_token2_amount) >= target_token2_amount);
502 let target_token1 = Token {token_id: *target_token1_id, amount: target_token1_amount.try_into().unwrap()};
503 let target_token2_amount_part1 = target_token2_amount / 2;
505 let target_token2_amount_part2 = target_token2_amount - target_token2_amount_part1;
506 let target_token2_part1 = Token {token_id: *target_token2_id,
507 amount: target_token2_amount_part1.try_into().unwrap()};
508 let target_token2_part2 = Token {token_id: *target_token2_id,
509 amount: target_token2_amount_part2.try_into().unwrap()};
510 let target_tokens = vec![target_token1.clone(), target_token2_part1.clone(), target_token2_part2.clone()];
511 let selection = s.select(inputs, target_balance, target_tokens.as_slice()).unwrap();
512 let out_box = ErgoBoxAssetsData {value: target_balance,
513 tokens: BoxTokens::from_vec(vec![target_token1, target_token2_part1, target_token2_part2]).ok()};
514 let mut change_boxes_plus_out = vec![out_box];
515 change_boxes_plus_out.append(&mut selection.change_boxes.clone());
516 prop_assert_eq!(sum_value(selection.boxes.as_slice()),
517 sum_value(change_boxes_plus_out.as_slice()),
518 "total value of the selected boxes should equal target balance + total value in change boxes");
519 prop_assert_eq!(sum_tokens_from_boxes(selection.boxes.as_slice()).unwrap(),
520 sum_tokens_from_boxes(change_boxes_plus_out.as_slice()).unwrap(),
521 "all tokens from selected boxes should equal all tokens from the change boxes + target tokens");
522 prop_assert!(
523 selection.boxes.iter()
524 .all(|b| b.tokens().into_iter().flatten().any(|t| t.token_id == *target_token1_id || t.token_id == *target_token2_id)),
525 "only boxes that have target tokens should be selected, got: {0:?}", selection.boxes
526 );
527 }
528
529 #[test]
530 fn test_select_not_enough_tokens(inputs in
531 vec(any_with::<ErgoBoxAssetsData>(
532 (BoxValue::MIN_RAW * 1000 .. BoxValue::MIN_RAW * 10000).into()), 1..10),
533 target_balance in
534 any_with::<BoxValue>((BoxValue::MIN_RAW * 100 .. BoxValue::MIN_RAW * 1000).into())) {
535 let s = SimpleBoxSelector::new();
536 let all_input_tokens = sum_tokens_from_boxes(inputs.as_slice()).unwrap();
537 prop_assume!(!all_input_tokens.is_empty());
538 let target_token_id = all_input_tokens.keys().collect::<Vec<&TokenId>>().first().cloned().unwrap();
539 let input_token_amount = u64::from(*all_input_tokens.get(target_token_id).unwrap()) / 2;
540 let target_token_amount = TokenAmount::MAX_RAW;
541 prop_assume!(input_token_amount < target_token_amount);
542 let target_token = Token {token_id: *target_token_id, amount: target_token_amount.try_into().unwrap()};
543 let selection = s.select(inputs, target_balance, vec![target_token].as_slice());
544 prop_assert!(selection.is_err());
545 }
546
547 #[test]
548 fn test_change_over_max_tokens_i590(
549 inputs in
550 vec(
551 any_with::<ErgoBoxAssetsData>(
552 ArbErgoBoxAssetsDataParam {
553 value_range: (BoxValue::MIN_RAW * 1000 .. BoxValue::MIN_RAW * 10000).into(),
554 tokens_param: ArbTokensParam {
555 token_id_param: ArbTokenIdParam::Arbitrary,
556 token_count_range: (ErgoBox::MAX_TOKENS_COUNT/2)..ErgoBox::MAX_TOKENS_COUNT,
558 }
559 }),
560 4..10
561 ),
562 target_balance in
563 any_with::<BoxValue>((BoxValue::MIN_RAW * 100 .. BoxValue::MIN_RAW * 1500).into())) {
564 let target_tokens = inputs.iter()
567 .map(|b| b.tokens().unwrap().first().clone())
568 .collect::<Vec<Token>>();
569 let s = SimpleBoxSelector::new();
570 let selection = s.select(inputs, target_balance, target_tokens.as_slice()).unwrap();
571 prop_assert!(!selection.change_boxes.is_empty());
572 prop_assert!(selection.change_boxes.iter().all(|b| b.tokens().is_some()));
573
574 let change_address_ergo_tree = AddressEncoder::new(NetworkPrefix::Mainnet)
575 .parse_address_from_str("9gmNsqrqdSppLUBqg2UzREmmivgqh1r3jmNcLAc53hk3YCvAGWE")
576 .unwrap().script().unwrap();
577 let change_boxes: Result<Vec<ErgoBoxCandidate>, ErgoBoxCandidateBuilderError> = selection
580 .change_boxes
581 .iter()
582 .map(|b| {
583 let mut candidate = ErgoBoxCandidateBuilder::new(
584 b.value,
585 change_address_ergo_tree.clone(),
586 1000000,
587 );
588 for token in b.tokens().into_iter().flatten() {
589 candidate.add_token(token.clone());
590 }
591 candidate.build()
592 })
593 .collect();
594 prop_assert!(change_boxes.is_ok());
595
596 let out_box = ErgoBoxAssetsData {value: target_balance, tokens: Some(BoxTokens::from_vec(target_tokens).unwrap())};
597 let mut change_boxes_plus_out = vec![out_box];
598 change_boxes_plus_out.append(&mut selection.change_boxes.clone());
599 prop_assert_eq!(sum_value(selection.boxes.as_slice()),
600 sum_value(change_boxes_plus_out.as_slice()),
601 "total value of the selected boxes should equal target balance + total value in change boxes");
602 prop_assert_eq!(sum_tokens_from_boxes(selection.boxes.as_slice()).unwrap(),
603 sum_tokens_from_boxes(change_boxes_plus_out.as_slice()).unwrap(),
604 "all tokens from selected boxes should equal all tokens from the change boxes + target tokens");
605 }
606
607 #[test]
608 fn test_select_over_max_tokens(
609 inputs in
610 vec(
611 any_with::<ErgoBoxAssetsData>(
612 ArbErgoBoxAssetsDataParam {
613 value_range: (BoxValue::MIN_RAW * 1000 .. BoxValue::MIN_RAW * 10000).into(),
614 tokens_param: ArbTokensParam {
615 token_id_param: ArbTokenIdParam::Arbitrary,
616 token_count_range: (ErgoBox::MAX_TOKENS_COUNT/2)..ErgoBox::MAX_TOKENS_COUNT,
618 }
619 }),
620 4..10
621 )) {
622 let target_tokens = inputs.iter()
623 .flat_map(|b| b.tokens().unwrap())
624 .take(ErgoBox::MAX_TOKENS_COUNT + 10)
625 .collect::<Vec<Token>>();
626 let target_balance = BoxValue::SAFE_USER_MIN.checked_mul_u32(2).unwrap();
627 let s = SimpleBoxSelector::new();
628 let selection = s.select(inputs, target_balance, target_tokens.as_slice()).unwrap();
629 prop_assert!(!selection.change_boxes.is_empty());
630
631 let change_address_ergo_tree = AddressEncoder::new(NetworkPrefix::Mainnet)
632 .parse_address_from_str("9gmNsqrqdSppLUBqg2UzREmmivgqh1r3jmNcLAc53hk3YCvAGWE")
633 .unwrap().script().unwrap();
634 let change_boxes: Result<Vec<ErgoBoxCandidate>, ErgoBoxCandidateBuilderError> = selection
637 .change_boxes
638 .iter()
639 .map(|b| {
640 let mut candidate = ErgoBoxCandidateBuilder::new(
641 b.value,
642 change_address_ergo_tree.clone(),
643 1000000,
644 );
645 for token in b.tokens().into_iter().flatten() {
646 candidate.add_token(token.clone());
647 }
648 candidate.build()
649 })
650 .collect();
651 prop_assert!(change_boxes.is_ok());
652
653 let out_box1 = ErgoBoxAssetsData {
654 value: BoxValue::SAFE_USER_MIN,
655 tokens: Some(BoxTokens::from_vec(target_tokens.clone().into_iter().take(target_tokens.len()/2).collect()).unwrap())
656 };
657 let out_box2 = ErgoBoxAssetsData {
658 value: BoxValue::SAFE_USER_MIN,
659 tokens: Some(BoxTokens::from_vec(target_tokens.clone().into_iter().skip(target_tokens.len()/2).collect()).unwrap())
660 };
661 let mut change_boxes_plus_out = vec![out_box1, out_box2];
662 change_boxes_plus_out.append(&mut selection.change_boxes.clone());
663 prop_assert_eq!(sum_value(selection.boxes.as_slice()),
664 sum_value(change_boxes_plus_out.as_slice()),
665 "total value of the selected boxes should equal target balance + total value in change boxes");
666 prop_assert_eq!(sum_tokens_from_boxes(selection.boxes.as_slice()).unwrap(),
667 sum_tokens_from_boxes(change_boxes_plus_out.as_slice()).unwrap(),
668 "all tokens from selected boxes should equal all tokens from the change boxes + target tokens");
669 }
670 }
671}