use crate::{Address, CoinData, CoinID, CoinValue, Denom, Transaction, TxKind};
use std::collections::{BTreeMap, BTreeSet};
use bytes::Bytes;
use tap::Pipe;
use thiserror::Error;
use tmelcrypt::Hashable;
#[derive(Error, Debug, Clone)]
pub enum TransactionBuildError {
#[error("not well-formed")]
NotWellFormed,
#[error("inputs and outputs unbalanced")]
Unbalanced,
#[error("missing a covenant with hash {0}")]
MissingCovenant(Address),
}
#[derive(Debug)]
pub struct TransactionBuilder {
in_progress: Transaction,
required_covenants: BTreeSet<Address>,
given_covenants: BTreeSet<Address>,
in_balance: BTreeMap<Denom, CoinValue>,
out_balance: BTreeMap<Denom, CoinValue>,
}
impl TransactionBuilder {
pub fn new() -> Self {
let in_progress = Transaction::default();
TransactionBuilder {
in_progress,
required_covenants: BTreeSet::new(),
given_covenants: BTreeSet::new(),
in_balance: BTreeMap::new(),
out_balance: BTreeMap::new(),
}
}
pub fn kind(mut self, kind: TxKind) -> Self {
self.in_progress.kind = kind;
self
}
pub fn input(mut self, coin_id: CoinID, coin_data: CoinData) -> Self {
self.in_progress.inputs.push(coin_id);
*self.in_balance.entry(coin_data.denom).or_default() += coin_data.value;
self.required_covenants.insert(coin_data.covhash);
self
}
pub fn output(mut self, data: CoinData) -> Self {
if data.denom != Denom::NewCustom {
*self.out_balance.entry(data.denom).or_default() += data.value;
}
self.in_progress.outputs.push(data);
self
}
pub fn covenant(mut self, script: Bytes) -> Self {
self.given_covenants.insert(Address(script.hash()));
self.in_progress.covenants.push(script);
self
}
pub fn fee(mut self, fee: CoinValue) -> Self {
*self.out_balance.entry(Denom::Mel).or_default() += fee;
self.in_progress.fee += fee;
self
}
pub fn auto_base_fee(
self,
fee_multiplier: u128,
max_sig_count: usize,
max_sig_size: usize,
cov_to_weight: impl Fn(&[u8]) -> u128,
) -> Self {
let fee = self.in_progress.clone().pipe(|mut tx| {
let range = 0..max_sig_count;
range
.into_iter()
.for_each(|_index| tx.sigs.push(vec![0; max_sig_size].into()));
tx.base_fee(fee_multiplier, 0, cov_to_weight)
});
self.fee(fee)
}
pub fn change(mut self, denom: Denom, change_addr: Address) -> Self {
let input = self.in_balance.get(&denom).copied().unwrap_or_default();
let output = self.out_balance.get(&denom).copied().unwrap_or_default();
if input >= output {
let delta = input - output;
self = self.output(CoinData {
covhash: change_addr,
value: delta,
denom,
additional_data: vec![].into(),
});
}
self
}
pub fn build(self) -> Result<Transaction, TransactionBuildError> {
if self.in_balance != self.out_balance && self.in_progress.kind != TxKind::Faucet {
Err(TransactionBuildError::Unbalanced)
} else if !self.in_progress.is_well_formed() {
Err(TransactionBuildError::NotWellFormed)
} else {
let was_covenant_creation_successful: Result<(), TransactionBuildError> =
self.required_covenants.iter().try_for_each(|cov| {
let is_covenant_missing_from_given_covenants: bool =
!self.given_covenants.contains(cov);
match is_covenant_missing_from_given_covenants {
true => Err(TransactionBuildError::MissingCovenant(*cov)),
false => Ok(()),
}
});
match was_covenant_creation_successful {
Ok(()) => Ok(self.in_progress),
Err(error) => Err(error),
}
}
}
pub fn data(mut self, data: Bytes) -> Self {
self.in_progress.data = data;
self
}
}
impl Default for TransactionBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn txbuilder_basic_balance() {
let init_coindata = CoinData {
denom: Denom::Mel,
value: CoinValue::from_millions(1000u64),
additional_data: vec![].into(),
covhash: Address::coin_destroy(),
};
TransactionBuilder::new()
.input(CoinID::zero_zero(), init_coindata)
.fee(20000.into())
.output(CoinData {
covhash: Address::coin_destroy(),
value: 1000.into(),
denom: Denom::Mel,
additional_data: vec![].into(),
})
.change(Denom::Mel, Address::coin_destroy())
.build()
.expect_err("build failed");
}
}