use rand::Rng;
use crate::address::Address;
use crate::network::ThorNode;
use crate::rlp::Bytes;
use crate::transactions::{Clause, Reserved, Transaction};
use crate::U256;
#[derive(Clone, Debug, Eq, PartialEq, Default)]
struct TransactionTemplate {
block_ref: Option<u64>,
expiration: Option<u32>,
clauses: Vec<Clause>,
gas_price_coef: Option<u8>,
gas: Option<u64>,
depends_on: Option<U256>,
nonce: Option<u64>,
delegated: bool,
}
#[derive(Clone, Debug)]
pub struct TransactionBuilder {
node: ThorNode,
template: TransactionTemplate,
}
impl TransactionBuilder {
pub fn new(node: ThorNode) -> Self {
Self {
node,
template: TransactionTemplate::default(),
}
}
pub const fn delegated(mut self) -> Self {
self.template.delegated = true;
self
}
pub const fn nonce(mut self, nonce: u64) -> Self {
self.template.nonce = Some(nonce);
self
}
pub const fn depends_on(mut self, depends_on: U256) -> Self {
self.template.depends_on = Some(depends_on);
self
}
pub const fn gas(mut self, gas: u64) -> Self {
self.template.gas = Some(gas);
self
}
pub const fn gas_price_coef(mut self, gas_price_coef: u8) -> Self {
self.template.gas_price_coef = Some(gas_price_coef);
self
}
pub const fn expiration(mut self, expiration: u32) -> Self {
self.template.expiration = Some(expiration);
self
}
pub const fn block_ref(mut self, block_ref: u64) -> Self {
self.template.block_ref = Some(block_ref);
self
}
pub fn add_transfer<T: Into<U256>>(self, recipient: Address, value: T) -> Self {
self.add_clause(Clause {
to: Some(recipient),
value: value.into(),
data: Bytes::new(),
})
}
pub fn add_contract_create(self, contract_bytes: Bytes) -> Self {
self.add_clause(Clause {
to: None,
value: U256::zero(),
data: contract_bytes,
})
}
pub fn add_contract_call(self, contract_address: Address, call_bytes: Bytes) -> Self {
self.add_clause(Clause {
to: Some(contract_address),
value: U256::zero(),
data: call_bytes,
})
}
pub fn add_clause(mut self, clause: Clause) -> Self {
self.template.clauses.push(clause);
self
}
pub async fn build(&self) -> Result<Transaction, TransactionBuilderError> {
if self.template.clauses.is_empty() {
return Err(TransactionBuilderError::EmptyTransaction);
}
let block_ref = match self.template.block_ref {
Some(r) => r,
None => self
.node
.fetch_best_block()
.await
.map_err(|_| TransactionBuilderError::NetworkError)?
.0
.block_ref(),
};
let mut tx = Transaction {
chain_tag: self.node.chain_tag,
block_ref,
expiration: self.template.expiration.unwrap_or(128),
clauses: self.template.clauses.clone(),
gas_price_coef: self.template.gas_price_coef.unwrap_or(0),
gas: self.template.gas.unwrap_or(0),
depends_on: self.template.depends_on,
nonce: self.template.nonce.unwrap_or_else(|| {
let mut rng = rand::rng();
rng.random::<u64>()
}),
reserved: if self.template.delegated {
Some(Reserved::new_delegated())
} else {
None
},
signature: None,
};
if self.template.gas.is_some() {
Ok(tx)
} else if tx.clauses.iter().all(|clause| clause.data.is_empty()) {
tx.gas = tx.intrinsic_gas();
Ok(tx)
} else {
Err(TransactionBuilderError::CannotEstimateGas)
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum TransactionBuilderError {
NetworkError,
EmptyTransaction,
CannotEstimateGas,
}
impl std::error::Error for TransactionBuilderError {}
impl std::fmt::Display for TransactionBuilderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NetworkError => f.write_str("Failed to retrieve data from network"),
Self::EmptyTransaction => f.write_str("Cannot build an empty transaction - make sure to add at least one clause first."),
Self::CannotEstimateGas => f.write_str("Transaction clauses involve contract interaction, please provide gas amount explicitly."),
}
}
}