Skip to content

Commit 6dae7b8

Browse files
committed
feat: Adds e2e tests for BatchTransactions
Signed-off-by: gsstoykov <[email protected]>
1 parent 07bbe15 commit 6dae7b8

File tree

3 files changed

+299
-2
lines changed

3 files changed

+299
-2
lines changed

src/batch_transaction.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,6 @@ impl ToTransactionDataProtobuf for BatchTransactionData {
209209
.to_signed_transaction_bytes()
210210
.expect("Inner transaction should be frozen and serializable");
211211

212-
println!("Signed transaction bytes: {:?}", signed_transaction_bytes);
213212
builder.transactions.push(signed_transaction_bytes);
214213
}
215214

src/transaction/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,6 @@ impl<D: TransactionExecute> Transaction<D> {
557557
self.set_batch_key(batch_key);
558558
// Set node account ID to 0.0.0 for batch transactions (as per HIP-551)
559559
self.node_account_ids([crate::AccountId::new(0, 0, 0)]);
560-
self.freeze_with(client)?;
561560
self.sign_with_operator(client)
562561
}
563562
/// # Errors

tests/e2e/batch_transaction.rs

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
use std::str::FromStr;
2+
use time::{OffsetDateTime, Duration};
23

34
use hedera::{
45
AccountCreateTransaction,
56
AccountId,
7+
AccountInfoQuery,
68
BatchTransaction,
9+
FileId,
10+
FreezeTransaction,
11+
FreezeType,
712
Hbar,
813
PrivateKey,
14+
TopicCreateTransaction,
15+
TopicMessageSubmitTransaction,
916
};
1017

1118
use crate::common::{
1219
setup_nonfree,
1320
TestEnvironment,
1421
};
22+
use crate::resources::BIG_CONTENTS;
1523

1624
#[tokio::test]
1725
async fn can_execute_batch_transaction() -> anyhow::Result<()> {
@@ -44,3 +52,294 @@ async fn can_execute_batch_transaction() -> anyhow::Result<()> {
4452

4553
Ok(())
4654
}
55+
56+
#[tokio::test]
57+
async fn can_execute_large_batch_transaction() -> anyhow::Result<()> {
58+
let Some(TestEnvironment { config: _, client }) = setup_nonfree() else {
59+
return Ok(());
60+
};
61+
62+
// Given
63+
let operator_id = AccountId::new(0, 0, 2);
64+
let operator_key = PrivateKey::from_str(
65+
"302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"
66+
)?;
67+
68+
client.set_operator(operator_id, operator_key.clone());
69+
70+
// When
71+
let mut batch_transaction = BatchTransaction::new();
72+
73+
// Create 15 account creation transactions (smaller batch to test limits)
74+
for i in 0..15 {
75+
let account_key = PrivateKey::generate_ed25519();
76+
77+
let mut inner_transaction = AccountCreateTransaction::new();
78+
inner_transaction
79+
.set_key_without_alias(account_key.public_key())
80+
.initial_balance(Hbar::new(1));
81+
82+
// Use batchify to prepare the transaction
83+
inner_transaction.batchify(&client, operator_key.public_key().into())?;
84+
85+
batch_transaction.add_inner_transaction(inner_transaction.into())?;
86+
}
87+
88+
// Then
89+
let tx_response = batch_transaction.execute(&client).await?;
90+
let _tx_receipt = tx_response.get_receipt(&client).await?;
91+
92+
// Verify we can get all inner transaction IDs
93+
let inner_tx_ids = batch_transaction.get_inner_transaction_ids();
94+
assert_eq!(inner_tx_ids.len(), 15);
95+
96+
Ok(())
97+
}
98+
99+
#[tokio::test]
100+
async fn cannot_execute_batch_transaction_without_inner_transactions() -> anyhow::Result<()> {
101+
let Some(TestEnvironment { config: _, client }) = setup_nonfree() else {
102+
return Ok(());
103+
};
104+
105+
// Given
106+
let operator_id = AccountId::new(0, 0, 2);
107+
let operator_key = PrivateKey::from_str(
108+
"302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"
109+
)?;
110+
111+
client.set_operator(operator_id, operator_key);
112+
113+
// When / Then
114+
let mut empty_batch = BatchTransaction::new();
115+
116+
// Attempting to execute an empty batch transaction should fail
117+
let result = empty_batch.execute(&client).await;
118+
119+
assert!(result.is_err(), "Expected batch transaction without inner transactions to fail");
120+
121+
Ok(())
122+
}
123+
124+
#[tokio::test]
125+
async fn cannot_execute_batch_transaction_with_blacklisted_transaction() -> anyhow::Result<()> {
126+
let Some(TestEnvironment { config: _, client }) = setup_nonfree() else {
127+
return Ok(());
128+
};
129+
130+
// Given
131+
let operator_id = AccountId::new(0, 0, 2);
132+
let operator_key = PrivateKey::from_str(
133+
"302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"
134+
)?;
135+
136+
client.set_operator(operator_id, operator_key.clone());
137+
138+
// Create a blacklisted transaction (FreezeTransaction)
139+
let mut freeze_transaction = FreezeTransaction::new();
140+
freeze_transaction
141+
.file_id(FileId::new(0, 0, 150)) // Use a default file ID
142+
.start_time(OffsetDateTime::now_utc() + Duration::seconds(30))
143+
.freeze_type(FreezeType::FreezeOnly);
144+
145+
// Try to batchify the freeze transaction (this should work)
146+
freeze_transaction.batchify(&client, operator_key.public_key().into())?;
147+
148+
// When / Then
149+
let mut batch_transaction = BatchTransaction::new();
150+
151+
// Attempting to add a blacklisted transaction should fail
152+
let result = batch_transaction.add_inner_transaction(freeze_transaction.into());
153+
154+
assert!(result.is_err(), "Expected adding blacklisted transaction to batch to fail");
155+
156+
Ok(())
157+
}
158+
159+
#[tokio::test]
160+
#[ignore]
161+
async fn cannot_execute_batch_transaction_with_invalid_inner_batch_key() -> anyhow::Result<()> {
162+
let Some(TestEnvironment { config: _, client }) = setup_nonfree() else {
163+
return Ok(());
164+
};
165+
166+
// Given
167+
let operator_id = AccountId::new(0, 0, 2);
168+
let operator_key = PrivateKey::from_str(
169+
"302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"
170+
)?;
171+
172+
let account_key = PrivateKey::generate_ed25519(); // Different key from operator
173+
174+
// Create an inner transaction with the WRONG batch key (accountKey instead of operatorKey)
175+
let mut inner_transaction = AccountCreateTransaction::new();
176+
inner_transaction
177+
.set_key_without_alias(account_key.public_key())
178+
.initial_balance(Hbar::new(1));
179+
180+
// Batchify with the wrong key - this should cause issues later
181+
inner_transaction.batchify(&client, account_key.public_key().into())?; // Wrong key!
182+
183+
client.set_operator(operator_id, operator_key.clone());
184+
185+
// When / Then
186+
let mut batch_transaction = BatchTransaction::new();
187+
batch_transaction.add_inner_transaction(inner_transaction.into())?; // This should succeed
188+
189+
// Attempting to execute should fail due to batch key mismatch
190+
let result = batch_transaction.execute(&client).await;
191+
192+
assert!(result.is_err(), "Expected batch transaction with invalid inner batch key to fail");
193+
194+
Ok(())
195+
}
196+
197+
#[tokio::test]
198+
async fn cannot_execute_batch_transaction_without_batchifying_inner() -> anyhow::Result<()> {
199+
let Some(TestEnvironment { config: _, client }) = setup_nonfree() else {
200+
return Ok(());
201+
};
202+
203+
// Given
204+
let operator_id = AccountId::new(0, 0, 2);
205+
let operator_key = PrivateKey::from_str(
206+
"302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"
207+
)?;
208+
209+
let account_key = PrivateKey::generate_ed25519();
210+
211+
client.set_operator(operator_id, operator_key);
212+
213+
// Create an inner transaction WITHOUT batchifying it (no freeze, no batch key)
214+
let mut inner_transaction = AccountCreateTransaction::new();
215+
inner_transaction
216+
.set_key_without_alias(account_key.public_key())
217+
.initial_balance(Hbar::new(1));
218+
219+
// NOTE: We deliberately do NOT call batchify() here!
220+
221+
// When / Then
222+
let mut batch_transaction = BatchTransaction::new();
223+
224+
// Attempting to add an un-batchified transaction should fail
225+
let result = batch_transaction.add_inner_transaction(inner_transaction.into());
226+
227+
assert!(result.is_err(), "Expected adding non-batchified transaction to batch to fail");
228+
229+
Ok(())
230+
}
231+
232+
#[tokio::test]
233+
async fn can_execute_batch_transaction_with_chunked_inner() -> anyhow::Result<()> {
234+
let Some(TestEnvironment { config: _, client }) = setup_nonfree() else {
235+
return Ok(());
236+
};
237+
238+
// Given
239+
let operator_id = AccountId::new(0, 0, 2);
240+
let operator_key = PrivateKey::from_str(
241+
"302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"
242+
)?;
243+
244+
client.set_operator(operator_id, operator_key.clone());
245+
246+
// When - First create a topic
247+
let mut topic_create = TopicCreateTransaction::new();
248+
topic_create
249+
.admin_key(operator_key.public_key())
250+
.topic_memo("testMemo");
251+
252+
let topic_response = topic_create.execute(&client).await?;
253+
let topic_receipt = topic_response.get_receipt(&client).await?;
254+
let topic_id = topic_receipt.topic_id.ok_or_else(|| anyhow::anyhow!("Topic ID not found in receipt"))?;
255+
256+
// Create a large topic message that will be chunked
257+
let mut inner_transaction = TopicMessageSubmitTransaction::new();
258+
inner_transaction
259+
.topic_id(topic_id)
260+
.message(BIG_CONTENTS.as_bytes().to_vec());
261+
262+
// Batchify the large message transaction
263+
inner_transaction.batchify(&client, operator_key.public_key().into())?;
264+
265+
// Then - Add to batch and execute
266+
let mut batch_transaction = BatchTransaction::new();
267+
batch_transaction.add_inner_transaction(inner_transaction.into())?;
268+
269+
let tx_response = batch_transaction.execute(&client).await?;
270+
let _tx_receipt = tx_response.get_receipt(&client).await?;
271+
272+
Ok(())
273+
}
274+
275+
#[tokio::test]
276+
async fn batch_transaction_incurs_fees_even_if_one_inner_failed() -> anyhow::Result<()> {
277+
let Some(TestEnvironment { config: _, client }) = setup_nonfree() else {
278+
return Ok(());
279+
};
280+
281+
// Given
282+
let operator_id = AccountId::new(0, 0, 2);
283+
let operator_key = PrivateKey::from_str(
284+
"302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"
285+
)?;
286+
287+
client.set_operator(operator_id, operator_key.clone());
288+
289+
// Get initial account balance
290+
let initial_balance = {
291+
let account_info = AccountInfoQuery::new()
292+
.account_id(operator_id)
293+
.execute(&client)
294+
.await?;
295+
account_info.balance
296+
};
297+
298+
// Create first inner transaction (should succeed)
299+
let account_key1 = PrivateKey::generate_ed25519();
300+
let mut inner_transaction1 = AccountCreateTransaction::new();
301+
inner_transaction1
302+
.set_key_without_alias(account_key1.public_key())
303+
.initial_balance(Hbar::new(1));
304+
inner_transaction1.batchify(&client, operator_key.public_key().into())?;
305+
306+
// Create second inner transaction (should fail due to receiver signature required)
307+
let account_key2 = PrivateKey::generate_ed25519();
308+
let mut inner_transaction2 = AccountCreateTransaction::new();
309+
inner_transaction2
310+
.set_key_without_alias(account_key2.public_key())
311+
.initial_balance(Hbar::new(1))
312+
.receiver_signature_required(true); // This will cause failure
313+
inner_transaction2.batchify(&client, operator_key.public_key().into())?;
314+
315+
// When
316+
let mut batch_transaction = BatchTransaction::new();
317+
batch_transaction.set_inner_transactions(vec![
318+
inner_transaction1.into(),
319+
inner_transaction2.into(),
320+
])?;
321+
322+
let tx_response = batch_transaction.execute(&client).await?;
323+
324+
// Expect the receipt to fail due to the second transaction requiring receiver signature
325+
let receipt_result = tx_response.get_receipt(&client).await;
326+
assert!(receipt_result.is_err(), "Expected batch transaction receipt to fail due to receiver signature requirement");
327+
328+
// Then - Check that fees were still charged despite the failure
329+
let final_balance = {
330+
let account_info = AccountInfoQuery::new()
331+
.account_id(operator_id)
332+
.execute(&client)
333+
.await?;
334+
account_info.balance
335+
};
336+
337+
assert!(
338+
final_balance < initial_balance,
339+
"Expected final balance ({}) to be less than initial balance ({}) due to fees",
340+
final_balance.to_tinybars(),
341+
initial_balance.to_tinybars()
342+
);
343+
344+
Ok(())
345+
}

0 commit comments

Comments
 (0)