use std::fmt;
use std::fmt::{
Debug,
Formatter,
};
use prost::Message;
use time::Duration;
use crate::execute::{
execute,
Execute,
};
use crate::signer::AnySigner;
use crate::{
AccountId,
Client,
Hbar,
Operator,
PrivateKey,
PublicKey,
Signer,
TransactionId,
TransactionResponse,
};
mod any;
mod execute;
mod protobuf;
#[cfg(feature = "ffi")]
pub use any::AnyTransaction;
#[cfg(feature = "ffi")]
pub(crate) use any::AnyTransactionBody;
pub(crate) use any::AnyTransactionData;
pub(crate) use execute::TransactionExecute;
pub(crate) use protobuf::ToTransactionDataProtobuf;
const DEFAULT_TRANSACTION_VALID_DURATION: Duration = Duration::seconds(120);
#[cfg_attr(feature = "ffi", derive(serde::Serialize))]
#[cfg_attr(feature = "ffi", serde(rename_all = "camelCase"))]
pub struct Transaction<D>
where
D: TransactionExecute,
{
#[cfg_attr(feature = "ffi", serde(flatten))]
body: TransactionBody<D>,
#[cfg_attr(feature = "ffi", serde(skip))]
signers: Vec<AnySigner>,
}
#[derive(Debug, Default, Clone)]
#[cfg_attr(feature = "ffi", serde_with::skip_serializing_none)]
#[cfg_attr(feature = "ffi", derive(serde::Serialize))]
#[cfg_attr(feature = "ffi", serde(rename_all = "camelCase"))]
#[allow(clippy::type_repetition_in_bounds)]
pub(crate) struct TransactionBody<D>
where
D: TransactionExecute,
{
#[cfg_attr(feature = "ffi", serde(flatten))]
#[cfg_attr(
feature = "ffi",
serde(with = "serde_with::As::<serde_with::FromInto<AnyTransactionData>>")
)]
pub(crate) data: D,
pub(crate) node_account_ids: Option<Vec<AccountId>>,
#[cfg_attr(
feature = "ffi",
serde(with = "serde_with::As::<Option<serde_with::DurationSeconds<i64>>>")
)]
pub(crate) transaction_valid_duration: Option<Duration>,
pub(crate) max_transaction_fee: Option<Hbar>,
#[cfg_attr(feature = "ffi", serde(skip_serializing_if = "String::is_empty"))]
pub(crate) transaction_memo: String,
pub(crate) transaction_id: Option<TransactionId>,
pub(crate) operator: Option<Operator>,
#[cfg_attr(feature = "ffi", serde(skip_serializing_if = "std::ops::Not::not"))]
pub(crate) is_frozen: bool,
}
impl<D> Default for Transaction<D>
where
D: Default + TransactionExecute,
{
fn default() -> Self {
Self {
body: TransactionBody {
data: D::default(),
node_account_ids: None,
transaction_valid_duration: None,
max_transaction_fee: None,
transaction_memo: String::new(),
transaction_id: None,
operator: None,
is_frozen: false,
},
signers: Vec::new(),
}
}
}
impl<D> Debug for Transaction<D>
where
D: Debug + TransactionExecute,
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Transaction").field("body", &self.body).finish()
}
}
impl<D> Transaction<D>
where
D: Default + TransactionExecute,
{
#[inline]
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
impl<D> Transaction<D>
where
D: TransactionExecute,
{
#[cfg(feature = "ffi")]
pub(crate) fn from_parts(body: TransactionBody<D>, signers: Vec<AnySigner>) -> Self {
Self { body, signers }
}
pub(crate) fn is_frozen(&self) -> bool {
self.body.is_frozen
}
#[track_caller]
pub(crate) fn require_not_frozen(&self) {
assert!(
!self.is_frozen(),
"transaction is immutable; it has at least one signature or has been explicitly frozen"
);
}
#[cfg(feature = "ffi")]
pub(crate) fn body(&self) -> &TransactionBody<D> {
&self.body
}
fn body_mut(&mut self) -> &mut TransactionBody<D> {
self.require_not_frozen();
&mut self.body
}
pub(crate) fn into_body(self) -> TransactionBody<D> {
self.body
}
pub(crate) fn data(&self) -> &D {
&self.body.data
}
pub(crate) fn data_mut(&mut self) -> &mut D {
self.require_not_frozen();
&mut self.body.data
}
#[must_use]
pub fn get_node_account_ids(&self) -> Option<&[AccountId]> {
self.body.node_account_ids.as_deref()
}
#[track_caller]
pub fn node_account_ids(&mut self, ids: impl IntoIterator<Item = AccountId>) -> &mut Self {
self.body_mut().node_account_ids = Some(ids.into_iter().collect());
self
}
#[must_use]
pub fn get_transaction_valid_duration(&self) -> Option<Duration> {
self.body.transaction_valid_duration
}
pub fn transaction_valid_duration(&mut self, duration: Duration) -> &mut Self {
self.body_mut().transaction_valid_duration = Some(duration);
self
}
#[must_use]
pub fn get_max_transaction_fee(&self) -> Option<Hbar> {
self.body.max_transaction_fee
}
pub fn max_transaction_fee(&mut self, fee: Hbar) -> &mut Self {
self.body_mut().max_transaction_fee = Some(fee);
self
}
#[must_use]
pub fn get_transaction_memo(&self) -> &str {
&self.body.transaction_memo
}
pub fn transaction_memo(&mut self, memo: impl AsRef<str>) -> &mut Self {
self.body_mut().transaction_memo = memo.as_ref().to_owned();
self
}
#[must_use]
pub fn get_transaction_id(&self) -> Option<TransactionId> {
self.body.transaction_id
}
pub fn transaction_id(&mut self, id: TransactionId) -> &mut Self {
self.body_mut().transaction_id = Some(id);
self
}
pub fn sign(&mut self, private_key: PrivateKey) -> &mut Self {
self.sign_signer(AnySigner::PrivateKey(private_key))
}
pub fn sign_with(&mut self, public_key: PublicKey, signer: Signer) -> &mut Self {
self.sign_signer(AnySigner::Arbitrary(Box::new(public_key), signer))
}
pub(crate) fn sign_signer(&mut self, signer: AnySigner) -> &mut Self {
if self.signers.iter().any(|it| it.public_key() == signer.public_key()) {
return self;
}
self.signers.push(signer);
self
}
pub fn freeze(&mut self) -> crate::Result<&mut Self> {
self.freeze_with(None)
}
pub fn freeze_with<'a>(
&mut self,
client: impl Into<Option<&'a Client>>,
) -> crate::Result<&mut Self> {
if self.is_frozen() {
return Ok(self);
}
let client: Option<&Client> = client.into();
let node_account_ids = match &self.body.node_account_ids {
Some(it) => it.clone(),
None => {
client.ok_or_else(|| crate::Error::FreezeUnsetNodeAccountIds)?.random_node_ids()
}
};
let max_transaction_fee = self.body.max_transaction_fee.or_else(|| {
let client_max_transaction_fee = client
.map(|it| it.max_transaction_fee().load(std::sync::atomic::Ordering::Relaxed));
match client_max_transaction_fee {
Some(max) if max > 1 => Some(Hbar::from_tinybars(max as i64)),
_ => None,
}
});
let operator = client.and_then(|it| it.operator_internal().as_deref().map(|it| it.clone()));
self.body.node_account_ids = Some(node_account_ids);
self.body.max_transaction_fee = max_transaction_fee;
self.body.operator = operator;
self.body.is_frozen = true;
if let Some(client) = client {
if client.auto_validate_checksums() {
if let Some(ledger_id) = &*client.ledger_id_internal() {
self.validate_checksums_for_ledger_id(ledger_id)?;
} else {
return Err(crate::Error::CannotPerformTaskWithoutLedgerId {
task: "validate checksums",
});
}
}
}
Ok(self)
}
pub fn to_bytes(&self) -> crate::Result<Vec<u8>> {
if !self.is_frozen() {
panic!("Transaction must be frozen to call `to_bytes`");
}
let transaction_id = match self.transaction_id() {
Some(id) => id,
None => self
.body
.operator
.as_ref()
.ok_or(crate::Error::NoPayerAccountOrTransactionId)?
.generate_transaction_id(),
};
let transaction_list: Result<_, _> = self
.body
.node_account_ids
.as_deref()
.unwrap()
.iter()
.copied()
.map(|node_account_id| {
self.make_request_inner(transaction_id, node_account_id).map(|it| it.0)
})
.collect();
let transaction_list = transaction_list?;
Ok(hedera_proto::sdk::TransactionList { transaction_list }.encode_to_vec())
}
}
impl<D> Transaction<D>
where
D: TransactionExecute,
{
pub async fn execute(&mut self, client: &Client) -> crate::Result<TransactionResponse> {
self.freeze_with(Some(client))?;
execute(client, self, None).await
}
pub(crate) async fn execute_with_optional_timeout(
&mut self,
client: &Client,
timeout: Option<std::time::Duration>,
) -> crate::Result<TransactionResponse> {
self.freeze_with(Some(client))?;
execute(client, self, timeout).await
}
#[allow(clippy::missing_errors_doc)]
pub async fn execute_with_timeout(
&mut self,
client: &Client,
timeout: std::time::Duration,
) -> crate::Result<TransactionResponse> {
self.execute_with_optional_timeout(client, Some(timeout)).await
}
}