ergo_lib/wallet/
box_selector.rs

1//! Box selection for transaction inputs
2
3mod simple;
4
5use std::collections::HashMap;
6
7use bounded_vec::BoundedVec;
8use ergotree_ir::chain::ergo_box::box_value::BoxValue;
9use ergotree_ir::chain::ergo_box::BoxId;
10use ergotree_ir::chain::ergo_box::BoxTokens;
11use ergotree_ir::chain::ergo_box::ErgoBox;
12use ergotree_ir::chain::ergo_box::ErgoBoxCandidate;
13use ergotree_ir::chain::token::Token;
14use ergotree_ir::chain::token::TokenAmount;
15use ergotree_ir::chain::token::TokenAmountError;
16use ergotree_ir::chain::token::TokenId;
17pub use simple::*;
18
19use thiserror::Error;
20
21/// Bounded vec with minimum 1 element and max i16::MAX elements
22pub type SelectedBoxes<T> = BoundedVec<T, 1, { i16::MAX as usize }>;
23
24/// Selected boxes (by [`BoxSelector`])
25#[derive(PartialEq, Eq, Debug, Clone)]
26pub struct BoxSelection<T: ErgoBoxAssets> {
27    /// Selected boxes to spend as transaction inputs
28    pub boxes: SelectedBoxes<T>,
29    /// box assets with returning change amounts (to be put in tx outputs)
30    pub change_boxes: Vec<ErgoBoxAssetsData>,
31}
32
33/// Box selector
34pub trait BoxSelector<T: ErgoBoxAssets> {
35    /// Selects boxes out of the provided inputs to satisfy target balance and tokens
36    /// `inputs` - spendable boxes
37    /// `target_balance` - value (in nanoERGs) to find in input boxes (inputs)
38    /// `target_tokens` - token amounts to find in input boxes(inputs)
39    fn select(
40        &self,
41        inputs: Vec<T>,
42        target_balance: BoxValue,
43        target_tokens: &[Token],
44    ) -> Result<BoxSelection<T>, BoxSelectorError>;
45}
46
47/// Errors of BoxSelector
48#[derive(Error, PartialEq, Eq, Debug, Clone)]
49pub enum BoxSelectorError {
50    /// Not enough coins
51    #[error("Not enough coins({0} nanoERGs are missing)")]
52    NotEnoughCoins(u64),
53
54    /// Not enough tokens
55    #[error("Not enough tokens, missing {0:?}")]
56    NotEnoughTokens(Vec<Token>),
57
58    /// Token amount err
59    #[error("TokenAmountError: {0:?}")]
60    TokenAmountError(#[from] TokenAmountError),
61
62    /// CheckPreservationError
63    #[error("CheckPreservationError: {0:?}")]
64    CheckPreservation(#[from] CheckPreservationError),
65
66    /// Not enough coins for change box
67    #[error("Not enough coins for change box: {0:?}")]
68    NotEnoughCoinsForChangeBox(#[from] NotEnoughCoinsForChangeBox),
69
70    /// Selected inputs out of bounds
71    #[error("Selected inputs out of bounds: {0}")]
72    SelectedInputsOutOfBounds(usize),
73}
74
75/// Assets that ErgoBox holds
76pub trait ErgoBoxAssets {
77    /// Box value
78    fn value(&self) -> BoxValue;
79    /// Tokens (ids and amounts)
80    fn tokens(&self) -> Option<BoxTokens>;
81}
82
83/// Simple struct to hold ErgoBoxAssets values
84#[derive(PartialEq, Eq, Debug, Clone)]
85pub struct ErgoBoxAssetsData {
86    /// Box value
87    pub value: BoxValue,
88    /// Tokens
89    pub tokens: Option<BoxTokens>,
90}
91
92impl ErgoBoxAssets for ErgoBoxAssetsData {
93    fn value(&self) -> BoxValue {
94        self.value
95    }
96
97    fn tokens(&self) -> Option<BoxTokens> {
98        self.tokens.clone()
99    }
100}
101
102impl ErgoBoxAssets for ErgoBoxCandidate {
103    fn value(&self) -> BoxValue {
104        self.value
105    }
106
107    fn tokens(&self) -> Option<BoxTokens> {
108        self.tokens.clone()
109    }
110}
111
112impl ErgoBoxAssets for ErgoBox {
113    fn value(&self) -> BoxValue {
114        self.value
115    }
116
117    fn tokens(&self) -> Option<BoxTokens> {
118        self.tokens.clone()
119    }
120}
121
122/// id of the ergo box
123pub trait ErgoBoxId {
124    /// Id of the ergo box
125    fn box_id(&self) -> BoxId;
126}
127
128impl ErgoBoxId for ErgoBox {
129    fn box_id(&self) -> BoxId {
130        self.box_id()
131    }
132}
133
134/// Returns the total value of the given boxes
135pub fn sum_value<T: ErgoBoxAssets>(bs: &[T]) -> u64 {
136    bs.iter().map(|b| *b.value().as_u64()).sum()
137}
138
139/// Returns the total token amounts (all tokens combined)
140pub fn sum_tokens(ts: Option<&[Token]>) -> Result<HashMap<TokenId, TokenAmount>, TokenAmountError> {
141    let mut res: HashMap<TokenId, TokenAmount> = HashMap::new();
142    ts.into_iter().flatten().try_for_each(|t| {
143        if let Some(amt) = res.get_mut(&t.token_id) {
144            *amt = amt.checked_add(&t.amount)?;
145        } else {
146            res.insert(t.token_id, t.amount);
147        }
148        Ok(())
149    })?;
150    Ok(res)
151}
152
153/// Returns the total token amounts (all tokens combined) of the given boxes
154pub fn sum_tokens_from_boxes<T: ErgoBoxAssets>(
155    bs: &[T],
156) -> Result<HashMap<TokenId, TokenAmount>, TokenAmountError> {
157    let mut res: HashMap<TokenId, TokenAmount> = HashMap::new();
158    bs.iter().try_for_each(|b| {
159        b.tokens().into_iter().flatten().try_for_each(|t| {
160            if let Some(amt) = res.get_mut(&t.token_id) {
161                *amt = amt.checked_add(&t.amount)?;
162            } else {
163                res.insert(t.token_id, t.amount);
164            }
165
166            Ok(())
167        })
168    })?;
169    Ok(res)
170}
171
172/// Sums two hashmaps of tokens (summing amounts of the same token)
173pub fn sum_tokens_from_hashmaps(
174    tokens1: HashMap<TokenId, TokenAmount>,
175    tokens2: HashMap<TokenId, TokenAmount>,
176) -> Result<HashMap<TokenId, TokenAmount>, TokenAmountError> {
177    let mut res: HashMap<TokenId, TokenAmount> = HashMap::new();
178    tokens1
179        .into_iter()
180        .chain(tokens2)
181        .try_for_each(|(id, t_amt)| {
182            if let Some(amt) = res.get_mut(&id) {
183                *amt = amt.checked_add(&t_amt)?;
184            } else {
185                res.insert(id, t_amt);
186            }
187            Ok(())
188        })?;
189    Ok(res)
190}
191
192/// Subtract tokens2 from tokens1
193/// subtracting amounts of the same token or removing the token if amount is the same
194/// Returns an error if trying to subtract more tokens than there are in tokens1
195pub fn subtract_tokens(
196    tokens1: &HashMap<TokenId, TokenAmount>,
197    tokens2: &HashMap<TokenId, TokenAmount>,
198) -> Result<HashMap<TokenId, TokenAmount>, TokenAmountError> {
199    let mut res: HashMap<TokenId, TokenAmount> = tokens1.clone();
200    tokens2.iter().try_for_each(|(id, t_amt)| {
201        if let Some(amt) = res.get_mut(id) {
202            if amt == t_amt {
203                res.remove(id);
204            } else {
205                *amt = amt.checked_sub(t_amt)?;
206            }
207        } else {
208            // trying to subtract a token not found in tokens1
209            return Err(TokenAmountError::OutOfBounds(-(*t_amt.as_u64() as i64)));
210        }
211        Ok(())
212    })?;
213    Ok(res)
214}
215
216/// Arbitrary impl for ErgoBoxAssetsData
217#[allow(clippy::unwrap_used, clippy::panic)]
218#[cfg(feature = "arbitrary")]
219pub mod arbitrary {
220    use std::ops::Range;
221
222    use ergotree_ir::chain::{
223        ergo_box::{
224            box_value::{arbitrary::ArbBoxValueRange, BoxValue},
225            BoxTokens,
226        },
227        token::{arbitrary::ArbTokenIdParam, Token},
228    };
229    use proptest::{arbitrary::Arbitrary, collection::vec, prelude::*};
230
231    use super::ErgoBoxAssetsData;
232
233    /// Parameters for generating a token
234    pub struct ArbTokensParam {
235        /// Predefined or random token ids
236        pub token_id_param: ArbTokenIdParam,
237        /// how many distincts tokens to generate
238        pub token_count_range: Range<usize>,
239    }
240
241    impl Default for ArbTokensParam {
242        fn default() -> Self {
243            ArbTokensParam {
244                token_id_param: ArbTokenIdParam::default(),
245                token_count_range: 0..3,
246            }
247        }
248    }
249
250    /// Parameters to generate ErgoBoxAssetsData
251    #[derive(Default)]
252    pub struct ArbErgoBoxAssetsDataParam {
253        /// how many nanoERGs to generate
254        pub value_range: ArbBoxValueRange,
255        /// what and how many tokens to generate
256        pub tokens_param: ArbTokensParam,
257    }
258
259    impl From<Range<u64>> for ArbErgoBoxAssetsDataParam {
260        fn from(r: Range<u64>) -> Self {
261            ArbErgoBoxAssetsDataParam {
262                value_range: r.into(),
263                tokens_param: ArbTokensParam::default(),
264            }
265        }
266    }
267
268    impl Arbitrary for ErgoBoxAssetsData {
269        type Parameters = ArbErgoBoxAssetsDataParam;
270
271        fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
272            (
273                any_with::<BoxValue>(args.value_range),
274                vec(
275                    any_with::<Token>(args.tokens_param.token_id_param),
276                    args.tokens_param.token_count_range,
277                ),
278            )
279                .prop_map(|(value, tokens)| Self {
280                    value,
281                    tokens: BoxTokens::from_vec(tokens).ok(),
282                })
283                .boxed()
284        }
285
286        type Strategy = BoxedStrategy<Self>;
287
288        fn arbitrary() -> Self::Strategy {
289            Self::arbitrary_with(Default::default())
290        }
291    }
292}
293
294#[cfg(test)]
295#[allow(clippy::unwrap_used, clippy::panic)]
296mod tests {
297
298    use ergotree_ir::chain::ergo_box::box_value::BoxValue;
299    use ergotree_ir::chain::ergo_box::BoxTokens;
300    use ergotree_ir::chain::token::Token;
301    use proptest::prelude::*;
302    use sigma_test_util::force_any_val;
303
304    use crate::wallet::box_selector::sum_tokens;
305    use crate::wallet::box_selector::sum_tokens_from_boxes;
306
307    use super::ErgoBoxAssetsData;
308
309    #[test]
310    fn test_sum_tokens_repeating_token_id() {
311        let token = force_any_val::<Token>();
312        let b = ErgoBoxAssetsData {
313            value: BoxValue::SAFE_USER_MIN,
314            tokens: BoxTokens::from_vec(vec![token.clone(), token.clone()]).ok(),
315        };
316        assert_eq!(
317            u64::from(
318                *sum_tokens_from_boxes(vec![b.clone(), b].as_slice())
319                    .unwrap()
320                    .get(&token.token_id)
321                    .unwrap()
322            ),
323            u64::from(token.amount) * 4
324        );
325    }
326
327    proptest! {
328
329        #[test]
330        fn sum_tokens_eq(b in any::<ErgoBoxAssetsData>()) {
331            prop_assert_eq!(sum_tokens(b.tokens.as_ref().map(BoxTokens::as_ref)), sum_tokens_from_boxes(vec![b].as_slice()))
332        }
333    }
334}