Skip to main content

hiero_sdk/transaction/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::borrow::Cow;
4use std::collections::HashMap;
5use std::fmt;
6use std::fmt::{
7    Debug,
8    Formatter,
9};
10use std::num::NonZeroUsize;
11
12use hiero_sdk_proto::services;
13use prost::Message;
14use time::Duration;
15use triomphe::Arc;
16
17use crate::custom_fee_limit::CustomFeeLimit;
18use crate::downcast::DowncastOwned;
19use crate::execute::execute;
20use crate::signer::AnySigner;
21use crate::{
22    AccountId,
23    Client,
24    Error,
25    Hbar,
26    Operator,
27    PrivateKey,
28    PublicKey,
29    ScheduleCreateTransaction,
30    ToProtobuf,
31    TransactionHash,
32    TransactionId,
33    TransactionResponse,
34    ValidateChecksums,
35};
36
37mod any;
38mod chunked;
39mod cost;
40mod execute;
41mod protobuf;
42mod signature_map;
43mod source;
44#[cfg(test)]
45mod tests;
46
47pub use any::AnyTransaction;
48pub(crate) use any::AnyTransactionData;
49pub(crate) use chunked::{
50    ChunkData,
51    ChunkInfo,
52    ChunkedTransactionData,
53};
54pub(crate) use cost::CostTransaction;
55pub(crate) use execute::{
56    TransactionData,
57    TransactionExecute,
58    TransactionExecuteChunked,
59};
60pub(crate) use protobuf::{
61    ToSchedulableTransactionDataProtobuf,
62    ToTransactionDataProtobuf,
63};
64pub(crate) use source::TransactionSources;
65
66const DEFAULT_TRANSACTION_VALID_DURATION: Duration = Duration::seconds(120);
67
68/// A transaction that can be executed on the Hiero network.
69#[derive(Clone)]
70pub struct Transaction<D> {
71    body: TransactionBody<D>,
72
73    signers: Vec<AnySigner>,
74
75    sources: Option<TransactionSources>,
76
77    /// The gRPC deadline for this transaction.
78    /// If set, this overrides the client's default grpc_deadline.
79    grpc_deadline: Option<std::time::Duration>,
80
81    /// The request timeout for this transaction (including retries).
82    /// If set, this overrides the client's request_timeout and any timeout passed to execute methods.
83    request_timeout: Option<std::time::Duration>,
84}
85
86#[derive(Debug, Default, Clone)]
87pub(crate) struct TransactionBody<D> {
88    pub(crate) data: D,
89
90    pub(crate) node_account_ids: Option<Vec<AccountId>>,
91
92    pub(crate) transaction_valid_duration: Option<Duration>,
93
94    pub(crate) max_transaction_fee: Option<Hbar>,
95
96    pub(crate) transaction_memo: String,
97
98    pub(crate) transaction_id: Option<TransactionId>,
99
100    pub(crate) operator: Option<Arc<Operator>>,
101
102    pub(crate) is_frozen: bool,
103
104    pub(crate) regenerate_transaction_id: Option<bool>,
105
106    /// The maximum custom fee that the user is willing to pay for the message.
107    /// If left empty, the user is willing to pay any custom fee.
108    /// If used with a transaction type that does not support custom fee limits, the transaction will fail.
109    pub(crate) custom_fee_limits: Vec<CustomFeeLimit>,
110
111    /// The public key of the trusted batch assembler.
112    pub(crate) batch_key: Option<crate::Key>,
113
114    /// Whether to use high-volume throttles for this transaction.
115    /// When true, enables high-volume throttles and pricing for entity creation.
116    /// Only affects supported transaction types; otherwise, it is ignored.
117    pub(crate) high_volume: bool,
118}
119
120impl<D> Default for Transaction<D>
121where
122    D: Default,
123{
124    fn default() -> Self {
125        Self {
126            body: TransactionBody {
127                data: D::default(),
128                node_account_ids: None,
129                transaction_valid_duration: None,
130                max_transaction_fee: None,
131                transaction_memo: String::new(),
132                transaction_id: None,
133                operator: None,
134                is_frozen: false,
135                regenerate_transaction_id: None,
136                custom_fee_limits: Vec::new(),
137                batch_key: None,
138                high_volume: false,
139            },
140            signers: Vec::new(),
141            sources: None,
142            grpc_deadline: None,
143            request_timeout: None,
144        }
145    }
146}
147
148impl<D> Debug for Transaction<D>
149where
150    D: Debug,
151{
152    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
153        f.debug_struct("Transaction").field("body", &self.body).finish()
154    }
155}
156
157impl<D> Transaction<D>
158where
159    D: Default,
160{
161    /// Create a new default transaction.
162    ///
163    /// Does the same thing as [`default`](Self::default)
164    #[inline]
165    #[must_use]
166    pub fn new() -> Self {
167        Self::default()
168    }
169}
170
171impl<D> Transaction<D> {
172    pub(crate) fn from_parts(body: TransactionBody<D>, signers: Vec<AnySigner>) -> Self {
173        Self { body, signers, sources: None, grpc_deadline: None, request_timeout: None }
174    }
175
176    pub(crate) fn is_frozen(&self) -> bool {
177        self.body.is_frozen
178    }
179
180    pub(crate) fn signers(&self) -> impl Iterator<Item = &AnySigner> {
181        self.signers.iter()
182    }
183
184    pub(crate) fn sources(&self) -> Option<&TransactionSources> {
185        self.sources.as_ref()
186    }
187
188    fn signed_sources(&self) -> Option<Cow<'_, TransactionSources>> {
189        self.sources().map(|it| it.sign_with(&self.signers))
190    }
191
192    /// # Panics
193    /// If `self.is_frozen()`.
194    #[track_caller]
195    pub(crate) fn require_not_frozen(&self) {
196        assert!(
197            !self.is_frozen(),
198            "transaction is immutable; it has at least one signature or has been explicitly frozen"
199        );
200    }
201
202    /// # Panics
203    /// If `self.is_frozen()`.
204    pub(crate) fn body_mut(&mut self) -> &mut TransactionBody<D> {
205        self.require_not_frozen();
206        &mut self.body
207    }
208
209    pub(crate) fn into_body(self) -> TransactionBody<D> {
210        self.body
211    }
212
213    #[inline(always)]
214    pub(crate) fn data(&self) -> &D {
215        &self.body.data
216    }
217
218    /// # Panics
219    /// If `self.is_frozen()`.
220    pub(crate) fn data_mut(&mut self) -> &mut D {
221        self.require_not_frozen();
222        &mut self.body.data
223    }
224
225    /// Returns the account IDs of the nodes that this transaction may be submitted to.
226    ///
227    /// `None` means any node configured on the client.
228    #[must_use]
229    pub fn get_node_account_ids(&self) -> Option<&[AccountId]> {
230        self.body.node_account_ids.as_deref()
231    }
232
233    /// Sets the account IDs of the nodes that this transaction may be submitted to.
234    ///
235    /// Defaults to the full list of nodes configured on the client.
236    #[track_caller]
237    pub fn node_account_ids(&mut self, ids: impl IntoIterator<Item = AccountId>) -> &mut Self {
238        let nodes: Vec<_> = ids.into_iter().collect();
239
240        if nodes.is_empty() {
241            log::warn!("Nodes list is empty, ignoring setter");
242        } else {
243            self.body_mut().node_account_ids = Some(nodes);
244        }
245
246        self
247    }
248
249    /// Returns the duration that this transaction is valid for, once finalized and signed.
250    #[must_use]
251    pub fn get_transaction_valid_duration(&self) -> Option<Duration> {
252        self.body.transaction_valid_duration
253    }
254
255    /// Sets the duration that this transaction is valid for, once finalized and signed.
256    ///
257    /// Defaults to 120 seconds (or two minutes).
258    pub fn transaction_valid_duration(&mut self, duration: Duration) -> &mut Self {
259        self.body_mut().transaction_valid_duration = Some(duration);
260        self
261    }
262
263    /// Returns the maximum transaction fee the paying account is willing to pay.
264    #[must_use]
265    pub fn get_max_transaction_fee(&self) -> Option<Hbar> {
266        self.body.max_transaction_fee
267    }
268
269    /// Sets the maximum transaction fee the paying account is willing to pay.
270    pub fn max_transaction_fee(&mut self, fee: Hbar) -> &mut Self {
271        self.body_mut().max_transaction_fee = Some(fee);
272        self
273    }
274
275    /// Returns the custom fee limits for the transaction.
276    #[must_use]
277    pub fn get_custom_fee_limits(&self) -> &[CustomFeeLimit] {
278        &self.body.custom_fee_limits
279    }
280
281    /// Sets the custom fee limits for the transaction.
282    pub fn custom_fee_limits(
283        &mut self,
284        limits: impl IntoIterator<Item = CustomFeeLimit>,
285    ) -> &mut Self {
286        self.body_mut().custom_fee_limits = limits.into_iter().collect();
287        self
288    }
289
290    /// Adds a custom fee limit to the transaction.
291    pub fn add_custom_fee_limit(&mut self, limit: CustomFeeLimit) -> &mut Self {
292        self.body_mut().custom_fee_limits.push(limit);
293        self
294    }
295
296    /// Removes all custom fee limits for the transaction.
297    pub fn clear_custom_fee_limits(&mut self) -> &mut Self {
298        self.body_mut().custom_fee_limits.clear();
299        self
300    }
301
302    /// Sets a note / description that should be recorded in the transaction record.
303    ///
304    /// Maximum length of 100 characters.
305    #[must_use]
306    pub fn get_transaction_memo(&self) -> &str {
307        &self.body.transaction_memo
308    }
309
310    /// Sets a note or description that should be recorded in the transaction record.
311    ///
312    /// Maximum length of 100 characters.
313    pub fn transaction_memo(&mut self, memo: impl AsRef<str>) -> &mut Self {
314        self.body_mut().transaction_memo = memo.as_ref().to_owned();
315        self
316    }
317
318    /// Returns the gRPC deadline for this transaction.
319    ///
320    /// If set, this overrides the client's default grpc_deadline.
321    #[must_use]
322    pub fn get_grpc_deadline(&self) -> Option<std::time::Duration> {
323        self.grpc_deadline
324    }
325
326    /// Sets the gRPC deadline for this transaction.
327    ///
328    /// This overrides the client's default grpc_deadline.
329    /// The deadline applies to both the channel connection timeout and the request execution timeout.
330    pub fn grpc_deadline(&mut self, deadline: std::time::Duration) -> &mut Self {
331        self.grpc_deadline = Some(deadline);
332        self
333    }
334
335    /// Returns the request timeout for this transaction (including retries).
336    ///
337    /// If set, this takes priority over the timeout passed to execute methods and the client's request_timeout.
338    #[must_use]
339    pub fn get_request_timeout(&self) -> Option<std::time::Duration> {
340        self.request_timeout
341    }
342
343    /// Sets the request timeout for this transaction (including retries).
344    ///
345    /// This takes priority over the timeout passed to execute methods and the client's request_timeout.
346    /// The timeout applies to the entire operation including all retry attempts.
347    pub fn request_timeout(&mut self, timeout: std::time::Duration) -> &mut Self {
348        self.request_timeout = Some(timeout);
349        self
350    }
351
352    /// Returns the explicit transaction ID to use to identify this transaction.
353    ///
354    /// Overrides the payer account defined on this transaction or on the client.
355    #[must_use]
356    pub fn get_transaction_id(&self) -> Option<TransactionId> {
357        self.body.transaction_id
358    }
359
360    /// Sets an explicit transaction ID to use to identify this transaction.
361    ///
362    /// Overrides the payer account defined on this transaction or on the client.
363    pub fn transaction_id(&mut self, id: TransactionId) -> &mut Self {
364        self.body_mut().transaction_id = Some(id);
365        self
366    }
367
368    /// Sign the transaction.
369    pub fn sign(&mut self, private_key: PrivateKey) -> &mut Self {
370        self.sign_signer(AnySigner::PrivateKey(private_key))
371    }
372
373    /// Sign the transaction.
374    pub fn sign_with<F: Fn(&[u8]) -> Vec<u8> + Send + Sync + 'static>(
375        &mut self,
376        public_key: PublicKey,
377        signer: F,
378    ) -> &mut Self {
379        self.sign_signer(AnySigner::arbitrary(Box::new(public_key), signer))
380    }
381
382    pub(crate) fn sign_signer(&mut self, signer: AnySigner) -> &mut Self {
383        // We're _supposed_ to require frozen here, but really there's no reason I can think of to do that.
384
385        // skip the signer if we already have it.
386        if self.signers.iter().any(|it| it.public_key() == signer.public_key()) {
387            return self;
388        }
389
390        self.signers.push(signer);
391        self
392    }
393}
394
395impl<D: ChunkedTransactionData> Transaction<D> {
396    /// Returns the maximum number of chunks this transaction will be split into.
397    #[must_use]
398    pub fn get_max_chunks(&self) -> usize {
399        self.data().chunk_data().max_chunks
400    }
401
402    /// Sets the maximum number of chunks this transaction will be split into.
403    pub fn max_chunks(&mut self, max_chunks: usize) -> &mut Self {
404        self.data_mut().chunk_data_mut().max_chunks = max_chunks;
405
406        self
407    }
408
409    // todo: just return a `NonZeroUsize` instead? Take something along the lines of a `u32`?
410    /// Returns the maximum size of any chunk.
411    pub fn get_chunk_size(&self) -> usize {
412        self.data().chunk_data().chunk_size.get()
413    }
414
415    // todo: just take a `NonZeroUsize` instead? Take something along the lines of a `u32`?
416    /// Sets the maximum size of any chunk.
417    ///
418    /// # Panics
419    /// If `size` == 0
420    pub fn chunk_size(&mut self, size: usize) -> &mut Self {
421        let Some(size) = NonZeroUsize::new(size) else {
422            panic!("Chunk size must be greater than zero")
423        };
424
425        self.data_mut().chunk_data_mut().chunk_size = size;
426
427        self
428    }
429
430    /// Returns whether or not the transaction ID should be refreshed if a [`Status::TransactionExpired`](crate::Status::TransactionExpired) occurs.
431    ///
432    /// By default, the value on Client will be used.
433    ///
434    /// Note: Some operations forcibly disable transaction ID regeneration, such as setting the transaction ID explicitly.
435    pub fn get_regenerate_transaction_id(&self) -> Option<bool> {
436        self.body.regenerate_transaction_id
437    }
438
439    /// Sets whether or not the transaction ID should be refreshed if a [`Status::TransactionExpired`](crate::Status::TransactionExpired) occurs.
440    ///
441    /// Various operations such as [`add_signature`](Self::add_signature) can forcibly disable transaction ID regeneration.
442    pub fn regenerate_transaction_id(&mut self, regenerate_transaction_id: bool) -> &mut Self {
443        self.body_mut().regenerate_transaction_id = Some(regenerate_transaction_id);
444
445        self
446    }
447}
448
449impl<D: ValidateChecksums> Transaction<D> {
450    /// Freeze the transaction so that no further modifications can be made.
451    ///
452    /// # Errors
453    /// - [`Error::FreezeUnsetNodeAccountIds`] if no [`node_account_ids`](Self::node_account_ids) were set.
454    ///
455    /// # Panics
456    /// - If `node_account_ids` is explicitly set to empty (IE: `tx.node_account_ids([]).freeze_with(None)`).
457    pub fn freeze(&mut self) -> crate::Result<&mut Self> {
458        self.freeze_with(None)
459    }
460
461    /// Freeze the transaction so that no further modifications can be made.
462    ///
463    /// # Errors
464    /// - [`Error::FreezeUnsetNodeAccountIds`] if no [`node_account_ids`](Self::node_account_ids) were set and `client.is_none()`.
465    ///
466    /// # Panics
467    /// - If `node_account_ids` is explicitly set to empty (IE: `tx.node_account_ids([]).freeze_with(None)`).
468    pub fn freeze_with<'a>(
469        &mut self,
470        client: impl Into<Option<&'a Client>>,
471    ) -> crate::Result<&mut Self> {
472        if self.is_frozen() {
473            return Ok(self);
474        }
475        let client: Option<&Client> = client.into();
476
477        // set transaction id if not set based on client operator
478        if self.get_transaction_id().is_none() {
479            let operator: Arc<Operator> =
480                client.and_then(Client::full_load_operator).expect("Client must have an operator");
481            let transaction_id = TransactionId::generate(operator.account_id);
482            self.transaction_id(transaction_id);
483        }
484
485        let node_account_ids = match &self.body.node_account_ids {
486            // the clone here is the lesser of two evils.
487            Some(it) => {
488                assert!(!it.is_empty());
489                it.clone()
490            }
491            #[allow(clippy::missing_panics_doc)]
492            None => {
493                let nodes = client
494                    .ok_or(Error::FreezeUnsetNodeAccountIds)?
495                    .net()
496                    .0
497                    .load()
498                    .random_node_ids();
499                assert!(!nodes.is_empty(), "BUG: Client didn't give any nodes (all unhealthy)");
500
501                nodes
502            }
503        };
504
505        // note to reviewer: this is intentionally still an option, fallback is used later, swift doesn't *have* default max transaction fee and fixing it is a massive PITA.
506        let max_transaction_fee = self.body.max_transaction_fee.or_else(|| {
507            // no max has been set on the *transaction*
508            // check if there is a global max set on the client
509            client.and_then(Client::default_max_transaction_fee)
510        });
511
512        let custom_fee_limits = self.body.custom_fee_limits.clone();
513
514        let operator = client.and_then(Client::full_load_operator);
515
516        // note: yes, there's an `Some(opt.unwrap())`, this is INTENTIONAL.
517        self.body.node_account_ids = Some(node_account_ids);
518        self.body.max_transaction_fee = max_transaction_fee;
519        self.body.operator = operator;
520        self.body.is_frozen = true;
521        self.body.custom_fee_limits = custom_fee_limits;
522
523        if let Some(client) = client {
524            if client.auto_validate_checksums() {
525                let ledger_id = client.ledger_id_internal();
526                let ledger_id = ledger_id
527                    .as_ref()
528                    .expect("Client had auto_validate_checksums enabled but no ledger ID");
529
530                self.validate_checksums(ledger_id.as_ref_ledger_id())?;
531            }
532        }
533
534        Ok(self)
535    }
536
537    /// Sign the transaction with the `client`'s operator.
538    ///
539    /// # Errors
540    /// - If [`freeze_with`](Self::freeze_with) would error for this transaction.
541    ///
542    /// # Panics
543    /// If `client` has no operator.
544    pub fn sign_with_operator(&mut self, client: &Client) -> crate::Result<&mut Self> {
545        let Some(op) = client.full_load_operator() else { panic!("Client had no operator") };
546
547        self.freeze_with(client)?;
548
549        self.sign_signer(op.signer.clone());
550
551        self.body.operator = Some(op);
552
553        Ok(self)
554    }
555
556    /// Set the key that will sign the batch of which this Transaction is a part of.
557    #[track_caller]
558    pub fn set_batch_key(&mut self, batch_key: crate::Key) -> &mut Self {
559        self.require_not_frozen();
560        self.body_mut().batch_key = Some(batch_key);
561        self
562    }
563
564    /// Get the key that will sign the batch of which this Transaction is a part of.
565    #[must_use]
566    pub fn get_batch_key(&self) -> Option<&crate::Key> {
567        self.body.batch_key.as_ref()
568    }
569
570    /// Set whether to use high-volume throttles for this transaction.
571    ///
572    /// When true, enables high-volume throttles and pricing for entity creation.
573    /// Only affects supported transaction types; otherwise, it is ignored.
574    #[track_caller]
575    pub fn set_high_volume(&mut self, high_volume: bool) -> &mut Self {
576        self.require_not_frozen();
577        self.body_mut().high_volume = high_volume;
578        self
579    }
580
581    /// Get whether high-volume throttles are enabled for this transaction.
582    #[must_use]
583    pub fn get_high_volume(&self) -> bool {
584        self.body.high_volume
585    }
586}
587
588impl<D: TransactionExecute> Transaction<D> {
589    /// Convert this transaction to signed transaction bytes.
590    ///
591    /// This is used internally for batch transactions to get the bytes
592    /// of each inner transaction.
593    ///
594    /// # Errors
595    ///
596    /// Returns an error if the transaction is not frozen or cannot be serialized.
597    pub fn to_signed_transaction_bytes(&self) -> crate::Result<Vec<u8>> {
598        if !self.is_frozen() {
599            return Err(crate::Error::basic_parse(
600                "Transaction must be frozen to get signed transaction bytes",
601            ));
602        }
603
604        let transaction_list = self.make_transaction_list()?;
605
606        // For batch transactions, we need the signed transaction bytes from the first transaction
607        if let Some(first_transaction) = transaction_list.first() {
608            Ok(first_transaction.signed_transaction_bytes.clone())
609        } else {
610            Err(crate::Error::basic_parse("No transactions found"))
611        }
612    }
613
614    /// Convenience method to mark a transaction as part of a batch transaction.
615    /// The Transaction will be frozen and signed by the operator of the client.
616    /// Per HIP-551, inner transactions use node account ID 0.0.0.
617    ///
618    /// # Errors
619    ///
620    /// Returns an error if the client has no operator configured.
621    pub fn batchify(
622        &mut self,
623        client: &crate::Client,
624        batch_key: crate::Key,
625    ) -> crate::Result<&mut Self> {
626        self.require_not_frozen();
627        self.set_batch_key(batch_key);
628        // Set node account ID to 0.0.0 for batch transactions (as per HIP-551)
629        self.node_account_ids([crate::AccountId::new(0, 0, 0)]);
630        self.sign_with_operator(client)
631    }
632    /// # Errors
633    /// - If the transaction needs multiple chunks, or has no explicit transaction ID *and* `self.operator` is not set.
634    ///
635    /// # Panics
636    /// - If `!self.is_frozen()`
637    fn make_transaction_list(&self) -> crate::Result<Vec<services::Transaction>> {
638        if self.data().maybe_chunk_data().is_some() {
639            self.make_transaction_list_chunked()
640        } else {
641            self.make_transaction_list_non_chunked()
642        }
643    }
644
645    pub(crate) fn make_sources(&self) -> crate::Result<Cow<'_, TransactionSources>> {
646        if let Some(sources) = self.signed_sources() {
647            return Ok(sources);
648        }
649
650        return Ok(Cow::Owned(TransactionSources::new(self.make_transaction_list()?).unwrap()));
651    }
652
653    /// Convert `self` to protobuf encoded bytes.
654    ///
655    /// # Errors
656    /// - If `freeze_with` wasn't called with an operator.
657    ///
658    /// # Panics
659    /// - If `!self.is_frozen()`.
660    pub fn to_bytes(&self) -> crate::Result<Vec<u8>> {
661        let transaction_list = self
662            .signed_sources()
663            .map_or_else(|| self.make_transaction_list(), |it| Ok(it.transactions().to_vec()))?;
664        Ok(hiero_sdk_proto::sdk::TransactionList { transaction_list }.encode_to_vec())
665    }
666
667    pub(crate) fn add_signature_signer(
668        &mut self,
669        signer: &AnySigner,
670    ) -> crate::transaction::signature_map::SignatureMap {
671        assert!(self.is_frozen());
672
673        let sources = self.make_sources().unwrap();
674
675        let sources = sources.sign_with(std::slice::from_ref(signer));
676
677        let mut sig_map = crate::transaction::signature_map::SignatureMap::new();
678
679        // hack: I don't care about perf here.
680        for (chunk, tx_id) in sources.chunks().into_iter().zip(sources._transaction_ids()) {
681            let tx_id = tx_id.expect("transaction ID should be set since transaction is frozen");
682            for (tx, node) in chunk.signed_transactions().iter().zip(chunk.node_ids()) {
683                let (tx, node) = (tx, node);
684                let (pk, sig) = signer.sign(&tx.body_bytes);
685                sig_map.insert_signature(node.clone(), tx_id, pk, sig);
686            }
687        }
688
689        // if we have a `Cow::Borrowed` that'd mean there was no modification
690        if let Cow::Owned(sources) = sources {
691            self.sources = Some(sources);
692        }
693
694        sig_map
695    }
696
697    pub(crate) fn add_signature_map_obj(
698        &mut self,
699        signature: crate::transaction::signature_map::SignatureMap,
700    ) {
701        assert!(self.is_frozen());
702
703        let sources = self.make_sources().unwrap();
704
705        let sources = sources.add_signature_map(signature);
706
707        self.sources = Some(sources);
708    }
709
710    // todo: should this return `Result<&mut Self>`?
711    /// Adds a signature directly to `self`.
712    ///
713    /// Only use this as a last resort.
714    ///
715    /// This forcibly disables transaction ID regeneration.
716    pub fn add_signature(&mut self, pk: PublicKey, signature: Vec<u8>) -> &mut Self {
717        assert!(self.is_frozen());
718
719        // note: the following pair of checks are for more detailed panic messages
720        // IE, they should *hopefully* be tripped first
721        assert_eq!(
722            self.get_node_account_ids().map_or(0, <[AccountId]>::len),
723            1,
724            "cannot manually add a single signature to a transaction with multiple nodes. Maybe you meant to use `add_signature_map`?"
725        );
726
727        if let Some(chunk_data) = self.data().maybe_chunk_data() {
728            assert!(
729                chunk_data.used_chunks() <= 1,
730                "cannot manually add a signature to a chunked transaction with multiple chunks. Maybe you meant to use `add_signature_map`?",
731            );
732        }
733
734        let mut signature_map = crate::transaction::signature_map::SignatureMap::new();
735
736        signature_map.insert_signature(
737            self.get_node_account_ids()
738                .expect("We already asserted we have 1 node")
739                .first()
740                .expect("We already asserted we have 1 node")
741                .clone(),
742            self.get_transaction_id()
743                .expect("transaction ID should be set since transaction is frozen"),
744            pk,
745            signature,
746        );
747        self.add_signature_map_obj(signature_map);
748
749        self
750    }
751
752    /// Adds a signature directly to `self`.
753    ///
754    /// These signatures are for all the sub transactions in this transaction. A map of signatures
755    /// for each node and transaction chunk is required.
756    ///
757    /// Only use this as a last resort.
758    ///
759    /// This forcibly disables transaction ID regeneration.
760    pub fn add_signature_map(
761        &mut self,
762        signature: HashMap<AccountId, HashMap<TransactionId, HashMap<PublicKey, Vec<u8>>>>,
763    ) -> &mut Self {
764        self.add_signature_map_obj(crate::transaction::signature_map::SignatureMap(signature));
765
766        self
767    }
768
769    /// # Panics
770    /// panics if the transaction is not schedulable, a transaction can be non-schedulable due to:
771    /// - if `self.is_frozen`
772    /// - being a transaction kind that's non-schedulable, IE, `EthereumTransaction`, or
773    /// - being a chunked transaction with multiple chunks.
774    pub fn schedule(self) -> ScheduleCreateTransaction {
775        self.require_not_frozen();
776        assert!(self.get_node_account_ids().is_none(), "The underlying transaction for a scheduled transaction cannot have node account IDs set");
777
778        let mut transaction = ScheduleCreateTransaction::new();
779
780        if let Some(transaction_id) = self.get_transaction_id() {
781            transaction.transaction_id(transaction_id);
782        }
783
784        transaction.scheduled_transaction(self);
785
786        transaction
787    }
788
789    /// Get the hash for this transaction.
790    ///
791    /// Note: Calling this function _disables_ transaction ID regeneration.
792    ///
793    /// # Errors
794    /// - [`Error::NoPayerAccountOrTransactionId`]
795    ///     if `freeze_with` wasn't called with an operator and no transaction ID was set.
796    ///
797    /// # Panics
798    /// - If `!self.is_frozen()`.
799    pub fn get_transaction_hash(&mut self) -> crate::Result<TransactionHash> {
800        // todo: error not frozen
801        assert!(
802            self.is_frozen(),
803            "Transaction must be frozen before calling `get_transaction_hash`"
804        );
805
806        let sources = self.make_sources()?;
807
808        let sources = match sources {
809            Cow::Borrowed(it) => it,
810            Cow::Owned(it) => &*self.sources.insert(it),
811        };
812
813        Ok(TransactionHash::new(&sources.transactions().first().unwrap().signed_transaction_bytes))
814    }
815
816    /// Get the hashes for this transaction.
817    ///
818    /// Note: Calling this function _disables_ transaction ID regeneration.
819    ///
820    /// # Errors
821    /// - [`Error::NoPayerAccountOrTransactionId`]
822    ///     if `freeze_with` wasn't called with an operator and no transaction ID was set.
823    ///
824    /// # Panics
825    /// - If `!self.is_frozen()`.
826    pub fn get_transaction_hash_per_node(
827        &mut self,
828    ) -> crate::Result<HashMap<AccountId, TransactionHash>> {
829        // todo: error not frozen
830        assert!(
831            self.is_frozen(),
832            "Transaction must be frozen before calling `get_transaction_hash`"
833        );
834
835        let sources = self.make_sources()?;
836
837        let chunk = sources.chunks().next().unwrap();
838
839        let iter = chunk
840            .node_ids()
841            .iter()
842            .zip(chunk.transactions())
843            .map(|(node, it)| (*node, TransactionHash::new(&it.signed_transaction_bytes)));
844
845        Ok(iter.collect())
846    }
847
848    #[allow(deprecated)]
849    fn make_transaction_list_chunked(&self) -> crate::Result<Vec<services::Transaction>> {
850        // todo: fix this with chunked transactions.
851        let used_chunks = self.data().maybe_chunk_data().map_or(1, ChunkData::used_chunks);
852        let node_account_ids = self.body.node_account_ids.as_deref().unwrap();
853
854        let mut transaction_list = Vec::with_capacity(used_chunks * node_account_ids.len());
855
856        if node_account_ids.is_empty() {
857            // Handle case with no node IDs
858            transaction_list.push(self.create_transaction_for_node(None));
859        } else {
860            // Handle case with node IDs
861            for node_account_id in node_account_ids {
862                transaction_list.push(self.create_transaction_for_node(Some(node_account_id)));
863            }
864        }
865
866        Ok(transaction_list)
867    }
868
869    #[allow(clippy::too_many_lines)]
870    #[allow(deprecated)]
871    fn make_transaction_list_non_chunked(&self) -> crate::Result<Vec<services::Transaction>> {
872        let mut transaction_list = Vec::new();
873
874        let node_account_ids = match &self.get_node_account_ids() {
875            Some(ids) => ids.iter().collect::<Vec<_>>(),
876            None => vec![], // Default if none specified
877        };
878
879        if node_account_ids.is_empty() {
880            // Handle case with no node IDs
881            transaction_list.push(self.create_transaction_for_node(None));
882        } else {
883            // Handle case with node IDs
884            for node_account_id in node_account_ids {
885                transaction_list.push(self.create_transaction_for_node(Some(node_account_id)));
886            }
887        }
888
889        Ok(transaction_list)
890    }
891
892    /// Creates a transaction for a specific node and adds it to the transaction list
893    fn create_transaction_for_node(&self, node_opt: Option<&AccountId>) -> services::Transaction {
894        let transaction_body = services::TransactionBody {
895            transaction_id: self.get_transaction_id().map(|id| id.to_protobuf()),
896            generate_record: false,
897            memo: self.body.transaction_memo.clone(),
898            data: Some(self.body.data.to_transaction_data_protobuf(&ChunkInfo {
899                current: 0,
900                total: 1,
901                initial_transaction_id: TransactionId::generate(AccountId::new(0, 0, 0)),
902                current_transaction_id: TransactionId::generate(AccountId::new(0, 0, 0)),
903                node_account_id: node_opt.cloned(),
904            })),
905            transaction_valid_duration: Some(
906                self.get_transaction_valid_duration()
907                    .unwrap_or_else(|| DEFAULT_TRANSACTION_VALID_DURATION)
908                    .to_protobuf(),
909            ),
910            node_account_id: node_opt.map(|id| id.to_protobuf()),
911            transaction_fee: self
912                .body
913                .max_transaction_fee
914                .unwrap_or_else(|| self.body.data.default_max_transaction_fee())
915                .to_tinybars() as u64,
916            max_custom_fees: { self.body.custom_fee_limits.to_protobuf() },
917            batch_key: self.body.batch_key.as_ref().map(|key| key.to_protobuf()),
918            high_volume: self.body.high_volume,
919        };
920
921        let body_bytes = transaction_body.encode_to_vec();
922        let mut signatures = Vec::with_capacity(1 + self.signers.len());
923
924        if let Some(operator) = &self.body.operator {
925            let operator_signature = operator.sign(&body_bytes);
926            let (pk, sig) = operator_signature;
927            signatures.push(services::SignaturePair {
928                pub_key_prefix: pk.to_bytes_raw(),
929                signature: Some(match pk.kind() {
930                    crate::key::KeyKind::Ed25519 => {
931                        services::signature_pair::Signature::Ed25519(sig)
932                    }
933                    crate::key::KeyKind::Ecdsa => {
934                        services::signature_pair::Signature::EcdsaSecp256k1(sig)
935                    }
936                }),
937            });
938        }
939
940        for signer in &self.signers {
941            let public_key = signer.public_key().to_bytes();
942            if !signatures.iter().any(|it| public_key.starts_with(&it.pub_key_prefix)) {
943                let (pk, sig) = signer.sign(&body_bytes);
944                signatures.push(services::SignaturePair {
945                    pub_key_prefix: pk.to_bytes_raw(),
946                    signature: Some(match pk.kind() {
947                        crate::key::KeyKind::Ed25519 => {
948                            services::signature_pair::Signature::Ed25519(sig)
949                        }
950                        crate::key::KeyKind::Ecdsa => {
951                            services::signature_pair::Signature::EcdsaSecp256k1(sig)
952                        }
953                    }),
954                });
955            }
956        }
957
958        let signed_transaction = services::SignedTransaction {
959            body_bytes,
960            sig_map: Some(services::SignatureMap { sig_pair: signatures.clone() }),
961            use_serialized_tx_message_hash_algorithm: false,
962        };
963        services::Transaction {
964            signed_transaction_bytes: signed_transaction.encode_to_vec(),
965            body: None,
966            sigs: None,
967            body_bytes: signed_transaction.body_bytes,
968            sig_map: Some(services::SignatureMap { sig_pair: signatures.clone() }),
969        }
970    }
971}
972
973impl<D> Transaction<D>
974where
975    D: TransactionData,
976{
977    /// Returns the maximum allowed transaction fee if none is specified.
978    ///
979    /// Specifically, this default will be used in the following case:
980    /// - The transaction itself (direct user input) has no `max_transaction_fee` specified, AND
981    /// - The [`Client`](Client) has no `max_transaction_fee` specified.
982    ///
983    /// Currently this is (but not guaranteed to be) `2 ℏ` for most transaction types.
984    pub fn default_max_transaction_fee(&self) -> Hbar {
985        self.data().default_max_transaction_fee()
986    }
987}
988
989impl<D> Transaction<D>
990where
991    D: TransactionExecute,
992{
993    /// Get the estimated transaction cost for this transaction.
994    pub async fn get_cost(&self, client: &Client) -> crate::Result<Hbar> {
995        let result = CostTransaction::from_transaction(self).execute(client).await;
996
997        match result {
998            Ok(response) => {
999                // unexpected response from Hiero, expecting to receive an `InsufficientTxFee` but received `Ok`
1000                return Err(Error::TransactionPreCheckStatus {
1001                    cost: None,
1002                    status: services::ResponseCodeEnum::Ok,
1003                    transaction_id: Box::new(response.transaction_id),
1004                });
1005            }
1006
1007            Err(Error::TransactionPreCheckStatus { status, cost: Some(cost), .. })
1008                if status == services::ResponseCodeEnum::InsufficientTxFee =>
1009            {
1010                return Ok(cost);
1011            }
1012
1013            Err(error) => Err(error),
1014        }
1015    }
1016
1017    /// Estimate the fee for this transaction using the mirror node REST API (HIP-1261).
1018    ///
1019    /// The transaction will be automatically frozen with the given client if not already frozen.
1020    ///
1021    /// # Errors
1022    /// - If the transaction cannot be frozen or serialized.
1023    /// - If the mirror node returns an error.
1024    pub async fn estimate_fee(
1025        &mut self,
1026        client: &Client,
1027    ) -> crate::Result<crate::FeeEstimateResponse> {
1028        crate::FeeEstimateQuery::new().set_transaction(self, client)?.execute(client).await
1029    }
1030
1031    /// Execute this transaction against the provided client of the Hiero network.
1032    pub async fn execute(&mut self, client: &Client) -> crate::Result<TransactionResponse> {
1033        self.execute_with_optional_timeout(client, None).await
1034    }
1035
1036    pub(crate) async fn execute_with_optional_timeout(
1037        &mut self,
1038        client: &Client,
1039        timeout: Option<std::time::Duration>,
1040    ) -> crate::Result<TransactionResponse> {
1041        // it's fine to call freeze while already frozen, so, let `freeze_with` handle the freeze check.
1042        self.freeze_with(Some(client))?;
1043
1044        if let Some(sources) = self.sources() {
1045            // Check if sources are "empty" (no transaction IDs and no node IDs)
1046            let has_transaction_ids =
1047                sources.chunks().any(|chunk| chunk.transaction_id().is_some());
1048            let has_node_ids = !sources.node_ids().is_empty();
1049
1050            if has_transaction_ids || has_node_ids {
1051                // Sources have useful data, use them
1052                return self::execute::SourceTransaction::new(self, sources)
1053                    .execute(client, timeout)
1054                    .await;
1055            } else {
1056                // Sources are empty, clear them and use regular execution
1057                self.sources = None;
1058            }
1059        }
1060
1061        if let Some(chunk_data) = self.data().maybe_chunk_data() {
1062            // todo: log a warning: user actually wanted `execute_all`.
1063            // instead of `panic`king we just pretend we were `execute_all` and
1064            // return the first result (*after* executing all the transactions).
1065            return self
1066                .execute_all_inner(chunk_data, client, timeout)
1067                .await
1068                .map(|mut it| it.swap_remove(0));
1069        }
1070
1071        execute(client, self, timeout).await
1072    }
1073
1074    // this is in *this* impl block rather than the `: TransactionExecuteChunked` impl block
1075    //because there's the off chance that someone calls `execute` on a Transaction that wants `execute_all`...
1076    async fn execute_all_inner(
1077        &self,
1078        chunk_data: &ChunkData,
1079        client: &Client,
1080        timeout_per_chunk: Option<std::time::Duration>,
1081    ) -> crate::Result<Vec<TransactionResponse>> {
1082        assert!(self.is_frozen());
1083
1084        let wait_for_receipts = self.data().wait_for_receipt();
1085
1086        if chunk_data.data.len() > chunk_data.max_message_len() {
1087            return Err(Error::basic_parse(format!(
1088                "Message with size {} too long for {} chunks",
1089                chunk_data.data.len(),
1090                chunk_data.max_chunks
1091            )));
1092        }
1093
1094        let used_chunks = chunk_data.used_chunks();
1095
1096        let mut responses = Vec::with_capacity(chunk_data.used_chunks());
1097
1098        let initial_transaction_id = {
1099            let resp = execute(
1100                client,
1101                &chunked::FirstChunkView { transaction: self, total_chunks: used_chunks },
1102                timeout_per_chunk,
1103            )
1104            .await?;
1105
1106            if wait_for_receipts {
1107                resp.get_receipt_query()
1108                    .execute_with_optional_timeout(client, timeout_per_chunk)
1109                    .await?;
1110            }
1111
1112            let initial_transaction_id = resp.transaction_id;
1113            responses.push(resp);
1114
1115            initial_transaction_id
1116        };
1117
1118        for chunk in 1..used_chunks {
1119            let resp = execute(
1120                client,
1121                &chunked::ChunkView {
1122                    transaction: self,
1123                    initial_transaction_id,
1124                    current_chunk: chunk,
1125                    total_chunks: used_chunks,
1126                },
1127                timeout_per_chunk,
1128            )
1129            .await?;
1130
1131            if wait_for_receipts {
1132                resp.get_receipt_query()
1133                    .execute_with_optional_timeout(client, timeout_per_chunk)
1134                    .await?;
1135            }
1136
1137            responses.push(resp);
1138        }
1139
1140        Ok(responses)
1141    }
1142
1143    /// Execute this transaction against the provided client of the Hiero network.
1144    // todo:
1145    #[allow(clippy::missing_errors_doc)]
1146    pub async fn execute_with_timeout(
1147        &mut self,
1148        client: &Client,
1149        // fixme: be consistent with `time::Duration`? Except `tokio::time` is `std::time`, and we depend on tokio.
1150        timeout: std::time::Duration,
1151    ) -> crate::Result<TransactionResponse> {
1152        self.execute_with_optional_timeout(client, Some(timeout)).await
1153    }
1154}
1155
1156impl<D> Transaction<D>
1157where
1158    D: TransactionExecuteChunked,
1159{
1160    /// Execute all transactions against the provided client of the Hiero network.
1161    pub async fn execute_all(
1162        &mut self,
1163        client: &Client,
1164    ) -> crate::Result<Vec<TransactionResponse>> {
1165        self.execute_all_with_optional_timeout(client, None).await
1166    }
1167
1168    pub(crate) async fn execute_all_with_optional_timeout(
1169        &mut self,
1170        client: &Client,
1171        timeout_per_chunk: Option<std::time::Duration>,
1172    ) -> crate::Result<Vec<TransactionResponse>> {
1173        // it's fine to call freeze while already frozen, so, let `freeze_with` handle the freeze check.
1174        self.freeze_with(Some(client))?;
1175
1176        // fixme: dedup this with `execute_with_optional_timeout`
1177        if let Some(sources) = self.sources() {
1178            // Check if sources are "empty" (no transaction IDs and no node IDs)
1179            let has_transaction_ids =
1180                sources.chunks().any(|chunk| chunk.transaction_id().is_some());
1181            let has_node_ids = !sources.node_ids().is_empty();
1182
1183            if has_transaction_ids || has_node_ids {
1184                // Sources have useful data, use them
1185                return self::execute::SourceTransaction::new(self, sources)
1186                    .execute_all(client, timeout_per_chunk)
1187                    .await;
1188            } else {
1189                // Sources are empty, clear them and use regular execution
1190                self.sources = None;
1191            }
1192        }
1193
1194        // sorry for the mess: this can technically infinite loop
1195        // (it won't, the loop condition would be dependent on chunk_data somehow being `Some` and `None` at the same time).
1196        let Some(chunk_data) = self.data().maybe_chunk_data() else {
1197            return Ok(Vec::from([self
1198                .execute_with_optional_timeout(client, timeout_per_chunk)
1199                .await?]));
1200        };
1201
1202        self.execute_all_inner(chunk_data, client, timeout_per_chunk).await
1203    }
1204}
1205
1206// these impls are on `AnyTransaction`, but they're here instead of in `any` because actually implementing them is only possible here.
1207impl AnyTransaction {
1208    /// # Examples
1209    /// ```
1210    /// # fn main() -> hiero_sdk::Result<()> {
1211    /// use hiero_sdk::AnyTransaction;
1212    /// let bytes = hex::decode(concat!(
1213    ///     "0a522a500a4c0a120a0c0885c8879e0610a8bdd9840312021865120218061880",
1214    ///     "94ebdc0322020877320c686920686173686772617068721a0a180a0a0a021802",
1215    ///     "108088debe010a0a0a02186510ff87debe0112000a522a500a4c0a120a0c0885",
1216    ///     "c8879e0610a8bdd984031202186512021807188094ebdc0322020877320c6869",
1217    ///     "20686173686772617068721a0a180a0a0a021802108088debe010a0a0a021865",
1218    ///     "10ff87debe011200"
1219    /// )).unwrap();
1220    /// let tx = AnyTransaction::from_bytes(&bytes)?;
1221    /// # let _ = tx;
1222    /// # Ok(())
1223    /// # }
1224    /// ```
1225    /// # Errors
1226    /// - [`Error::FromProtobuf`] if a valid transaction cannot be parsed from the bytes.
1227    #[allow(deprecated)]
1228    pub fn from_bytes(bytes: &[u8]) -> crate::Result<Self> {
1229        let list: hiero_sdk_proto::sdk::TransactionList =
1230            hiero_sdk_proto::sdk::TransactionList::decode(bytes).map_err(Error::from_protobuf)?;
1231
1232        let list = if list.transaction_list.is_empty() {
1233            Vec::from([services::Transaction::decode(bytes).map_err(Error::from_protobuf)?])
1234        } else {
1235            list.transaction_list
1236        };
1237
1238        let sources = TransactionSources::new(list)?;
1239
1240        let transaction_bodies: Result<Vec<_>, _> = if !sources.signed_transactions().is_empty() {
1241            sources
1242                .signed_transactions()
1243                .iter()
1244                .map(|transaction| {
1245                    services::TransactionBody::decode(&*transaction.body_bytes)
1246                        .map_err(Error::from_protobuf)
1247                })
1248                .collect()
1249        } else {
1250            sources
1251                .transactions()
1252                .iter()
1253                .map(|transaction| {
1254                    services::TransactionBody::decode(&*transaction.body_bytes)
1255                        .map_err(Error::from_protobuf)
1256                })
1257                .collect()
1258        };
1259
1260        let transaction_bodies = transaction_bodies?;
1261        {
1262            let (first, transaction_bodies) = transaction_bodies
1263                .split_first()
1264                .ok_or_else(|| Error::from_protobuf("no transactions found"))?;
1265
1266            for it in transaction_bodies {
1267                if !pb_transaction_body_eq(first, it) {
1268                    return Err(Error::from_protobuf("transaction parts unexpectedly unequal"));
1269                }
1270            }
1271        }
1272
1273        // todo: reuse work
1274        let transaction_data = {
1275            let data: Result<_, _> = sources
1276                .chunks()
1277                .map(|it| {
1278                    if it.transactions().first().unwrap().body_bytes.len() == 0 {
1279                        services::TransactionBody::decode(
1280                            &*it.signed_transactions().first().unwrap().body_bytes,
1281                        )
1282                    } else {
1283                        services::TransactionBody::decode(
1284                            &*it.transactions().first().unwrap().body_bytes,
1285                        )
1286                    }
1287                    .map_err(Error::from_protobuf)
1288                    .and_then(|pb| pb_getf!(pb, data))
1289                })
1290                .collect();
1291
1292            data?
1293        };
1294
1295        let mut res = Self::from_protobuf(transaction_bodies[0].clone(), transaction_data)?;
1296
1297        // note: this doesn't check freeze for obvious reasons.
1298
1299        let node_ids = sources.node_ids().to_vec();
1300
1301        res.body.node_account_ids = if node_ids.is_empty() { None } else { Some(node_ids) };
1302        res.sources = Some(sources);
1303
1304        Ok(res)
1305    }
1306}
1307
1308/// Returns `true` if lhs == rhs other than `transaction_id` and `node_account_id`, `false` otherwise.
1309#[allow(deprecated)]
1310fn pb_transaction_body_eq(
1311    lhs: &services::TransactionBody,
1312    rhs: &services::TransactionBody,
1313) -> bool {
1314    // destructure one side to ensure we don't miss any fields.
1315    let services::TransactionBody {
1316        transaction_id: _,
1317        node_account_id: _,
1318        transaction_fee,
1319        transaction_valid_duration,
1320        generate_record,
1321        memo,
1322        data,
1323        max_custom_fees,
1324        batch_key: _,
1325        high_volume: _,
1326    } = rhs;
1327
1328    if &lhs.transaction_fee != transaction_fee {
1329        return false;
1330    }
1331
1332    if &lhs.transaction_valid_duration != transaction_valid_duration {
1333        return false;
1334    }
1335
1336    if &lhs.generate_record != generate_record {
1337        return false;
1338    }
1339
1340    if &lhs.memo != memo {
1341        return false;
1342    }
1343
1344    if &lhs.max_custom_fees != max_custom_fees {
1345        return false;
1346    }
1347
1348    match (&lhs.data, data) {
1349        (None, None) => {}
1350        (Some(lhs), Some(rhs)) => match (lhs, rhs) {
1351            (
1352                services::transaction_body::Data::ConsensusSubmitMessage(lhs),
1353                services::transaction_body::Data::ConsensusSubmitMessage(rhs),
1354            ) => {
1355                let services::ConsensusSubmitMessageTransactionBody {
1356                    topic_id,
1357                    message: _,
1358                    chunk_info,
1359                } = rhs;
1360
1361                if &lhs.topic_id != topic_id {
1362                    return false;
1363                }
1364
1365                match (lhs.chunk_info.as_ref(), chunk_info.as_ref()) {
1366                    (None, None) => {}
1367                    (Some(lhs), Some(rhs)) => {
1368                        let services::ConsensusMessageChunkInfo {
1369                            initial_transaction_id,
1370                            total,
1371                            number: _,
1372                        } = rhs;
1373
1374                        if &lhs.initial_transaction_id != initial_transaction_id {
1375                            return false;
1376                        }
1377
1378                        if &lhs.total != total {
1379                            return false;
1380                        }
1381                    }
1382                    (Some(_), None) | (None, Some(_)) => return false,
1383                }
1384            }
1385            (
1386                services::transaction_body::Data::FileAppend(lhs),
1387                services::transaction_body::Data::FileAppend(rhs),
1388            ) => {
1389                let services::FileAppendTransactionBody { file_id, contents: _ } = rhs;
1390
1391                if &lhs.file_id != file_id {
1392                    return false;
1393                }
1394            }
1395            (_, _) if lhs != rhs => return false,
1396            _ => {}
1397        },
1398        (Some(_), None) | (None, Some(_)) => return false,
1399    }
1400
1401    true
1402}
1403
1404// note: This impl is why this has to be a trait (overlapping impls if `D == U` with TryFrom).
1405impl<D, U> DowncastOwned<Transaction<U>> for Transaction<D>
1406where
1407    D: DowncastOwned<U>,
1408{
1409    fn downcast_owned(self) -> Result<Transaction<U>, Self> {
1410        let Self { body, signers, sources, grpc_deadline, request_timeout } = self;
1411        let TransactionBody {
1412            data,
1413            node_account_ids,
1414            transaction_valid_duration,
1415            max_transaction_fee,
1416            transaction_memo,
1417            transaction_id,
1418            operator,
1419            is_frozen,
1420            regenerate_transaction_id,
1421            custom_fee_limits,
1422            batch_key,
1423            high_volume,
1424        } = body;
1425
1426        // not a `map().map_err()` because ownership.
1427        match data.downcast_owned() {
1428            Ok(data) => Ok(Transaction {
1429                body: TransactionBody {
1430                    data,
1431                    node_account_ids,
1432                    transaction_valid_duration,
1433                    max_transaction_fee,
1434                    transaction_memo,
1435                    transaction_id,
1436                    operator,
1437                    is_frozen,
1438                    regenerate_transaction_id,
1439                    custom_fee_limits,
1440                    batch_key,
1441                    high_volume,
1442                },
1443                signers,
1444                sources,
1445                grpc_deadline,
1446                request_timeout,
1447            }),
1448
1449            Err(data) => Err(Self {
1450                body: TransactionBody {
1451                    data,
1452                    node_account_ids,
1453                    transaction_valid_duration,
1454                    max_transaction_fee,
1455                    transaction_memo,
1456                    transaction_id,
1457                    operator,
1458                    is_frozen,
1459                    regenerate_transaction_id,
1460                    custom_fee_limits,
1461                    batch_key: batch_key.clone(),
1462                    high_volume,
1463                },
1464                signers,
1465                sources,
1466                grpc_deadline,
1467                request_timeout,
1468            }),
1469        }
1470    }
1471}
1472
1473#[cfg(test)]
1474pub(crate) mod test_helpers {
1475    use hiero_sdk_proto::services;
1476    use prost::Message;
1477    use time::{
1478        Duration,
1479        OffsetDateTime,
1480    };
1481
1482    use super::TransactionExecute;
1483    use crate::protobuf::ToProtobuf;
1484    use crate::{
1485        AccountId,
1486        Hbar,
1487        NftId,
1488        PrivateKey,
1489        TokenId,
1490        Transaction,
1491        TransactionId,
1492    };
1493
1494    impl<D: Default> Transaction<D> {
1495        // todo: bikeshed name, idc.
1496        /// Creates a transaction with some fields set to regular values to make serialization more regular.
1497        ///
1498        /// Currently the fields set are `node_account_ids`, `transaction_id`, and `max_transaction_fee`.
1499        /// Additionally the transaction is signed with 1 key.
1500        pub(crate) fn new_for_tests() -> Self {
1501            let mut tx = Self::new();
1502
1503            tx.node_account_ids(TEST_NODE_ACCOUNT_IDS)
1504                .transaction_id(TEST_TX_ID)
1505                .max_transaction_fee(Hbar::new(2))
1506                .sign(unused_private_key());
1507
1508            tx
1509        }
1510    }
1511
1512    #[track_caller]
1513    pub(crate) fn transaction_body<D: TransactionExecute>(
1514        tx: Transaction<D>,
1515    ) -> services::TransactionBody {
1516        // if you're thinking "ghee, that sure is a silly way to get a transaction body" you aren't wrong.
1517        services::TransactionBody::decode(&*tx.make_sources().unwrap().transactions()[0].body_bytes)
1518            .unwrap()
1519    }
1520
1521    #[track_caller]
1522    pub(crate) fn transaction_bodies<D: TransactionExecute>(
1523        tx: Transaction<D>,
1524    ) -> Vec<services::TransactionBody> {
1525        tx.make_sources()
1526            .unwrap()
1527            .transactions()
1528            .iter()
1529            .map(|it| services::TransactionBody::decode(&*it.body_bytes).unwrap())
1530            .collect()
1531    }
1532
1533    /// Checks the entire traknsaction body *other than* `data` and returns that.
1534    ///
1535    /// This is basically a boilerplate reducer, however, it failing means that [`Transaction::new_for_tests`] is probably buggy.
1536    pub(crate) fn check_body(body: services::TransactionBody) -> services::transaction_body::Data {
1537        #[allow(deprecated)]
1538        let services::TransactionBody {
1539            transaction_id,
1540            node_account_id,
1541            transaction_fee,
1542            transaction_valid_duration,
1543            generate_record,
1544            memo,
1545            data,
1546            max_custom_fees,
1547            batch_key: _,
1548            high_volume: _,
1549        } = body;
1550
1551        assert_eq!(transaction_id, Some(TEST_TX_ID.to_protobuf()));
1552
1553        assert_eq!(transaction_fee, Hbar::new(2).to_tinybars() as u64);
1554        assert_eq!(transaction_valid_duration, Some(services::Duration { seconds: 120 }));
1555        assert_eq!(generate_record, false);
1556        assert_eq!(memo, "");
1557        assert_eq!(max_custom_fees, vec![]);
1558        data.unwrap()
1559    }
1560
1561    pub(crate) fn unused_private_key() -> PrivateKey {
1562        "302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c97732394482538e10".parse().unwrap()
1563    }
1564
1565    pub(crate) const TEST_TOKEN_ID: TokenId = TokenId::new(1, 2, 3);
1566
1567    pub(crate) const TEST_TOKEN_IDS: [TokenId; 3] =
1568        [TokenId::new(1, 2, 3), TokenId::new(2, 3, 4), TokenId::new(3, 4, 5)];
1569
1570    pub(crate) const TEST_NFT_IDS: [NftId; 3] = [
1571        NftId { token_id: TokenId::new(4, 2, 3), serial: 1 },
1572        NftId { token_id: TokenId::new(4, 2, 4), serial: 2 },
1573        NftId { token_id: TokenId::new(4, 2, 5), serial: 3 },
1574    ];
1575
1576    pub(crate) const TEST_ACCOUNT_ID: AccountId = AccountId::new(0, 0, 5006);
1577
1578    pub(crate) const TEST_NODE_ACCOUNT_IDS: [AccountId; 2] =
1579        [AccountId::new(0, 0, 5005), AccountId::new(0, 0, 5006)];
1580
1581    pub(crate) const TEST_TX_ID: TransactionId = TransactionId {
1582        account_id: TEST_ACCOUNT_ID,
1583        valid_start: VALID_START,
1584        nonce: None,
1585        scheduled: false,
1586    };
1587
1588    pub(crate) const VALID_START: OffsetDateTime =
1589        OffsetDateTime::UNIX_EPOCH.saturating_add(Duration::seconds(1554158542));
1590}