1use 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#[derive(Clone)]
70pub struct Transaction<D> {
71 body: TransactionBody<D>,
72
73 signers: Vec<AnySigner>,
74
75 sources: Option<TransactionSources>,
76
77 grpc_deadline: Option<std::time::Duration>,
80
81 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 pub(crate) custom_fee_limits: Vec<CustomFeeLimit>,
110
111 pub(crate) batch_key: Option<crate::Key>,
113
114 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 #[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 #[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 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 pub(crate) fn data_mut(&mut self) -> &mut D {
221 self.require_not_frozen();
222 &mut self.body.data
223 }
224
225 #[must_use]
229 pub fn get_node_account_ids(&self) -> Option<&[AccountId]> {
230 self.body.node_account_ids.as_deref()
231 }
232
233 #[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 #[must_use]
251 pub fn get_transaction_valid_duration(&self) -> Option<Duration> {
252 self.body.transaction_valid_duration
253 }
254
255 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 #[must_use]
265 pub fn get_max_transaction_fee(&self) -> Option<Hbar> {
266 self.body.max_transaction_fee
267 }
268
269 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 #[must_use]
277 pub fn get_custom_fee_limits(&self) -> &[CustomFeeLimit] {
278 &self.body.custom_fee_limits
279 }
280
281 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 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 pub fn clear_custom_fee_limits(&mut self) -> &mut Self {
298 self.body_mut().custom_fee_limits.clear();
299 self
300 }
301
302 #[must_use]
306 pub fn get_transaction_memo(&self) -> &str {
307 &self.body.transaction_memo
308 }
309
310 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 #[must_use]
322 pub fn get_grpc_deadline(&self) -> Option<std::time::Duration> {
323 self.grpc_deadline
324 }
325
326 pub fn grpc_deadline(&mut self, deadline: std::time::Duration) -> &mut Self {
331 self.grpc_deadline = Some(deadline);
332 self
333 }
334
335 #[must_use]
339 pub fn get_request_timeout(&self) -> Option<std::time::Duration> {
340 self.request_timeout
341 }
342
343 pub fn request_timeout(&mut self, timeout: std::time::Duration) -> &mut Self {
348 self.request_timeout = Some(timeout);
349 self
350 }
351
352 #[must_use]
356 pub fn get_transaction_id(&self) -> Option<TransactionId> {
357 self.body.transaction_id
358 }
359
360 pub fn transaction_id(&mut self, id: TransactionId) -> &mut Self {
364 self.body_mut().transaction_id = Some(id);
365 self
366 }
367
368 pub fn sign(&mut self, private_key: PrivateKey) -> &mut Self {
370 self.sign_signer(AnySigner::PrivateKey(private_key))
371 }
372
373 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 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 #[must_use]
398 pub fn get_max_chunks(&self) -> usize {
399 self.data().chunk_data().max_chunks
400 }
401
402 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 pub fn get_chunk_size(&self) -> usize {
412 self.data().chunk_data().chunk_size.get()
413 }
414
415 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 pub fn get_regenerate_transaction_id(&self) -> Option<bool> {
436 self.body.regenerate_transaction_id
437 }
438
439 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 pub fn freeze(&mut self) -> crate::Result<&mut Self> {
458 self.freeze_with(None)
459 }
460
461 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 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 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 let max_transaction_fee = self.body.max_transaction_fee.or_else(|| {
507 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 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 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 #[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 #[must_use]
566 pub fn get_batch_key(&self) -> Option<&crate::Key> {
567 self.body.batch_key.as_ref()
568 }
569
570 #[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 #[must_use]
583 pub fn get_high_volume(&self) -> bool {
584 self.body.high_volume
585 }
586}
587
588impl<D: TransactionExecute> Transaction<D> {
589 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 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 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 self.node_account_ids([crate::AccountId::new(0, 0, 0)]);
630 self.sign_with_operator(client)
631 }
632 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 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 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 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 pub fn add_signature(&mut self, pk: PublicKey, signature: Vec<u8>) -> &mut Self {
717 assert!(self.is_frozen());
718
719 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 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 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 pub fn get_transaction_hash(&mut self) -> crate::Result<TransactionHash> {
800 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 pub fn get_transaction_hash_per_node(
827 &mut self,
828 ) -> crate::Result<HashMap<AccountId, TransactionHash>> {
829 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 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 transaction_list.push(self.create_transaction_for_node(None));
859 } else {
860 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![], };
878
879 if node_account_ids.is_empty() {
880 transaction_list.push(self.create_transaction_for_node(None));
882 } else {
883 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 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 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 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 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 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 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 self.freeze_with(Some(client))?;
1043
1044 if let Some(sources) = self.sources() {
1045 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 return self::execute::SourceTransaction::new(self, sources)
1053 .execute(client, timeout)
1054 .await;
1055 } else {
1056 self.sources = None;
1058 }
1059 }
1060
1061 if let Some(chunk_data) = self.data().maybe_chunk_data() {
1062 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 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 #[allow(clippy::missing_errors_doc)]
1146 pub async fn execute_with_timeout(
1147 &mut self,
1148 client: &Client,
1149 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 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 self.freeze_with(Some(client))?;
1175
1176 if let Some(sources) = self.sources() {
1178 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 return self::execute::SourceTransaction::new(self, sources)
1186 .execute_all(client, timeout_per_chunk)
1187 .await;
1188 } else {
1189 self.sources = None;
1191 }
1192 }
1193
1194 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
1206impl AnyTransaction {
1208 #[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 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 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#[allow(deprecated)]
1310fn pb_transaction_body_eq(
1311 lhs: &services::TransactionBody,
1312 rhs: &services::TransactionBody,
1313) -> bool {
1314 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
1404impl<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 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 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 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 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}