ergo_lib/wallet/
box_selector.rs1mod 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
21pub type SelectedBoxes<T> = BoundedVec<T, 1, { i16::MAX as usize }>;
23
24#[derive(PartialEq, Eq, Debug, Clone)]
26pub struct BoxSelection<T: ErgoBoxAssets> {
27 pub boxes: SelectedBoxes<T>,
29 pub change_boxes: Vec<ErgoBoxAssetsData>,
31}
32
33pub trait BoxSelector<T: ErgoBoxAssets> {
35 fn select(
40 &self,
41 inputs: Vec<T>,
42 target_balance: BoxValue,
43 target_tokens: &[Token],
44 ) -> Result<BoxSelection<T>, BoxSelectorError>;
45}
46
47#[derive(Error, PartialEq, Eq, Debug, Clone)]
49pub enum BoxSelectorError {
50 #[error("Not enough coins({0} nanoERGs are missing)")]
52 NotEnoughCoins(u64),
53
54 #[error("Not enough tokens, missing {0:?}")]
56 NotEnoughTokens(Vec<Token>),
57
58 #[error("TokenAmountError: {0:?}")]
60 TokenAmountError(#[from] TokenAmountError),
61
62 #[error("CheckPreservationError: {0:?}")]
64 CheckPreservation(#[from] CheckPreservationError),
65
66 #[error("Not enough coins for change box: {0:?}")]
68 NotEnoughCoinsForChangeBox(#[from] NotEnoughCoinsForChangeBox),
69
70 #[error("Selected inputs out of bounds: {0}")]
72 SelectedInputsOutOfBounds(usize),
73}
74
75pub trait ErgoBoxAssets {
77 fn value(&self) -> BoxValue;
79 fn tokens(&self) -> Option<BoxTokens>;
81}
82
83#[derive(PartialEq, Eq, Debug, Clone)]
85pub struct ErgoBoxAssetsData {
86 pub value: BoxValue,
88 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
122pub trait ErgoBoxId {
124 fn box_id(&self) -> BoxId;
126}
127
128impl ErgoBoxId for ErgoBox {
129 fn box_id(&self) -> BoxId {
130 self.box_id()
131 }
132}
133
134pub fn sum_value<T: ErgoBoxAssets>(bs: &[T]) -> u64 {
136 bs.iter().map(|b| *b.value().as_u64()).sum()
137}
138
139pub 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
153pub 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
172pub 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
192pub 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 return Err(TokenAmountError::OutOfBounds(-(*t_amt.as_u64() as i64)));
210 }
211 Ok(())
212 })?;
213 Ok(res)
214}
215
216#[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 pub struct ArbTokensParam {
235 pub token_id_param: ArbTokenIdParam,
237 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 #[derive(Default)]
252 pub struct ArbErgoBoxAssetsDataParam {
253 pub value_range: ArbBoxValueRange,
255 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}