ergo_lib/wallet/
tx_builder.rs

1//! Builder for an UnsignedTransaction
2
3use ergotree_interpreter::eval::context::TxIoVec;
4use ergotree_interpreter::sigma_protocol::prover::ContextExtension;
5use ergotree_ir::chain::token::TokenAmount;
6use ergotree_ir::chain::token::TokenAmountError;
7use ergotree_ir::ergo_tree::ErgoTree;
8use std::collections::HashMap;
9use std::collections::HashSet;
10use std::convert::TryInto;
11
12use bounded_vec::BoundedVecOutOfBounds;
13use ergotree_interpreter::sigma_protocol;
14use ergotree_interpreter::sigma_protocol::prover::ProofBytes;
15use ergotree_ir::chain::address::Address;
16use ergotree_ir::chain::ergo_box::box_value::BoxValue;
17use ergotree_ir::chain::ergo_box::BoxId;
18use ergotree_ir::chain::ergo_box::ErgoBoxCandidate;
19use ergotree_ir::chain::token::Token;
20use ergotree_ir::chain::token::TokenId;
21use ergotree_ir::serialization::{SigmaParsingError, SigmaSerializable, SigmaSerializationError};
22use thiserror::Error;
23
24use crate::chain::contract::Contract;
25use crate::chain::ergo_box::box_builder::{ErgoBoxCandidateBuilder, ErgoBoxCandidateBuilderError};
26use crate::chain::transaction::unsigned::UnsignedTransaction;
27use crate::chain::transaction::{DataInput, Input, Transaction, UnsignedInput};
28
29use super::box_selector::subtract_tokens;
30use super::box_selector::sum_tokens_from_boxes;
31use super::box_selector::sum_value;
32use super::box_selector::BoxSelection;
33use super::box_selector::ErgoBoxAssets;
34use super::box_selector::ErgoBoxId;
35use super::miner_fee::MINERS_FEE_BASE16_BYTES;
36
37/// Unsigned transaction builder
38#[derive(Clone)]
39pub struct TxBuilder<S: ErgoBoxAssets> {
40    box_selection: BoxSelection<S>,
41    data_inputs: Vec<DataInput>,
42    output_candidates: Vec<ErgoBoxCandidate>,
43    current_height: u32,
44    fee_amount: BoxValue,
45    change_address: Address,
46    context_extensions: HashMap<BoxId, ContextExtension>,
47    token_burn_permit: Vec<Token>,
48}
49
50impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
51    /// Creates new TxBuilder
52    /// `box_selection` - selected input boxes  (via [`super::box_selector::BoxSelector`])
53    /// `output_candidates` - output boxes to be "created" in this transaction,
54    /// `current_height` - chain height that will be used in additionally created boxes (change, miner's fee, etc.),
55    /// `fee_amount` - miner's fee (higher values will speed up inclusion in blocks),
56    /// `change_address` - change (inputs - outputs) will be sent to this address,
57    /// will be given to miners,
58    pub fn new(
59        box_selection: BoxSelection<S>,
60        output_candidates: Vec<ErgoBoxCandidate>,
61        current_height: u32,
62        fee_amount: BoxValue,
63        change_address: Address,
64    ) -> TxBuilder<S> {
65        TxBuilder {
66            box_selection,
67            data_inputs: vec![],
68            output_candidates,
69            current_height,
70            fee_amount,
71            change_address,
72            context_extensions: HashMap::new(),
73            token_burn_permit: Vec::new(),
74        }
75    }
76
77    /// Get inputs
78    pub fn box_selection(&self) -> BoxSelection<S> {
79        self.box_selection.clone()
80    }
81
82    /// Get data inputs
83    pub fn data_inputs(&self) -> Vec<DataInput> {
84        self.data_inputs.clone()
85    }
86
87    /// Get outputs
88    pub fn output_candidates(&self) -> Vec<ErgoBoxCandidate> {
89        self.output_candidates.clone()
90    }
91
92    /// Get current height
93    pub fn current_height(&self) -> u32 {
94        self.current_height
95    }
96
97    /// Get fee amount
98    pub fn fee_amount(&self) -> BoxValue {
99        self.fee_amount
100    }
101
102    /// Get change
103    pub fn change_address(&self) -> Address {
104        self.change_address.clone()
105    }
106
107    /// Set transaction's data inputs
108    pub fn set_data_inputs(&mut self, data_inputs: Vec<DataInput>) {
109        self.data_inputs = data_inputs;
110    }
111
112    /// Set context extension for a given input
113    pub fn set_context_extension(&mut self, box_id: BoxId, context_extension: ContextExtension) {
114        self.context_extensions.insert(box_id, context_extension);
115    }
116
117    /// Estimated serialized transaction size in bytes after signing (assuming P2PK box spending)
118    pub fn estimate_tx_size_bytes(&self) -> Result<usize, TxBuilderError> {
119        let tx = self.build_tx()?;
120        let inputs = tx.inputs.mapped(|ui| {
121            // mock proof of the size of ProveDlog's proof (P2PK box spending)
122            // as it's the most often used proof
123            let proof = ProofBytes::Some(vec![0u8, sigma_protocol::SOUNDNESS_BYTES as u8]);
124            Input::new(
125                ui.box_id,
126                crate::chain::transaction::input::prover_result::ProverResult {
127                    proof,
128                    extension: ui.extension,
129                },
130            )
131        });
132        let signed_tx_mock = Transaction::new(inputs, tx.data_inputs, tx.output_candidates)?;
133        Ok(signed_tx_mock.sigma_serialize_bytes()?.len())
134    }
135
136    /// Permits the burn of the given token amount, i.e. allows this token amount to be omitted in the outputs
137    pub fn set_token_burn_permit(&mut self, tokens: Vec<Token>) {
138        self.token_burn_permit = tokens;
139    }
140
141    fn build_tx(&self) -> Result<UnsignedTransaction, TxBuilderError> {
142        if self.box_selection.boxes.is_empty() {
143            return Err(TxBuilderError::InvalidArgs("inputs are empty".to_string()));
144        }
145        if self.output_candidates.is_empty() {
146            return Err(TxBuilderError::InvalidArgs("outputs are empty".to_string()));
147        }
148        if self.box_selection.boxes.len() > u16::MAX as usize {
149            return Err(TxBuilderError::InvalidArgs("too many inputs".to_string()));
150        }
151        if self
152            .box_selection
153            .boxes
154            .clone()
155            .into_iter()
156            .map(|b| b.box_id())
157            .collect::<HashSet<BoxId>>()
158            .len()
159            != self.box_selection.boxes.len()
160        {
161            return Err(TxBuilderError::InvalidArgs(
162                "duplicate inputs found".to_string(),
163            ));
164        }
165        if self.data_inputs.len() > u16::MAX as usize {
166            return Err(TxBuilderError::InvalidArgs(
167                "too many data inputs".to_string(),
168            ));
169        }
170
171        let mut output_candidates = self.output_candidates.clone();
172        let change_address_ergo_tree = Contract::pay_to_address(&self.change_address)?.ergo_tree();
173        let change_boxes: Result<Vec<ErgoBoxCandidate>, ErgoBoxCandidateBuilderError> = self
174            .box_selection
175            .change_boxes
176            .iter()
177            .map(|b| {
178                let mut candidate = ErgoBoxCandidateBuilder::new(
179                    b.value,
180                    change_address_ergo_tree.clone(),
181                    self.current_height,
182                );
183                for token in b.tokens().into_iter().flatten() {
184                    candidate.add_token(token.clone());
185                }
186                candidate.build()
187            })
188            .collect();
189        output_candidates.append(&mut change_boxes?);
190
191        // add miner's fee
192        let miner_fee_box = new_miner_fee_box(self.fee_amount, self.current_height)?;
193        output_candidates.push(miner_fee_box);
194        if output_candidates.len() > Transaction::MAX_OUTPUTS_COUNT {
195            return Err(TxBuilderError::InvalidArgs("too many outputs".to_string()));
196        }
197        // check input's coins preservation
198        let total_input_value = sum_value(self.box_selection.boxes.as_slice());
199        let total_output_value = sum_value(output_candidates.as_slice());
200        #[allow(clippy::comparison_chain)]
201        if total_output_value > total_input_value {
202            return Err(TxBuilderError::NotEnoughCoinsInInputs(
203                total_output_value - total_input_value,
204            ));
205        } else if total_output_value < total_input_value {
206            return Err(TxBuilderError::NotEnoughCoinsInOutputs(
207                total_input_value - total_output_value,
208            ));
209        }
210
211        // check that inputs have enough tokens
212        let input_tokens = sum_tokens_from_boxes(self.box_selection.boxes.as_slice())
213            .map_err(TxBuilderError::TooManyTokensInInputBoxes)?;
214        let output_tokens = sum_tokens_from_boxes(output_candidates.as_slice())
215            .map_err(TxBuilderError::TooManyTokensInOutputCandidates)?;
216        let first_input_box_id: TokenId = self.box_selection.boxes.first().box_id().into();
217        let output_tokens_len = output_tokens.len();
218        let output_tokens_without_minted: HashMap<TokenId, TokenAmount> = output_tokens
219            .into_iter()
220            .filter(|(id, _)| id != &first_input_box_id)
221            .collect();
222        if output_tokens_len - output_tokens_without_minted.len() > 1 {
223            return Err(TxBuilderError::InvalidArgs(
224                "cannot mint more than one token".to_string(),
225            ));
226        }
227        output_tokens_without_minted
228            .iter()
229            .try_for_each(|(id, amt)| match input_tokens.get(id).cloned() {
230                Some(input_token_amount) if input_token_amount >= *amt => Ok(()),
231                _ => Err(TxBuilderError::NotEnoughTokens(vec![(*id, *amt).into()])),
232            })?;
233
234        // check token burn
235        let burned_tokens = subtract_tokens(&input_tokens, &output_tokens_without_minted)
236            .map_err(TxBuilderError::TokensInOutputsExceedInputs)?;
237        let token_burn_permits = vec_tokens_to_map(self.token_burn_permit.clone())
238            .map_err(TxBuilderError::TooManyTokensInBurnPermit)?;
239        check_enough_token_burn_permit(&burned_tokens, &token_burn_permits)?;
240        check_unused_token_burn_permit(&burned_tokens, &token_burn_permits)?;
241
242        let unsigned_inputs = self.box_selection.boxes.clone().mapped(|b| {
243            let ctx_ext = self
244                .context_extensions
245                .get(&b.box_id())
246                .cloned()
247                .unwrap_or_else(ContextExtension::empty);
248            UnsignedInput::new(b.box_id(), ctx_ext)
249        });
250        Ok(UnsignedTransaction::new(
251            unsigned_inputs,
252            TxIoVec::opt_empty_vec(self.data_inputs.clone())?,
253            output_candidates.try_into()?,
254        )?)
255    }
256
257    /// Build the unsigned transaction
258    pub fn build(self) -> Result<UnsignedTransaction, TxBuilderError> {
259        self.build_tx()
260    }
261}
262
263/// Suggested transaction fee (1100000 nanoERGs, semi-default value used across wallets and dApps as of Oct 2020)
264#[allow(non_snake_case, clippy::unwrap_used)]
265pub fn SUGGESTED_TX_FEE() -> BoxValue {
266    BoxValue::new(1100000u64).unwrap()
267}
268
269/// Create a box with miner's contract and a given value
270#[allow(clippy::unwrap_used)]
271pub fn new_miner_fee_box(
272    fee_amount: BoxValue,
273    creation_height: u32,
274) -> Result<ErgoBoxCandidate, ErgoBoxCandidateBuilderError> {
275    let ergo_tree =
276        ErgoTree::sigma_parse_bytes(base16::decode(MINERS_FEE_BASE16_BYTES).unwrap().as_slice())
277            .unwrap();
278    ErgoBoxCandidateBuilder::new(fee_amount, ergo_tree, creation_height).build()
279}
280
281/// Errors of TxBuilder
282#[allow(missing_docs)]
283#[derive(Error, PartialEq, Eq, Debug, Clone)]
284pub enum TxBuilderError {
285    #[error("SigmaParsingError: {0}")]
286    ParsingError(#[from] SigmaParsingError),
287    #[error("Invalid arguments: {0}")]
288    InvalidArgs(String),
289    #[error("ErgoBoxCandidateBuilder error: {0}")]
290    ErgoBoxCandidateBuilderError(#[from] ErgoBoxCandidateBuilderError),
291    #[error("Not enougn tokens: {0:?}")]
292    NotEnoughTokens(Vec<Token>),
293    #[error("Not enough coins({0} nanoERGs are missing)")]
294    NotEnoughCoinsInInputs(u64),
295    #[error("Transaction serialization failed: {0}")]
296    SerializationError(#[from] SigmaSerializationError),
297    #[error("Invalid tx inputs count: {0}")]
298    InvalidInputsCount(#[from] BoundedVecOutOfBounds),
299    #[error("Empty input box")]
300    EmptyInputBoxSelection,
301    #[error("Token burn permit exceeded. Permitted limit: {permit:?}, trying to burn: {try_to_burn:?}. Revisit the input to `set_token_burn_permit()` to increase the limit")]
302    TokenBurnPermitExceeded { permit: Token, try_to_burn: Token },
303    #[error("Token burn permit is missing. Trying to burn: {try_to_burn:?}. Call `set_token_burn_permit()` to set the limit")]
304    TokenBurnPermitMissing { try_to_burn: Token },
305    #[error("Unused token burn permit: token id {token_id:?}, amount {amount:?}")]
306    TokenBurnPermitUnused { token_id: TokenId, amount: u64 },
307    #[error("Too many tokens in burn permit: {0}")]
308    TooManyTokensInBurnPermit(TokenAmountError),
309    #[error("Too many tokens in input boxes: {0}")]
310    TooManyTokensInInputBoxes(TokenAmountError),
311    #[error("Too many tokens in output candidate boxes: {0}")]
312    TooManyTokensInOutputCandidates(TokenAmountError),
313    #[error("Tokens in output candidate exceed tokens in input boxes: {0}")]
314    TokensInOutputsExceedInputs(TokenAmountError),
315    #[error("Coins in outputs are less than coins in inputs for {0} nanoERGs")]
316    NotEnoughCoinsInOutputs(u64),
317}
318
319/// Sums up the tokens into a hash map
320pub(crate) fn vec_tokens_to_map(
321    tokens: Vec<Token>,
322) -> Result<HashMap<TokenId, TokenAmount>, TokenAmountError> {
323    let mut res: HashMap<TokenId, TokenAmount> = HashMap::new();
324    tokens.iter().try_for_each(|b| {
325        if let Some(amt) = res.get_mut(&b.token_id) {
326            *amt = amt.checked_add(&b.amount)?;
327        } else {
328            res.insert(b.token_id, b.amount);
329        }
330        Ok(())
331    })?;
332    Ok(res)
333}
334
335fn check_enough_token_burn_permit(
336    burned_tokens: &HashMap<TokenId, TokenAmount>,
337    permits: &HashMap<TokenId, TokenAmount>,
338) -> Result<(), TxBuilderError> {
339    for (burn_token_id, burn_amt) in burned_tokens {
340        if let Some(burn_amt_permit) = permits.get(burn_token_id) {
341            if burn_amt > burn_amt_permit {
342                return Err(TxBuilderError::TokenBurnPermitExceeded {
343                    permit: (*burn_token_id, *burn_amt_permit).into(),
344                    try_to_burn: (*burn_token_id, *burn_amt).into(),
345                });
346            }
347        } else {
348            return Err(TxBuilderError::TokenBurnPermitMissing {
349                try_to_burn: (*burn_token_id, *burn_amt).into(),
350            });
351        }
352    }
353    Ok(())
354}
355
356fn check_unused_token_burn_permit(
357    burned_tokens: &HashMap<TokenId, TokenAmount>,
358    permits: &HashMap<TokenId, TokenAmount>,
359) -> Result<(), TxBuilderError> {
360    for (permit_token_id, permit_amt) in permits {
361        if let Some(burn_amt) = burned_tokens.get(permit_token_id) {
362            if burn_amt < permit_amt {
363                return Err(TxBuilderError::TokenBurnPermitUnused {
364                    token_id: *permit_token_id,
365                    amount: *permit_amt.as_u64() - *burn_amt.as_u64(),
366                });
367            }
368        } else {
369            return Err(TxBuilderError::TokenBurnPermitUnused {
370                token_id: *permit_token_id,
371                amount: *permit_amt.as_u64(),
372            });
373        }
374    }
375    Ok(())
376}
377
378#[cfg(test)]
379#[allow(clippy::unwrap_used, clippy::panic)]
380mod tests {
381
382    use std::convert::TryInto;
383
384    use ergotree_ir::chain::ergo_box::arbitrary::ArbBoxParameters;
385    use ergotree_ir::chain::ergo_box::box_value::checked_sum;
386    use ergotree_ir::chain::ergo_box::ErgoBox;
387    use ergotree_ir::chain::ergo_box::NonMandatoryRegisters;
388    use ergotree_ir::chain::token::arbitrary::ArbTokenIdParam;
389    use ergotree_ir::chain::token::TokenAmount;
390    use ergotree_ir::chain::tx_id::TxId;
391    use ergotree_ir::ergo_tree::ErgoTree;
392    use proptest::{collection::vec, prelude::*};
393    use sigma_test_util::force_any_val;
394    use sigma_test_util::force_any_val_with;
395
396    use crate::wallet::box_selector::{BoxSelector, SimpleBoxSelector};
397
398    use super::*;
399
400    #[test]
401    fn test_duplicate_inputs() {
402        let input_box = force_any_val::<ErgoBox>();
403        let box_selection: BoxSelection<ErgoBox> = BoxSelection {
404            boxes: vec![input_box.clone(), input_box].try_into().unwrap(),
405            change_boxes: vec![],
406        };
407        let r = TxBuilder::new(
408            box_selection,
409            vec![force_any_val::<ErgoBoxCandidate>()],
410            1,
411            force_any_val::<BoxValue>(),
412            force_any_val::<Address>(),
413        );
414        assert!(matches!(r.build(), Err(TxBuilderError::InvalidArgs(_))));
415    }
416
417    #[test]
418    fn test_empty_outputs() {
419        let inputs = vec![force_any_val::<ErgoBox>()];
420        let outputs: Vec<ErgoBoxCandidate> = vec![];
421        let r = TxBuilder::new(
422            SimpleBoxSelector::new()
423                .select(inputs, BoxValue::MIN, &[])
424                .unwrap(),
425            outputs,
426            1,
427            force_any_val::<BoxValue>(),
428            force_any_val::<Address>(),
429        );
430        assert!(matches!(r.build(), Err(TxBuilderError::InvalidArgs(_))));
431    }
432
433    #[test]
434    fn test_burn_token_wo_permit() {
435        let token_pair = Token {
436            token_id: force_any_val::<TokenId>(),
437            amount: 100.try_into().unwrap(),
438        };
439        let input_box = ErgoBox::new(
440            10000000i64.try_into().unwrap(),
441            force_any_val::<ErgoTree>(),
442            vec![token_pair.clone()].try_into().ok(),
443            NonMandatoryRegisters::empty(),
444            1,
445            force_any_val::<TxId>(),
446            0,
447        )
448        .unwrap();
449        let inputs: Vec<ErgoBox> = vec![input_box];
450        let tx_fee = BoxValue::SAFE_USER_MIN;
451        let out_box_value = BoxValue::SAFE_USER_MIN;
452        let target_balance = out_box_value.checked_add(&tx_fee).unwrap();
453        let target_token = Token {
454            amount: 10.try_into().unwrap(),
455            ..token_pair
456        };
457        let target_tokens = vec![target_token.clone()];
458        let box_selection = SimpleBoxSelector::new()
459            .select(inputs, target_balance, target_tokens.as_slice())
460            .unwrap();
461        let box_builder =
462            ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
463        let out_box = box_builder.build().unwrap();
464        let outputs = vec![out_box];
465        let tx_builder = TxBuilder::new(
466            box_selection,
467            outputs,
468            0,
469            tx_fee,
470            force_any_val::<Address>(),
471        );
472        let res = tx_builder.build();
473        assert_eq!(
474            res,
475            Err(TxBuilderError::TokenBurnPermitMissing {
476                try_to_burn: target_token
477            })
478        );
479    }
480
481    #[test]
482    fn test_burn_token_w_permit_too_low() {
483        let token_pair = Token {
484            token_id: force_any_val::<TokenId>(),
485            amount: 100.try_into().unwrap(),
486        };
487        let input_box = ErgoBox::new(
488            10000000i64.try_into().unwrap(),
489            force_any_val::<ErgoTree>(),
490            vec![token_pair.clone()].try_into().ok(),
491            NonMandatoryRegisters::empty(),
492            1,
493            force_any_val::<TxId>(),
494            0,
495        )
496        .unwrap();
497        let inputs: Vec<ErgoBox> = vec![input_box];
498        let tx_fee = BoxValue::SAFE_USER_MIN;
499        let out_box_value = BoxValue::SAFE_USER_MIN;
500        let target_balance = out_box_value.checked_add(&tx_fee).unwrap();
501        let token_to_burn = Token {
502            amount: 10.try_into().unwrap(),
503            ..token_pair
504        };
505        let target_tokens = vec![token_to_burn.clone()];
506        let box_selection = SimpleBoxSelector::new()
507            .select(inputs, target_balance, target_tokens.as_slice())
508            .unwrap();
509        let box_builder =
510            ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
511        let out_box = box_builder.build().unwrap();
512        let outputs = vec![out_box];
513        let mut tx_builder = TxBuilder::new(
514            box_selection,
515            outputs,
516            0,
517            tx_fee,
518            force_any_val::<Address>(),
519        );
520        let token_burn_permit = Token {
521            amount: 5.try_into().unwrap(),
522            ..token_pair
523        };
524        tx_builder.set_token_burn_permit(vec![token_burn_permit.clone()]);
525        let res = tx_builder.build();
526        assert_eq!(
527            res,
528            Err(TxBuilderError::TokenBurnPermitExceeded {
529                try_to_burn: token_to_burn,
530                permit: token_burn_permit,
531            })
532        );
533    }
534
535    #[test]
536    fn test_burn_token() {
537        let token_pair = Token {
538            token_id: force_any_val::<TokenId>(),
539            amount: 100.try_into().unwrap(),
540        };
541        let input_box = ErgoBox::new(
542            10000000i64.try_into().unwrap(),
543            force_any_val::<ErgoTree>(),
544            vec![token_pair.clone()].try_into().ok(),
545            NonMandatoryRegisters::empty(),
546            1,
547            force_any_val::<TxId>(),
548            0,
549        )
550        .unwrap();
551        let inputs: Vec<ErgoBox> = vec![input_box];
552        let tx_fee = BoxValue::SAFE_USER_MIN;
553        let out_box_value = BoxValue::SAFE_USER_MIN;
554        let target_balance = out_box_value.checked_add(&tx_fee).unwrap();
555        let token_to_burn = Token {
556            amount: 10.try_into().unwrap(),
557            ..token_pair
558        };
559        let target_tokens = vec![token_to_burn.clone()];
560        let box_selection = SimpleBoxSelector::new()
561            .select(inputs, target_balance, target_tokens.as_slice())
562            .unwrap();
563        let box_builder =
564            ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
565        let out_box = box_builder.build().unwrap();
566        let outputs = vec![out_box];
567        let mut tx_builder = TxBuilder::new(
568            box_selection,
569            outputs,
570            0,
571            tx_fee,
572            force_any_val::<Address>(),
573        );
574        tx_builder.set_token_burn_permit(vec![token_to_burn]);
575        let _ = tx_builder.build().unwrap();
576    }
577
578    #[test]
579    fn test_token_burn_permit_wo_burn() {
580        let token_pair = Token {
581            token_id: force_any_val::<TokenId>(),
582            amount: 100.try_into().unwrap(),
583        };
584        let input_box = ErgoBox::new(
585            10000000i64.try_into().unwrap(),
586            force_any_val::<ErgoTree>(),
587            vec![token_pair.clone()].try_into().ok(),
588            NonMandatoryRegisters::empty(),
589            1,
590            force_any_val::<TxId>(),
591            0,
592        )
593        .unwrap();
594        let inputs: Vec<ErgoBox> = vec![input_box];
595        let tx_fee = BoxValue::SAFE_USER_MIN;
596        let out_box_value = BoxValue::SAFE_USER_MIN;
597        let target_balance = out_box_value.checked_add(&tx_fee).unwrap();
598        let token_to_burn = Token {
599            amount: 10.try_into().unwrap(),
600            ..token_pair
601        };
602        let box_selection = SimpleBoxSelector::new()
603            .select(inputs, target_balance, &Vec::new())
604            .unwrap();
605        let box_builder =
606            ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
607        let out_box = box_builder.build().unwrap();
608        let outputs = vec![out_box];
609        let mut tx_builder = TxBuilder::new(
610            box_selection,
611            outputs,
612            0,
613            tx_fee,
614            force_any_val::<Address>(),
615        );
616        tx_builder.set_token_burn_permit(vec![token_to_burn.clone()]);
617        let res = tx_builder.build();
618        assert_eq!(
619            res,
620            Err(TxBuilderError::TokenBurnPermitUnused {
621                token_id: token_to_burn.token_id,
622                amount: *token_to_burn.amount.as_u64(),
623            })
624        );
625    }
626
627    #[test]
628    fn test_mint_token() {
629        let input_box = ErgoBox::new(
630            100000000i64.try_into().unwrap(),
631            force_any_val::<ErgoTree>(),
632            None,
633            NonMandatoryRegisters::empty(),
634            1,
635            force_any_val::<TxId>(),
636            0,
637        )
638        .unwrap();
639        let token_pair = Token {
640            token_id: TokenId::from(input_box.box_id()),
641            amount: 1.try_into().unwrap(),
642        };
643        let out_box_value = BoxValue::SAFE_USER_MIN;
644        let token_name = "TKN".to_string();
645        let token_desc = "token desc".to_string();
646        let token_num_dec = 2;
647        let mut box_builder =
648            ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
649        box_builder.mint_token(token_pair.clone(), token_name, token_desc, token_num_dec);
650        let out_box = box_builder.build().unwrap();
651
652        let inputs: Vec<ErgoBox> = vec![input_box];
653        let tx_fee = BoxValue::SAFE_USER_MIN;
654        let target_balance = out_box_value.checked_add(&tx_fee).unwrap();
655        let box_selection = SimpleBoxSelector::new()
656            .select(inputs, target_balance, vec![].as_slice())
657            .unwrap();
658        let outputs = vec![out_box];
659        let tx_builder = TxBuilder::new(
660            box_selection,
661            outputs,
662            0,
663            tx_fee,
664            force_any_val::<Address>(),
665        );
666        let tx = tx_builder.build().unwrap();
667        assert_eq!(
668            tx.output_candidates
669                .get(0)
670                .unwrap()
671                .tokens()
672                .unwrap()
673                .first()
674                .token_id,
675            token_pair.token_id,
676            "expected minted token in the first output box"
677        );
678    }
679
680    #[test]
681    fn test_tokens_balance_error() {
682        let input_box = force_any_val_with::<ErgoBox>(ArbBoxParameters {
683            value_range: (BoxValue::MIN_RAW * 5000..BoxValue::MIN_RAW * 10000).into(),
684            ..Default::default()
685        });
686        let token_pair = Token {
687            token_id: force_any_val_with::<TokenId>(ArbTokenIdParam::Arbitrary),
688            amount: force_any_val::<TokenAmount>(),
689        };
690        let out_box_value = BoxValue::SAFE_USER_MIN;
691        let mut box_builder =
692            ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
693        // try to spend a token that is not in inputs
694        box_builder.add_token(token_pair.clone());
695        let out_box = box_builder.build().unwrap();
696        let inputs: Vec<ErgoBox> = vec![input_box];
697        let tx_fee = BoxValue::SAFE_USER_MIN;
698        let target_balance = out_box_value.checked_add(&tx_fee).unwrap();
699        let box_selection = SimpleBoxSelector::new()
700            .select(inputs, target_balance, vec![].as_slice())
701            .unwrap();
702        let outputs = vec![out_box];
703        let tx_builder = TxBuilder::new(
704            box_selection,
705            outputs,
706            0,
707            tx_fee,
708            force_any_val::<Address>(),
709        );
710        assert_eq!(
711            tx_builder.build(),
712            Err(TxBuilderError::NotEnoughTokens(vec![token_pair])),
713        );
714    }
715
716    #[test]
717    fn test_balance_error_not_enough_inputs() {
718        let input_box = force_any_val_with::<ErgoBox>(ArbBoxParameters {
719            value_range: (BoxValue::MIN_RAW * 5000..BoxValue::MIN_RAW * 10000).into(),
720            ..Default::default()
721        });
722        // tx fee on top of this leads to overspending
723        let out_box_value = input_box.value();
724        let mut box_builder =
725            ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
726        input_box.tokens.iter().for_each(|tokens| {
727            tokens.iter().for_each(|t| {
728                box_builder.add_token(t.clone());
729            })
730        });
731        let out_box = box_builder.build().unwrap();
732        let inputs: Vec<ErgoBox> = vec![input_box];
733        let tx_fee = BoxValue::SAFE_USER_MIN;
734        let box_selection = BoxSelection {
735            boxes: inputs.try_into().unwrap(),
736            change_boxes: vec![],
737        };
738        let outputs = vec![out_box];
739        let tx_builder = TxBuilder::new(
740            box_selection,
741            outputs,
742            0,
743            tx_fee,
744            force_any_val::<Address>(),
745        );
746        assert_eq!(
747            tx_builder.build(),
748            Err(TxBuilderError::NotEnoughCoinsInInputs(
749                *BoxValue::SAFE_USER_MIN.as_u64()
750            )),
751        );
752    }
753
754    #[test]
755    fn test_balance_error_not_enough_outputs() {
756        let input_box = force_any_val_with::<ErgoBox>(ArbBoxParameters {
757            value_range: (BoxValue::MIN_RAW * 5000..BoxValue::MIN_RAW * 10000).into(),
758            ..Default::default()
759        });
760        // spend not all inputs
761        let out_box_value = input_box
762            .value()
763            // goes to tx fee
764            .checked_sub(&BoxValue::SAFE_USER_MIN)
765            .unwrap()
766            .checked_sub(&BoxValue::SAFE_USER_MIN)
767            .unwrap();
768        let mut box_builder =
769            ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
770        input_box.tokens.iter().for_each(|tokens| {
771            tokens.iter().for_each(|t| {
772                box_builder.add_token(t.clone());
773            })
774        });
775        let out_box = box_builder.build().unwrap();
776        let inputs: Vec<ErgoBox> = vec![input_box];
777        let tx_fee = BoxValue::SAFE_USER_MIN;
778        let box_selection = BoxSelection {
779            boxes: inputs.try_into().unwrap(),
780            change_boxes: vec![],
781        };
782        let outputs = vec![out_box];
783        let tx_builder = TxBuilder::new(
784            box_selection,
785            outputs,
786            0,
787            tx_fee,
788            force_any_val::<Address>(),
789        );
790        assert_eq!(
791            tx_builder.build(),
792            Err(TxBuilderError::NotEnoughCoinsInOutputs(
793                *BoxValue::SAFE_USER_MIN.as_u64()
794            )),
795        );
796    }
797
798    #[test]
799    fn test_est_tx_size() {
800        let input = ErgoBox::new(
801            10000000i64.try_into().unwrap(),
802            force_any_val::<ErgoTree>(),
803            None,
804            NonMandatoryRegisters::empty(),
805            1,
806            force_any_val::<TxId>(),
807            0,
808        )
809        .unwrap();
810        let tx_fee = super::SUGGESTED_TX_FEE();
811        let out_box_value = input.value.checked_sub(&tx_fee).unwrap();
812        let box_builder =
813            ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
814        let out_box = box_builder.build().unwrap();
815        let outputs = vec![out_box];
816        let tx_builder = TxBuilder::new(
817            BoxSelection {
818                boxes: vec![input].try_into().unwrap(),
819                change_boxes: vec![],
820            },
821            outputs,
822            0,
823            tx_fee,
824            force_any_val::<Address>(),
825        );
826        assert!(tx_builder.estimate_tx_size_bytes().unwrap() > 0);
827    }
828
829    proptest! {
830
831        #![proptest_config(ProptestConfig::with_cases(16))]
832
833        #[test]
834        fn test_build_tx(inputs in vec(any_with::<ErgoBox>(ArbBoxParameters { value_range: (BoxValue::MIN_RAW * 5000..BoxValue::MIN_RAW * 10000).into(), ..Default::default() }), 1..10),
835                         outputs in vec(any_with::<ErgoBoxCandidate>(ArbBoxParameters { value_range: (BoxValue::MIN_RAW * 5000..BoxValue::MIN_RAW * 10000).into(), ..Default::default() }), 1..2),
836                         change_address in any::<Address>(),
837                         miners_fee in any_with::<BoxValue>((BoxValue::MIN_RAW * 100..BoxValue::MIN_RAW * 200).into()),
838                         data_inputs in vec(any::<DataInput>(), 0..2),
839                         ctx_ext in any::<ContextExtension>()) {
840            prop_assume!(sum_tokens_from_boxes(outputs.as_slice()).unwrap().is_empty());
841            let all_outputs = checked_sum(outputs.iter().map(|b| b.value)).unwrap()
842                .checked_add(&miners_fee)
843                .unwrap();
844            let all_inputs = checked_sum(inputs.iter().map(|b| b.value)).unwrap();
845            prop_assume!(all_outputs < all_inputs);
846            let total_output_value: BoxValue = checked_sum(outputs.iter().map(|b| b.value))
847                .unwrap()
848                .checked_add(&miners_fee).unwrap();
849            let selection = SimpleBoxSelector::new().select(inputs.clone(), total_output_value, &[]).unwrap();
850            let mut tx_builder = TxBuilder::new(
851                selection.clone(),
852                outputs.clone(),
853                1,
854                miners_fee,
855                change_address.clone(),
856            );
857            tx_builder.set_data_inputs(data_inputs.clone());
858            tx_builder.set_context_extension(selection.boxes.first().box_id(), ctx_ext.clone());
859            let tx = tx_builder.build().unwrap();
860            prop_assert!(outputs.into_iter().all(|i| tx.output_candidates.iter().any(|o| *o == i)),
861                         "tx.output_candidates is missing some outputs");
862            let tx_all_inputs_vals = tx.inputs.iter()
863                .map(|i| inputs.iter()
864                    .find(|ib| ib.box_id() == i.box_id).unwrap().value);
865            let tx_all_inputs_sum = checked_sum(tx_all_inputs_vals).unwrap();
866            let expected_change = tx_all_inputs_sum.checked_sub(&all_outputs).unwrap();
867            prop_assert!(tx.output_candidates.iter().any(|b| {
868                b.value == expected_change && b.ergo_tree == change_address.script().unwrap()
869            }), "box with change {:?} is not found in outputs: {:?}", expected_change, tx.output_candidates);
870            prop_assert!(tx.output_candidates.iter().any(|b| {
871                b.value == miners_fee
872            }), "box with miner's fee {:?} is not found in outputs: {:?}", miners_fee, tx.output_candidates);
873            prop_assert_eq!(tx.data_inputs.map(|i| i.as_vec().clone()).unwrap_or_default(), data_inputs, "unexpected data inputs");
874            prop_assert_eq!(&tx.inputs.first().extension, &ctx_ext);
875        }
876    }
877}