ergo_lib/wallet/box_selector/
simple.rs

1//! Naive box selector, collects inputs until target balance is reached
2
3use 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/// Simple box selector, collects inputs(sorted by targeted assets) until target balance is reached
27#[derive(Default)]
28pub struct SimpleBoxSelector {}
29
30impl SimpleBoxSelector {
31    /// Create new boxed instance
32    pub fn new() -> Self {
33        SimpleBoxSelector {}
34    }
35}
36
37impl<T: ErgoBoxAssets + Clone> BoxSelector<T> for SimpleBoxSelector {
38    /// Selects inputs to satisfy target balance and tokens.
39    /// `inputs` - available inputs (returns an error, if empty),
40    /// `target_balance` - coins (in nanoERGs) needed,
41    /// `target_tokens` - amount of tokens needed.
42    /// Returns selected inputs and box assets(value+tokens) with change.
43    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        // sum all target tokens into hash map (think repeating token ids)
54        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        // reverse, so they'll be sorted by descending order (boxes with target tokens will be first)
75        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/// Error on checking if inputs are preserved
186#[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
196/// Check if the selected inputs value and tokens are equal to the target + change
197fn 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/// Not enough coins for change box(es)
225#[derive(Error, PartialEq, Eq, Debug, Clone)]
226#[error("Not enough coins for change box(es)")]
227pub struct NotEnoughCoinsForChangeBox(String);
228
229/// Split change tokens into a multiple boxes if over ErgoBox::MAX_TOKENS_COUNT distinct tokens
230fn 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        // unwrap_used is ok here because we checked that change_tokens.len() <= ErgoBox::MAX_TOKENS_COUNT
242        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                // unwrap_used is ok here because we checked that change_tokens_left.len() <= ErgoBox::MAX_TOKENS_COUNT
257                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)] // safe for the box value upper bound
265                // doubled due to larger box size to accomodate so many tokens
266                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                // safe since tokens_to_drain is ErgoBox::MAX_TOKENS_COUNT
272                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            // simulate repeating token ids (e.g the same token id mentioned twice)
504            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                                // with min 4 boxes below gives us minimum ErgoBox::MAX_TOKENS_COUNT * 2 distinct tokens total
557                                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            // take the first token in all input boxes as a target
565            // we want to have as much tokens in the change as possible
566            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            // check that a box can be created for each change box,
578            // checking that box value is enough for large box size (maxed tokens)
579            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                                // with min 4 boxes below gives us minimum ErgoBox::MAX_TOKENS_COUNT * 2 distinct tokens total
617                                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            // check that a box can be created for each change box,
635            // checking that box value is enough for large box size (maxed tokens)
636            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}