Skip to content

Commit 28cadea

Browse files
authored
feat: Added BatchTransaction class (#811)
Signed-off-by: Manish Dait <daitmanish88@gmail.com>
1 parent 84acb5d commit 28cadea

File tree

9 files changed

+1251
-6
lines changed

9 files changed

+1251
-6
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
2525
- `alias`, `staked_account_id`, `staked_node_id` and `decline_staking_reward` fields to AccountCreateTransaction
2626
- `staked_account_id`, `staked_node_id` and `decline_staking_reward` fields to AccountInfo
2727
- Added `examples/token_create_transaction_supply_key.py` to demonstrate token creation with and without a supply key.
28+
- Added BatchTransaction class
29+
2830

2931
### Changed
3032

docs/sdk_users/running_examples.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ You can choose either syntax or even mix both styles in your projects.
7979
- [Creating a Node](#creating-a-node)
8080
- [Updating a Node](#updating-a-node)
8181
- [Deleting a Node](#deleting-a-node)
82+
- [Batch Transaction](#batch-transaction)
83+
- [Create a Batch Transaction](#create-a-batch-transaction)
8284
- [Miscellaneous Queries](#miscellaneous-queries)
8385
- [Querying Transaction Record](#querying-transaction-record)
8486
- [Miscellaneous Transactions](#miscellaneous-transactions)
@@ -2057,6 +2059,70 @@ transaction.sign(admin_key) # Sign with the admin key
20572059

20582060
receipt = transaction.execute(client)
20592061
```
2062+
## Batch Transaction
2063+
2064+
### Create a Batch Transaction
2065+
2066+
#### Pythonic Syntax:
2067+
```python
2068+
# Create a transaction to be batched (e.g., a transfer transaction)
2069+
transfer_tx = TransferTransaction(
2070+
hbar_transfers={
2071+
sender_id: -amount,
2072+
recipient_id: amount
2073+
}
2074+
)
2075+
2076+
# There are two approaches to mark a transaction as an inner transaction:
2077+
2078+
# Approach 1: Use transaction.batchify(client, batch_key)
2079+
# This sets the batch key, freezes the transaction, and signs it with the client's private key.
2080+
transfer_tx.batchify(client=client, batch_key=batch_key)
2081+
2082+
# Approach 2: Manually configure the transaction for batching
2083+
transfer_tx.set_batch_key(batch_key)
2084+
transfer_tx.freeze_with(client)
2085+
transfer_tx.sign(client.operator_private_key)
2086+
2087+
# Create the BatchTransaction and add inner transactions
2088+
batch_tx = BatchTransaction()
2089+
batch_tx.add_inner_transaction(transfer_tx)
2090+
2091+
# Freeze and sign the batch transaction
2092+
batch_tx.freeze_with(client)
2093+
batch_tx.sign(batch_key)
2094+
```
2095+
2096+
#### Method Chaining:
2097+
```python
2098+
# Create a transaction to be batched (e.g., a transfer transaction)
2099+
# Approch 1: Use transaction.batchify(client, batch_key)
2100+
transfer_tx = (
2101+
TransferTransaction()
2102+
.add_hbar_transfer(sender_id, -amount)
2103+
.add_hbar_transfer(recipient_id, amount)
2104+
.batchify(client, batch_key)
2105+
)
2106+
2107+
#Approch 2: Manually configure the transaction for batching
2108+
transfer_tx = (
2109+
TransferTransaction()
2110+
.add_hbar_transfer(sender_id, -amount)
2111+
.add_hbar_transfer(recipient_id, amount)
2112+
.set_batch_key(batch_key)
2113+
.freeze_with(client)
2114+
.sign(client.operator_private_key)
2115+
)
2116+
2117+
# Build the BatchTransaction with method chaining
2118+
receipt = (
2119+
BatchTransaction()
2120+
.add_inner_transaction(transfer_tx)
2121+
.freeze_with(client)
2122+
.sign(batch_key)
2123+
.execute(client)
2124+
)
2125+
```
20602126

20612127
## Miscellaneous Queries
20622128

examples/batch_transaction.py

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import os
2+
import sys
3+
4+
from dotenv import load_dotenv
5+
6+
from hiero_sdk_python import (
7+
AccountId,
8+
Client,
9+
Network,
10+
PrivateKey,
11+
AccountCreateTransaction,
12+
CryptoGetAccountBalanceQuery,
13+
ResponseCode,
14+
TokenCreateTransaction,
15+
TokenFreezeTransaction,
16+
TokenType,
17+
TokenUnfreezeTransaction,
18+
BatchTransaction,
19+
TransferTransaction
20+
)
21+
22+
load_dotenv()
23+
24+
def get_balance(client, account_id, token_id):
25+
tokens_balance = (
26+
CryptoGetAccountBalanceQuery(account_id=account_id)
27+
.execute(client)
28+
.token_balances
29+
)
30+
31+
print(f"Account: {account_id}: {tokens_balance[token_id] if tokens_balance else 0}")
32+
33+
def setup_client():
34+
"""
35+
Set up and configure a Hedera client for testnet operations.
36+
"""
37+
network_name = os.getenv('NETWORK', 'testnet').lower()
38+
39+
print(f"Connecting to Hedera {network_name} network!")
40+
41+
try :
42+
network = Network(network_name)
43+
client = Client(network)
44+
45+
operator_id = AccountId.from_string(os.getenv('OPERATOR_ID',''))
46+
operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY',''))
47+
48+
client.set_operator(operator_id, operator_key)
49+
print(f"Client initialized with operator: {operator_id}")
50+
return client
51+
except Exception as e:
52+
print(f"Failed to set up client: {e}")
53+
sys.exit(1)
54+
55+
def create_account(client):
56+
"""
57+
Create a new recipient account.
58+
"""
59+
print("\nCreating new recipient account...")
60+
try:
61+
key = PrivateKey.generate()
62+
tx = (
63+
AccountCreateTransaction()
64+
.set_key_without_alias(key.public_key())
65+
.set_max_automatic_token_associations(2) # to transfer token without associating it
66+
.set_initial_balance(1)
67+
)
68+
69+
receipt = tx.freeze_with(client).execute(client)
70+
recipient_id = receipt.account_id
71+
72+
print(f"New account created: {receipt.account_id}")
73+
return recipient_id
74+
except Exception as e:
75+
print(f"Error creating new account: {e}")
76+
sys.exit(1)
77+
78+
def create_fungible_token(client, freeze_key):
79+
"""
80+
Create a fungible token with freeze_key.
81+
"""
82+
print("\nCreating fungible token...")
83+
try:
84+
tx = (
85+
TokenCreateTransaction()
86+
.set_token_name("FiniteFungibleToken")
87+
.set_token_symbol("FFT")
88+
.set_initial_supply(2)
89+
.set_treasury_account_id(client.operator_account_id)
90+
.set_token_type(TokenType.FUNGIBLE_COMMON)
91+
.set_freeze_key(freeze_key)
92+
.freeze_with(client)
93+
.sign(client.operator_private_key)
94+
.sign(freeze_key)
95+
)
96+
receipt = tx.execute(client)
97+
token_id = receipt.token_id
98+
99+
print(f"Token created: {receipt.token_id}")
100+
101+
return token_id
102+
except Exception as e:
103+
print(f"Error creating token: {e}")
104+
sys.exit(1)
105+
106+
def freeze_token(client, account_id, token_id, freeze_key):
107+
"""
108+
Freeze token for an account.
109+
"""
110+
print(f"\nFreezing token for account {account_id}")
111+
try:
112+
tx = (
113+
TokenFreezeTransaction()
114+
.set_account_id(account_id)
115+
.set_token_id(token_id)
116+
.freeze_with(client)
117+
.sign(freeze_key)
118+
)
119+
120+
receipt = tx.execute(client)
121+
122+
if receipt.status != ResponseCode.SUCCESS:
123+
print(f"Freeze failed: {ResponseCode(receipt.status).name})")
124+
sys.exit(1)
125+
126+
print("Token freeze successful!")
127+
except Exception as e:
128+
print(f"Error freezing token for account: {e}")
129+
sys.exit(1)
130+
131+
def transfer_token(client, sender, recipient, token_id):
132+
"""
133+
Perform a token trasfer transaction.
134+
"""
135+
print(f"\nTransferring token {token_id} from {sender}{recipient}")
136+
try:
137+
tx = (
138+
TransferTransaction()
139+
.add_token_transfer(token_id=token_id, account_id=sender, amount=-1)
140+
.add_token_transfer(token_id=token_id, account_id=recipient, amount=1)
141+
)
142+
143+
receipt = tx.execute(client)
144+
145+
return receipt
146+
except Exception as e:
147+
print(f"Error transfering token: {e}")
148+
sys.exit(1)
149+
150+
def perform_batch_tx(client, sender, recipient, token_id, freeze_key):
151+
"""
152+
Perform a batch transaction.
153+
"""
154+
print("\nPerforming batch transaction (unfreeze → transfer → freeze)...")
155+
batch_key = PrivateKey.generate()
156+
157+
unfreeze_tx = (
158+
TokenUnfreezeTransaction()
159+
.set_account_id(sender)
160+
.set_token_id(token_id)
161+
.batchify(client, batch_key)
162+
.sign(freeze_key)
163+
)
164+
165+
transfer_tx = (
166+
TransferTransaction()
167+
.add_token_transfer(token_id, sender, -1)
168+
.add_token_transfer(token_id, recipient, 1)
169+
.batchify(client, batch_key)
170+
)
171+
172+
freeze_tx = (
173+
TokenFreezeTransaction()
174+
.set_account_id(sender)
175+
.set_token_id(token_id)
176+
.batchify(client, batch_key)
177+
.sign(freeze_key)
178+
)
179+
180+
# 50 is the maximum limit for internal transaction inside a BatchTransaction
181+
batch = (
182+
BatchTransaction()
183+
.add_inner_transaction(unfreeze_tx)
184+
.add_inner_transaction(transfer_tx)
185+
.add_inner_transaction(freeze_tx)
186+
.freeze_with(client)
187+
.sign(batch_key)
188+
)
189+
190+
receipt = batch.execute(client)
191+
print(f"Batch transaction status: {ResponseCode(receipt.status).name}")
192+
193+
def main():
194+
client = setup_client()
195+
freeze_key = PrivateKey.generate()
196+
197+
recipient_id = create_account(client)
198+
token_id = create_fungible_token(client, freeze_key)
199+
200+
# Freeze operator for token
201+
freeze_token(client, client.operator_account_id, token_id, freeze_key)
202+
203+
# Confirm transfer fails
204+
receipt = transfer_token(client, client.operator_account_id, recipient_id, token_id)
205+
if receipt.status == ResponseCode.ACCOUNT_FROZEN_FOR_TOKEN:
206+
print("\nCorrect: Account is frozen for token transfers.")
207+
else:
208+
print("\nExpected freeze to block transfer!")
209+
sys.exit(1)
210+
211+
# Show balances
212+
print("\nBalances before batch:")
213+
get_balance(client, client.operator_account_id, token_id)
214+
get_balance(client, recipient_id, token_id)
215+
216+
# Batch unfreeze → transfer → freeze
217+
perform_batch_tx(client, client.operator_account_id, recipient_id, token_id, freeze_key)
218+
219+
print("\nBalances after batch:")
220+
get_balance(client, client.operator_account_id, token_id)
221+
get_balance(client, recipient_id,token_id)
222+
223+
# Should fail again Verify that token is again freeze for account
224+
receipt = transfer_token(client, client.operator_account_id, recipient_id, token_id)
225+
if receipt.status == ResponseCode.ACCOUNT_FROZEN_FOR_TOKEN:
226+
print("\nCorrect: Account is frozen again")
227+
else:
228+
print("\nAccount should be frozen again!")
229+
sys.exit(1)
230+
231+
232+
if __name__ == "__main__":
233+
main()

src/hiero_sdk_python/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
from .transaction.transaction_receipt import TransactionReceipt
6060
from .transaction.transaction_response import TransactionResponse
6161
from .transaction.transaction_record import TransactionRecord
62+
from .transaction.batch_transaction import BatchTransaction
6263

6364
# Response / Codes
6465
from .response_code import ResponseCode
@@ -208,6 +209,7 @@
208209
"TransactionReceipt",
209210
"TransactionResponse",
210211
"TransactionRecord",
212+
"BatchTransaction",
211213

212214
# Response
213215
"ResponseCode",

src/hiero_sdk_python/response_code.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,13 @@ class ResponseCode(IntEnum):
348348
DUPLICATE_DENOMINATION_IN_MAX_CUSTOM_FEE_LIST = 385
349349
DUPLICATE_ACCOUNT_ID_IN_MAX_CUSTOM_FEE_LIST = 386
350350
MAX_CUSTOM_FEES_IS_NOT_SUPPORTED = 387
351+
BATCH_LIST_EMPTY = 388
352+
BATCH_LIST_CONTAINS_DUPLICATES = 389
353+
BATCH_TRANSACTION_IN_BLACKLIST = 390
354+
INNER_TRANSACTION_FAILED = 391
355+
MISSING_BATCH_KEY = 392
356+
BATCH_KEY_SET_ON_NON_INNER_TRANSACTION = 393
357+
INVALID_BATCH_KEY = 394
351358

352359
@classmethod
353360
def _missing_(cls, value: object) -> "ResponseCode":

0 commit comments

Comments
 (0)