Skip to content

Commit 3241d95

Browse files
authored
fix: support public for batch_key in Transaction (hiero-ledger#1051)
Signed-off-by: Aubrey Du <[email protected]>
1 parent d53489c commit 3241d95

File tree

5 files changed

+396
-16
lines changed

5 files changed

+396
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
5858
- Move `account_allowance_delete_transaction_hbar.py` from `examples/` to `examples/account/` for better organization (#1003)
5959
- Improved consistency of transaction examples (#1120)
6060
- Refactored `account_create_transaction_with_fallback_alias.py` by splitting the monolithic `create_account_with_fallback_alias` function into modular functions: `generate_fallback_key`, `fetch_account_info`, and `print_account_summary`. The existing `setup_client()` function was reused for improved readability and structure (#1018)
61+
- Allow `PublicKey` for batch_key in `Transaction`, enabling both `PrivateKey` and `PublicKey` for batched transactions
6162
- Allow `PublicKey` for `TokenUpdateKeys` in `TokenUpdateTransaction`, enabling non-custodial workflows where operators can build transactions using only public keys (#934).
6263
- Bump protobuf toml to protobuf==6.33.2
6364
- chore: Move account allowance example to correct folder

examples/transaction/batch_transaction.py

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,9 @@ def transfer_token(client, sender, recipient, token_id):
162162

163163
def perform_batch_tx(client, sender, recipient, token_id, freeze_key):
164164
"""
165-
Perform a batch transaction.
165+
Perform a batch transaction using PrivateKey as batch_key.
166166
"""
167-
print("\nPerforming batch transaction (unfreeze → transfer → freeze)...")
167+
print("\nPerforming batch transaction with PrivateKey (unfreeze → transfer → freeze)...")
168168
batch_key = PrivateKey.generate()
169169

170170
unfreeze_tx = (
@@ -204,6 +204,58 @@ def perform_batch_tx(client, sender, recipient, token_id, freeze_key):
204204
print(f"Batch transaction status: {ResponseCode(receipt.status).name}")
205205

206206

207+
def perform_batch_tx_with_public_key(client, sender, recipient, token_id, freeze_key):
208+
"""
209+
Perform a batch transaction using PublicKey as batch_key.
210+
Demonstrates that batch_key can accept both PrivateKey and PublicKey.
211+
"""
212+
print("\n✨ Performing batch transaction with PublicKey (unfreeze → transfer → freeze)...")
213+
214+
# Generate a key pair - we'll use the PublicKey as batch_key
215+
batch_private_key = PrivateKey.generate()
216+
batch_public_key = batch_private_key.public_key()
217+
218+
print(f"Using PublicKey as batch_key: {batch_public_key}")
219+
220+
# Create inner transactions using PublicKey as batch_key
221+
unfreeze_tx = (
222+
TokenUnfreezeTransaction()
223+
.set_account_id(sender)
224+
.set_token_id(token_id)
225+
.batchify(client, batch_public_key) # Using PublicKey!
226+
.sign(freeze_key)
227+
)
228+
229+
transfer_tx = (
230+
TransferTransaction()
231+
.add_token_transfer(token_id, sender, -1)
232+
.add_token_transfer(token_id, recipient, 1)
233+
.batchify(client, batch_public_key) # Using PublicKey!
234+
)
235+
236+
freeze_tx = (
237+
TokenFreezeTransaction()
238+
.set_account_id(sender)
239+
.set_token_id(token_id)
240+
.batchify(client, batch_public_key) # Using PublicKey!
241+
.sign(freeze_key)
242+
)
243+
244+
# Assemble the batch transaction
245+
batch = (
246+
BatchTransaction()
247+
.add_inner_transaction(unfreeze_tx)
248+
.add_inner_transaction(transfer_tx)
249+
.add_inner_transaction(freeze_tx)
250+
.freeze_with(client)
251+
.sign(batch_private_key) # Sign with PrivateKey for execution
252+
)
253+
254+
receipt = batch.execute(client)
255+
print(f"Batch transaction with PublicKey status: {ResponseCode(receipt.status).name}")
256+
print(" This demonstrates that batch_key now accepts both PrivateKey and PublicKey!")
257+
258+
207259
def main():
208260
client = setup_client()
209261
freeze_key = PrivateKey.generate()
@@ -227,19 +279,36 @@ def main():
227279
get_balance(client, client.operator_account_id, token_id)
228280
get_balance(client, recipient_id, token_id)
229281

230-
# Batch unfreeze → transfer → freeze
231-
perform_batch_tx(
232-
client, client.operator_account_id, recipient_id, token_id, freeze_key
233-
)
282+
# Batch unfreeze → transfer → freeze (using PrivateKey)
283+
perform_batch_tx(client, client.operator_account_id, recipient_id, token_id, freeze_key)
234284

235-
print("\nBalances after batch:")
285+
print("\nBalances after first batch:")
236286
get_balance(client, client.operator_account_id, token_id)
237287
get_balance(client, recipient_id, token_id)
238-
239-
# Should fail again Verify that token is again freeze for account
288+
289+
# Verify that token is frozen again
290+
receipt = transfer_token(client, client.operator_account_id, recipient_id, token_id)
291+
if receipt.status == ResponseCode.ACCOUNT_FROZEN_FOR_TOKEN:
292+
print("\n✅ Correct: Account is frozen again after first batch")
293+
else:
294+
print("\nAccount should be frozen again!")
295+
sys.exit(1)
296+
297+
# Now demonstrate using PublicKey as batch_key
298+
print("\n" + "="*80)
299+
print("Demonstrating PublicKey support for batch_key")
300+
print("="*80)
301+
302+
perform_batch_tx_with_public_key(client, client.operator_account_id, recipient_id, token_id, freeze_key)
303+
304+
print("\nBalances after second batch (with PublicKey):")
305+
get_balance(client, client.operator_account_id, token_id)
306+
get_balance(client, recipient_id, token_id)
307+
308+
# Verify that token is frozen again
240309
receipt = transfer_token(client, client.operator_account_id, recipient_id, token_id)
241310
if receipt.status == ResponseCode.ACCOUNT_FROZEN_FOR_TOKEN:
242-
print("\nCorrect: Account is frozen again")
311+
print("\n✅ Success! Account is frozen again, PublicKey batch_key works correctly!")
243312
else:
244313
print("\nAccount should be frozen again!")
245314
sys.exit(1)

src/hiero_sdk_python/transaction/transaction.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from hiero_sdk_python.response_code import ResponseCode
1717
from hiero_sdk_python.transaction.transaction_id import TransactionId
1818
from hiero_sdk_python.transaction.transaction_response import TransactionResponse
19+
from hiero_sdk_python.utils.key_utils import Key, key_to_proto
1920

2021
if TYPE_CHECKING:
2122
from hiero_sdk_python.schedule.schedule_create_transaction import (
@@ -65,7 +66,7 @@ def __init__(self) -> None:
6566
# changed from int: 2_000_000 to Hbar: 0.02
6667
self._default_transaction_fee = Hbar(0.02)
6768
self.operator_account_id = None
68-
self.batch_key: Optional[PrivateKey] = None
69+
self.batch_key: Optional[Key] = None
6970

7071
def _make_request(self):
7172
"""
@@ -434,7 +435,7 @@ def build_base_transaction_body(self) -> transaction_pb2.TransactionBody:
434435
transaction_body.max_custom_fees.extend(custom_fee_limits)
435436

436437
if self.batch_key:
437-
transaction_body.batch_key.CopyFrom(self.batch_key.public_key()._to_proto())
438+
transaction_body.batch_key.CopyFrom(key_to_proto(self.batch_key))
438439

439440
return transaction_body
440441

@@ -807,12 +808,12 @@ def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map):
807808

808809
return transaction
809810

810-
def set_batch_key(self, key: PrivateKey):
811+
def set_batch_key(self, key: Key):
811812
"""
812813
Set the batch key required for batch transaction.
813814
814815
Args:
815-
batch_key (PrivateKey): Private key to use as batch key.
816+
batch_key (Key): Key to use as batch key (accepts both PrivateKey and PublicKey).
816817
817818
Returns:
818819
Transaction: A reconstructed transaction instance of the appropriate subclass.
@@ -821,13 +822,13 @@ def set_batch_key(self, key: PrivateKey):
821822
self.batch_key = key
822823
return self
823824

824-
def batchify(self, client: Client, batch_key: PrivateKey):
825+
def batchify(self, client: Client, batch_key: Key):
825826
"""
826827
Marks the current transaction as an inner (batched) transaction.
827828
828829
Args:
829830
client (Client): The client instance to use for setting defaults.
830-
batch_key (PrivateKey): Private key to use as batch key.
831+
batch_key (Key): Key to use as batch key (accepts both PrivateKey and PublicKey).
831832
832833
Returns:
833834
Transaction: A reconstructed transaction instance of the appropriate subclass.

tests/integration/batch_transaction_e2e_test.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import pytest
33
from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction
44
from hiero_sdk_python.crypto.private_key import PrivateKey
5+
from hiero_sdk_python.crypto.public_key import PublicKey
56
from hiero_sdk_python.file.file_id import FileId
67
from hiero_sdk_python.query.account_info_query import AccountInfoQuery
78
from hiero_sdk_python.query.transaction_get_receipt_query import TransactionGetReceiptQuery
@@ -352,3 +353,154 @@ def test_batch_transaction_with_inner_schedule_transaction(env):
352353

353354
batch_receipt = batch_tx.execute(env.client)
354355
assert batch_receipt.status == ResponseCode.BATCH_TRANSACTION_IN_BLACKLIST
356+
357+
358+
def test_batch_transaction_with_public_key_as_batch_key(env):
359+
"""Test batch transaction can use PublicKey as batch_key."""
360+
# Generate a key pair - we'll use the PublicKey as batch_key
361+
batch_private_key = PrivateKey.generate()
362+
batch_public_key = batch_private_key.public_key()
363+
364+
receiver_id = create_account_tx(PrivateKey.generate().public_key(), env.client)
365+
366+
# Use PublicKey in batchify
367+
transfer_tx = (
368+
TransferTransaction()
369+
.add_hbar_transfer(account_id=env.operator_id, amount=-1)
370+
.add_hbar_transfer(account_id=receiver_id, amount=1)
371+
.batchify(env.client, batch_public_key) # Using PublicKey!
372+
)
373+
374+
# Verify batch_key was set to PublicKey
375+
assert isinstance(transfer_tx.batch_key, PublicKey)
376+
assert transfer_tx.batch_key == batch_public_key
377+
378+
# Sign and execute with PrivateKey
379+
batch_tx = (
380+
BatchTransaction()
381+
.add_inner_transaction(transfer_tx)
382+
.freeze_with(env.client)
383+
.sign(batch_private_key) # Sign with corresponding PrivateKey
384+
)
385+
386+
batch_receipt = batch_tx.execute(env.client)
387+
assert batch_receipt.status == ResponseCode.SUCCESS
388+
389+
# Inner Transaction Receipt
390+
transfer_tx_id = batch_tx.get_inner_transaction_ids()[0]
391+
transfer_tx_receipt = (
392+
TransactionGetReceiptQuery()
393+
.set_transaction_id(transfer_tx_id)
394+
.execute(env.client)
395+
)
396+
assert transfer_tx_receipt.status == ResponseCode.SUCCESS
397+
398+
399+
def test_batch_transaction_with_mixed_public_and_private_keys(env):
400+
"""Test batch transaction can handle inner transactions with mixed PrivateKey and PublicKey."""
401+
# Generate different keys
402+
batch_key1_private = PrivateKey.generate()
403+
batch_key2_private = PrivateKey.generate()
404+
batch_key2_public = batch_key2_private.public_key()
405+
batch_key3_private = PrivateKey.generate()
406+
batch_key3_public = batch_key3_private.public_key()
407+
408+
# Create receivers
409+
receiver_id1 = create_account_tx(PrivateKey.generate().public_key(), env.client)
410+
receiver_id2 = create_account_tx(PrivateKey.generate().public_key(), env.client)
411+
receiver_id3 = create_account_tx(PrivateKey.generate().public_key(), env.client)
412+
413+
# First inner transaction uses PrivateKey
414+
transfer_tx1 = (
415+
TransferTransaction()
416+
.add_hbar_transfer(account_id=env.operator_id, amount=-1)
417+
.add_hbar_transfer(account_id=receiver_id1, amount=1)
418+
.batchify(env.client, batch_key1_private) # PrivateKey
419+
)
420+
421+
# Second inner transaction uses PublicKey
422+
transfer_tx2 = (
423+
TransferTransaction()
424+
.add_hbar_transfer(account_id=env.operator_id, amount=-1)
425+
.add_hbar_transfer(account_id=receiver_id2, amount=1)
426+
.batchify(env.client, batch_key2_public) # PublicKey
427+
)
428+
429+
# Third inner transaction uses PublicKey
430+
transfer_tx3 = (
431+
TransferTransaction()
432+
.add_hbar_transfer(account_id=env.operator_id, amount=-1)
433+
.add_hbar_transfer(account_id=receiver_id3, amount=1)
434+
.batchify(env.client, batch_key3_public) # PublicKey
435+
)
436+
437+
# Verify key types
438+
assert isinstance(transfer_tx1.batch_key, PrivateKey)
439+
assert isinstance(transfer_tx2.batch_key, PublicKey)
440+
assert isinstance(transfer_tx3.batch_key, PublicKey)
441+
442+
# Assemble and sign batch transaction
443+
batch_tx = (
444+
BatchTransaction()
445+
.add_inner_transaction(transfer_tx1)
446+
.add_inner_transaction(transfer_tx2)
447+
.add_inner_transaction(transfer_tx3)
448+
.freeze_with(env.client)
449+
.sign(batch_key1_private)
450+
.sign(batch_key2_private) # Sign with PrivateKey for PublicKey batch_key
451+
.sign(batch_key3_private) # Sign with PrivateKey for PublicKey batch_key
452+
)
453+
454+
batch_receipt = batch_tx.execute(env.client)
455+
assert batch_receipt.status == ResponseCode.SUCCESS
456+
457+
# Verify all inner transactions succeeded
458+
for transfer_tx_id in batch_tx.get_inner_transaction_ids():
459+
transfer_tx_receipt = (
460+
TransactionGetReceiptQuery()
461+
.set_transaction_id(transfer_tx_id)
462+
.execute(env.client)
463+
)
464+
assert transfer_tx_receipt.status == ResponseCode.SUCCESS
465+
466+
467+
def test_batch_transaction_set_batch_key_with_public_key(env):
468+
"""Test batch transaction inner transaction can use set_batch_key with PublicKey."""
469+
# Generate a key pair
470+
batch_private_key = PrivateKey.generate()
471+
batch_public_key = batch_private_key.public_key()
472+
473+
receiver_id = create_account_tx(PrivateKey.generate().public_key(), env.client)
474+
475+
# Use set_batch_key with PublicKey instead of batchify
476+
transfer_tx = (
477+
TransferTransaction()
478+
.add_hbar_transfer(account_id=env.operator_id, amount=-1)
479+
.add_hbar_transfer(account_id=receiver_id, amount=1)
480+
.set_batch_key(batch_public_key) # Using set_batch_key with PublicKey
481+
.freeze_with(env.client)
482+
.sign(env.operator_key) # Sign inner transaction with operator key
483+
)
484+
485+
# Verify batch_key was set correctly
486+
assert transfer_tx.batch_key == batch_public_key
487+
assert isinstance(transfer_tx.batch_key, PublicKey)
488+
489+
batch_tx = (
490+
BatchTransaction()
491+
.add_inner_transaction(transfer_tx)
492+
.freeze_with(env.client)
493+
.sign(batch_private_key) # Sign batch transaction with batch key
494+
)
495+
496+
batch_receipt = batch_tx.execute(env.client)
497+
assert batch_receipt.status == ResponseCode.SUCCESS
498+
499+
# Inner Transaction Receipt
500+
transfer_tx_id = batch_tx.get_inner_transaction_ids()[0]
501+
transfer_tx_receipt = (
502+
TransactionGetReceiptQuery()
503+
.set_transaction_id(transfer_tx_id)
504+
.execute(env.client)
505+
)
506+
assert transfer_tx_receipt.status == ResponseCode.SUCCESS

0 commit comments

Comments
 (0)