Skip to content

Commit d8b9f16

Browse files
feat: Add support for include_children to TransactionRecordQuery (hiero-ledger#1959)
Signed-off-by: Antonio Ceppellini <antonio.ceppellini@gmail.com>
1 parent a4d9a8c commit d8b9f16

7 files changed

Lines changed: 391 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
88

99
### Added
1010
- Added CodeRabbit review instructions in `.coderabbit.yaml` for account module `src/hiero_sdk_python/account/`.
11+
- Add support for `include_children` to TransactionRecordQuery ([#1512](https://github.com/hiero-ledger/hiero-sdk-python/issues/1512))
1112

1213
### Changed
1314
- Changed pytest version to "pytest>=8.3.4,<10" (#1917)
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""
2+
Example demonstrating transaction record query with child records.
3+
4+
To run the example run:
5+
- uv run examples/query/transaction_record_query_with_children.py
6+
- python examples/query/transaction_record_query_with_children.py
7+
"""
8+
9+
import sys
10+
11+
from hiero_sdk_python import (
12+
AccountId,
13+
Client,
14+
Hbar,
15+
PrivateKey,
16+
ResponseCode,
17+
TransactionRecordQuery,
18+
TransferTransaction,
19+
)
20+
21+
22+
def submit_alias_auto_create_transfer(client):
23+
"""Transfer HBAR to a fresh EVM alias to trigger auto-account creation."""
24+
try:
25+
alias_key = PrivateKey.generate_ecdsa()
26+
alias_account_id = AccountId.from_evm_address(
27+
alias_key.public_key().to_evm_address(), 0, 0
28+
)
29+
30+
transaction = (
31+
TransferTransaction()
32+
.add_hbar_transfer(alias_account_id, Hbar(1).to_tinybars())
33+
.add_hbar_transfer(client.operator_account_id, Hbar(-1).to_tinybars())
34+
)
35+
receipt = transaction.execute(client)
36+
37+
return transaction.transaction_id
38+
except Exception as e:
39+
print(f"Error submitting alias auto-create transfer: {e}")
40+
sys.exit(1)
41+
42+
43+
def print_transaction_record(record, title):
44+
"""Print a full transaction record, including child details."""
45+
print(f"\n{title}")
46+
print(f"Transaction ID: {record.transaction_id}")
47+
print(f"Transaction Fee: {record.transaction_fee}")
48+
print(f"Transaction Hash: {record.transaction_hash.hex()}")
49+
print(f"Transaction Memo: {record.transaction_memo}")
50+
print(f"Receipt Status: {ResponseCode(record.receipt.status).name}")
51+
print(f"Receipt Account ID: {record.receipt.account_id}")
52+
print(f"Children: {record.children}")
53+
print(f"Duplicates Count: {len(record.duplicates)}")
54+
55+
56+
def print_child_records(record):
57+
"""Print all child transaction records in detail."""
58+
print(f"\nChild records count: {len(record.children)}")
59+
60+
if not record.children:
61+
sys.exit(1)
62+
63+
print_transaction_record(record.children[0], f"Child record")
64+
65+
66+
def main():
67+
try:
68+
client = Client.from_env()
69+
70+
print(
71+
"\nSTEP 1: Create a parent transaction with child records"
72+
)
73+
transaction_id = submit_alias_auto_create_transfer(client)
74+
print(f"Parent transaction ID: {transaction_id}")
75+
76+
print(
77+
"\nSTEP 2: Querying parent transaction record with include_children=True..."
78+
)
79+
record = (
80+
TransactionRecordQuery()
81+
.set_transaction_id(transaction_id)
82+
.set_include_children(True)
83+
.execute(client)
84+
)
85+
86+
print_transaction_record(record, "Parent Transaction Record")
87+
print_child_records(record)
88+
except Exception as e:
89+
print(f"Error running example: {e}")
90+
sys.exit(1)
91+
92+
93+
if __name__ == "__main__":
94+
main()

src/hiero_sdk_python/query/transaction_record_query.py

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,35 @@ class TransactionRecordQuery(Query):
2525
def __init__(
2626
self,
2727
transaction_id: Optional[TransactionId] = None,
28+
include_children: bool = False,
2829
include_duplicates: bool = False,
2930
) -> None:
3031
"""
3132
Initializes the TransactionRecordQuery with the provided transaction ID.
33+
34+
Args:
35+
transaction_id (TransactionId, optional): The ID of the transaction.
36+
include_children (bool): Whether to include child transaction records.
37+
include_duplicates (bool): Whether to include duplicate transaction records.
3238
"""
3339
super().__init__()
3440
if not isinstance(include_duplicates, bool):
3541
raise TypeError(
3642
f"include_duplicates must be a bool (True or False), got {type(include_duplicates).__name__}"
3743
)
38-
44+
3945
if transaction_id is not None and not isinstance(transaction_id, TransactionId):
4046
raise TypeError(
4147
f"transaction_id must be TransactionId or None, got {type(transaction_id).__name__}"
4248
)
4349

50+
if not isinstance(include_children, bool):
51+
raise TypeError(
52+
f"include_children must be a bool (True or False), got {type(include_children).__name__}"
53+
)
54+
4455
self.transaction_id: Optional[TransactionId] = transaction_id
56+
self.include_children: bool = bool(include_children)
4557
self.include_duplicates: bool = bool(include_duplicates)
4658

4759
def set_include_duplicates(
@@ -57,10 +69,12 @@ def set_include_duplicates(
5769
TransactionRecordQuery: The current instance for method chaining.
5870
"""
5971
if not isinstance(include_duplicates, bool):
60-
raise TypeError(f"include_duplicates must be a boolean, got {type(include_duplicates).__name__}")
72+
raise TypeError(
73+
f"include_duplicates must be a boolean, got {type(include_duplicates).__name__}"
74+
)
6175
self.include_duplicates = include_duplicates
6276
return self
63-
77+
6478
def set_transaction_id(
6579
self,
6680
transaction_id: Optional[TransactionId],
@@ -76,7 +90,7 @@ def set_transaction_id(
7690
Returns:
7791
TransactionRecordQuery: This query instance for chaining.
7892
"""
79-
93+
8094
if transaction_id is not None and not isinstance(transaction_id, TransactionId):
8195
raise TypeError(
8296
f"transaction_id must be TransactionId or None, got {type(transaction_id).__name__}"
@@ -85,6 +99,24 @@ def set_transaction_id(
8599
self.transaction_id = transaction_id
86100
return self
87101

102+
def set_include_children(self, include_children: bool) -> "TransactionRecordQuery":
103+
"""
104+
Sets include_children for which to retrieve the child transaction records.
105+
106+
Args:
107+
include_children: bool.
108+
109+
Returns:
110+
TransactionRecordQuery: The current instance for method chaining.
111+
"""
112+
if not isinstance(include_children, bool):
113+
raise TypeError(
114+
f"include_children must be a boolean, got {type(include_children).__name__}"
115+
)
116+
117+
self.include_children = include_children
118+
return self
119+
88120
def _make_request(self):
89121
"""
90122
Constructs the protobuf request for the transaction record query.
@@ -108,6 +140,7 @@ def _make_request(self):
108140
transaction_get_record.header.CopyFrom(query_header)
109141
transaction_get_record.transactionID.CopyFrom(self.transaction_id._to_proto())
110142
transaction_get_record.includeDuplicates = self.include_duplicates
143+
transaction_get_record.include_child_records = self.include_children
111144

112145
query = query_pb2.Query()
113146
query.transactionGetRecord.CopyFrom(transaction_get_record)
@@ -236,9 +269,13 @@ def _map_status_error(
236269
return PrecheckError(status)
237270

238271
receipt = response.transactionGetRecord.transactionRecord.receipt
239-
240-
return ReceiptStatusError(status, self.transaction_id, TransactionReceipt._from_proto(receipt, self.transaction_id))
241-
272+
273+
return ReceiptStatusError(
274+
status,
275+
self.transaction_id,
276+
TransactionReceipt._from_proto(receipt, self.transaction_id),
277+
)
278+
242279
def execute(self, client: Client, timeout: Optional[Union[int, float]] = None):
243280
"""
244281
Executes the transaction record query.
@@ -263,14 +300,24 @@ def execute(self, client: Client, timeout: Optional[Union[int, float]] = None):
263300
self._before_execute(client)
264301
response = self._execute(client, timeout)
265302
primary_proto = response.transactionGetRecord.transactionRecord
303+
children = []
266304
if self.include_duplicates:
267305
duplicates = self._map_record_list(
268306
response.transactionGetRecord.duplicateTransactionRecords
269307
)
270308
else:
271309
duplicates = []
310+
311+
if self.include_children:
312+
children = self._map_record_list(
313+
response.transactionGetRecord.child_transaction_records
314+
)
315+
272316
return TransactionRecord._from_proto(
273-
primary_proto, transaction_id=self.transaction_id, duplicates=duplicates
317+
primary_proto,
318+
transaction_id=self.transaction_id,
319+
duplicates=duplicates,
320+
children=children,
274321
)
275322

276323
def _get_query_response(self, response: Any):

src/hiero_sdk_python/transaction/transaction_record.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ class TransactionRecord:
5858
prng_bytes (Optional[bytes]): A pseudo-random byte array generated by the network (if applicable).
5959
duplicates (list[TransactionRecord]): A list of duplicate transaction records returned when queried
6060
with include_duplicates=True. Empty by default.
61+
62+
children (list[TransactionRecord]): A list of children transaction records returned when queried
63+
with include_children=True. Empty by default.
6164
"""
6265

6366
transaction_id: Optional[TransactionId] = None
@@ -75,6 +78,7 @@ class TransactionRecord:
7578
prng_number: Optional[int] = None
7679
prng_bytes: Optional[bytes] = None
7780
duplicates: list['TransactionRecord'] = field(default_factory=list)
81+
children: list['TransactionRecord'] = field(default_factory=list)
7882

7983
def __repr__(self) -> str:
8084
"""Returns a human-readable string representation of the TransactionRecord.
@@ -107,14 +111,16 @@ def __repr__(self) -> str:
107111
f"call_result={self.call_result}, "
108112
f"prng_number={self.prng_number}, "
109113
f"prng_bytes={self.prng_bytes}, "
110-
f"duplicates_count={len(self.duplicates)})")
114+
f"duplicates_count={len(self.duplicates)}, "
115+
f"children_count={len(self.children)})")
111116

112117
@classmethod
113118
def _from_proto(
114119
cls,
115120
proto: transaction_record_pb2.TransactionRecord,
116121
transaction_id: Optional[TransactionId] = None,
117122
duplicates: Optional[list['TransactionRecord']] = None,
123+
children: Optional[list['TransactionRecord']] = None,
118124
) -> 'TransactionRecord':
119125
"""Creates a TransactionRecord instance from a protobuf transaction record.
120126
@@ -136,12 +142,15 @@ def _from_proto(
136142
transaction_id: The transaction ID to associate with this record (required).
137143
duplicates: Optional list of duplicate transaction records to attach.
138144
Defaults to an empty list.
145+
children: Optional list of child transaction records to attach.
146+
Defaults to an empty list.
139147
140148
Returns:
141149
TransactionRecord: A new instance containing all processed and structured data.
142150
"""
143151
tx_id = cls._resolve_transaction_id(proto, transaction_id)
144152
duplicates = duplicates or []
153+
children = children or []
145154

146155
token_transfers, nft_transfers = cls._parse_token_transfers(proto)
147156
transfers = cls._parse_hbar_transfers(proto)
@@ -162,6 +171,7 @@ def _from_proto(
162171
prng_number=proto.prng_number,
163172
prng_bytes=proto.prng_bytes,
164173
duplicates=duplicates,
174+
children=children,
165175
)
166176

167177
@staticmethod
@@ -294,4 +304,4 @@ def _to_proto(self) -> transaction_record_pb2.TransactionRecord:
294304
record_proto.new_pending_airdrops.add().CopyFrom(pending_airdrop._to_proto())
295305

296306
return record_proto
297-
307+

tests/integration/transaction_record_query_e2e_test.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22
import os
3-
from hiero_sdk_python import Client, TransactionRecord
3+
from hiero_sdk_python import AccountId, Client, TransactionRecord
44
from hiero_sdk_python.crypto.private_key import PrivateKey
55
from hiero_sdk_python.hbar import Hbar
66
from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction
@@ -16,9 +16,29 @@
1616
IntegrationTestEnv,
1717
create_fungible_token,
1818
create_nft_token,
19+
env,
1920
)
2021

2122

23+
def _submit_alias_auto_create_transfer(env: IntegrationTestEnv):
24+
"""Submit a transfer to an EVM alias to trigger child auto-account creation."""
25+
alias_key = PrivateKey.generate_ecdsa()
26+
alias_account_id = AccountId.from_evm_address(
27+
alias_key.public_key().to_evm_address(), 0, 0
28+
)
29+
30+
transaction = (
31+
TransferTransaction()
32+
.add_hbar_transfer(alias_account_id, Hbar(1).to_tinybars())
33+
.add_hbar_transfer(env.operator_id, Hbar(-1).to_tinybars())
34+
)
35+
receipt = transaction.execute(env.client)
36+
37+
assert receipt.status == ResponseCode.SUCCESS
38+
39+
return transaction.transaction_id
40+
41+
2242
@pytest.mark.integration
2343
def test_transaction_record_query_can_execute():
2444
env = IntegrationTestEnv()
@@ -54,6 +74,40 @@ def test_transaction_record_query_can_execute():
5474
env.close()
5575

5676

77+
@pytest.mark.integration
78+
def test_transaction_record_query_include_children_returns_child_records(env):
79+
"""Querying an alias auto-create parent record should return parsed child records."""
80+
parent_transaction_id = _submit_alias_auto_create_transfer(env)
81+
parent_account_id = parent_transaction_id.account_id
82+
83+
parent_record = (
84+
TransactionRecordQuery()
85+
.set_transaction_id(parent_transaction_id)
86+
.set_include_children(True)
87+
.execute(env.client)
88+
)
89+
90+
assert parent_record.transaction_id == parent_transaction_id
91+
assert parent_record.receipt.status == ResponseCode.SUCCESS
92+
assert len(parent_record.children) > 0
93+
assert parent_record.transfers[parent_account_id] < 0
94+
95+
child_record = parent_record.children[0]
96+
created_account_id = child_record.receipt.account_id
97+
98+
assert isinstance(child_record, TransactionRecord)
99+
assert child_record.receipt.status == ResponseCode.SUCCESS
100+
assert child_record.transaction_id == parent_transaction_id
101+
assert created_account_id is not None
102+
assert created_account_id.shard == 0
103+
assert created_account_id.realm == 0
104+
assert created_account_id.num > 0
105+
assert child_record.transaction_hash != parent_record.transaction_hash
106+
assert child_record.transaction_memo == ""
107+
assert child_record.children == []
108+
assert child_record.duplicates == []
109+
110+
57111
@pytest.mark.integration
58112
def test_transaction_record_query_can_execute_nft_transfer():
59113
env = IntegrationTestEnv()
@@ -274,4 +328,4 @@ def test_query_with_include_duplicates():
274328
# print(f"Found {len(record.duplicates)} duplicates") # for debug
275329
finally:
276330
env.close()
277-
331+

0 commit comments

Comments
 (0)