1use hiero_sdk_proto::services;
4use hiero_sdk_proto::services::util_service_client::UtilServiceClient;
5use prost::Message;
6use tonic::transport::Channel;
7
8use crate::ledger_id::RefLedgerId;
9use crate::protobuf::FromProtobuf;
10use crate::transaction::{
11 AnyTransactionData,
12 ChunkInfo,
13 ToTransactionDataProtobuf,
14 TransactionData,
15 TransactionExecute,
16};
17use crate::{
18 AnyTransaction,
19 BoxGrpcFuture,
20 Error,
21 Hbar,
22 Transaction,
23 TransactionId,
24 ValidateChecksums,
25};
26
27pub type BatchTransaction = Transaction<BatchTransactionData>;
76
77#[derive(Debug, Clone, Default)]
78pub struct BatchTransactionData {
79 inner_transactions: Vec<AnyTransaction>,
80}
81
82impl BatchTransaction {
83 pub fn add_inner_transaction(
99 &mut self,
100 transaction: AnyTransaction,
101 ) -> crate::Result<&mut Self> {
102 self.require_not_frozen();
103 self.validate_inner_transaction(&transaction)?;
104 self.data_mut().inner_transactions.push(transaction);
105 Ok(self)
106 }
107
108 pub fn set_inner_transactions(
124 &mut self,
125 transactions: Vec<AnyTransaction>,
126 ) -> crate::Result<&mut Self> {
127 self.require_not_frozen();
128
129 for transaction in &transactions {
131 self.validate_inner_transaction(transaction)?;
132 }
133
134 self.data_mut().inner_transactions = transactions;
135 Ok(self)
136 }
137
138 pub fn get_inner_transactions(&self) -> &[AnyTransaction] {
140 &self.data().inner_transactions
141 }
142
143 pub fn get_inner_transaction_ids(&self) -> Vec<Option<TransactionId>> {
153 self.data().inner_transactions.iter().map(|tx| tx.get_transaction_id()).collect()
154 }
155
156 fn validate_inner_transaction(&self, transaction: &AnyTransaction) -> crate::Result<()> {
163 match transaction.data() {
165 AnyTransactionData::Freeze(_) => {
166 return Err(Error::basic_parse(
167 "Transaction type FreezeTransaction is not allowed in a batch transaction",
168 ));
169 }
170 AnyTransactionData::Batch(_) => {
171 return Err(Error::basic_parse(
172 "Transaction type BatchTransaction is not allowed in a batch transaction",
173 ));
174 }
175 _ => {}
176 }
177
178 if !transaction.is_frozen() {
180 return Err(Error::basic_parse("Inner transaction should be frozen"));
181 }
182
183 if transaction.get_batch_key().is_none() {
185 return Err(Error::basic_parse("Batch key needs to be set"));
186 }
187
188 Ok(())
189 }
190}
191
192impl TransactionData for BatchTransactionData {
193 fn default_max_transaction_fee(&self) -> Hbar {
194 Hbar::new(2)
195 }
196}
197
198impl ToTransactionDataProtobuf for BatchTransactionData {
199 fn to_transaction_data_protobuf(
200 &self,
201 _chunk_info: &ChunkInfo,
202 ) -> services::transaction_body::Data {
203 let mut builder = services::AtomicBatchTransactionBody::default();
204
205 for transaction in &self.inner_transactions {
206 let signed_transaction_bytes = transaction
209 .to_signed_transaction_bytes()
210 .expect("Inner transaction should be frozen and serializable");
211
212 builder.transactions.push(signed_transaction_bytes);
213 }
214
215 services::transaction_body::Data::AtomicBatch(builder)
216 }
217}
218
219impl TransactionExecute for BatchTransactionData {
220 fn execute(
221 &self,
222 channel: Channel,
223 request: services::Transaction,
224 ) -> BoxGrpcFuture<'_, services::TransactionResponse> {
225 Box::pin(async move { UtilServiceClient::new(channel).atomic_batch(request).await })
226 }
227}
228
229impl ValidateChecksums for BatchTransactionData {
230 fn validate_checksums(&self, ledger_id: &RefLedgerId) -> Result<(), Error> {
231 for transaction in &self.inner_transactions {
232 transaction.validate_checksums(ledger_id)?;
233 }
234 Ok(())
235 }
236}
237
238impl FromProtobuf<services::AtomicBatchTransactionBody> for BatchTransactionData {
239 fn from_protobuf(pb: services::AtomicBatchTransactionBody) -> crate::Result<Self> {
240 let mut inner_transactions = Vec::new();
241
242 for signed_transaction_bytes in pb.transactions {
243 let proto_transaction =
245 services::Transaction { signed_transaction_bytes, ..Default::default() };
246
247 let transaction = AnyTransaction::from_bytes(&proto_transaction.encode_to_vec())?;
248 inner_transactions.push(transaction);
249 }
250
251 Ok(Self { inner_transactions })
252 }
253}
254
255impl From<BatchTransactionData> for AnyTransactionData {
256 fn from(value: BatchTransactionData) -> Self {
257 Self::Batch(value)
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use std::str::FromStr;
264
265 use super::*;
266 use crate::account::AccountCreateTransactionData;
267 use crate::{
268 AccountCreateTransaction,
269 AccountId,
270 Client,
271 FreezeTransaction,
272 Hbar,
273 PrivateKey,
274 Transaction,
275 TransactionId,
276 TransferTransaction,
277 };
278
279 fn create_test_client() -> Client {
280 Client::for_testnet()
281 }
282
283 fn create_test_operator_key() -> PrivateKey {
284 PrivateKey::from_str(
285 "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"
286 ).unwrap()
287 }
288
289 fn create_valid_inner_transaction() -> crate::Result<Transaction<AccountCreateTransactionData>>
290 {
291 let client = create_test_client();
292 let operator_key = create_test_operator_key();
293 let operator_id = AccountId::new(0, 0, 2);
294 client.set_operator(operator_id, operator_key.clone());
295
296 let account_key = PrivateKey::generate_ed25519();
297
298 let mut transaction = AccountCreateTransaction::new();
299 transaction.set_key_without_alias(account_key.public_key()).initial_balance(Hbar::new(1));
300
301 transaction.batchify(&client, operator_key.public_key().into())?;
302 Ok(transaction)
303 }
304
305 fn create_unfrozen_transaction() -> Transaction<AccountCreateTransactionData> {
306 let account_key = PrivateKey::generate_ed25519();
307
308 let mut transaction = AccountCreateTransaction::new();
309 transaction.set_key_without_alias(account_key.public_key()).initial_balance(Hbar::new(1));
310
311 transaction
312 }
313
314 fn create_frozen_no_batch_key_transaction(
315 ) -> crate::Result<Transaction<AccountCreateTransactionData>> {
316 let client = create_test_client();
317 let operator_key = create_test_operator_key();
318 let operator_id = AccountId::new(0, 0, 2);
319 client.set_operator(operator_id, operator_key);
320
321 let account_key = PrivateKey::generate_ed25519();
322
323 let mut transaction = AccountCreateTransaction::new();
324 transaction.set_key_without_alias(account_key.public_key()).initial_balance(Hbar::new(1));
325
326 transaction.freeze_with(&client)?;
327 Ok(transaction)
328 }
329
330 fn create_blacklisted_transaction(
331 ) -> crate::Result<Transaction<crate::system::FreezeTransactionData>> {
332 let client = create_test_client();
333 let operator_key = create_test_operator_key();
334 let operator_id = AccountId::new(0, 0, 2);
335 client.set_operator(operator_id, operator_key.clone());
336
337 let mut transaction = FreezeTransaction::new();
338 transaction.freeze_type(crate::FreezeType::FreezeOnly);
339
340 transaction.batchify(&client, operator_key.public_key().into())?;
341 Ok(transaction)
342 }
343
344 #[test]
345 fn test_new_batch_transaction() {
346 let batch = BatchTransaction::new();
347 assert_eq!(batch.get_inner_transactions().len(), 0);
348 assert_eq!(batch.get_inner_transaction_ids().len(), 0);
349 }
350
351 #[tokio::test]
352 async fn test_add_valid_inner_transaction() -> crate::Result<()> {
353 let mut batch = BatchTransaction::new();
354 let inner_transaction = create_valid_inner_transaction()?;
355
356 let result = batch.add_inner_transaction(inner_transaction.into());
357 assert!(result.is_ok());
358 assert_eq!(batch.get_inner_transactions().len(), 1);
359 assert_eq!(batch.get_inner_transaction_ids().len(), 1);
360
361 Ok(())
362 }
363
364 #[tokio::test]
365 async fn test_add_multiple_inner_transactions() -> crate::Result<()> {
366 let mut batch = BatchTransaction::new();
367
368 let inner1 = create_valid_inner_transaction()?;
370 batch.add_inner_transaction(inner1.into())?;
371
372 let inner2 = create_valid_inner_transaction()?;
374 batch.add_inner_transaction(inner2.into())?;
375
376 assert_eq!(batch.get_inner_transactions().len(), 2);
377 assert_eq!(batch.get_inner_transaction_ids().len(), 2);
378
379 Ok(())
380 }
381
382 #[test]
383 fn test_add_unfrozen_transaction_fails() {
384 let mut batch = BatchTransaction::new();
385 let unfrozen_transaction = create_unfrozen_transaction();
386
387 let result = batch.add_inner_transaction(unfrozen_transaction.into());
388 assert!(result.is_err());
389 assert!(result.unwrap_err().to_string().contains("frozen"));
390 }
391
392 #[tokio::test]
393 async fn test_add_transaction_without_batch_key_fails() -> crate::Result<()> {
394 let mut batch = BatchTransaction::new();
395 let transaction = create_frozen_no_batch_key_transaction()?;
396
397 let result = batch.add_inner_transaction(transaction.into());
398 assert!(result.is_err());
399 let error_msg = result.unwrap_err().to_string();
400 assert!(error_msg.contains("batch key") || error_msg.contains("needs to be set"));
401
402 Ok(())
403 }
404
405 #[tokio::test]
406 async fn test_add_blacklisted_transaction_fails() -> crate::Result<()> {
407 let mut batch = BatchTransaction::new();
408 let blacklisted_transaction = create_blacklisted_transaction()?;
409
410 let result = batch.add_inner_transaction(blacklisted_transaction.into());
411 assert!(result.is_err());
412 assert!(result.unwrap_err().to_string().contains("FreezeTransaction"));
413
414 Ok(())
415 }
416
417 #[test]
418 fn test_add_batch_transaction_to_batch_fails() -> crate::Result<()> {
419 let mut batch = BatchTransaction::new();
420 let inner_batch = BatchTransaction::new();
421
422 let result = batch.add_inner_transaction(inner_batch.into());
423 assert!(result.is_err());
424 assert!(result.unwrap_err().to_string().contains("BatchTransaction"));
425
426 Ok(())
427 }
428
429 #[tokio::test]
430 async fn test_set_inner_transactions() -> crate::Result<()> {
431 let mut batch = BatchTransaction::new();
432
433 let inner1 = create_valid_inner_transaction()?;
434 let inner2 = create_valid_inner_transaction()?;
435
436 let transactions = vec![inner1.into(), inner2.into()];
437 let result = batch.set_inner_transactions(transactions);
438
439 assert!(result.is_ok());
440 assert_eq!(batch.get_inner_transactions().len(), 2);
441
442 Ok(())
443 }
444
445 #[tokio::test]
446 async fn test_set_inner_transactions_with_invalid_transaction_fails() -> crate::Result<()> {
447 let mut batch = BatchTransaction::new();
448
449 let valid_transaction = create_valid_inner_transaction()?;
450 let invalid_transaction = create_unfrozen_transaction();
451
452 let transactions = vec![valid_transaction.into(), invalid_transaction.into()];
453 let result = batch.set_inner_transactions(transactions);
454
455 assert!(result.is_err());
456 assert!(result.unwrap_err().to_string().contains("frozen"));
457
458 Ok(())
459 }
460
461 #[tokio::test]
462 async fn test_set_inner_transactions_replaces_existing() -> crate::Result<()> {
463 let mut batch = BatchTransaction::new();
464
465 let initial_transaction = create_valid_inner_transaction()?;
467 batch.add_inner_transaction(initial_transaction.into())?;
468 assert_eq!(batch.get_inner_transactions().len(), 1);
469
470 let new1 = create_valid_inner_transaction()?;
472 let new2 = create_valid_inner_transaction()?;
473 let new_transactions = vec![new1.into(), new2.into()];
474
475 batch.set_inner_transactions(new_transactions)?;
476 assert_eq!(batch.get_inner_transactions().len(), 2);
477
478 Ok(())
479 }
480
481 #[tokio::test]
482 async fn test_get_inner_transaction_ids() -> crate::Result<()> {
483 let mut batch = BatchTransaction::new();
484
485 let inner1 = create_valid_inner_transaction()?;
486 let inner2 = create_valid_inner_transaction()?;
487
488 batch.add_inner_transaction(inner1.into())?;
489 batch.add_inner_transaction(inner2.into())?;
490
491 let transaction_ids = batch.get_inner_transaction_ids();
492 assert_eq!(transaction_ids.len(), 2);
493
494 for tx_id in transaction_ids {
496 if let Some(tx_id) = tx_id {
497 assert!(
499 tx_id.account_id.num > 0
500 || tx_id.account_id.alias.is_some()
501 || tx_id.account_id.evm_address.is_some()
502 );
503 assert!(tx_id.valid_start.unix_timestamp() > 0);
504 }
505 }
506
507 Ok(())
508 }
509
510 #[test]
511 fn test_empty_batch_has_no_transactions() {
512 let batch = BatchTransaction::new();
513 assert!(batch.get_inner_transactions().is_empty());
514 assert!(batch.get_inner_transaction_ids().is_empty());
515 }
516
517 #[test]
518 fn test_default_max_transaction_fee() {
519 let batch_data = BatchTransactionData::default();
520 let default_fee = batch_data.default_max_transaction_fee();
521 assert!(default_fee > Hbar::from_tinybars(0));
523 }
524
525 #[test]
526 fn test_transaction_data_trait_implementation() {
527 let batch_data = BatchTransactionData::default();
528
529 assert!(batch_data.default_max_transaction_fee() > Hbar::from_tinybars(0));
531 }
533
534 #[tokio::test]
535 async fn test_validate_checksums() -> crate::Result<()> {
536 use crate::ledger_id::RefLedgerId;
537
538 let mut batch = BatchTransaction::new();
539 let inner_transaction = create_valid_inner_transaction()?;
540 batch.add_inner_transaction(inner_transaction.into())?;
541
542 let result = batch.data().validate_checksums(&RefLedgerId::TESTNET);
544 assert!(result.is_ok());
545
546 Ok(())
547 }
548
549 #[tokio::test]
550 async fn test_to_transaction_data_protobuf() -> crate::Result<()> {
551 let mut batch = BatchTransaction::new();
552 let inner_transaction = create_valid_inner_transaction()?;
553 batch.add_inner_transaction(inner_transaction.into())?;
554
555 let chunk_info = crate::transaction::ChunkInfo::single(
556 TransactionId::generate(AccountId::new(0, 0, 2)),
557 AccountId::new(0, 0, 3),
558 );
559 let protobuf_data = batch.data().to_transaction_data_protobuf(&chunk_info);
560
561 match protobuf_data {
563 hiero_sdk_proto::services::transaction_body::Data::AtomicBatch(atomic_batch) => {
564 assert_eq!(atomic_batch.transactions.len(), 1);
565 assert!(!atomic_batch.transactions[0].is_empty());
566 }
567 _ => panic!("Expected AtomicBatch variant"),
568 }
569
570 Ok(())
571 }
572
573 #[tokio::test]
574 async fn test_from_protobuf_roundtrip() -> crate::Result<()> {
575 let mut original_batch = BatchTransaction::new();
576 let inner_transaction = create_valid_inner_transaction()?;
577 original_batch.add_inner_transaction(inner_transaction.into())?;
578
579 let chunk_info = crate::transaction::ChunkInfo::single(
581 TransactionId::generate(AccountId::new(0, 0, 2)),
582 AccountId::new(0, 0, 3),
583 );
584 let protobuf_data = original_batch.data().to_transaction_data_protobuf(&chunk_info);
585
586 let atomic_batch = match protobuf_data {
588 hiero_sdk_proto::services::transaction_body::Data::AtomicBatch(atomic_batch) => {
589 atomic_batch
590 }
591 _ => panic!("Expected AtomicBatch variant"),
592 };
593
594 let reconstructed_data = BatchTransactionData::from_protobuf(atomic_batch)?;
596
597 assert_eq!(
599 reconstructed_data.inner_transactions.len(),
600 original_batch.data().inner_transactions.len()
601 );
602
603 Ok(())
604 }
605
606 #[test]
607 fn test_empty_batch_protobuf() {
608 let empty_batch = BatchTransaction::new();
609 let chunk_info = crate::transaction::ChunkInfo::single(
610 TransactionId::generate(AccountId::new(0, 0, 2)),
611 AccountId::new(0, 0, 3),
612 );
613 let protobuf_data = empty_batch.data().to_transaction_data_protobuf(&chunk_info);
614
615 match protobuf_data {
616 hiero_sdk_proto::services::transaction_body::Data::AtomicBatch(atomic_batch) => {
617 assert!(atomic_batch.transactions.is_empty());
618 }
619 _ => panic!("Expected AtomicBatch variant"),
620 }
621 }
622
623 #[tokio::test]
624 async fn test_large_number_of_transactions() -> crate::Result<()> {
625 let mut batch = BatchTransaction::new();
626
627 for _ in 0..10 {
629 let inner_transaction = create_valid_inner_transaction()?;
630 batch.add_inner_transaction(inner_transaction.into())?;
631 }
632
633 assert_eq!(batch.get_inner_transactions().len(), 10);
634 assert_eq!(batch.get_inner_transaction_ids().len(), 10);
635
636 Ok(())
637 }
638
639 #[test]
641 fn test_validate_non_frozen_transaction() {
642 let mut batch = BatchTransaction::new();
643 let inner_tx = TransferTransaction::new();
644
645 let result = batch.add_inner_transaction(inner_tx.into());
646 assert!(result.is_err());
647 assert!(result.unwrap_err().to_string().contains("Inner transaction should be frozen"));
648 }
649
650 #[test]
651 fn test_validate_batch_key_required() {
652 let mut batch = BatchTransaction::new();
653 let inner_tx = TransferTransaction::new();
654 let result = batch.add_inner_transaction(inner_tx.into());
658 assert!(result.is_err());
659 assert!(result.unwrap_err().to_string().contains("Inner transaction should be frozen"));
662 }
663}