#![allow(deprecated)]
use crate::*;
use super::*;
use crate::builders::fakes::{fake_bootstrap_witness, fake_raw_key_public, fake_raw_key_sig};
use crate::fees;
use crate::utils;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
fn count_needed_vkeys(tx_builder: &TransactionBuilder) -> usize {
let mut input_hashes: Ed25519KeyHashes = Ed25519KeyHashes::from(&tx_builder.inputs);
input_hashes.extend_move(Ed25519KeyHashes::from(&tx_builder.collateral));
input_hashes.extend_move(tx_builder.required_signers.clone());
if let Some(mint_builder) = &tx_builder.mint {
input_hashes.extend_move(Ed25519KeyHashes::from(&mint_builder.get_native_scripts()));
}
if let Some(withdrawals_builder) = &tx_builder.withdrawals {
input_hashes.extend_move(withdrawals_builder.get_required_signers());
}
if let Some(certs_builder) = &tx_builder.certs {
input_hashes.extend_move(certs_builder.get_required_signers());
}
if let Some(voting_builder) = &tx_builder.voting_procedures {
input_hashes.extend_move(voting_builder.get_required_signers());
}
input_hashes.len()
}
pub(crate) fn fake_full_tx(
tx_builder: &TransactionBuilder,
body: TransactionBody,
) -> Result<Transaction, JsError> {
let fake_sig = fake_raw_key_sig();
let vkeys = match count_needed_vkeys(tx_builder) {
0 => None,
x => {
let mut result = Vkeywitnesses::new();
for i in 0..x {
let raw_key_public = fake_raw_key_public(i as u64);
let fake_vkey_witness = Vkeywitness::new(&Vkey::new(&raw_key_public), &fake_sig);
result.add(&fake_vkey_witness.clone());
}
Some(result)
}
};
let bootstraps = get_bootstraps(&tx_builder.inputs);
let bootstrap_keys = match bootstraps.len() {
0 => None,
_x => {
let mut result = BootstrapWitnesses::new();
let mut number = 1;
for addr in bootstraps {
number += 1;
result.add(&fake_bootstrap_witness(number, &ByronAddress::from_bytes(addr)?));
}
Some(result)
}
};
let (plutus_scripts, mut plutus_data, redeemers) = {
if let Some(s) = tx_builder.get_combined_plutus_scripts() {
let (s, d, r) = s.collect();
(Some(s), d, Some(r))
} else {
(None, None, None)
}
};
if let Some(extra_datums) = &tx_builder.extra_datums {
if let Some(d) = &mut plutus_data {
d.extend(extra_datums);
} else {
plutus_data = Some(extra_datums.clone());
}
}
let witness_set = TransactionWitnessSet::new_with_partial_dedup(
vkeys,
tx_builder.get_combined_native_scripts(),
bootstrap_keys,
plutus_scripts,
plutus_data,
redeemers,
);
Ok(Transaction {
body,
witness_set,
is_valid: true,
auxiliary_data: tx_builder.auxiliary_data.clone(),
})
}
fn assert_required_mint_scripts(
mint: &Mint,
maybe_mint_scripts: Option<&NativeScripts>,
) -> Result<(), JsError> {
if maybe_mint_scripts.is_none_or_empty() {
return Err(JsError::from_str(
"Mint is present in the builder, but witness scripts are not provided!",
));
}
let mint_scripts = maybe_mint_scripts.unwrap();
let witness_hashes: HashSet<ScriptHash> =
mint_scripts.iter().map(|script| script.hash()).collect();
for mint_hash in mint.keys().0.iter() {
if !witness_hashes.contains(mint_hash) {
return Err(JsError::from_str(&format!(
"No witness script is found for mint policy '{:?}'! Script is required!",
hex::encode(mint_hash.to_bytes()),
)));
}
}
Ok(())
}
fn min_fee(tx_builder: &TransactionBuilder) -> Result<Coin, JsError> {
let full_tx = fake_full_tx(tx_builder, tx_builder.build()?)?;
let mut fee: Coin = fees::min_fee(&full_tx, &tx_builder.config.fee_algo)?;
if let Some(ex_unit_prices) = &tx_builder.config.ex_unit_prices {
let script_fee: Coin = fees::min_script_fee(&full_tx, &ex_unit_prices)?;
fee = fee.checked_add(&script_fee)?;
} else {
if tx_builder.has_plutus_inputs() {
return Err(JsError::from_str(
"Plutus inputs are present but ex_unit_prices are missing in the config!",
));
}
}
let total_ref_script_size = tx_builder.get_total_ref_scripts_size()?;
if let Some(ref_script_coins_per_byte) = &tx_builder.config.ref_script_coins_per_byte {
let script_ref_fee = min_ref_script_fee(total_ref_script_size, ref_script_coins_per_byte)?;
fee = fee.checked_add(&script_ref_fee)?;
} else {
if total_ref_script_size > 0 {
return Err(JsError::from_str(
"Plutus scripts are present but ref_script_coins_per_byte are missing in the config!",
));
}
}
Ok(fee)
}
#[wasm_bindgen]
pub enum CoinSelectionStrategyCIP2 {
LargestFirst,
RandomImprove,
LargestFirstMultiAsset,
RandomImproveMultiAsset,
}
#[wasm_bindgen]
#[derive(Clone, Debug)]
pub struct TransactionBuilderConfig {
pub(crate) fee_algo: fees::LinearFee,
pub(crate) pool_deposit: Coin, pub(crate) key_deposit: Coin, pub(crate) max_value_size: u32, pub(crate) max_tx_size: u32, pub(crate) data_cost: DataCost, pub(crate) ex_unit_prices: Option<ExUnitPrices>, pub(crate) ref_script_coins_per_byte: Option<UnitInterval>, pub(crate) prefer_pure_change: bool,
pub(crate) deduplicate_explicit_ref_inputs_with_regular_inputs: bool,
}
impl TransactionBuilderConfig {
pub(crate) fn utxo_cost(&self) -> DataCost {
self.data_cost.clone()
}
}
#[wasm_bindgen]
#[derive(Clone, Debug)]
pub struct TransactionBuilderConfigBuilder {
fee_algo: Option<fees::LinearFee>,
pool_deposit: Option<Coin>, key_deposit: Option<Coin>, max_value_size: Option<u32>, max_tx_size: Option<u32>, data_cost: Option<DataCost>, ex_unit_prices: Option<ExUnitPrices>, ref_script_coins_per_byte: Option<UnitInterval>, prefer_pure_change: bool,
deduplicate_explicit_ref_inputs_with_regular_inputs: bool,
}
#[wasm_bindgen]
impl TransactionBuilderConfigBuilder {
pub fn new() -> Self {
Self {
fee_algo: None,
pool_deposit: None,
key_deposit: None,
max_value_size: None,
max_tx_size: None,
data_cost: None,
ex_unit_prices: None,
ref_script_coins_per_byte: None,
prefer_pure_change: false,
deduplicate_explicit_ref_inputs_with_regular_inputs: false,
}
}
pub fn fee_algo(&self, fee_algo: &fees::LinearFee) -> Self {
let mut cfg = self.clone();
cfg.fee_algo = Some(fee_algo.clone());
cfg
}
pub fn coins_per_utxo_byte(&self, coins_per_utxo_byte: &Coin) -> Self {
let mut cfg = self.clone();
cfg.data_cost = Some(DataCost::new_coins_per_byte(coins_per_utxo_byte));
cfg
}
pub fn ex_unit_prices(&self, ex_unit_prices: &ExUnitPrices) -> Self {
let mut cfg = self.clone();
cfg.ex_unit_prices = Some(ex_unit_prices.clone());
cfg
}
pub fn pool_deposit(&self, pool_deposit: &BigNum) -> Self {
let mut cfg = self.clone();
cfg.pool_deposit = Some(pool_deposit.clone());
cfg
}
pub fn key_deposit(&self, key_deposit: &BigNum) -> Self {
let mut cfg = self.clone();
cfg.key_deposit = Some(key_deposit.clone());
cfg
}
pub fn max_value_size(&self, max_value_size: u32) -> Self {
let mut cfg = self.clone();
cfg.max_value_size = Some(max_value_size);
cfg
}
pub fn max_tx_size(&self, max_tx_size: u32) -> Self {
let mut cfg = self.clone();
cfg.max_tx_size = Some(max_tx_size);
cfg
}
pub fn ref_script_coins_per_byte(&self, ref_script_coins_per_byte: &UnitInterval) -> Self {
let mut cfg = self.clone();
cfg.ref_script_coins_per_byte = Some(ref_script_coins_per_byte.clone());
cfg
}
pub fn prefer_pure_change(&self, prefer_pure_change: bool) -> Self {
let mut cfg = self.clone();
cfg.prefer_pure_change = prefer_pure_change;
cfg
}
pub fn deduplicate_explicit_ref_inputs_with_regular_inputs(&self, deduplicate_explicit_ref_inputs_with_regular_inputs: bool) -> Self {
let mut cfg = self.clone();
cfg.deduplicate_explicit_ref_inputs_with_regular_inputs = deduplicate_explicit_ref_inputs_with_regular_inputs;
cfg
}
pub fn build(&self) -> Result<TransactionBuilderConfig, JsError> {
let cfg: Self = self.clone();
Ok(TransactionBuilderConfig {
fee_algo: cfg
.fee_algo
.ok_or(JsError::from_str("uninitialized field: fee_algo"))?,
pool_deposit: cfg
.pool_deposit
.ok_or(JsError::from_str("uninitialized field: pool_deposit"))?,
key_deposit: cfg
.key_deposit
.ok_or(JsError::from_str("uninitialized field: key_deposit"))?,
max_value_size: cfg
.max_value_size
.ok_or(JsError::from_str("uninitialized field: max_value_size"))?,
max_tx_size: cfg
.max_tx_size
.ok_or(JsError::from_str("uninitialized field: max_tx_size"))?,
data_cost: cfg.data_cost.ok_or(JsError::from_str(
"uninitialized field: coins_per_utxo_byte or coins_per_utxo_word",
))?,
ex_unit_prices: cfg.ex_unit_prices,
ref_script_coins_per_byte: cfg.ref_script_coins_per_byte,
prefer_pure_change: cfg.prefer_pure_change,
deduplicate_explicit_ref_inputs_with_regular_inputs: cfg.deduplicate_explicit_ref_inputs_with_regular_inputs,
})
}
}
#[wasm_bindgen]
#[derive(Clone, Debug)]
pub struct ChangeConfig {
address: Address,
plutus_data: Option<OutputDatum>,
script_ref: Option<ScriptRef>,
}
#[wasm_bindgen]
impl ChangeConfig {
pub fn new(address: &Address) -> Self {
Self {
address: address.clone(),
plutus_data: None,
script_ref: None,
}
}
pub fn change_address(&self, address: &Address) -> Self {
let mut c_cfg = self.clone();
c_cfg.address = address.clone();
c_cfg
}
pub fn change_plutus_data(&self, plutus_data: &OutputDatum) -> Self {
let mut c_cfg = self.clone();
c_cfg.plutus_data = Some(plutus_data.clone());
c_cfg
}
pub fn change_script_ref(&self, script_ref: &ScriptRef) -> Self {
let mut c_cfg = self.clone();
c_cfg.script_ref = Some(script_ref.clone());
c_cfg
}
}
#[wasm_bindgen]
#[derive(Clone, Debug)]
pub struct TransactionBuilder {
pub(crate) config: TransactionBuilderConfig,
pub(crate) inputs: TxInputsBuilder,
pub(crate) collateral: TxInputsBuilder,
pub(crate) outputs: TransactionOutputs,
pub(crate) fee: Option<Coin>,
pub(crate) ttl: Option<SlotBigNum>, pub(crate) certs: Option<CertificatesBuilder>,
pub(crate) withdrawals: Option<WithdrawalsBuilder>,
pub(crate) auxiliary_data: Option<AuxiliaryData>,
pub(crate) validity_start_interval: Option<SlotBigNum>,
pub(crate) mint: Option<MintBuilder>,
pub(crate) script_data_hash: Option<ScriptDataHash>,
pub(crate) required_signers: Ed25519KeyHashes,
pub(crate) collateral_return: Option<TransactionOutput>,
pub(crate) total_collateral: Option<Coin>,
pub(crate) reference_inputs: HashMap<TransactionInput, usize>,
pub(crate) extra_datums: Option<PlutusList>,
pub(crate) voting_procedures: Option<VotingBuilder>,
pub(crate) voting_proposals: Option<VotingProposalBuilder>,
pub(crate) current_treasury_value: Option<Coin>,
pub(crate) donation: Option<Coin>,
}
#[wasm_bindgen]
impl TransactionBuilder {
pub fn add_inputs_from(
&mut self,
inputs: &TransactionUnspentOutputs,
strategy: CoinSelectionStrategyCIP2,
) -> Result<(), JsError> {
let mut available_inputs: Vec<&TransactionUnspentOutput> = inputs.0.iter().collect();
let have_no_inputs_in_tx = !self.inputs.has_inputs();
let mut input_total = self.get_total_input()?;
let mut output_total = self
.get_total_output()?
.checked_add(&Value::new(&self.min_fee()?))?;
if (input_total.coin >= output_total.coin) && have_no_inputs_in_tx {
if available_inputs.is_empty() {
return Err(JsError::from_str("No inputs to add. Transaction should have at least one input"));
}
let input = available_inputs.pop().unwrap();
self.add_regular_input(
&input.output.address,
&input.input,
&input.output.amount,
)?;
input_total = input_total.checked_add(&input.output.amount)?;
}
match strategy {
CoinSelectionStrategyCIP2::LargestFirst => {
if self
.outputs
.0
.iter()
.any(|output| output.amount.multiasset.is_some())
{
return Err(JsError::from_str("Multiasset values not supported by LargestFirst. Please use LargestFirstMultiAsset"));
}
self.cip2_largest_first_by(
&available_inputs,
&mut (0..available_inputs.len()).collect(),
&mut input_total,
&mut output_total,
|value| Some(value.coin),
)?;
}
CoinSelectionStrategyCIP2::RandomImprove => {
if self
.outputs
.0
.iter()
.any(|output| output.amount.multiasset.is_some())
{
return Err(JsError::from_str("Multiasset values not supported by RandomImprove. Please use RandomImproveMultiAsset"));
}
use rand::Rng;
let mut rng = rand::thread_rng();
let mut available_indices =
(0..available_inputs.len()).collect::<BTreeSet<usize>>();
self.cip2_random_improve_by(
&available_inputs,
&mut available_indices,
&mut input_total,
&mut output_total,
|value| Some(value.coin),
&mut rng,
true,
)?;
while input_total.coin < output_total.coin {
if available_indices.is_empty() {
return Err(JsError::from_str("UTxO Balance Insufficient[x]"));
}
let i = *available_indices
.iter()
.nth(rng.gen_range(0..available_indices.len()))
.unwrap();
available_indices.remove(&i);
let input = &available_inputs[i];
let input_fee = self.fee_for_input(
&input.output.address,
&input.input,
&input.output.amount,
)?;
self.add_regular_input(
&input.output.address,
&input.input,
&input.output.amount,
)?;
input_total = input_total.checked_add(&input.output.amount)?;
output_total = output_total.checked_add(&Value::new(&input_fee))?;
}
}
CoinSelectionStrategyCIP2::LargestFirstMultiAsset => {
let mut available_indices = (0..available_inputs.len()).collect::<Vec<usize>>();
if let Some(ma) = output_total.multiasset.clone() {
for (policy_id, assets) in ma.0.iter() {
for (asset_name, _) in assets.0.iter() {
self.cip2_largest_first_by(
&available_inputs,
&mut available_indices,
&mut input_total,
&mut output_total,
|value| value.multiasset.as_ref()?.get(policy_id)?.get(asset_name),
)?;
}
}
}
self.cip2_largest_first_by(
&available_inputs,
&mut available_indices,
&mut input_total,
&mut output_total,
|value| Some(value.coin),
)?;
}
CoinSelectionStrategyCIP2::RandomImproveMultiAsset => {
use rand::Rng;
let mut rng = rand::thread_rng();
let mut available_indices =
(0..available_inputs.len()).collect::<BTreeSet<usize>>();
if let Some(ma) = output_total.multiasset.clone() {
for (policy_id, assets) in ma.0.iter() {
for (asset_name, _) in assets.0.iter() {
self.cip2_random_improve_by(
&available_inputs,
&mut available_indices,
&mut input_total,
&mut output_total,
|value| value.multiasset.as_ref()?.get(policy_id)?.get(asset_name),
&mut rng,
false,
)?;
}
}
}
self.cip2_random_improve_by(
&available_inputs,
&mut available_indices,
&mut input_total,
&mut output_total,
|value| Some(value.coin),
&mut rng,
false,
)?;
while input_total.coin < output_total.coin {
if available_indices.is_empty() {
return Err(JsError::from_str("UTxO Balance Insufficient[x]"));
}
let i = *available_indices
.iter()
.nth(rng.gen_range(0..available_indices.len()))
.unwrap();
available_indices.remove(&i);
let input = &available_inputs[i];
let input_fee = self.fee_for_input(
&input.output.address,
&input.input,
&input.output.amount,
)?;
self.add_regular_input(
&input.output.address,
&input.input,
&input.output.amount,
)?;
input_total = input_total.checked_add(&input.output.amount)?;
output_total = output_total.checked_add(&Value::new(&input_fee))?;
}
}
}
Ok(())
}
fn cip2_largest_first_by<F>(
&mut self,
available_inputs: &Vec<&TransactionUnspentOutput>,
available_indices: &mut Vec<usize>,
input_total: &mut Value,
output_total: &mut Value,
by: F,
) -> Result<(), JsError>
where
F: Fn(&Value) -> Option<BigNum>,
{
let mut relevant_indices = available_indices.clone();
relevant_indices.retain(|i| by(&available_inputs[*i].output.amount).is_some());
relevant_indices
.sort_by_key(|i| by(&available_inputs[*i].output.amount).expect("filtered above"));
for i in relevant_indices.iter().rev() {
if by(input_total).unwrap_or(BigNum::zero())
>= by(output_total).expect("do not call on asset types that aren't in the output")
{
break;
}
let input = &available_inputs[*i];
let input_fee =
self.fee_for_input(&input.output.address, &input.input, &input.output.amount)?;
self.add_regular_input(&input.output.address, &input.input, &input.output.amount)?;
*input_total = input_total.checked_add(&input.output.amount)?;
*output_total = output_total.checked_add(&Value::new(&input_fee))?;
available_indices.swap_remove(available_indices.iter().position(|j| i == j).unwrap());
}
if by(input_total).unwrap_or(BigNum::zero())
< by(output_total).expect("do not call on asset types that aren't in the output")
{
return Err(JsError::from_str("UTxO Balance Insufficient"));
}
Ok(())
}
fn cip2_random_improve_by<F>(
&mut self,
available_inputs: &Vec<&TransactionUnspentOutput>,
available_indices: &mut BTreeSet<usize>,
input_total: &mut Value,
output_total: &mut Value,
by: F,
rng: &mut rand::rngs::ThreadRng,
pure_ada: bool,
) -> Result<(), JsError>
where
F: Fn(&Value) -> Option<BigNum>,
{
use rand::Rng;
let mut relevant_indices = available_indices
.iter()
.filter(|i| by(&available_inputs[**i].output.amount).is_some())
.cloned()
.collect::<Vec<usize>>();
let mut associated_indices: BTreeMap<TransactionOutput, Vec<usize>> = BTreeMap::new();
let mut outputs = self
.outputs
.0
.iter()
.filter(|output| by(&output.amount).is_some())
.cloned()
.collect::<Vec<TransactionOutput>>();
outputs.sort_by_key(|output| by(&output.amount).expect("filtered above"));
let mut available_coins = by(input_total).unwrap_or(BigNum::zero());
for output in outputs.iter().rev() {
let mut added = available_coins.clone();
let needed = by(&output.amount).unwrap();
while added < needed {
if relevant_indices.is_empty() {
return Err(JsError::from_str("UTxO Balance Insufficient"));
}
let random_index = rng.gen_range(0..relevant_indices.len());
let i = relevant_indices.swap_remove(random_index);
available_indices.remove(&i);
let input = &available_inputs[i];
added = added.checked_add(
&by(&input.output.amount)
.expect("do not call on asset types that aren't in the output"),
)?;
associated_indices
.entry(output.clone())
.or_default()
.push(i);
}
available_coins = added.checked_sub(&needed)?;
}
if !relevant_indices.is_empty() && pure_ada {
for output in outputs.iter_mut() {
let associated = associated_indices.get_mut(output);
if let Some(associated) = associated {
for i in associated.iter_mut() {
let random_index = rng.gen_range(0..relevant_indices.len());
let j: &mut usize = relevant_indices.get_mut(random_index).unwrap();
let input = &available_inputs[*i];
let new_input = &available_inputs[*j];
let cur: u64 = (&by(&input.output.amount).unwrap_or(BigNum::zero())).into();
let new: u64 = (&by(&new_input.output.amount).unwrap_or(BigNum::zero())).into();
let min: u64 = (&by(&output.amount).unwrap_or(BigNum::zero())).into();
let ideal = 2 * min;
let max = 3 * min;
let move_closer =
(ideal as i128 - new as i128).abs() < (ideal as i128 - cur as i128).abs();
let not_exceed_max = new < max;
if move_closer && not_exceed_max {
std::mem::swap(i, j);
available_indices.insert(*i);
available_indices.remove(j);
}
}
}
}
}
for output in outputs.iter() {
if let Some(associated) = associated_indices.get(output) {
for i in associated.iter() {
let input = &available_inputs[*i];
let input_fee = self.fee_for_input(
&input.output.address,
&input.input,
&input.output.amount,
)?;
self.add_regular_input(
&input.output.address,
&input.input,
&input.output.amount,
)?;
*input_total = input_total.checked_add(&input.output.amount)?;
*output_total = output_total.checked_add(&Value::new(&input_fee))?;
}
}
}
Ok(())
}
pub fn set_inputs(&mut self, inputs: &TxInputsBuilder) {
self.inputs = inputs.clone();
}
pub fn set_collateral(&mut self, collateral: &TxInputsBuilder) {
self.collateral = collateral.clone();
}
pub fn set_collateral_return(&mut self, collateral_return: &TransactionOutput) {
self.collateral_return = Some(collateral_return.clone());
}
pub fn remove_collateral_return(&mut self) {
self.collateral_return = None;
}
pub fn set_collateral_return_and_total(
&mut self,
collateral_return: &TransactionOutput,
) -> Result<(), JsError> {
let collateral = &self.collateral;
if collateral.len() == 0 {
return Err(JsError::from_str(
"Cannot calculate total collateral value when collateral inputs are missing",
));
}
let col_input_value: Value = collateral.total_value()?;
let total_col: Value = col_input_value.checked_sub(&collateral_return.amount())?;
if total_col.multiasset.is_some() {
return Err(JsError::from_str(
"Total collateral value cannot contain assets!",
));
}
let min_ada = min_ada_for_output(&collateral_return, &self.config.utxo_cost())?;
if min_ada > collateral_return.amount.coin {
return Err(JsError::from_str(&format!(
"Not enough coin to make return on the collateral value!\
Increase amount of return coins. \
Min ada for return {}, but was {}",
min_ada, collateral_return.amount.coin
)));
}
self.set_collateral_return(collateral_return);
self.total_collateral = Some(total_col.coin);
Ok(())
}
pub fn set_total_collateral(&mut self, total_collateral: &Coin) {
self.total_collateral = Some(total_collateral.clone());
}
pub fn remove_total_collateral(&mut self) {
self.total_collateral = None;
}
pub fn set_total_collateral_and_return(
&mut self,
total_collateral: &Coin,
return_address: &Address,
) -> Result<(), JsError> {
let collateral = &self.collateral;
if collateral.len() == 0 {
return Err(JsError::from_str(
"Cannot calculate collateral return when collateral inputs are missing",
));
}
let col_input_value: Value = collateral.total_value()?;
let col_return: Value = col_input_value.checked_sub(&Value::new(&total_collateral))?;
if col_return.multiasset.is_some() || col_return.coin > BigNum::zero() {
let return_output = TransactionOutput::new(return_address, &col_return);
let min_ada = min_ada_for_output(&return_output, &self.config.utxo_cost())?;
if min_ada > col_return.coin {
return Err(JsError::from_str(&format!(
"Not enough coin to make return on the collateral value!\
Decrease the total collateral value or add more collateral inputs. \
Min ada for return {}, but was {}",
min_ada, col_return.coin
)));
}
self.collateral_return = Some(return_output);
}
self.set_total_collateral(total_collateral);
Ok(())
}
pub fn add_reference_input(&mut self, reference_input: &TransactionInput) {
self.reference_inputs.insert(reference_input.clone(), 0);
}
pub fn add_script_reference_input(
&mut self,
reference_input: &TransactionInput,
script_size: usize,
) {
self.reference_inputs
.insert(reference_input.clone(), script_size);
}
#[deprecated(since = "10.2.0", note = "Use `.set_inputs`")]
pub fn add_key_input(
&mut self,
hash: &Ed25519KeyHash,
input: &TransactionInput,
amount: &Value,
) {
self.inputs.add_key_input(hash, input, amount);
}
#[deprecated(since = "10.2.0", note = "Use `.set_inputs`")]
pub fn add_native_script_input(
&mut self,
script: &NativeScript,
input: &TransactionInput,
amount: &Value,
) {
self.inputs.add_native_script_input(
&NativeScriptSource::new(script),
input,
amount);
}
#[deprecated(since = "10.2.0", note = "Use `.set_inputs`")]
pub fn add_plutus_script_input(
&mut self,
witness: &PlutusWitness,
input: &TransactionInput,
amount: &Value,
) {
self.inputs.add_plutus_script_input(witness, input, amount);
}
#[deprecated(since = "10.2.0", note = "Use `.set_inputs`")]
pub fn add_bootstrap_input(
&mut self,
hash: &ByronAddress,
input: &TransactionInput,
amount: &Value,
) {
self.inputs.add_bootstrap_input(hash, input, amount);
}
#[deprecated(since = "12.0.0", note = "Use `.set_inputs`")]
pub fn add_regular_input(
&mut self,
address: &Address,
input: &TransactionInput,
amount: &Value,
) -> Result<(), JsError> {
self.inputs.add_regular_input(address, input, amount)
}
pub fn add_inputs_from_and_change(
&mut self,
inputs: &TransactionUnspentOutputs,
strategy: CoinSelectionStrategyCIP2,
change_config: &ChangeConfig,
) -> Result<bool, JsError> {
self.add_inputs_from(inputs, strategy)?;
if self.fee.is_some() {
return Err(JsError::from_str(
"Cannot calculate change if fee was explicitly specified",
))
}
let mut add_change_result = self
.add_change_if_needed_with_optional_script_and_datum(
&change_config.address,
change_config
.plutus_data
.clone()
.map_or(None, |od| Some(od.0)),
change_config.script_ref.clone(),
);
match add_change_result {
Ok(v) => Ok(v),
Err(e) => {
let mut unused_inputs = TransactionUnspentOutputs::new();
for input in inputs.into_iter() {
if self
.inputs
.inputs()
.into_iter()
.all(|used_input| input.input() != *used_input)
{
unused_inputs.add(input)
}
}
unused_inputs.0.sort_by_key(|input| {
input
.clone()
.output
.amount
.multiasset
.map_or(0, |ma| ma.len())
});
unused_inputs.0.reverse();
while unused_inputs.0.len() > 0 {
let last_input = unused_inputs.0.pop();
match last_input {
Some(input) => {
self.add_regular_input(
&input.output().address(),
&input.input(),
&input.output().amount(),
)?;
add_change_result = self
.add_change_if_needed_with_optional_script_and_datum(
&change_config.address,
change_config
.plutus_data
.clone()
.map_or(None, |od| Some(od.0)),
change_config.script_ref.clone(),
);
if let Ok(value) = add_change_result {
return Ok(value);
}
}
None => {
return Err(JsError::from_str(
"Unable to balance tx with available inputs",
))
}
}
}
Err(e)
}
}
}
pub fn add_inputs_from_and_change_with_collateral_return(
&mut self,
inputs: &TransactionUnspentOutputs,
strategy: CoinSelectionStrategyCIP2,
change_config: &ChangeConfig,
collateral_percentage: &BigNum,
) -> Result<(), JsError> {
let mut total_collateral = Value::zero();
for collateral_input in self.collateral.iter() {
total_collateral = total_collateral.checked_add(&collateral_input.amount)?;
}
self.set_total_collateral(&total_collateral.coin());
self.set_collateral_return(&TransactionOutput::new(
&change_config.address,
&total_collateral,
));
let add_change_result = self.add_inputs_from_and_change(inputs, strategy, change_config);
self.remove_collateral_return();
self.remove_total_collateral();
add_change_result?;
let fee = self.get_fee_if_set().ok_or(JsError::from_str(
"Cannot calculate collateral return if fee was not set",
))?;
let collateral_required = fee
.checked_mul(&collateral_percentage)?
.div_floor(&BigNum(100))
.checked_add(&BigNum::one())?;
let set_collateral_result =
self.set_total_collateral_and_return(&collateral_required, &change_config.address);
if let Err(e) = set_collateral_result {
self.remove_collateral_return();
self.remove_total_collateral();
return Err(e);
}
Ok(())
}
#[deprecated(since = "10.2.0", note = "Use `.set_inputs`")]
pub fn get_native_input_scripts(&self) -> Option<NativeScripts> {
self.inputs.get_native_input_scripts()
}
#[deprecated(since = "10.2.0", note = "Use `.set_inputs`")]
pub fn get_plutus_input_scripts(&self) -> Option<PlutusWitnesses> {
self.inputs.get_plutus_input_scripts()
}
pub fn fee_for_input(
&self,
address: &Address,
input: &TransactionInput,
amount: &Value,
) -> Result<Coin, JsError> {
let mut self_copy = self.clone();
self_copy.set_fee(&BigNum::zero());
let fee_before = min_fee(&self_copy)?;
self_copy.add_regular_input(&address, &input, &amount)?;
let fee_after = min_fee(&self_copy)?;
fee_after.checked_sub(&fee_before)
}
pub fn add_output(&mut self, output: &TransactionOutput) -> Result<(), JsError> {
let value_size = output.amount.to_bytes().len();
if value_size > self.config.max_value_size as usize {
return Err(JsError::from_str(&format!(
"Maximum value size of {} exceeded. Found: {}",
self.config.max_value_size, value_size
)));
}
let min_ada = min_ada_for_output(&output, &self.config.utxo_cost())?;
if output.amount().coin() < min_ada {
Err(JsError::from_str(&format!(
"Value {} less than the minimum UTXO value {}",
output.amount().coin(),
min_ada
)))
} else {
self.outputs.add(output);
Ok(())
}
}
pub fn fee_for_output(&self, output: &TransactionOutput) -> Result<Coin, JsError> {
let mut self_copy = self.clone();
self_copy.set_fee(&BigNum::zero());
let fee_before = min_fee(&self_copy)?;
self_copy.add_output(&output)?;
let fee_after = min_fee(&self_copy)?;
fee_after.checked_sub(&fee_before)
}
pub fn set_fee(&mut self, fee: &Coin) {
self.fee = Some(fee.clone())
}
#[deprecated(
since = "10.1.0",
note = "Underlying value capacity of ttl (BigNum u64) bigger then Slot32. Use set_ttl_bignum instead."
)]
pub fn set_ttl(&mut self, ttl: Slot32) {
self.ttl = Some(ttl.into())
}
pub fn set_ttl_bignum(&mut self, ttl: &SlotBigNum) {
self.ttl = Some(ttl.clone())
}
pub fn remove_ttl(&mut self) {
self.ttl = None;
}
#[deprecated(
since = "10.1.0",
note = "Underlying value capacity of validity_start_interval (BigNum u64) bigger then Slot32. Use set_validity_start_interval_bignum instead."
)]
pub fn set_validity_start_interval(&mut self, validity_start_interval: Slot32) {
self.validity_start_interval = Some(validity_start_interval.into())
}
pub fn set_validity_start_interval_bignum(&mut self, validity_start_interval: SlotBigNum) {
self.validity_start_interval = Some(validity_start_interval.clone())
}
pub fn remove_validity_start_interval(&mut self) {
self.validity_start_interval = None;
}
#[deprecated(
since = "11.4.1",
note = "Can emit an error if you add a cert with script credential. Use set_certs_builder instead."
)]
pub fn set_certs(&mut self, certs: &Certificates) -> Result<(), JsError> {
let mut builder = CertificatesBuilder::new();
for cert in &certs.certs {
builder.add(cert)?;
}
self.certs = Some(builder);
Ok(())
}
pub fn remove_certs(&mut self) {
self.certs = None;
}
pub fn set_certs_builder(&mut self, certs: &CertificatesBuilder) {
self.certs = Some(certs.clone());
}
#[deprecated(
since = "11.4.1",
note = "Can emit an error if you add a withdrawal with script credential. Use set_withdrawals_builder instead."
)]
pub fn set_withdrawals(&mut self, withdrawals: &Withdrawals) -> Result<(), JsError> {
let mut withdrawals_builder = WithdrawalsBuilder::new();
for (withdrawal, coin) in &withdrawals.0 {
withdrawals_builder.add(&withdrawal, &coin)?;
}
self.withdrawals = Some(withdrawals_builder);
Ok(())
}
pub fn set_withdrawals_builder(&mut self, withdrawals: &WithdrawalsBuilder) {
self.withdrawals = Some(withdrawals.clone());
}
pub fn set_voting_builder(&mut self, voting_builder: &VotingBuilder) {
self.voting_procedures = Some(voting_builder.clone());
}
pub fn set_voting_proposal_builder(&mut self, voting_proposal_builder: &VotingProposalBuilder) {
self.voting_proposals = Some(voting_proposal_builder.clone());
}
pub fn remove_withdrawals(&mut self) {
self.withdrawals = None;
}
pub fn get_auxiliary_data(&self) -> Option<AuxiliaryData> {
self.auxiliary_data.clone()
}
pub fn set_auxiliary_data(&mut self, auxiliary_data: &AuxiliaryData) {
self.auxiliary_data = Some(auxiliary_data.clone())
}
pub fn remove_auxiliary_data(&mut self) {
self.auxiliary_data = None;
}
pub fn set_metadata(&mut self, metadata: &GeneralTransactionMetadata) {
let mut aux = self
.auxiliary_data
.as_ref()
.cloned()
.unwrap_or(AuxiliaryData::new());
aux.set_metadata(metadata);
self.set_auxiliary_data(&aux);
}
pub fn add_metadatum(&mut self, key: &TransactionMetadatumLabel, val: &TransactionMetadatum) {
let mut metadata = self
.auxiliary_data
.as_ref()
.map(|aux| aux.metadata().as_ref().cloned())
.unwrap_or(None)
.unwrap_or(GeneralTransactionMetadata::new());
metadata.insert(key, val);
self.set_metadata(&metadata);
}
pub fn add_json_metadatum(
&mut self,
key: &TransactionMetadatumLabel,
val: String,
) -> Result<(), JsError> {
self.add_json_metadatum_with_schema(key, val, MetadataJsonSchema::NoConversions)
}
pub fn add_json_metadatum_with_schema(
&mut self,
key: &TransactionMetadatumLabel,
val: String,
schema: MetadataJsonSchema,
) -> Result<(), JsError> {
let metadatum = encode_json_str_to_metadatum(val, schema)?;
self.add_metadatum(key, &metadatum);
Ok(())
}
pub fn set_mint_builder(&mut self, mint_builder: &MintBuilder) {
self.mint = Some(mint_builder.clone());
}
pub fn remove_mint_builder(&mut self) {
self.mint = None;
}
pub fn get_mint_builder(&self) -> Option<MintBuilder> {
self.mint.clone()
}
#[deprecated(
since = "11.2.0",
note = "Mints are defining by MintBuilder now. Use `.set_mint_builder()` and `MintBuilder` instead."
)]
pub fn set_mint(&mut self, mint: &Mint, mint_scripts: &NativeScripts) -> Result<(), JsError> {
assert_required_mint_scripts(mint, Some(mint_scripts))?;
let mut scripts_policies = HashMap::new();
for scipt in mint_scripts {
scripts_policies.insert(scipt.hash(), scipt.clone());
}
let mut mint_builder = MintBuilder::new();
for (policy_id, asset_map) in &mint.0 {
for (asset_name, amount) in &asset_map.0 {
if let Some(script) = scripts_policies.get(policy_id) {
let native_script_source = NativeScriptSource::new(script);
let mint_witness = MintWitness::new_native_script(&native_script_source);
mint_builder.set_asset(&mint_witness, asset_name, amount)?;
} else {
return Err(JsError::from_str(
"Mint policy does not have a matching script",
));
}
}
}
self.mint = Some(mint_builder);
Ok(())
}
#[deprecated(
since = "11.2.0",
note = "Mints are defining by MintBuilder now. Use `.get_mint_builder()` and `.build()` instead."
)]
pub fn get_mint(&self) -> Option<Mint> {
match &self.mint {
Some(mint) => Some(mint.build().expect("MintBuilder is invalid")),
None => None,
}
}
pub fn get_mint_scripts(&self) -> Option<NativeScripts> {
match &self.mint {
Some(mint) => Some(mint.get_native_scripts()),
None => None,
}
}
#[deprecated(
since = "11.2.0",
note = "Mints are defining by MintBuilder now. Use `.set_mint_builder()` and `MintBuilder` instead."
)]
pub fn set_mint_asset(&mut self, policy_script: &NativeScript, mint_assets: &MintAssets) -> Result<(), JsError> {
let native_script_source = NativeScriptSource::new(policy_script);
let mint_witness = MintWitness::new_native_script(&native_script_source);
if let Some(mint) = &mut self.mint {
for (asset, amount) in mint_assets.0.iter() {
mint.set_asset(&mint_witness, asset, amount)?;
}
} else {
let mut mint = MintBuilder::new();
for (asset, amount) in mint_assets.0.iter() {
mint.set_asset(&mint_witness, asset, amount)?;
}
self.mint = Some(mint);
}
Ok(())
}
#[deprecated(
since = "11.2.0",
note = "Mints are defining by MintBuilder now. Use `.set_mint_builder()` and `MintBuilder` instead."
)]
pub fn add_mint_asset(
&mut self,
policy_script: &NativeScript,
asset_name: &AssetName,
amount: &Int,
) -> Result<(), JsError> {
let native_script_source = NativeScriptSource::new(policy_script);
let mint_witness = MintWitness::new_native_script(&native_script_source);
if let Some(mint) = &mut self.mint {
mint.add_asset(&mint_witness, asset_name, &amount)?;
} else {
let mut mint = MintBuilder::new();
mint.add_asset(&mint_witness, asset_name, &amount)?;
self.mint = Some(mint);
}
Ok(())
}
pub fn add_mint_asset_and_output(
&mut self,
policy_script: &NativeScript,
asset_name: &AssetName,
amount: &Int,
output_builder: &TransactionOutputAmountBuilder,
output_coin: &Coin,
) -> Result<(), JsError> {
if !amount.is_positive() {
return Err(JsError::from_str("Output value must be positive!"));
}
let policy_id: PolicyID = policy_script.hash();
self.add_mint_asset(policy_script, asset_name, amount)?;
let multiasset =
Mint::new_from_entry(&policy_id, &MintAssets::new_from_entry(asset_name, amount)?)
.as_positive_multiasset();
self.add_output(
&output_builder
.with_coin_and_asset(&output_coin, &multiasset)
.build()?,
)
}
pub fn add_mint_asset_and_output_min_required_coin(
&mut self,
policy_script: &NativeScript,
asset_name: &AssetName,
amount: &Int,
output_builder: &TransactionOutputAmountBuilder,
) -> Result<(), JsError> {
if !amount.is_positive() {
return Err(JsError::from_str("Output value must be positive!"));
}
let policy_id: PolicyID = policy_script.hash();
self.add_mint_asset(policy_script, asset_name, amount)?;
let multiasset =
Mint::new_from_entry(&policy_id, &MintAssets::new_from_entry(asset_name, amount)?)
.as_positive_multiasset();
self.add_output(
&output_builder
.with_asset_and_min_required_coin_by_utxo_cost(
&multiasset,
&self.config.utxo_cost(),
)?
.build()?,
)
}
pub fn add_extra_witness_datum(&mut self, datum: &PlutusData) {
if let Some(extra_datums) = &mut self.extra_datums {
extra_datums.add(datum);
} else {
let mut extra_datums = PlutusList::new();
extra_datums.add(datum);
self.extra_datums = Some(extra_datums);
}
}
pub fn get_extra_witness_datums(&self) -> Option<PlutusList> {
self.extra_datums.clone()
}
pub fn set_donation(&mut self, donation: &Coin) {
self.donation = Some(donation.clone());
}
pub fn get_donation(&self) -> Option<Coin> {
self.donation.clone()
}
pub fn set_current_treasury_value(
&mut self,
current_treasury_value: &Coin,
) -> Result<(), JsError> {
if current_treasury_value == &Coin::zero() {
return Err(JsError::from_str("Current treasury value cannot be zero!"));
}
self.current_treasury_value = Some(current_treasury_value.clone());
Ok(())
}
pub fn get_current_treasury_value(&self) -> Option<Coin> {
self.current_treasury_value.clone()
}
pub fn new(cfg: &TransactionBuilderConfig) -> Self {
Self {
config: cfg.clone(),
inputs: TxInputsBuilder::new(),
collateral: TxInputsBuilder::new(),
outputs: TransactionOutputs::new(),
fee: None,
ttl: None,
certs: None,
withdrawals: None,
auxiliary_data: None,
validity_start_interval: None,
mint: None,
script_data_hash: None,
required_signers: Ed25519KeyHashes::new(),
collateral_return: None,
total_collateral: None,
reference_inputs: HashMap::new(),
extra_datums: None,
voting_procedures: None,
voting_proposals: None,
donation: None,
current_treasury_value: None,
}
}
pub fn get_reference_inputs(&self) -> TransactionInputs {
let mut inputs: HashSet<TransactionInput> = HashSet::new();
let mut add_ref_inputs_set = |ref_inputs: TransactionInputs| {
for input in &ref_inputs {
if !self.inputs.has_input(&input) {
inputs.insert(input.clone());
}
}
};
add_ref_inputs_set(self.inputs.get_ref_inputs());
if let Some(mint) = &self.mint {
add_ref_inputs_set(mint.get_ref_inputs());
}
if let Some(withdrawals) = &self.withdrawals {
add_ref_inputs_set(withdrawals.get_ref_inputs());
}
if let Some(certs) = &self.certs {
add_ref_inputs_set(certs.get_ref_inputs());
}
if let Some(voting_procedures) = &self.voting_procedures {
add_ref_inputs_set(voting_procedures.get_ref_inputs());
}
if let Some(voting_proposals) = &self.voting_proposals {
add_ref_inputs_set(voting_proposals.get_ref_inputs());
}
if self.config.deduplicate_explicit_ref_inputs_with_regular_inputs {
add_ref_inputs_set(TransactionInputs::from_vec(
self.reference_inputs.keys().cloned().collect())
)
} else {
for input in self.reference_inputs.keys().cloned() {
inputs.insert(input);
}
}
let vec_inputs = inputs.into_iter().collect();
TransactionInputs::from_vec(vec_inputs)
}
fn validate_inputs_intersection(&self) -> Result<(), JsError> {
let ref_inputs = self.get_reference_inputs();
for input in &ref_inputs {
if self.inputs.has_input(input) {
return Err(JsError::from_str(&format!(
"The reference input {:?} is also present in the regular transaction inputs set. \
It's not allowed to have the same inputs in both the transaction's inputs set and the reference inputs set. \
You can use the `deduplicate_explicit_ref_inputs_with_regular_inputs` parameter in the `TransactionConfigBuilder` \
to enforce the removal of duplicate reference inputs."
, input)));
}
}
Ok(())
}
fn get_total_ref_scripts_size(&self) -> Result<usize, JsError> {
let mut sizes_map = HashMap::new();
fn add_to_map<'a>(
item: (&'a TransactionInput, usize),
sizes_map: &mut HashMap<&'a TransactionInput, usize>,
) -> Result<(), JsError> {
if sizes_map.entry(item.0).or_insert(item.1) != &item.1 {
Err(JsError::from_str(&format!(
"Different script sizes for the same ref input {}",
item.0
)))
} else {
Ok(())
}
}
for item in self.inputs.get_script_ref_inputs_with_size() {
add_to_map(item, &mut sizes_map)?
}
for (tx_in, size) in &self.reference_inputs {
add_to_map((tx_in, *size), &mut sizes_map)?
}
if let Some(mint) = &self.mint {
for item in mint.get_script_ref_inputs_with_size() {
add_to_map(item, &mut sizes_map)?
}
}
if let Some(withdrawals) = &self.withdrawals {
for item in withdrawals.get_script_ref_inputs_with_size() {
add_to_map(item, &mut sizes_map)?
}
}
if let Some(certs) = &self.certs {
for item in certs.get_script_ref_inputs_with_size() {
add_to_map(item, &mut sizes_map)?
}
}
if let Some(voting_procedures) = &self.voting_procedures {
for item in voting_procedures.get_script_ref_inputs_with_size() {
add_to_map(item, &mut sizes_map)?
}
}
if let Some(voting_proposals) = &self.voting_proposals {
for item in voting_proposals.get_script_ref_inputs_with_size() {
add_to_map(item, &mut sizes_map)?
}
}
Ok(sizes_map.values().sum())
}
pub fn get_explicit_input(&self) -> Result<Value, JsError> {
self.inputs
.iter()
.try_fold(Value::zero(), |acc, ref tx_builder_input| {
acc.checked_add(&tx_builder_input.amount)
})
}
pub fn get_implicit_input(&self) -> Result<Value, JsError> {
let mut implicit_input = Value::zero();
if let Some(withdrawals) = &self.withdrawals {
implicit_input = implicit_input.checked_add(&withdrawals.get_total_withdrawals()?)?;
}
if let Some(refunds) = &self.certs {
implicit_input = implicit_input.checked_add(
&refunds
.get_certificates_refund(&self.config.pool_deposit, &self.config.key_deposit)?,
)?;
}
Ok(implicit_input)
}
fn get_mint_as_values(&self) -> (Value, Value) {
self.mint
.as_ref()
.map(|m| {
let mint = m.build_unchecked();
(
Value::new_from_assets(&mint.as_positive_multiasset()),
Value::new_from_assets(&mint.as_negative_multiasset()),
)
})
.unwrap_or((Value::zero(), Value::zero()))
}
pub fn get_total_input(&self) -> Result<Value, JsError> {
let (mint_value, _) = self.get_mint_as_values();
self.get_explicit_input()?
.checked_add(&self.get_implicit_input()?)?
.checked_add(&mint_value)
}
pub fn get_total_output(&self) -> Result<Value, JsError> {
let (_, burn_value) = self.get_mint_as_values();
let mut total = self
.get_explicit_output()?
.checked_add(&Value::new(&self.get_deposit()?))?
.checked_add(&burn_value)?;
if let Some(donation) = &self.donation {
total = total.checked_add(&Value::new(donation))?;
}
Ok(total)
}
pub fn get_explicit_output(&self) -> Result<Value, JsError> {
self.outputs
.0
.iter()
.try_fold(Value::new(&BigNum::zero()), |acc, ref output| {
acc.checked_add(&output.amount())
})
}
pub fn get_deposit(&self) -> Result<Coin, JsError> {
let mut total_deposit = Coin::zero();
if let Some(certs) = &self.certs {
total_deposit =
total_deposit.checked_add(&certs.get_certificates_deposit(
&self.config.pool_deposit,
&self.config.key_deposit,
)?)?;
}
if let Some(voting_proposal_builder) = &self.voting_proposals {
total_deposit =
total_deposit.checked_add(&voting_proposal_builder.get_total_deposit()?)?;
}
Ok(total_deposit)
}
pub fn get_fee_if_set(&self) -> Option<Coin> {
self.fee.clone()
}
pub fn add_change_if_needed(&mut self, address: &Address) -> Result<bool, JsError> {
self.add_change_if_needed_with_optional_script_and_datum(address, None, None)
}
pub fn add_change_if_needed_with_datum(
&mut self,
address: &Address,
plutus_data: &OutputDatum,
) -> Result<bool, JsError> {
self.add_change_if_needed_with_optional_script_and_datum(
address,
Some(plutus_data.0.clone()),
None,
)
}
fn add_change_if_needed_with_optional_script_and_datum(
&mut self,
address: &Address,
plutus_data: Option<DataOption>,
script_ref: Option<ScriptRef>,
) -> Result<bool, JsError> {
let fee = match &self.fee {
None => self.min_fee(),
Some(_x) => {
return Err(JsError::from_str(
"Cannot calculate change if fee was explicitly specified",
))
}
}?;
let input_total = self.get_total_input()?;
let output_total = self.get_total_output()?;
let shortage = get_input_shortage(&input_total, &output_total, &fee)?;
if let Some(shortage) = shortage {
return Err(JsError::from_str(&format!(
"Insufficient input in transaction. {}",
shortage
)));
}
use std::cmp::Ordering;
match &input_total.partial_cmp(&output_total.checked_add(&Value::new(&fee))?) {
Some(Ordering::Equal) => {
self.set_fee(&input_total.checked_sub(&output_total)?.coin());
Ok(false)
}
Some(Ordering::Less) => Err(JsError::from_str("Insufficient input in transaction")),
Some(Ordering::Greater) => {
fn has_assets(ma: Option<MultiAsset>) -> bool {
ma.map(|assets| assets.len() > 0).unwrap_or(false)
}
let change_estimator = input_total.checked_sub(&output_total)?;
if has_assets(change_estimator.multiasset()) {
fn will_adding_asset_make_output_overflow(
output: &TransactionOutput,
current_assets: &Assets,
asset_to_add: (PolicyID, AssetName, BigNum),
max_value_size: u32,
data_cost: &DataCost,
) -> Result<bool, JsError> {
let (policy, asset_name, value) = asset_to_add;
let mut current_assets_clone = current_assets.clone();
current_assets_clone.insert(&asset_name, &value);
let mut amount_clone = output.amount.clone();
let mut val = Value::new(&Coin::zero());
let mut ma = MultiAsset::new();
ma.insert(&policy, ¤t_assets_clone);
val.set_multiasset(&ma);
amount_clone = amount_clone.checked_add(&val)?;
let mut calc = MinOutputAdaCalculator::new_empty(data_cost)?;
calc.set_amount(&val);
let min_ada = calc.calculate_ada()?;
amount_clone.set_coin(&min_ada);
Ok(amount_clone.to_bytes().len() > max_value_size as usize)
}
fn pack_nfts_for_change(
max_value_size: u32,
data_cost: &DataCost,
change_address: &Address,
change_estimator: &Value,
plutus_data: &Option<DataOption>,
script_ref: &Option<ScriptRef>,
) -> Result<Vec<MultiAsset>, JsError> {
let mut change_assets: Vec<MultiAsset> = Vec::new();
let mut base_coin = Value::new(&change_estimator.coin());
base_coin.set_multiasset(&MultiAsset::new());
let mut output = TransactionOutput {
address: change_address.clone(),
amount: base_coin.clone(),
plutus_data: plutus_data.clone(),
script_ref: script_ref.clone(),
serialization_format: None,
};
for (policy, assets) in change_estimator.multiasset().unwrap().0.iter() {
let mut old_amount = output.amount.clone();
let mut val = Value::new(&Coin::zero());
let mut next_nft = MultiAsset::new();
let asset_names = assets.keys();
let mut rebuilt_assets = Assets::new();
for n in 0..asset_names.len() {
let asset_name = asset_names.get(n);
let value = assets.get(&asset_name).unwrap();
if will_adding_asset_make_output_overflow(
&output,
&rebuilt_assets,
(policy.clone(), asset_name.clone(), value),
max_value_size,
data_cost,
)? {
next_nft.insert(policy, &rebuilt_assets);
val.set_multiasset(&next_nft);
output.amount = output.amount.checked_add(&val)?;
change_assets.push(output.amount.multiasset().unwrap());
base_coin = Value::new(&Coin::zero());
base_coin.set_multiasset(&MultiAsset::new());
output = TransactionOutput {
address: change_address.clone(),
amount: base_coin.clone(),
plutus_data: plutus_data.clone(),
script_ref: script_ref.clone(),
serialization_format: None,
};
old_amount = output.amount.clone();
val = Value::new(&Coin::zero());
next_nft = MultiAsset::new();
rebuilt_assets = Assets::new();
}
rebuilt_assets.insert(&asset_name, &value);
}
next_nft.insert(policy, &rebuilt_assets);
val.set_multiasset(&next_nft);
output.amount = output.amount.checked_add(&val)?;
let mut amount_clone = output.amount.clone();
let mut calc = MinOutputAdaCalculator::new_empty(data_cost)?;
calc.set_amount(&val);
let min_ada = calc.calculate_ada()?;
amount_clone.set_coin(&min_ada);
if amount_clone.to_bytes().len() > max_value_size as usize {
output.amount = old_amount;
break;
}
}
change_assets.push(output.amount.multiasset().unwrap());
Ok(change_assets)
}
let mut change_left = input_total.checked_sub(&output_total)?;
let mut new_fee = fee.clone();
let utxo_cost = self.config.utxo_cost();
let mut calc = MinOutputAdaCalculator::new_empty(&utxo_cost)?;
if let Some(data) = &plutus_data {
match data {
DataOption::DataHash(data_hash) => calc.set_data_hash(data_hash),
DataOption::Data(datum) => calc.set_plutus_data(datum),
};
}
if let Some(script_ref) = &script_ref {
calc.set_script_ref(script_ref);
}
let minimum_utxo_val = calc.calculate_ada()?;
while let Some(Ordering::Greater) = change_left
.multiasset
.as_ref()
.map_or_else(|| None, |ma| ma.partial_cmp(&MultiAsset::new()))
{
let nft_changes = pack_nfts_for_change(
self.config.max_value_size,
&utxo_cost,
address,
&change_left,
&plutus_data.clone(),
&script_ref.clone(),
)?;
if nft_changes.len() == 0 {
return Err(JsError::from_str("NFTs too large for change output"));
}
let mut change_value = Value::new(&Coin::zero());
for nft_change in nft_changes.iter() {
change_value.set_multiasset(&nft_change);
let mut calc = MinOutputAdaCalculator::new_empty(&utxo_cost)?;
let mut fake_change = change_value.clone();
fake_change.set_coin(&change_left.coin);
calc.set_amount(&fake_change);
if let Some(data) = &plutus_data {
match data {
DataOption::DataHash(data_hash) => {
calc.set_data_hash(data_hash)
}
DataOption::Data(datum) => calc.set_plutus_data(datum),
};
}
if let Some(script_ref) = &script_ref {
calc.set_script_ref(script_ref);
}
let min_ada = calc.calculate_ada()?;
change_value.set_coin(&min_ada);
let change_output = TransactionOutput {
address: address.clone(),
amount: change_value.clone(),
plutus_data: plutus_data.clone(),
script_ref: script_ref.clone(),
serialization_format: None,
};
let fee_for_change = self.fee_for_output(&change_output)?;
new_fee = new_fee.checked_add(&fee_for_change)?;
if change_left.coin() < min_ada.checked_add(&new_fee)? {
return Err(JsError::from_str("Not enough ADA leftover to include non-ADA assets in a change address"));
}
change_left = change_left.checked_sub(&change_value)?;
self.add_output(&change_output)?;
}
}
change_left = change_left.checked_sub(&Value::new(&new_fee))?;
let left_above_minimum = change_left.coin.compare(&minimum_utxo_val) > 0;
if self.config.prefer_pure_change && left_above_minimum {
let pure_output = TransactionOutput {
address: address.clone(),
amount: change_left.clone(),
plutus_data: plutus_data.clone(),
script_ref: script_ref.clone(),
serialization_format: None,
};
let additional_fee = self.fee_for_output(&pure_output)?;
let potential_pure_value =
change_left.checked_sub(&Value::new(&additional_fee))?;
let potential_pure_above_minimum =
potential_pure_value.coin.compare(&minimum_utxo_val) > 0;
if potential_pure_above_minimum {
new_fee = new_fee.checked_add(&additional_fee)?;
change_left = Value::zero();
self.add_output(&TransactionOutput {
address: address.clone(),
amount: potential_pure_value.clone(),
plutus_data: plutus_data.clone(),
script_ref: script_ref.clone(),
serialization_format: None,
})?;
}
}
self.set_fee(&new_fee);
if !change_left.is_zero() {
self.outputs.0.last_mut().unwrap().amount = self
.outputs
.0
.last()
.unwrap()
.amount
.checked_add(&change_left)?;
}
Ok(true)
} else {
let mut calc = MinOutputAdaCalculator::new_empty(&self.config.utxo_cost())?;
calc.set_amount(&change_estimator);
if let Some(data) = &plutus_data {
match data {
DataOption::DataHash(data_hash) => calc.set_data_hash(data_hash),
DataOption::Data(datum) => calc.set_plutus_data(datum),
};
}
if let Some(script_ref) = &script_ref {
calc.set_script_ref(script_ref);
}
let min_ada = calc.calculate_ada()?;
fn burn_extra(
builder: &mut TransactionBuilder,
burn_amount: &BigNum,
) -> Result<bool, JsError> {
builder.set_fee(burn_amount);
Ok(false) }
match change_estimator.coin() >= min_ada {
false => burn_extra(self, &change_estimator.coin()),
true => {
let fee_for_change = self.fee_for_output(&TransactionOutput {
address: address.clone(),
amount: change_estimator.clone(),
plutus_data: plutus_data.clone(),
script_ref: script_ref.clone(),
serialization_format: None,
})?;
let new_fee = fee.checked_add(&fee_for_change)?;
match change_estimator.coin()
>= min_ada.checked_add(&Value::new(&new_fee).coin())?
{
false => burn_extra(self, &change_estimator.coin()),
true => {
self.set_fee(&new_fee);
self.add_output(&TransactionOutput {
address: address.clone(),
amount: change_estimator
.checked_sub(&Value::new(&new_fee.clone()))?,
plutus_data: plutus_data.clone(),
script_ref: script_ref.clone(),
serialization_format: None,
})?;
Ok(true)
}
}
}
}
}
}
None => Err(JsError::from_str(
"missing input or output for some native asset",
)),
}
}
pub fn calc_script_data_hash(&mut self, cost_models: &Costmdls) -> Result<(), JsError> {
let mut used_langs = BTreeSet::new();
let mut retained_cost_models = Costmdls::new();
let mut plutus_witnesses = PlutusWitnesses::new();
if let Some(mut inputs_plutus) = self.inputs.get_plutus_input_scripts() {
used_langs.append(&mut self.inputs.get_used_plutus_lang_versions());
plutus_witnesses.0.append(&mut inputs_plutus.0)
}
if let Some(mut collateral_plutus) = self.collateral.get_plutus_input_scripts() {
used_langs.append(&mut self.collateral.get_used_plutus_lang_versions());
plutus_witnesses.0.append(&mut collateral_plutus.0)
}
if let Some(mint_builder) = &self.mint {
used_langs.append(&mut mint_builder.get_used_plutus_lang_versions());
plutus_witnesses
.0
.append(&mut mint_builder.get_plutus_witnesses().0)
}
if let Some(certs_builder) = &self.certs {
used_langs.append(&mut certs_builder.get_used_plutus_lang_versions());
plutus_witnesses
.0
.append(&mut certs_builder.get_plutus_witnesses().0)
}
if let Some(withdrawals_builder) = &self.withdrawals {
used_langs.append(&mut withdrawals_builder.get_used_plutus_lang_versions());
plutus_witnesses
.0
.append(&mut withdrawals_builder.get_plutus_witnesses().0)
}
if let Some(voting_builder) = &self.voting_procedures {
used_langs.append(&mut voting_builder.get_used_plutus_lang_versions());
plutus_witnesses
.0
.append(&mut voting_builder.get_plutus_witnesses().0)
}
if let Some(voting_proposal_builder) = &self.voting_proposals {
used_langs.append(&mut voting_proposal_builder.get_used_plutus_lang_versions());
plutus_witnesses
.0
.append(&mut voting_proposal_builder.get_plutus_witnesses().0)
}
let (_scripts, mut datums, redeemers) = plutus_witnesses.collect();
for lang in used_langs {
match cost_models.get(&lang) {
Some(cost) => {
retained_cost_models.insert(&lang, &cost);
}
_ => {
return Err(JsError::from_str(&format!(
"Missing cost model for language version: {:?}",
lang
)))
}
}
}
if let Some(extra_datum) = &self.extra_datums {
if datums.is_none() {
datums = Some(PlutusList::new());
}
for datum in extra_datum {
if let Some(datums) = &mut datums {
datums.add(datum);
}
}
}
if datums.is_some() || redeemers.len() > 0 || retained_cost_models.len() > 0 {
self.script_data_hash =
Some(hash_script_data(&redeemers, &retained_cost_models, datums));
}
Ok(())
}
pub fn set_script_data_hash(&mut self, hash: &ScriptDataHash) {
self.script_data_hash = Some(hash.clone());
}
pub fn remove_script_data_hash(&mut self) {
self.script_data_hash = None;
}
pub fn add_required_signer(&mut self, key: &Ed25519KeyHash) {
self.required_signers.add(key);
}
fn build_and_size(&self) -> Result<(TransactionBody, usize), JsError> {
let fee = self
.fee
.ok_or_else(|| JsError::from_str("Fee not specified"))?;
let built = TransactionBody {
inputs: self.inputs.inputs(),
outputs: self.outputs.clone(),
fee,
ttl: self.ttl,
certs: self.certs.as_ref().map(|x| x.build()),
withdrawals: self.withdrawals.as_ref().map(|x| x.build()),
update: None,
auxiliary_data_hash: self
.auxiliary_data
.as_ref()
.map(|x| utils::hash_auxiliary_data(x)),
validity_start_interval: self.validity_start_interval,
mint: self.mint.as_ref()
.map(|x| x.build())
.transpose()?,
script_data_hash: self.script_data_hash.clone(),
collateral: self.collateral.inputs_option(),
required_signers: self.required_signers.to_option(),
network_id: None,
collateral_return: self.collateral_return.clone(),
total_collateral: self.total_collateral.clone(),
reference_inputs: self.get_reference_inputs().to_option(),
voting_procedures: self.voting_procedures.as_ref().map(|x| x.build()),
voting_proposals: self.voting_proposals.as_ref().map(|x| x.build()),
donation: self.donation.clone(),
current_treasury_value: self.current_treasury_value.clone(),
};
let full_tx = fake_full_tx(self, built)?;
let full_tx_size = full_tx.to_bytes().len();
return Ok((full_tx.body, full_tx_size));
}
pub fn full_size(&self) -> Result<usize, JsError> {
return self.build_and_size().map(|r| r.1);
}
pub fn output_sizes(&self) -> Vec<usize> {
return self.outputs.0.iter().map(|o| o.to_bytes().len()).collect();
}
pub fn build(&self) -> Result<TransactionBody, JsError> {
let (body, full_tx_size) = self.build_and_size()?;
if full_tx_size > self.config.max_tx_size as usize {
Err(JsError::from_str(&format!(
"Maximum transaction size of {} exceeded. Found: {}",
self.config.max_tx_size, full_tx_size
)))
} else {
Ok(body)
}
}
fn get_combined_native_scripts(&self) -> Option<NativeScripts> {
let mut ns = NativeScripts::new();
if let Some(input_scripts) = self.inputs.get_native_input_scripts() {
input_scripts.iter().for_each(|s| {
ns.add(s);
});
}
if let Some(input_scripts) = self.collateral.get_native_input_scripts() {
input_scripts.iter().for_each(|s| {
ns.add(s);
});
}
if let Some(mint_builder) = &self.mint {
mint_builder.get_native_scripts().iter().for_each(|s| {
ns.add(s);
});
}
if let Some(certificates_builder) = &self.certs {
certificates_builder
.get_native_scripts()
.iter()
.for_each(|s| {
ns.add(s);
});
}
if let Some(withdrawals_builder) = &self.withdrawals {
withdrawals_builder
.get_native_scripts()
.iter()
.for_each(|s| {
ns.add(s);
});
}
if let Some(voting_builder) = &self.voting_procedures {
voting_builder.get_native_scripts().iter().for_each(|s| {
ns.add(s);
});
}
if ns.len() > 0 {
Some(ns)
} else {
None
}
}
fn get_combined_plutus_scripts(&self) -> Option<PlutusWitnesses> {
let mut res = PlutusWitnesses::new();
if let Some(scripts) = self.inputs.get_plutus_input_scripts() {
scripts.0.iter().for_each(|s| {
res.add(s);
})
}
if let Some(scripts) = self.collateral.get_plutus_input_scripts() {
scripts.0.iter().for_each(|s| {
res.add(s);
})
}
if let Some(mint_builder) = &self.mint {
mint_builder.get_plutus_witnesses().0.iter().for_each(|s| {
res.add(s);
})
}
if let Some(certificates_builder) = &self.certs {
certificates_builder
.get_plutus_witnesses()
.0
.iter()
.for_each(|s| {
res.add(s);
})
}
if let Some(withdrawals_builder) = &self.withdrawals {
withdrawals_builder
.get_plutus_witnesses()
.0
.iter()
.for_each(|s| {
res.add(s);
})
}
if let Some(voting_builder) = &self.voting_procedures {
voting_builder
.get_plutus_witnesses()
.0
.iter()
.for_each(|s| {
res.add(s);
})
}
if let Some(voting_proposal_builder) = &self.voting_proposals {
voting_proposal_builder
.get_plutus_witnesses()
.0
.iter()
.for_each(|s| {
res.add(s);
})
}
if res.len() > 0 {
Some(res)
} else {
None
}
}
pub(crate) fn get_witness_set(&self) -> TransactionWitnessSet {
let mut wit = TransactionWitnessSet::new();
if let Some(scripts) = self.get_combined_native_scripts() {
wit.set_native_scripts(&scripts);
}
let mut all_datums = None;
if let Some(pw) = self.get_combined_plutus_scripts() {
let (scripts, datums, redeemers) = pw.collect();
wit.set_plutus_scripts(&scripts);
all_datums = datums;
wit.set_redeemers(&redeemers);
}
if let Some(extra_datum) = &self.extra_datums {
if all_datums.is_none() {
all_datums = Some(PlutusList::new());
}
for datum in extra_datum {
if let Some(datums) = &mut all_datums {
datums.add(datum);
}
}
}
if let Some(datums) = &all_datums {
wit.set_plutus_data(datums);
}
wit
}
fn has_plutus_inputs(&self) -> bool {
if self.inputs.has_plutus_scripts() {
return true;
}
if self.mint.as_ref().map_or(false, |m| m.has_plutus_scripts()) {
return true;
}
if self
.certs
.as_ref()
.map_or(false, |c| c.has_plutus_scripts())
{
return true;
}
if self
.withdrawals
.as_ref()
.map_or(false, |w| w.has_plutus_scripts())
{
return true;
}
if self
.voting_procedures
.as_ref()
.map_or(false, |w| w.has_plutus_scripts())
{
return true;
}
if self
.voting_proposals
.as_ref()
.map_or(false, |w| w.has_plutus_scripts())
{
return true;
}
return false;
}
pub fn build_tx(&self) -> Result<Transaction, JsError> {
if self.has_plutus_inputs() {
if self.script_data_hash.is_none() {
return Err(JsError::from_str(
"Plutus inputs are present, but script data hash is not specified",
));
}
if self.collateral.len() == 0 {
return Err(JsError::from_str(
"Plutus inputs are present, but no collateral inputs are added",
));
}
}
self.validate_inputs_intersection()?;
self.build_tx_unsafe()
}
pub fn build_tx_unsafe(&self) -> Result<Transaction, JsError> {
Ok(Transaction {
body: self.build()?,
witness_set: self.get_witness_set(),
is_valid: true,
auxiliary_data: self.auxiliary_data.clone(),
})
}
pub fn min_fee(&self) -> Result<Coin, JsError> {
let mut self_copy = self.clone();
self_copy.set_fee(&(0x1_00_00_00_00u64).into());
min_fee(&self_copy)
}
}