1use 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#[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 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 pub fn box_selection(&self) -> BoxSelection<S> {
79 self.box_selection.clone()
80 }
81
82 pub fn data_inputs(&self) -> Vec<DataInput> {
84 self.data_inputs.clone()
85 }
86
87 pub fn output_candidates(&self) -> Vec<ErgoBoxCandidate> {
89 self.output_candidates.clone()
90 }
91
92 pub fn current_height(&self) -> u32 {
94 self.current_height
95 }
96
97 pub fn fee_amount(&self) -> BoxValue {
99 self.fee_amount
100 }
101
102 pub fn change_address(&self) -> Address {
104 self.change_address.clone()
105 }
106
107 pub fn set_data_inputs(&mut self, data_inputs: Vec<DataInput>) {
109 self.data_inputs = data_inputs;
110 }
111
112 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 pub fn estimate_tx_size_bytes(&self) -> Result<usize, TxBuilderError> {
119 let tx = self.build_tx()?;
120 let inputs = tx.inputs.mapped(|ui| {
121 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 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 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 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 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 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 pub fn build(self) -> Result<UnsignedTransaction, TxBuilderError> {
259 self.build_tx()
260 }
261}
262
263#[allow(non_snake_case, clippy::unwrap_used)]
265pub fn SUGGESTED_TX_FEE() -> BoxValue {
266 BoxValue::new(1100000u64).unwrap()
267}
268
269#[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#[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
319pub(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 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 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 let out_box_value = input_box
762 .value()
763 .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}