Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
- Format `tests/unit/custom_fee_test.py` with black for code style consistency. (#1525)

### Added
- Added support for include_children to the TransactionRecordQuery class
- Added new members to the mentor roster. (#1693)
- Added support for the `includeDuplicates` flag in `TransactionRecordQuery` and `duplicates` field in `TransactionRecord` (#1635)
- Added logging in bot-gfi-assign-on-comment.js to prevent silent skips. (`#1668`)
Expand Down Expand Up @@ -250,6 +251,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
- Enhance TopicInfo `__str__` method and tests with additional coverage, and update the format_key function in `key_format.py` to handle objects with a \_to_proto method.
- Update changelog workflow to trigger automatically on pull requests instead of manual dispatch (#1567)
- Formatted key-related unit test files (`key_utils_test.py`, `test_key_format.py`, `test_key_list.py`) using the black formatter
- chore: update maintainer guidelines link in MAINTAINERS.md (#1605)
- Add return type hint to `ContractId.__str__`. (#1654)
- chore: update maintainer guidelines link in MAINTAINERS.md (#1605)
- chore: update merge conflict bot message with web editor tips (#1592)
Expand Down
33 changes: 33 additions & 0 deletions examples/transaction/transaction_record_with_children.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import os
from hiero_sdk_python.client import Client
from hiero_sdk_python.query.transaction_record_query import TransactionRecordQuery

def main():
# Setup client
client = Client.for_testnet()
# ... (add operator credentials) ...

# 1. Execute a transaction that is likely to have child records
# (e.g., a complex Smart Contract call)
print("Executing transaction...")

# 2. Query the record and explicitly request children
tx_id = "..." # your transaction id

query = (
TransactionRecordQuery()
.set_transaction_id(tx_id)
.set_include_children(True) # The new feature!
)

record = query.execute(client)

# 3. Demonstrate accessing the children
print(f"Parent Transaction ID: {record.transaction_id}")
print(f"Number of child records found: {len(record.children)}")

for i, child in enumerate(record.children):
print(f"Child {i+1} Status: {child.receipt.status}")

Comment on lines +23 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, find and read the file in question
fd "transaction_record_with_children.py" --type f

Repository: hiero-ledger/hiero-sdk-python

Length of output: 129


🏁 Script executed:

# Check if the file exists and read its content
if [ -f "examples/transaction/transaction_record_with_children.py" ]; then
  wc -l examples/transaction/transaction_record_with_children.py
  echo "---"
  cat -n examples/transaction/transaction_record_with_children.py
else
  echo "File not found"
fi

Repository: hiero-ledger/hiero-sdk-python

Length of output: 1383


🏁 Script executed:

# Verify ResponseCode exists in hiero_sdk_python
find . -type f -name "*.py" | xargs grep -l "class ResponseCode" | head -5

Repository: hiero-ledger/hiero-sdk-python

Length of output: 112


🏁 Script executed:

# Check how ResponseCode is imported in other example files
find examples -type f -name "*.py" -exec grep -l "ResponseCode" {} \; | head -5

Repository: hiero-ledger/hiero-sdk-python

Length of output: 3862


🏁 Script executed:

# Look for patterns of receipt.status usage in examples
rg "receipt\.status" examples --type py -B 2 -A 2 | head -40

Repository: hiero-ledger/hiero-sdk-python

Length of output: 3540


🏁 Script executed:

# Check how ResponseCode is imported in those example files
head -20 examples/tokens/token_fee_schedule_update_transaction_fungible.py | grep -E "^(import|from)" | head -10

Repository: hiero-ledger/hiero-sdk-python

Length of output: 498


🏁 Script executed:

# Check if there's a general pattern for error handling in examples
head -30 examples/tokens/token_create_transaction_token_fee_schedule_key.py | grep -E "^(import|from)" | head -10

Repository: hiero-ledger/hiero-sdk-python

Length of output: 454


🏁 Script executed:

# Verify the ResponseCode import path can be used as suggested
head -5 src/hiero_sdk_python/response_code.py

Repository: hiero-ledger/hiero-sdk-python

Length of output: 154


🏁 Script executed:

# Check other transaction_record examples for patterns
find examples -name "*transaction_record*" -type f

Repository: hiero-ledger/hiero-sdk-python

Length of output: 172


🏁 Script executed:

# Look at how examples handle query results without receipt checks (queries vs transactions)
rg "\.execute\(client\)" examples/query --type py -A 3 | head -20

Repository: hiero-ledger/hiero-sdk-python

Length of output: 988


🏁 Script executed:

# Check the other transaction_record query example
cat -n examples/query/transaction_record_query.py | head -40

Repository: hiero-ledger/hiero-sdk-python

Length of output: 1536


🏁 Script executed:

# Verify ResponseCode is used consistently in query examples
rg "ResponseCode" examples/query --type py -B 1 -A 1 | head -30

Repository: hiero-ledger/hiero-sdk-python

Length of output: 2088


Validate child receipt status using ResponseCode enum.

Line 30 prints the raw numeric status without converting to ResponseCode, making it unclear to users whether children succeeded or failed. This contradicts the pattern used throughout examples where all receipt statuses are validated against ResponseCode.SUCCESS.

✅ Suggested improvement
 from hiero_sdk_python.query.transaction_record_query import TransactionRecordQuery
+from hiero_sdk_python.response_code import ResponseCode
@@
     for i, child in enumerate(record.children):
-        print(f"Child {i+1} Status: {child.receipt.status}")
+        status = ResponseCode(child.receipt.status)
+        print(f"Child {i+1} Status: {status.name}")
+        if status != ResponseCode.SUCCESS:
+            print(f"Warning: Child {i+1} failed with status {status.name}")

if __name__ == "__main__":
main()
73 changes: 73 additions & 0 deletions src/hiero_sdk_python/query/transaction_record_query.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from typing import Optional, Any, Union, List
from typing import Optional, Any, Union
from hiero_sdk_python.hapi.services import (
query_header_pb2,
transaction_get_record_pb2,
query_pb2,
)
transaction_record_pb2,
)
Comment on lines +1 to 9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

File has unresolved merge-conflict residue — will not parse as valid Python.

The file contains duplicate/conflicting code from what appears to be an incomplete merge with the include_duplicates feature branch. Multiple constructs are duplicated:

  • Two typing imports (lines 1–2)
  • Two __init__ signatures (lines 27–33)
  • Two _make_request implementations (lines 102–143 vs 148–160)
  • Two _map_record_list definitions (lines 162–188 vs 292–300)
  • Two execute methods (lines 302–336 vs 306–347)
  • Two set_transaction_id definitions (lines 41–43 vs 74–96)

Ruff's static analysis confirms numerous syntax errors throughout the file. The entire file needs to be properly rebased/merged against main before it can be reviewed meaningfully. The two feature branches (include_children and include_duplicates) need to be combined into a single coherent implementation.

🧰 Tools
🪛 Ruff (0.14.14)

[warning] 8-8: Unexpected indentation

(invalid-syntax)


[warning] 9-9: Expected a statement

(invalid-syntax)

from hiero_sdk_python.client.client import Client
Expand All @@ -25,12 +27,20 @@
def __init__(
self,
transaction_id: Optional[TransactionId] = None,
include_children: bool = False,
):
include_duplicates: bool = False,
) -> None:
"""
Initializes the TransactionRecordQuery with the provided transaction ID.
"""
super().__init__()
self.transaction_id: Optional[TransactionId] = transaction_id
self.include_children = include_children
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing type hint

Suggested change
self.include_children = include_children
self.include_children: Optional[bool] = include_children


def set_transaction_id(self, transaction_id: TransactionId):

Check warning on line 41 in src/hiero_sdk_python/query/transaction_record_query.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/hiero_sdk_python/query/transaction_record_query.py#L41

Method set_transaction_id has 90 lines of code (limit is 80)
"""
Sets the transaction ID for the query.
if not isinstance(include_duplicates, bool):
raise TypeError(
f"include_duplicates must be a bool (True or False), got {type(include_duplicates).__name__}"
Expand Down Expand Up @@ -85,6 +95,10 @@
self.transaction_id = transaction_id
return self

def set_include_children(self, include_children):
self.include_children = include_children
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor suggestion consider adding a docstring

return self
Comment on lines +98 to +100
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

set_include_children lacks input validation, inconsistent with set_include_duplicates.

set_include_duplicates (lines 57–72) validates its input with isinstance(include_duplicates, bool) and raises TypeError for invalid types. set_include_children does no validation at all, creating an inconsistency.

     def set_include_children(self, include_children):
+        if not isinstance(include_children, bool):
+            raise TypeError(
+                f"include_children must be a boolean, got {type(include_children).__name__}"
+            )
         self.include_children = include_children
         return self

As per coding guidelines, "Ensure naming matches existing query patterns" — the validation pattern used by set_include_duplicates should be followed here.


def _make_request(self):
"""
Constructs the protobuf request for the transaction record query.
Expand All @@ -100,6 +114,37 @@
AttributeError: If the Query protobuf structure is invalid.
Exception: If any other error occurs during request construction.
"""
try:
if not self.transaction_id:
raise ValueError(
"Transaction ID must be set before making the request."
)

query_header = self._make_request_header()
transaction_get_record = (
transaction_get_record_pb2.TransactionGetRecordQuery()
)
transaction_get_record.header.CopyFrom(query_header)

transaction_get_record.transactionID.CopyFrom(
self.transaction_id._to_proto()
)
transaction_get_record.include_child_records = self.include_children
query = query_pb2.Query()
if not hasattr(query, "transactionGetRecord"):
raise AttributeError(
"Query object has no attribute 'transactionGetRecord'"
)
query.transactionGetRecord.CopyFrom(transaction_get_record)

return query
except Exception as e:
print(f"Exception in _make_request: {e}")
raise
Comment on lines +117 to +143
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Remove try/except + print() from _make_request — introduces side effects.

Production query code must not use print() (per Review Focus 6: "Introduces side effects (logging, prints, stack traces)"). The try/except block at lines 141–143 catches all exceptions, prints to stdout, and re-raises — this adds no value and pollutes output. Additionally, the hasattr check at line 134 is unnecessary defensive coding for a well-known protobuf type.

Proposed simplified _make_request
     def _make_request(self):
-        try:
-            if not self.transaction_id:
-                raise ValueError(
-                    "Transaction ID must be set before making the request."
-                )
-
-            query_header = self._make_request_header()
-            transaction_get_record = (
-                transaction_get_record_pb2.TransactionGetRecordQuery()
-            )
-            transaction_get_record.header.CopyFrom(query_header)
-
-            transaction_get_record.transactionID.CopyFrom(
-                self.transaction_id._to_proto()
-            )
-            transaction_get_record.include_child_records = self.include_children
-            query = query_pb2.Query()
-            if not hasattr(query, "transactionGetRecord"):
-                raise AttributeError(
-                    "Query object has no attribute 'transactionGetRecord'"
-                )
-            query.transactionGetRecord.CopyFrom(transaction_get_record)
-
-            return query
-        except Exception as e:
-            print(f"Exception in _make_request: {e}")
-            raise
+        if self.transaction_id is None:
+            raise ValueError("Transaction ID must be set before making the request.")
+
+        query_header = self._make_request_header()
+        transaction_get_record = transaction_get_record_pb2.TransactionGetRecordQuery()
+        transaction_get_record.header.CopyFrom(query_header)
+        transaction_get_record.transactionID.CopyFrom(self.transaction_id._to_proto())
+        transaction_get_record.include_child_records = self.include_children
+        transaction_get_record.includeDuplicates = self.include_duplicates
+
+        query = query_pb2.Query()
+        query.transactionGetRecord.CopyFrom(transaction_get_record)
+        return query

As per coding guidelines, Review Focus 6: "Introduces side effects (logging, prints, stack traces)" should be flagged. Review Focus 3: "_make_request() MUST... Call _make_request_header() exactly once... Avoid manual QueryHeader mutation".


def _get_method(self, channel: _Channel) -> _Method:
"""
Returns the appropriate gRPC method for the transaction receipt query.
if self.transaction_id is None:
raise ValueError("Transaction ID must be set before making the request.")

Expand Down Expand Up @@ -189,6 +234,7 @@
== query_header_pb2.ResponseType.COST_ANSWER
):
return _ExecutionState.FINISHED
pass
elif (
status in retryable_statuses
or status == ResponseCode.PLATFORM_TRANSACTION_NOT_CREATED
Expand Down Expand Up @@ -236,6 +282,24 @@
return PrecheckError(status)

receipt = response.transactionGetRecord.transactionRecord.receipt

return ReceiptStatusError(
status,
self.transaction_id,
TransactionReceipt._from_proto(receipt, self.transaction_id),
)

def _map_record_list(
self,
proto_records: List[transaction_get_record_pb2.TransactionGetRecordResponse],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

must be

Suggested change
proto_records: List[transaction_get_record_pb2.TransactionGetRecordResponse],
proto_records: List[transaction_record_pb2.TransactionRecord],

) -> List[TransactionRecord]:
records: List[TransactionRecord] = []
for record in proto_records:
records.append(TransactionRecord._from_proto(record, self.transaction_id))

return records

def execute(self, client):

return ReceiptStatusError(status, self.transaction_id, TransactionReceipt._from_proto(receipt, self.transaction_id))

Expand All @@ -261,6 +325,15 @@
ReceiptStatusError: If the transaction record contains an error status
"""
self._before_execute(client)
response = self._execute(client)
if not response.HasField("transactionGetRecord"):
raise AttributeError("Response does not contain 'transactionGetRecord'")
record_response = response.transactionGetRecord
children = self._map_record_list(record_response.child_transaction_records)
return TransactionRecord._from_proto(
response.transactionGetRecord.transactionRecord,
self.transaction_id,
children=children,
response = self._execute(client, timeout)
primary_proto = response.transactionGetRecord.transactionRecord
if self.include_duplicates:
Expand Down
88 changes: 74 additions & 14 deletions src/hiero_sdk_python/transaction/transaction_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from collections import defaultdict
from dataclasses import dataclass, field
from typing import Optional
from typing import Optional, List

from hiero_sdk_python.account.account_id import AccountId
from hiero_sdk_python.contract.contract_function_result import ContractFunctionResult
Expand All @@ -36,7 +36,7 @@ class TransactionRecord:
Represents a record of a completed transaction on the Hiero network.
This class combines detailed information about the a transaction including the
transaction ID, receipt, token and NFT transfers, fees & other
metadata such as pseudo-random number generation(PRNG) results and
metadata such as pseudo-random number generation(PRNG) results and
pending airdrop records.

Attributes:
Expand All @@ -46,16 +46,17 @@ class TransactionRecord:
transaction_fee (Optional[int]): The total network fee (in tinybars) charged for the transaction.
receipt (Optional[TransactionReceipt]): The receipt summarizing the outcome and status of the transaction.
call_result (Optional[ContractFunctionResult]): The result of a contract call if the transaction was a smart contract execution.
token_transfers (defaultdict[TokenId, defaultdict[AccountId, int]]):
A mapping of token IDs to account-level transfer amounts.
Represents fungible token movements within the transaction.
nft_transfers (defaultdict[TokenId, list[TokenNftTransfer]]):
token_transfers (defaultdict[TokenId, defaultdict[AccountId, int]]):
A mapping of token IDs to account-level transfer amounts.
Represents fungible token movements within the transaction.
nft_transfers (defaultdict[TokenId, list[TokenNftTransfer]]):
A mapping of token IDs to lists of NFT transfers for that token.
transfers (defaultdict[AccountId, int]): A mapping of account IDs to hbar transfer amounts (positive for credit, negative for debit).
new_pending_airdrops (list[PendingAirdropRecord]):A list of new airdrop records created by this transaction.

prng_number (Optional[int]): A pseudo-random integer generated by the network (if applicable).
prng_bytes (Optional[bytes]): A pseudo-random byte array generated by the network (if applicable).

duplicates (list[TransactionRecord]): A list of duplicate transaction records returned when queried
with include_duplicates=True. Empty by default.
"""
Expand All @@ -67,23 +68,30 @@ class TransactionRecord:
receipt: Optional[TransactionReceipt] = None
call_result: Optional[ContractFunctionResult] = None

token_transfers: defaultdict[TokenId, defaultdict[AccountId, int]] = field(default_factory=lambda: defaultdict(lambda: defaultdict(int)))
nft_transfers: defaultdict[TokenId, list[TokenNftTransfer]] = field(default_factory=lambda: defaultdict(list[TokenNftTransfer]))
transfers: defaultdict[AccountId, int] = field(default_factory=lambda: defaultdict(int))
token_transfers: defaultdict[TokenId, defaultdict[AccountId, int]] = field(
default_factory=lambda: defaultdict(lambda: defaultdict(int))
)
nft_transfers: defaultdict[TokenId, list[TokenNftTransfer]] = field(
default_factory=lambda: defaultdict(list[TokenNftTransfer])
)
transfers: defaultdict[AccountId, int] = field(
default_factory=lambda: defaultdict(int)
)
new_pending_airdrops: list[PendingAirdropRecord] = field(default_factory=list)

prng_number: Optional[int] = None
prng_bytes: Optional[bytes] = None
children: List[TransactionId] = None
duplicates: list['TransactionRecord'] = field(default_factory=list)

def __repr__(self) -> str:
"""Returns a human-readable string representation of the TransactionRecord.
This method constructs a detailed string containing all significant fields of the

This method constructs a detailed string containing all significant fields of the
transaction record including transaction ID, hash, memo, fees, status, transfers,
and PRNG results. For the receipt status, it attempts to resolve the numeric status
to a human-readable ResponseCode name.

Returns:
str: A string representation showing all significant fields of the TransactionRecord.
"""
Expand All @@ -95,6 +103,20 @@ def __repr__(self) -> str:
status = ResponseCode(self.receipt.status).name
except (ValueError, AttributeError):
status = self.receipt.status
return (
f"TransactionRecord(transaction_id='{self.transaction_id}', "
f"transaction_hash={self.transaction_hash}, "
f"transaction_memo='{self.transaction_memo}', "
f"transaction_fee={self.transaction_fee}, "
f"receipt_status='{status}', "
f"token_transfers={dict(self.token_transfers)}, "
f"nft_transfers={dict(self.nft_transfers)}, "
f"transfers={dict(self.transfers)}, "
f"new_pending_airdrops={list(self.new_pending_airdrops)}, "
f"call_result={self.call_result}, "
f"prng_number={self.prng_number}, "
f"prng_bytes={self.prng_bytes})"
)
return (f"TransactionRecord(transaction_id='{self.transaction_id}', "
f"transaction_hash={self.transaction_hash}, "
f"transaction_memo='{self.transaction_memo}', "
Expand All @@ -114,6 +136,8 @@ def _from_proto(
cls,
proto: transaction_record_pb2.TransactionRecord,
transaction_id: Optional[TransactionId] = None,
children=None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add type hinting children: Optional[List["TransactionRecord"]]=[]

) -> "TransactionRecord":
duplicates: Optional[list['TransactionRecord']] = None,
) -> 'TransactionRecord':
"""Creates a TransactionRecord instance from a protobuf transaction record.
Expand All @@ -132,6 +156,11 @@ def _from_proto(
into appropriate defaultdict collections for efficient access.

Args:
proto (transaction_record_pb2.TransactionRecord): The raw protobuf
transaction record containing all transaction data.
transaction_id (Optional[TransactionId]): Optional transaction ID to
associate with the record. If not provided, will be extracted from
the protobuf message if available.
proto: The raw protobuf transaction record containing all transaction data.
transaction_id: The transaction ID to associate with this record (required).
duplicates: Optional list of duplicate transaction records to attach.
Expand All @@ -140,6 +169,28 @@ def _from_proto(
Returns:
TransactionRecord: A new instance containing all processed and structured data.
"""
token_transfers = defaultdict(lambda: defaultdict(int))
for token_transfer_list in proto.tokenTransferLists:
token_id = TokenId._from_proto(token_transfer_list.token)
for transfer in token_transfer_list.transfers:
account_id = AccountId._from_proto(transfer.accountID)
token_transfers[token_id][account_id] = transfer.amount

nft_transfers = defaultdict(list[TokenNftTransfer])
for token_transfer_list in proto.tokenTransferLists:
token_id = TokenId._from_proto(token_transfer_list.token)
nft_transfers[token_id] = TokenNftTransfer._from_proto(token_transfer_list)

transfers = defaultdict(int)
for transfer in proto.transferList.accountAmounts:
account_id = AccountId._from_proto(transfer.accountID)
transfers[account_id] += transfer.amount

new_pending_airdrops: list[PendingAirdropRecord] = []
for pending_airdrop in proto.new_pending_airdrops:
new_pending_airdrops.append(
PendingAirdropRecord._from_proto(pending_airdrop)
)
tx_id = cls._resolve_transaction_id(proto, transaction_id)
duplicates = duplicates or []

Expand All @@ -148,6 +199,7 @@ def _from_proto(
new_pending_airdrops = cls._parse_pending_airdrops(proto)
call_result = cls._parse_contract_call_result(proto)

children: Optional[List["TransactionRecord"]] = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this line can be romove

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical bug: children parameter is overwritten to None before use.

Line 202 declares children: Optional[List["TransactionRecord"]] = None, which shadows and discards the children parameter passed into _from_proto at line 139. This means the children field on the constructed TransactionRecord will always be None, completely breaking the include_children feature.

Remove this line so the children parameter flows through to the constructor:

-        children: Optional[List["TransactionRecord"]] = None
         return cls(
             transaction_id=tx_id,
             ...
             children=children,

return cls(
transaction_id=tx_id,
transaction_hash=proto.transactionHash,
Expand All @@ -158,6 +210,12 @@ def _from_proto(
nft_transfers=nft_transfers,
transfers=transfers,
new_pending_airdrops=new_pending_airdrops,
children=children,
call_result=(
ContractFunctionResult._from_proto(proto.contractCallResult)
if proto.HasField("contractCallResult")
else None
),
call_result=call_result,
prng_number=proto.prng_number,
prng_bytes=proto.prng_bytes,
Expand Down Expand Up @@ -291,7 +349,9 @@ def _to_proto(self) -> transaction_record_pb2.TransactionRecord:
transfer.amount = amount

for pending_airdrop in self.new_pending_airdrops:
record_proto.new_pending_airdrops.add().CopyFrom(pending_airdrop._to_proto())
record_proto.new_pending_airdrops.add().CopyFrom(
pending_airdrop._to_proto()
)

return record_proto

Loading