use std::str::FromStr;
use hedera::{
AccountCreateTransaction,
AccountId,
AccountInfoQuery,
BatchTransaction,
FileId,
FreezeTransaction,
FreezeType,
Hbar,
PrivateKey,
TopicCreateTransaction,
TopicMessageSubmitTransaction,
};
use time::{
Duration,
OffsetDateTime,
};
use crate::common::{
setup_nonfree,
TestEnvironment,
};
use crate::resources::BIG_CONTENTS;
#[tokio::test]
async fn can_execute_batch_transaction() -> anyhow::Result<()> {
let Some(TestEnvironment { config: _, client }) = setup_nonfree() else {
return Ok(());
};
let operator_id = AccountId::new(0, 0, 2);
let operator_key = PrivateKey::from_str(
"302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"
)?;
let account_key = PrivateKey::generate_ed25519();
let mut inner_transaction = AccountCreateTransaction::new();
inner_transaction.set_key_without_alias(account_key.public_key()).initial_balance(Hbar::new(1));
inner_transaction.batchify(&client, operator_key.public_key().into())?;
let mut batch_transaction = BatchTransaction::new();
batch_transaction.add_inner_transaction(inner_transaction.into())?;
client.set_operator(operator_id, operator_key);
let tx_response = batch_transaction.execute(&client).await?;
let _tx_receipt = tx_response.get_receipt(&client).await?;
Ok(())
}
#[tokio::test]
async fn can_execute_large_batch_transaction() -> anyhow::Result<()> {
let Some(TestEnvironment { config: _, client }) = setup_nonfree() else {
return Ok(());
};
let operator_id = AccountId::new(0, 0, 2);
let operator_key = PrivateKey::from_str(
"302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"
)?;
client.set_operator(operator_id, operator_key.clone());
let mut batch_transaction = BatchTransaction::new();
for i in 0..15 {
let account_key = PrivateKey::generate_ed25519();
let mut inner_transaction = AccountCreateTransaction::new();
inner_transaction
.set_key_without_alias(account_key.public_key())
.initial_balance(Hbar::new(1));
inner_transaction.batchify(&client, operator_key.public_key().into())?;
batch_transaction.add_inner_transaction(inner_transaction.into())?;
}
let tx_response = batch_transaction.execute(&client).await?;
let _tx_receipt = tx_response.get_receipt(&client).await?;
let inner_tx_ids = batch_transaction.get_inner_transaction_ids();
assert_eq!(inner_tx_ids.len(), 15);
Ok(())
}
#[tokio::test]
async fn cannot_execute_batch_transaction_without_inner_transactions() -> anyhow::Result<()> {
let Some(TestEnvironment { config: _, client }) = setup_nonfree() else {
return Ok(());
};
let operator_id = AccountId::new(0, 0, 2);
let operator_key = PrivateKey::from_str(
"302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"
)?;
client.set_operator(operator_id, operator_key);
let mut empty_batch = BatchTransaction::new();
let result = empty_batch.execute(&client).await;
assert!(result.is_err(), "Expected batch transaction without inner transactions to fail");
Ok(())
}
#[tokio::test]
async fn cannot_execute_batch_transaction_with_blacklisted_transaction() -> anyhow::Result<()> {
let Some(TestEnvironment { config: _, client }) = setup_nonfree() else {
return Ok(());
};
let operator_id = AccountId::new(0, 0, 2);
let operator_key = PrivateKey::from_str(
"302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"
)?;
client.set_operator(operator_id, operator_key.clone());
let mut freeze_transaction = FreezeTransaction::new();
freeze_transaction
.file_id(FileId::new(0, 0, 150)) .start_time(OffsetDateTime::now_utc() + Duration::seconds(30))
.freeze_type(FreezeType::FreezeOnly);
freeze_transaction.batchify(&client, operator_key.public_key().into())?;
let mut batch_transaction = BatchTransaction::new();
let result = batch_transaction.add_inner_transaction(freeze_transaction.into());
assert!(result.is_err(), "Expected adding blacklisted transaction to batch to fail");
Ok(())
}
#[tokio::test]
#[ignore] async fn cannot_execute_batch_transaction_with_invalid_inner_batch_key() -> anyhow::Result<()> {
let Some(TestEnvironment { config: _, client }) = setup_nonfree() else {
return Ok(());
};
let operator_id = AccountId::new(0, 0, 2);
let operator_key = PrivateKey::from_str(
"302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"
)?;
let account_key = PrivateKey::generate_ed25519();
let mut inner_transaction = AccountCreateTransaction::new();
inner_transaction.set_key_without_alias(account_key.public_key()).initial_balance(Hbar::new(1));
inner_transaction.batchify(&client, account_key.public_key().into())?;
client.set_operator(operator_id, operator_key.clone());
let mut batch_transaction = BatchTransaction::new();
batch_transaction.add_inner_transaction(inner_transaction.into())?;
let result = batch_transaction.execute(&client).await;
assert!(result.is_err(), "Expected batch transaction with invalid inner batch key to fail");
Ok(())
}
#[tokio::test]
async fn cannot_execute_batch_transaction_without_batchifying_inner() -> anyhow::Result<()> {
let Some(TestEnvironment { config: _, client }) = setup_nonfree() else {
return Ok(());
};
let operator_id = AccountId::new(0, 0, 2);
let operator_key = PrivateKey::from_str(
"302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"
)?;
let account_key = PrivateKey::generate_ed25519();
client.set_operator(operator_id, operator_key);
let mut inner_transaction = AccountCreateTransaction::new();
inner_transaction.set_key_without_alias(account_key.public_key()).initial_balance(Hbar::new(1));
let mut batch_transaction = BatchTransaction::new();
let result = batch_transaction.add_inner_transaction(inner_transaction.into());
assert!(result.is_err(), "Expected adding non-batchified transaction to batch to fail");
Ok(())
}
#[tokio::test]
async fn can_execute_batch_transaction_with_chunked_inner() -> anyhow::Result<()> {
let Some(TestEnvironment { config: _, client }) = setup_nonfree() else {
return Ok(());
};
let operator_id = AccountId::new(0, 0, 2);
let operator_key = PrivateKey::from_str(
"302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"
)?;
client.set_operator(operator_id, operator_key.clone());
let mut topic_create = TopicCreateTransaction::new();
topic_create.admin_key(operator_key.public_key()).topic_memo("testMemo");
let topic_response = topic_create.execute(&client).await?;
let topic_receipt = topic_response.get_receipt(&client).await?;
let topic_id =
topic_receipt.topic_id.ok_or_else(|| anyhow::anyhow!("Topic ID not found in receipt"))?;
let mut inner_transaction = TopicMessageSubmitTransaction::new();
inner_transaction.topic_id(topic_id).message(BIG_CONTENTS.as_bytes().to_vec());
inner_transaction.batchify(&client, operator_key.public_key().into())?;
let mut batch_transaction = BatchTransaction::new();
batch_transaction.add_inner_transaction(inner_transaction.into())?;
let tx_response = batch_transaction.execute(&client).await?;
let _tx_receipt = tx_response.get_receipt(&client).await?;
Ok(())
}
#[tokio::test]
#[ignore] async fn batch_transaction_incurs_fees_even_if_one_inner_failed() -> anyhow::Result<()> {
let Some(TestEnvironment { config: _, client }) = setup_nonfree() else {
return Ok(());
};
let operator_id = AccountId::new(0, 0, 2);
let operator_key = PrivateKey::from_str(
"302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"
)?;
client.set_operator(operator_id, operator_key.clone());
let initial_balance = {
let account_info = AccountInfoQuery::new().account_id(operator_id).execute(&client).await?;
account_info.balance
};
let account_key1 = PrivateKey::generate_ed25519();
let mut inner_transaction1 = AccountCreateTransaction::new();
inner_transaction1
.set_key_without_alias(account_key1.public_key())
.initial_balance(Hbar::new(1));
inner_transaction1.batchify(&client, operator_key.public_key().into())?;
let account_key2 = PrivateKey::generate_ed25519();
let mut inner_transaction2 = AccountCreateTransaction::new();
inner_transaction2
.set_key_without_alias(account_key2.public_key())
.initial_balance(Hbar::new(1))
.receiver_signature_required(true); inner_transaction2.batchify(&client, operator_key.public_key().into())?;
let mut batch_transaction = BatchTransaction::new();
batch_transaction
.set_inner_transactions(vec![inner_transaction1.into(), inner_transaction2.into()])?;
let tx_response = batch_transaction.execute(&client).await?;
let receipt_result = tx_response.get_receipt(&client).await;
assert!(
receipt_result.is_err(),
"Expected batch transaction receipt to fail due to receiver signature requirement"
);
let final_balance = {
let account_info = AccountInfoQuery::new().account_id(operator_id).execute(&client).await?;
account_info.balance
};
assert!(
final_balance < initial_balance,
"Expected final balance ({}) to be less than initial balance ({}) due to fees",
final_balance.to_tinybars(),
initial_balance.to_tinybars()
);
Ok(())
}