use ergotree_interpreter::eval::context::TxIoVec;
use ergotree_interpreter::sigma_protocol::prover::ContextExtension;
use ergotree_ir::chain::token::TokenAmount;
use ergotree_ir::chain::token::TokenAmountError;
use ergotree_ir::ergo_tree::ErgoTree;
use std::collections::HashMap;
use std::collections::HashSet;
use std::convert::TryInto;
use bounded_vec::BoundedVecOutOfBounds;
use ergotree_interpreter::sigma_protocol;
use ergotree_interpreter::sigma_protocol::prover::ProofBytes;
use ergotree_ir::chain::address::Address;
use ergotree_ir::chain::ergo_box::box_value::BoxValue;
use ergotree_ir::chain::ergo_box::BoxId;
use ergotree_ir::chain::ergo_box::ErgoBoxCandidate;
use ergotree_ir::chain::token::Token;
use ergotree_ir::chain::token::TokenId;
use ergotree_ir::serialization::{SigmaParsingError, SigmaSerializable, SigmaSerializationError};
use thiserror::Error;
use crate::chain::contract::Contract;
use crate::chain::ergo_box::box_builder::{ErgoBoxCandidateBuilder, ErgoBoxCandidateBuilderError};
use crate::chain::transaction::unsigned::UnsignedTransaction;
use crate::chain::transaction::{DataInput, Input, Transaction, UnsignedInput};
use super::box_selector::subtract_tokens;
use super::box_selector::sum_tokens_from_boxes;
use super::box_selector::sum_value;
use super::box_selector::BoxSelection;
use super::box_selector::ErgoBoxAssets;
use super::box_selector::ErgoBoxId;
use super::miner_fee::MINERS_FEE_BASE16_BYTES;
#[derive(Clone)]
pub struct TxBuilder<S: ErgoBoxAssets> {
box_selection: BoxSelection<S>,
data_inputs: Vec<DataInput>,
output_candidates: Vec<ErgoBoxCandidate>,
current_height: u32,
fee_amount: BoxValue,
change_address: Address,
context_extensions: HashMap<BoxId, ContextExtension>,
token_burn_permit: Vec<Token>,
}
impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
pub fn new(
box_selection: BoxSelection<S>,
output_candidates: Vec<ErgoBoxCandidate>,
current_height: u32,
fee_amount: BoxValue,
change_address: Address,
) -> TxBuilder<S> {
TxBuilder {
box_selection,
data_inputs: vec![],
output_candidates,
current_height,
fee_amount,
change_address,
context_extensions: HashMap::new(),
token_burn_permit: Vec::new(),
}
}
pub fn box_selection(&self) -> BoxSelection<S> {
self.box_selection.clone()
}
pub fn data_inputs(&self) -> Vec<DataInput> {
self.data_inputs.clone()
}
pub fn output_candidates(&self) -> Vec<ErgoBoxCandidate> {
self.output_candidates.clone()
}
pub fn current_height(&self) -> u32 {
self.current_height
}
pub fn fee_amount(&self) -> BoxValue {
self.fee_amount
}
pub fn change_address(&self) -> Address {
self.change_address.clone()
}
pub fn set_data_inputs(&mut self, data_inputs: Vec<DataInput>) {
self.data_inputs = data_inputs;
}
pub fn set_context_extension(&mut self, box_id: BoxId, context_extension: ContextExtension) {
self.context_extensions.insert(box_id, context_extension);
}
pub fn estimate_tx_size_bytes(&self) -> Result<usize, TxBuilderError> {
let tx = self.build_tx()?;
let inputs = tx.inputs.mapped(|ui| {
let proof = ProofBytes::Some(vec![0u8, sigma_protocol::SOUNDNESS_BYTES as u8]);
Input::new(
ui.box_id,
crate::chain::transaction::input::prover_result::ProverResult {
proof,
extension: ui.extension,
},
)
});
let signed_tx_mock = Transaction::new(inputs, tx.data_inputs, tx.output_candidates)?;
Ok(signed_tx_mock.sigma_serialize_bytes()?.len())
}
pub fn set_token_burn_permit(&mut self, tokens: Vec<Token>) {
self.token_burn_permit = tokens;
}
fn build_tx(&self) -> Result<UnsignedTransaction, TxBuilderError> {
if self.box_selection.boxes.is_empty() {
return Err(TxBuilderError::InvalidArgs("inputs are empty".to_string()));
}
if self.output_candidates.is_empty() {
return Err(TxBuilderError::InvalidArgs("outputs are empty".to_string()));
}
if self.box_selection.boxes.len() > u16::MAX as usize {
return Err(TxBuilderError::InvalidArgs("too many inputs".to_string()));
}
if self
.box_selection
.boxes
.clone()
.into_iter()
.map(|b| b.box_id())
.collect::<HashSet<BoxId>>()
.len()
!= self.box_selection.boxes.len()
{
return Err(TxBuilderError::InvalidArgs(
"duplicate inputs found".to_string(),
));
}
if self.data_inputs.len() > u16::MAX as usize {
return Err(TxBuilderError::InvalidArgs(
"too many data inputs".to_string(),
));
}
let mut output_candidates = self.output_candidates.clone();
let change_address_ergo_tree = Contract::pay_to_address(&self.change_address)?.ergo_tree();
let change_boxes: Result<Vec<ErgoBoxCandidate>, ErgoBoxCandidateBuilderError> = self
.box_selection
.change_boxes
.iter()
.map(|b| {
let mut candidate = ErgoBoxCandidateBuilder::new(
b.value,
change_address_ergo_tree.clone(),
self.current_height,
);
for token in b.tokens().into_iter().flatten() {
candidate.add_token(token.clone());
}
candidate.build()
})
.collect();
output_candidates.append(&mut change_boxes?);
let miner_fee_box = new_miner_fee_box(self.fee_amount, self.current_height)?;
output_candidates.push(miner_fee_box);
if output_candidates.len() > Transaction::MAX_OUTPUTS_COUNT {
return Err(TxBuilderError::InvalidArgs("too many outputs".to_string()));
}
let total_input_value = sum_value(self.box_selection.boxes.as_slice());
let total_output_value = sum_value(output_candidates.as_slice());
#[allow(clippy::comparison_chain)]
if total_output_value > total_input_value {
return Err(TxBuilderError::NotEnoughCoinsInInputs(
total_output_value - total_input_value,
));
} else if total_output_value < total_input_value {
return Err(TxBuilderError::NotEnoughCoinsInOutputs(
total_input_value - total_output_value,
));
}
let input_tokens = sum_tokens_from_boxes(self.box_selection.boxes.as_slice())
.map_err(TxBuilderError::TooManyTokensInInputBoxes)?;
let output_tokens = sum_tokens_from_boxes(output_candidates.as_slice())
.map_err(TxBuilderError::TooManyTokensInOutputCandidates)?;
let first_input_box_id: TokenId = self.box_selection.boxes.first().box_id().into();
let output_tokens_len = output_tokens.len();
let output_tokens_without_minted: HashMap<TokenId, TokenAmount> = output_tokens
.into_iter()
.filter(|(id, _)| id != &first_input_box_id)
.collect();
if output_tokens_len - output_tokens_without_minted.len() > 1 {
return Err(TxBuilderError::InvalidArgs(
"cannot mint more than one token".to_string(),
));
}
output_tokens_without_minted
.iter()
.try_for_each(|(id, amt)| match input_tokens.get(id).cloned() {
Some(input_token_amount) if input_token_amount >= *amt => Ok(()),
_ => Err(TxBuilderError::NotEnoughTokens(vec![(*id, *amt).into()])),
})?;
let burned_tokens = subtract_tokens(&input_tokens, &output_tokens_without_minted)
.map_err(TxBuilderError::TokensInOutputsExceedInputs)?;
let token_burn_permits = vec_tokens_to_map(self.token_burn_permit.clone())
.map_err(TxBuilderError::TooManyTokensInBurnPermit)?;
check_enough_token_burn_permit(&burned_tokens, &token_burn_permits)?;
check_unused_token_burn_permit(&burned_tokens, &token_burn_permits)?;
let unsigned_inputs = self.box_selection.boxes.clone().mapped(|b| {
let ctx_ext = self
.context_extensions
.get(&b.box_id())
.cloned()
.unwrap_or_else(ContextExtension::empty);
UnsignedInput::new(b.box_id(), ctx_ext)
});
Ok(UnsignedTransaction::new(
unsigned_inputs,
TxIoVec::opt_empty_vec(self.data_inputs.clone())?,
output_candidates.try_into()?,
)?)
}
pub fn build(self) -> Result<UnsignedTransaction, TxBuilderError> {
self.build_tx()
}
}
#[allow(non_snake_case, clippy::unwrap_used)]
pub fn SUGGESTED_TX_FEE() -> BoxValue {
BoxValue::new(1100000u64).unwrap()
}
#[allow(clippy::unwrap_used)]
pub fn new_miner_fee_box(
fee_amount: BoxValue,
creation_height: u32,
) -> Result<ErgoBoxCandidate, ErgoBoxCandidateBuilderError> {
let ergo_tree =
ErgoTree::sigma_parse_bytes(base16::decode(MINERS_FEE_BASE16_BYTES).unwrap().as_slice())
.unwrap();
ErgoBoxCandidateBuilder::new(fee_amount, ergo_tree, creation_height).build()
}
#[allow(missing_docs)]
#[derive(Error, PartialEq, Eq, Debug, Clone)]
pub enum TxBuilderError {
#[error("SigmaParsingError: {0}")]
ParsingError(#[from] SigmaParsingError),
#[error("Invalid arguments: {0}")]
InvalidArgs(String),
#[error("ErgoBoxCandidateBuilder error: {0}")]
ErgoBoxCandidateBuilderError(#[from] ErgoBoxCandidateBuilderError),
#[error("Not enougn tokens: {0:?}")]
NotEnoughTokens(Vec<Token>),
#[error("Not enough coins({0} nanoERGs are missing)")]
NotEnoughCoinsInInputs(u64),
#[error("Transaction serialization failed: {0}")]
SerializationError(#[from] SigmaSerializationError),
#[error("Invalid tx inputs count: {0}")]
InvalidInputsCount(#[from] BoundedVecOutOfBounds),
#[error("Empty input box")]
EmptyInputBoxSelection,
#[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")]
TokenBurnPermitExceeded { permit: Token, try_to_burn: Token },
#[error("Token burn permit is missing. Trying to burn: {try_to_burn:?}. Call `set_token_burn_permit()` to set the limit")]
TokenBurnPermitMissing { try_to_burn: Token },
#[error("Unused token burn permit: token id {token_id:?}, amount {amount:?}")]
TokenBurnPermitUnused { token_id: TokenId, amount: u64 },
#[error("Too many tokens in burn permit: {0}")]
TooManyTokensInBurnPermit(TokenAmountError),
#[error("Too many tokens in input boxes: {0}")]
TooManyTokensInInputBoxes(TokenAmountError),
#[error("Too many tokens in output candidate boxes: {0}")]
TooManyTokensInOutputCandidates(TokenAmountError),
#[error("Tokens in output candidate exceed tokens in input boxes: {0}")]
TokensInOutputsExceedInputs(TokenAmountError),
#[error("Coins in outputs are less than coins in inputs for {0} nanoERGs")]
NotEnoughCoinsInOutputs(u64),
}
pub(crate) fn vec_tokens_to_map(
tokens: Vec<Token>,
) -> Result<HashMap<TokenId, TokenAmount>, TokenAmountError> {
let mut res: HashMap<TokenId, TokenAmount> = HashMap::new();
tokens.iter().try_for_each(|b| {
if let Some(amt) = res.get_mut(&b.token_id) {
*amt = amt.checked_add(&b.amount)?;
} else {
res.insert(b.token_id, b.amount);
}
Ok(())
})?;
Ok(res)
}
fn check_enough_token_burn_permit(
burned_tokens: &HashMap<TokenId, TokenAmount>,
permits: &HashMap<TokenId, TokenAmount>,
) -> Result<(), TxBuilderError> {
for (burn_token_id, burn_amt) in burned_tokens {
if let Some(burn_amt_permit) = permits.get(burn_token_id) {
if burn_amt > burn_amt_permit {
return Err(TxBuilderError::TokenBurnPermitExceeded {
permit: (*burn_token_id, *burn_amt_permit).into(),
try_to_burn: (*burn_token_id, *burn_amt).into(),
});
}
} else {
return Err(TxBuilderError::TokenBurnPermitMissing {
try_to_burn: (*burn_token_id, *burn_amt).into(),
});
}
}
Ok(())
}
fn check_unused_token_burn_permit(
burned_tokens: &HashMap<TokenId, TokenAmount>,
permits: &HashMap<TokenId, TokenAmount>,
) -> Result<(), TxBuilderError> {
for (permit_token_id, permit_amt) in permits {
if let Some(burn_amt) = burned_tokens.get(permit_token_id) {
if burn_amt < permit_amt {
return Err(TxBuilderError::TokenBurnPermitUnused {
token_id: *permit_token_id,
amount: *permit_amt.as_u64() - *burn_amt.as_u64(),
});
}
} else {
return Err(TxBuilderError::TokenBurnPermitUnused {
token_id: *permit_token_id,
amount: *permit_amt.as_u64(),
});
}
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic)]
mod tests {
use std::convert::TryInto;
use ergotree_ir::chain::ergo_box::box_value::checked_sum;
use ergotree_ir::chain::ergo_box::ErgoBox;
use ergotree_ir::chain::ergo_box::NonMandatoryRegisters;
use ergotree_ir::chain::token::arbitrary::ArbTokenIdParam;
use ergotree_ir::chain::token::TokenAmount;
use ergotree_ir::chain::tx_id::TxId;
use ergotree_ir::ergo_tree::ErgoTree;
use proptest::{collection::vec, prelude::*};
use sigma_test_util::force_any_val;
use sigma_test_util::force_any_val_with;
use crate::wallet::box_selector::{BoxSelector, SimpleBoxSelector};
use super::*;
#[test]
fn test_duplicate_inputs() {
let input_box = force_any_val::<ErgoBox>();
let box_selection: BoxSelection<ErgoBox> = BoxSelection {
boxes: vec![input_box.clone(), input_box].try_into().unwrap(),
change_boxes: vec![],
};
let r = TxBuilder::new(
box_selection,
vec![force_any_val::<ErgoBoxCandidate>()],
1,
force_any_val::<BoxValue>(),
force_any_val::<Address>(),
);
assert!(matches!(r.build(), Err(TxBuilderError::InvalidArgs(_))));
}
#[test]
fn test_empty_outputs() {
let inputs = vec![force_any_val::<ErgoBox>()];
let outputs: Vec<ErgoBoxCandidate> = vec![];
let r = TxBuilder::new(
SimpleBoxSelector::new()
.select(inputs, BoxValue::MIN, &[])
.unwrap(),
outputs,
1,
force_any_val::<BoxValue>(),
force_any_val::<Address>(),
);
assert!(matches!(r.build(), Err(TxBuilderError::InvalidArgs(_))));
}
#[test]
fn test_burn_token_wo_permit() {
let token_pair = Token {
token_id: force_any_val::<TokenId>(),
amount: 100.try_into().unwrap(),
};
let input_box = ErgoBox::new(
10000000i64.try_into().unwrap(),
force_any_val::<ErgoTree>(),
vec![token_pair.clone()].try_into().ok(),
NonMandatoryRegisters::empty(),
1,
force_any_val::<TxId>(),
0,
)
.unwrap();
let inputs: Vec<ErgoBox> = vec![input_box];
let tx_fee = BoxValue::SAFE_USER_MIN;
let out_box_value = BoxValue::SAFE_USER_MIN;
let target_balance = out_box_value.checked_add(&tx_fee).unwrap();
let target_token = Token {
amount: 10.try_into().unwrap(),
..token_pair
};
let target_tokens = vec![target_token.clone()];
let box_selection = SimpleBoxSelector::new()
.select(inputs, target_balance, target_tokens.as_slice())
.unwrap();
let box_builder =
ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
let out_box = box_builder.build().unwrap();
let outputs = vec![out_box];
let tx_builder = TxBuilder::new(
box_selection,
outputs,
0,
tx_fee,
force_any_val::<Address>(),
);
let res = tx_builder.build();
assert_eq!(
res,
Err(TxBuilderError::TokenBurnPermitMissing {
try_to_burn: target_token
})
);
}
#[test]
fn test_burn_token_w_permit_too_low() {
let token_pair = Token {
token_id: force_any_val::<TokenId>(),
amount: 100.try_into().unwrap(),
};
let input_box = ErgoBox::new(
10000000i64.try_into().unwrap(),
force_any_val::<ErgoTree>(),
vec![token_pair.clone()].try_into().ok(),
NonMandatoryRegisters::empty(),
1,
force_any_val::<TxId>(),
0,
)
.unwrap();
let inputs: Vec<ErgoBox> = vec![input_box];
let tx_fee = BoxValue::SAFE_USER_MIN;
let out_box_value = BoxValue::SAFE_USER_MIN;
let target_balance = out_box_value.checked_add(&tx_fee).unwrap();
let token_to_burn = Token {
amount: 10.try_into().unwrap(),
..token_pair
};
let target_tokens = vec![token_to_burn.clone()];
let box_selection = SimpleBoxSelector::new()
.select(inputs, target_balance, target_tokens.as_slice())
.unwrap();
let box_builder =
ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
let out_box = box_builder.build().unwrap();
let outputs = vec![out_box];
let mut tx_builder = TxBuilder::new(
box_selection,
outputs,
0,
tx_fee,
force_any_val::<Address>(),
);
let token_burn_permit = Token {
amount: 5.try_into().unwrap(),
..token_pair
};
tx_builder.set_token_burn_permit(vec![token_burn_permit.clone()]);
let res = tx_builder.build();
assert_eq!(
res,
Err(TxBuilderError::TokenBurnPermitExceeded {
try_to_burn: token_to_burn,
permit: token_burn_permit,
})
);
}
#[test]
fn test_burn_token() {
let token_pair = Token {
token_id: force_any_val::<TokenId>(),
amount: 100.try_into().unwrap(),
};
let input_box = ErgoBox::new(
10000000i64.try_into().unwrap(),
force_any_val::<ErgoTree>(),
vec![token_pair.clone()].try_into().ok(),
NonMandatoryRegisters::empty(),
1,
force_any_val::<TxId>(),
0,
)
.unwrap();
let inputs: Vec<ErgoBox> = vec![input_box];
let tx_fee = BoxValue::SAFE_USER_MIN;
let out_box_value = BoxValue::SAFE_USER_MIN;
let target_balance = out_box_value.checked_add(&tx_fee).unwrap();
let token_to_burn = Token {
amount: 10.try_into().unwrap(),
..token_pair
};
let target_tokens = vec![token_to_burn.clone()];
let box_selection = SimpleBoxSelector::new()
.select(inputs, target_balance, target_tokens.as_slice())
.unwrap();
let box_builder =
ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
let out_box = box_builder.build().unwrap();
let outputs = vec![out_box];
let mut tx_builder = TxBuilder::new(
box_selection,
outputs,
0,
tx_fee,
force_any_val::<Address>(),
);
tx_builder.set_token_burn_permit(vec![token_to_burn]);
let _ = tx_builder.build().unwrap();
}
#[test]
fn test_token_burn_permit_wo_burn() {
let token_pair = Token {
token_id: force_any_val::<TokenId>(),
amount: 100.try_into().unwrap(),
};
let input_box = ErgoBox::new(
10000000i64.try_into().unwrap(),
force_any_val::<ErgoTree>(),
vec![token_pair.clone()].try_into().ok(),
NonMandatoryRegisters::empty(),
1,
force_any_val::<TxId>(),
0,
)
.unwrap();
let inputs: Vec<ErgoBox> = vec![input_box];
let tx_fee = BoxValue::SAFE_USER_MIN;
let out_box_value = BoxValue::SAFE_USER_MIN;
let target_balance = out_box_value.checked_add(&tx_fee).unwrap();
let token_to_burn = Token {
amount: 10.try_into().unwrap(),
..token_pair
};
let box_selection = SimpleBoxSelector::new()
.select(inputs, target_balance, &Vec::new())
.unwrap();
let box_builder =
ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
let out_box = box_builder.build().unwrap();
let outputs = vec![out_box];
let mut tx_builder = TxBuilder::new(
box_selection,
outputs,
0,
tx_fee,
force_any_val::<Address>(),
);
tx_builder.set_token_burn_permit(vec![token_to_burn.clone()]);
let res = tx_builder.build();
assert_eq!(
res,
Err(TxBuilderError::TokenBurnPermitUnused {
token_id: token_to_burn.token_id,
amount: *token_to_burn.amount.as_u64(),
})
);
}
#[test]
fn test_mint_token() {
let input_box = ErgoBox::new(
100000000i64.try_into().unwrap(),
force_any_val::<ErgoTree>(),
None,
NonMandatoryRegisters::empty(),
1,
force_any_val::<TxId>(),
0,
)
.unwrap();
let token_pair = Token {
token_id: TokenId::from(input_box.box_id()),
amount: 1.try_into().unwrap(),
};
let out_box_value = BoxValue::SAFE_USER_MIN;
let token_name = "TKN".to_string();
let token_desc = "token desc".to_string();
let token_num_dec = 2;
let mut box_builder =
ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
box_builder.mint_token(token_pair.clone(), token_name, token_desc, token_num_dec);
let out_box = box_builder.build().unwrap();
let inputs: Vec<ErgoBox> = vec![input_box];
let tx_fee = BoxValue::SAFE_USER_MIN;
let target_balance = out_box_value.checked_add(&tx_fee).unwrap();
let box_selection = SimpleBoxSelector::new()
.select(inputs, target_balance, vec![].as_slice())
.unwrap();
let outputs = vec![out_box];
let tx_builder = TxBuilder::new(
box_selection,
outputs,
0,
tx_fee,
force_any_val::<Address>(),
);
let tx = tx_builder.build().unwrap();
assert_eq!(
tx.output_candidates
.get(0)
.unwrap()
.tokens()
.unwrap()
.first()
.token_id,
token_pair.token_id,
"expected minted token in the first output box"
);
}
#[test]
fn test_tokens_balance_error() {
let input_box = force_any_val_with::<ErgoBox>(
(BoxValue::MIN_RAW * 5000..BoxValue::MIN_RAW * 10000).into(),
);
let token_pair = Token {
token_id: force_any_val_with::<TokenId>(ArbTokenIdParam::Arbitrary),
amount: force_any_val::<TokenAmount>(),
};
let out_box_value = BoxValue::SAFE_USER_MIN;
let mut box_builder =
ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
box_builder.add_token(token_pair.clone());
let out_box = box_builder.build().unwrap();
let inputs: Vec<ErgoBox> = vec![input_box];
let tx_fee = BoxValue::SAFE_USER_MIN;
let target_balance = out_box_value.checked_add(&tx_fee).unwrap();
let box_selection = SimpleBoxSelector::new()
.select(inputs, target_balance, vec![].as_slice())
.unwrap();
let outputs = vec![out_box];
let tx_builder = TxBuilder::new(
box_selection,
outputs,
0,
tx_fee,
force_any_val::<Address>(),
);
assert_eq!(
tx_builder.build(),
Err(TxBuilderError::NotEnoughTokens(vec![token_pair])),
);
}
#[test]
fn test_balance_error_not_enough_inputs() {
let input_box = force_any_val_with::<ErgoBox>(
(BoxValue::MIN_RAW * 5000..BoxValue::MIN_RAW * 10000).into(),
);
let out_box_value = input_box.value();
let mut box_builder =
ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
input_box.tokens.iter().for_each(|tokens| {
tokens.iter().for_each(|t| {
box_builder.add_token(t.clone());
})
});
let out_box = box_builder.build().unwrap();
let inputs: Vec<ErgoBox> = vec![input_box];
let tx_fee = BoxValue::SAFE_USER_MIN;
let box_selection = BoxSelection {
boxes: inputs.try_into().unwrap(),
change_boxes: vec![],
};
let outputs = vec![out_box];
let tx_builder = TxBuilder::new(
box_selection,
outputs,
0,
tx_fee,
force_any_val::<Address>(),
);
assert_eq!(
tx_builder.build(),
Err(TxBuilderError::NotEnoughCoinsInInputs(
*BoxValue::SAFE_USER_MIN.as_u64()
)),
);
}
#[test]
fn test_balance_error_not_enough_outputs() {
let input_box = force_any_val_with::<ErgoBox>(
(BoxValue::MIN_RAW * 5000..BoxValue::MIN_RAW * 10000).into(),
);
let out_box_value = input_box
.value()
.checked_sub(&BoxValue::SAFE_USER_MIN)
.unwrap()
.checked_sub(&BoxValue::SAFE_USER_MIN)
.unwrap();
let mut box_builder =
ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
input_box.tokens.iter().for_each(|tokens| {
tokens.iter().for_each(|t| {
box_builder.add_token(t.clone());
})
});
let out_box = box_builder.build().unwrap();
let inputs: Vec<ErgoBox> = vec![input_box];
let tx_fee = BoxValue::SAFE_USER_MIN;
let box_selection = BoxSelection {
boxes: inputs.try_into().unwrap(),
change_boxes: vec![],
};
let outputs = vec![out_box];
let tx_builder = TxBuilder::new(
box_selection,
outputs,
0,
tx_fee,
force_any_val::<Address>(),
);
assert_eq!(
tx_builder.build(),
Err(TxBuilderError::NotEnoughCoinsInOutputs(
*BoxValue::SAFE_USER_MIN.as_u64()
)),
);
}
#[test]
fn test_est_tx_size() {
let input = ErgoBox::new(
10000000i64.try_into().unwrap(),
force_any_val::<ErgoTree>(),
None,
NonMandatoryRegisters::empty(),
1,
force_any_val::<TxId>(),
0,
)
.unwrap();
let tx_fee = super::SUGGESTED_TX_FEE();
let out_box_value = input.value.checked_sub(&tx_fee).unwrap();
let box_builder =
ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
let out_box = box_builder.build().unwrap();
let outputs = vec![out_box];
let tx_builder = TxBuilder::new(
BoxSelection {
boxes: vec![input].try_into().unwrap(),
change_boxes: vec![],
},
outputs,
0,
tx_fee,
force_any_val::<Address>(),
);
assert!(tx_builder.estimate_tx_size_bytes().unwrap() > 0);
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(16))]
#[test]
fn test_build_tx(inputs in vec(any_with::<ErgoBox>((BoxValue::MIN_RAW * 5000 .. BoxValue::MIN_RAW * 10000).into()), 1..10),
outputs in vec(any_with::<ErgoBoxCandidate>((BoxValue::MIN_RAW * 1000 ..BoxValue::MIN_RAW * 2000).into()), 1..2),
change_address in any::<Address>(),
miners_fee in any_with::<BoxValue>((BoxValue::MIN_RAW * 100..BoxValue::MIN_RAW * 200).into()),
data_inputs in vec(any::<DataInput>(), 0..2),
ctx_ext in any::<ContextExtension>()) {
prop_assume!(sum_tokens_from_boxes(outputs.as_slice()).unwrap().is_empty());
let all_outputs = checked_sum(outputs.iter().map(|b| b.value)).unwrap()
.checked_add(&miners_fee)
.unwrap();
let all_inputs = checked_sum(inputs.iter().map(|b| b.value)).unwrap();
prop_assume!(all_outputs < all_inputs);
let total_output_value: BoxValue = checked_sum(outputs.iter().map(|b| b.value))
.unwrap()
.checked_add(&miners_fee).unwrap();
let selection = SimpleBoxSelector::new().select(inputs.clone(), total_output_value, &[]).unwrap();
let mut tx_builder = TxBuilder::new(
selection.clone(),
outputs.clone(),
1,
miners_fee,
change_address.clone(),
);
tx_builder.set_data_inputs(data_inputs.clone());
tx_builder.set_context_extension(selection.boxes.first().box_id(), ctx_ext.clone());
let tx = tx_builder.build().unwrap();
prop_assert!(outputs.into_iter().all(|i| tx.output_candidates.iter().any(|o| *o == i)),
"tx.output_candidates is missing some outputs");
let tx_all_inputs_vals = tx.inputs.iter()
.map(|i| inputs.iter()
.find(|ib| ib.box_id() == i.box_id).unwrap().value);
let tx_all_inputs_sum = checked_sum(tx_all_inputs_vals).unwrap();
let expected_change = tx_all_inputs_sum.checked_sub(&all_outputs).unwrap();
prop_assert!(tx.output_candidates.iter().any(|b| {
b.value == expected_change && b.ergo_tree == change_address.script().unwrap()
}), "box with change {:?} is not found in outputs: {:?}", expected_change, tx.output_candidates);
prop_assert!(tx.output_candidates.iter().any(|b| {
b.value == miners_fee
}), "box with miner's fee {:?} is not found in outputs: {:?}", miners_fee, tx.output_candidates);
prop_assert_eq!(tx.data_inputs.map(|i| i.as_vec().clone()).unwrap_or_default(), data_inputs, "unexpected data inputs");
prop_assert_eq!(&tx.inputs.first().extension, &ctx_ext);
}
}
}