Skip to content

Commit a27b26c

Browse files
feat: add transaction bytes serialization support (#648)
Signed-off-by: Pratyush Kumar <[email protected]> Signed-off-by: nadineloepfe <[email protected]> Co-authored-by: Pratyush Kumar <[email protected]>
1 parent b7eceb5 commit a27b26c

File tree

9 files changed

+1392
-12
lines changed

9 files changed

+1392
-12
lines changed

.github/dependabot.yml

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,4 @@ updates:
1414
directory: "/"
1515
schedule:
1616
interval: "daily"
17-
open-pull-requests-limit: 10
18-
- package-ecosystem: "pip-compile"
19-
directory: "/"
20-
schedule:
21-
interval: "daily"
22-
open-pull-requests-limit: 10
23-
- package-ecosystem: "pipenv"
24-
directory: "/"
25-
schedule:
26-
interval: "daily"
27-
open-pull-requests-limit: 10
17+
open-pull-requests-limit: 10

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
1212
- Add Google-style docstrings to `AccountInfo` class and its methods in `account_info.py`.
1313
- Added comprehensive Google-style docstrings to the `Logger` class and all utility functions in `src/hiero_sdk_python/logger/logger.py` (#639).
1414
- add AccountRecordsQuery class
15+
- Transaction bytes serialization support: `Transaction.freeze()`, `Transaction.to_bytes()`, and `Transaction.from_bytes()` methods for offline signing and transaction storage
1516

1617
- docs: Add Google-style docstrings to `ContractId` class and methods in `contract_id.py`.
1718

docs/sdk_users/running_examples.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ You can choose either syntax or even mix both styles in your projects.
8181
- [Miscellaneous Queries](#miscellaneous-queries)
8282
- [Querying Transaction Record](#querying-transaction-record)
8383
- [Miscellaneous Transactions](#miscellaneous-transactions)
84+
- [Transaction Bytes Serialization](#transaction-bytes-serialization)
8485
- [PRNG Transaction](#prng-transaction)
8586

8687

@@ -2029,6 +2030,16 @@ print(f"Transaction Account ID: {record.receipt.account_id}")
20292030

20302031
## Miscellaneous Transactions
20312032

2033+
### Transaction Bytes Serialization
2034+
2035+
For detailed information about freezing transactions and converting them to bytes for offline signing, external signing services (HSMs, hardware wallets), and transaction storage/transmission, see [Transaction Bytes Serialization](transaction_bytes.md).
2036+
2037+
This guide covers:
2038+
- `Transaction.freeze()` and `Transaction.freeze_with(client)` methods
2039+
- `Transaction.to_bytes()` for serialization
2040+
- `Transaction.from_bytes()` for deserialization
2041+
- Use cases for offline signing, HSM integration, transaction storage, and batch processing
2042+
20322043
### PRNG Transaction
20332044

20342045
#### Pythonic Syntax:
Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
# Transaction Bytes Serialization
2+
3+
## Overview
4+
5+
Support for freezing transactions and converting them to bytes instead of executing immediately. This enables offline signing, external signing services (HSMs, hardware wallets), and transaction storage/transmission.
6+
7+
## New Methods
8+
9+
### `Transaction.freeze()`
10+
11+
Freezes a transaction with manually set IDs for **single-node execution**.
12+
13+
**Requirements:**
14+
- `transaction_id` must be set
15+
- `node_account_id` must be set
16+
17+
**Returns:** `Transaction` (self) for method chaining
18+
19+
**⚠️ Important Limitation:**
20+
This method only builds the transaction body for the single node specified. If the network needs to retry with a different node, the transaction will fail. For production use, prefer `freeze_with(client)`.
21+
22+
**Example:**
23+
```python
24+
from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction
25+
from hiero_sdk_python.transaction.transaction_id import TransactionId
26+
from hiero_sdk_python.account.account_id import AccountId
27+
28+
transaction = TransferTransaction().add_hbar_transfer(...)
29+
transaction.transaction_id = TransactionId.generate(AccountId.from_string("0.0.1234"))
30+
transaction.node_account_id = AccountId.from_string("0.0.3")
31+
transaction.freeze()
32+
```
33+
34+
### `Transaction.freeze_with(client)`
35+
36+
Freezes a transaction using client for **multi-node execution with automatic failover**.
37+
38+
**Advantages:**
39+
- Automatically sets transaction_id from client's operator
40+
- Builds transaction bodies for **all nodes** in the network
41+
- Enables automatic node failover if a node is unavailable
42+
- Recommended for production use
43+
44+
**Example:**
45+
```python
46+
from hiero_sdk_python.client.client import Client
47+
from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction
48+
49+
client = Client.for_testnet()
50+
client.set_operator(operator_id, operator_key)
51+
52+
transaction = TransferTransaction().add_hbar_transfer(...)
53+
transaction.freeze_with(client) # Builds for all network nodes
54+
```
55+
56+
### `Transaction.to_bytes()`
57+
58+
Serializes a frozen transaction to protobuf bytes.
59+
60+
**Requirements:** Transaction must be frozen (with `freeze()` or `freeze_with()`)
61+
62+
**Signing:** Optional - works with both signed and unsigned transactions
63+
- **Unsigned bytes**: Can be sent to external signing services or HSMs
64+
- **Signed bytes**: Ready for submission to the network
65+
66+
**Returns:** `bytes`
67+
68+
**Examples:**
69+
70+
**Unsigned transaction (for external signing):**
71+
```python
72+
transaction.freeze_with(client)
73+
unsigned_bytes = transaction.to_bytes()
74+
# Send unsigned_bytes to HSM or hardware wallet for signing
75+
```
76+
77+
**Signed transaction (ready for submission):**
78+
```python
79+
transaction.freeze_with(client)
80+
transaction.sign(private_key)
81+
signed_bytes = transaction.to_bytes()
82+
# signed_bytes can be stored, transmitted, or submitted to network
83+
```
84+
85+
**Multiple signatures:**
86+
```python
87+
transaction.freeze_with(client)
88+
transaction.sign(key1)
89+
transaction.sign(key2)
90+
transaction.sign(key3)
91+
multi_sig_bytes = transaction.to_bytes()
92+
```
93+
94+
### `Transaction.from_bytes(transaction_bytes)`
95+
96+
Deserializes a transaction from protobuf-encoded bytes.
97+
98+
**Requirements:** The bytes must have been created using `to_bytes()`
99+
100+
**What is restored:**
101+
- Transaction type and all transaction-specific fields
102+
- Common fields (transaction ID, node ID, memo, fee, etc.)
103+
- All signatures (if the transaction was signed)
104+
- Transaction state (frozen)
105+
106+
**Returns:** The appropriate `Transaction` subclass instance (e.g., `TransferTransaction`)
107+
108+
**Examples:**
109+
110+
**Basic round-trip:**
111+
```python
112+
# Serialize
113+
tx = TransferTransaction().add_hbar_transfer(...)
114+
tx.freeze_with(client)
115+
tx_bytes = tx.to_bytes()
116+
117+
# Deserialize
118+
restored_tx = Transaction.from_bytes(tx_bytes)
119+
```
120+
121+
**External signing:**
122+
```python
123+
# System A: Create and serialize unsigned transaction
124+
tx = TransferTransaction().add_hbar_transfer(...)
125+
tx.freeze_with(client)
126+
unsigned_bytes = tx.to_bytes()
127+
128+
# System B: Restore, sign, and serialize
129+
tx = Transaction.from_bytes(unsigned_bytes)
130+
tx.sign(hsm_key)
131+
signed_bytes = tx.to_bytes()
132+
133+
# System A: Restore and execute
134+
final_tx = Transaction.from_bytes(signed_bytes)
135+
receipt = final_tx.execute(client)
136+
```
137+
138+
## Method Comparison
139+
140+
| Feature | `freeze()` | `freeze_with(client)` |
141+
|---------|-----------|----------------------|
142+
| Sets transaction_id | ❌ Manual required | ✅ Automatic |
143+
| Sets node_account_id | ❌ Manual required | ✅ Automatic |
144+
| Builds for single node | ✅ Yes | ❌ No |
145+
| Builds for all nodes | ❌ No | ✅ Yes |
146+
| Supports node failover | ❌ No | ✅ Yes |
147+
| Use for offline signing | ✅ Yes | ✅ Yes |
148+
| Use for execute(client) | ⚠️ Single node only | ✅ Recommended |
149+
150+
## Use Cases
151+
152+
### 1. Offline Transaction Signing
153+
154+
Create transaction bytes on an online system, transfer to an offline system for signing:
155+
156+
```python
157+
# Online system
158+
transaction = TransferTransaction().add_hbar_transfer(...)
159+
transaction.transaction_id = TransactionId.generate(account_id)
160+
transaction.node_account_id = AccountId.from_string("0.0.3")
161+
transaction.freeze()
162+
unsigned_bytes = transaction.to_bytes()
163+
164+
# Transfer unsigned_bytes to offline system...
165+
166+
# Offline system (air-gapped)
167+
tx = Transaction.from_bytes(unsigned_bytes)
168+
tx.sign(offline_private_key)
169+
signed_bytes = tx.to_bytes()
170+
171+
# Transfer signed_bytes back to online system...
172+
173+
# Online system
174+
final_tx = Transaction.from_bytes(signed_bytes)
175+
receipt = final_tx.execute(client)
176+
```
177+
178+
### 2. Hardware Wallet / HSM Integration
179+
180+
```python
181+
# Prepare transaction
182+
transaction = TransferTransaction().add_hbar_transfer(...)
183+
transaction.freeze_with(client)
184+
unsigned_bytes = transaction.to_bytes()
185+
186+
# Send to HSM for signing
187+
signed_bytes = hsm.sign_transaction(unsigned_bytes)
188+
189+
# Reconstruct and submit to network
190+
tx = Transaction.from_bytes(signed_bytes)
191+
receipt = tx.execute(client)
192+
```
193+
194+
### 3. Transaction Storage
195+
196+
```python
197+
# Create and sign transaction
198+
transaction = TransferTransaction().add_hbar_transfer(...)
199+
transaction.freeze_with(client)
200+
transaction.sign(private_key)
201+
transaction_bytes = transaction.to_bytes()
202+
203+
# Store in database
204+
database.store("pending_tx_123", transaction_bytes)
205+
206+
# Later, retrieve and execute
207+
stored_bytes = database.get("pending_tx_123")
208+
transaction = Transaction.from_bytes(stored_bytes)
209+
receipt = transaction.execute(client)
210+
```
211+
212+
### 4. Batch Processing
213+
214+
```python
215+
# Create multiple transactions
216+
transactions = []
217+
for payment in payments_list:
218+
tx = TransferTransaction().add_hbar_transfer(...)
219+
tx.freeze_with(client)
220+
transactions.append(tx)
221+
222+
# Sign all at once
223+
for tx in transactions:
224+
tx.sign(private_key)
225+
226+
# Serialize for batch transmission
227+
batch_bytes = [tx.to_bytes() for tx in transactions]
228+
```
229+
230+
## Common Patterns
231+
232+
### Pattern 1: Immediate Execution (Most Common)
233+
234+
```python
235+
# Don't call freeze manually - execute() does it automatically
236+
client = Client.for_testnet()
237+
client.set_operator(operator_id, operator_key)
238+
239+
receipt = (
240+
TransferTransaction()
241+
.add_hbar_transfer(sender, -amount)
242+
.add_hbar_transfer(receiver, amount)
243+
.execute(client) # Automatically freezes and signs
244+
)
245+
```
246+
247+
### Pattern 2: Manual Freeze for Inspection
248+
249+
```python
250+
# Freeze to inspect transaction before signing
251+
transaction = TransferTransaction().add_hbar_transfer(...)
252+
transaction.freeze_with(client)
253+
254+
# Inspect transaction details
255+
print(f"Transaction ID: {transaction.transaction_id}")
256+
print(f"Transaction fee: {transaction.transaction_fee}")
257+
258+
# Then sign and execute
259+
transaction.sign(client.operator_private_key)
260+
receipt = transaction.execute(client)
261+
```
262+
263+
### Pattern 3: External Signing
264+
265+
```python
266+
# Prepare transaction
267+
transaction = TransferTransaction().add_hbar_transfer(...)
268+
transaction.freeze_with(client)
269+
270+
# Get bytes for external signing
271+
unsigned_bytes = transaction.to_bytes()
272+
273+
# External system signs the transaction
274+
tx = Transaction.from_bytes(unsigned_bytes)
275+
tx.sign(external_key)
276+
signed_bytes = tx.to_bytes()
277+
278+
# Original system executes the signed transaction
279+
final_tx = Transaction.from_bytes(signed_bytes)
280+
receipt = final_tx.execute(client)
281+
```
282+
283+
## Limitations
284+
285+
1. **`freeze()` single-node limitation**: Only builds for one node, no automatic failover
286+
2. **Signature verification**: No built-in method to verify signatures before submission
287+
3. **Transaction type support**: `from_bytes()` currently only supports `TransferTransaction`. Other transaction types will need to implement the `_from_protobuf()` method to be fully supported
288+
289+
## Migration from Execute-Only Pattern
290+
291+
**Before (execute only):**
292+
```python
293+
receipt = TransferTransaction().add_hbar_transfer(...).execute(client)
294+
```
295+
296+
**After (with bytes serialization):**
297+
```python
298+
tx = TransferTransaction().add_hbar_transfer(...)
299+
tx.freeze_with(client)
300+
tx.sign(client.operator_private_key)
301+
tx_bytes = tx.to_bytes()
302+
303+
# Store, transmit, or inspect tx_bytes as needed
304+
305+
# Then execute
306+
receipt = tx.execute(client)
307+
```
308+
309+
## See Also
310+
311+
- `examples/transaction_bytes_example.py` - Complete working example
312+
- Transaction signing documentation
313+
- Client configuration guide

0 commit comments

Comments
 (0)