Skip to content

feat: transaction record query include duplicates#1646

Closed
gangulysiddhartha22-cmyk wants to merge 6 commits intohiero-ledger:mainfrom
gangulysiddhartha22-cmyk:feature/1635-transaction-record-query-include-duplicates
Closed

feat: transaction record query include duplicates#1646
gangulysiddhartha22-cmyk wants to merge 6 commits intohiero-ledger:mainfrom
gangulysiddhartha22-cmyk:feature/1635-transaction-record-query-include-duplicates

Conversation

@gangulysiddhartha22-cmyk
Copy link
Contributor

Description:
Add support for the includeDuplicates flag

Related issue(s):

Fixes #1635

Notes for reviewer:

  • No breaking changes
  • Unit/Integration tests added

Checklist

  • Documented (Code comments, README, etc.)
  • Tested (unit, integration, etc.)

Signed-off-by: Siddhartha Ganguly <gangulysiddhartha22@gmail.com>
Signed-off-by: gangulysiddhartha22-cmyk <gangulysiddhartha22@gmail.com>
@gangulysiddhartha22-cmyk gangulysiddhartha22-cmyk changed the title feature: transaction record query include duplicates feat: transaction record query include duplicates Jan 30, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 30, 2026

Walkthrough

This PR adds support for the include_duplicates feature to TransactionRecordQuery. It introduces a new field and setter method to enable requesting duplicate transaction records, updates the TransactionRecord class to store duplicates, and adds comprehensive unit and integration tests.

Changes

Cohort / File(s) Summary
Core Query Implementation
src/hiero_sdk_python/query/transaction_record_query.py
Added include_duplicates parameter to __init__ with default False. Added set_include_duplicates() setter with chaining support and type validation. Implemented _map_record_list() to convert proto records to TransactionRecord objects. Updated _make_request() to set transactionGetRecord.includeDuplicates. Updated execute() to build and pass duplicates list to TransactionRecord._from_proto. Updated _map_status_error() signature to accept response parameter.
Core Transaction Record
src/hiero_sdk_python/transaction/transaction_record.py
Added public duplicates: list['TransactionRecord'] field with default empty list. Extended _from_proto() signature to accept optional duplicates parameter and propagate to instance. Updated __repr__ to include duplicates_count in output.
Changelog
CHANGELOG.md
Added entry documenting support for includeDuplicates in TransactionRecordQuery (#1635).
Unit Tests
tests/unit/transaction_record_test.py, tests/unit/transaction_record_query_test.py
Added tests validating duplicates field existence and type. Added unit tests for set_include_duplicates() chaining and validation. Added tests for _make_request() protobuf construction with includeDuplicates. Added tests for _map_record_list() conversion logic and backward compatibility of _from_proto() with duplicates parameter.
Integration Tests
tests/integration/transaction_record_query_e2e_test.py
Added new integration test creating an account and querying with include_duplicates enabled, verifying response contains TransactionRecord with matching transaction_id and duplicates list.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Query as TransactionRecordQuery
    participant Client
    participant CryptoAPI as Crypto Service
    participant Response as Proto Response
    participant Mapper as _map_record_list()
    participant TxRecord as TransactionRecord

    User->>Query: set_transaction_id(tx_id)
    User->>Query: set_include_duplicates(True)
    User->>Client: execute(query)
    Client->>Query: execute(client)
    Query->>CryptoAPI: getTxRecordByTxID (with includeDuplicates=True)
    CryptoAPI-->>Response: Returns record + duplicateTransactionRecords
    Query->>Mapper: _map_record_list(duplicateTransactionRecords)
    loop For each duplicate proto record
        Mapper->>TxRecord: _from_proto(proto, transaction_id)
        TxRecord-->>Mapper: TransactionRecord instance
    end
    Mapper-->>Query: List[TransactionRecord]
    Query->>TxRecord: _from_proto(main_record, transaction_id, duplicates=[...])
    TxRecord-->>Query: TransactionRecord with duplicates
    Query-->>Client: TransactionRecord
    Client-->>User: Result with duplicates list
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: transaction record query include duplicates' clearly summarizes the main change—adding support for the include_duplicates flag to TransactionRecordQuery.
Description check ✅ Passed The description is related to the changeset, mentioning the includeDuplicates flag support, references the linked issue #1635, and notes tests and documentation.
Linked Issues check ✅ Passed All coding requirements from issue #1635 are met: include_duplicates field and setter added [#1635], duplicates field in TransactionRecord added [#1635], _map_record_list logic implemented [#1635], protobuf flag set in _make_request [#1635], _from_proto and execute updated [#1635], and unit/integration tests added [#1635].
Out of Scope Changes check ✅ Passed All changes directly support the include_duplicates feature implementation: CHANGELOG entry, query logic, record mapping, data model extension, and comprehensive test coverage align with issue #1635 scope.
Docstring Coverage ✅ Passed Docstring coverage is 92.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/hiero_sdk_python/query/transaction_record_query.py (1)

19-27: ⚠️ Potential issue | 🔴 Critical

Constructor references undefined include_duplicates.

include_duplicates isn’t a parameter, so __init__ will raise NameError on instantiation. Add a parameter (default False) or set explicitly.

🛠️ Proposed fix
-    def __init__(self, transaction_id: Optional[TransactionId] = None):
+    def __init__(
+        self,
+        transaction_id: Optional[TransactionId] = None,
+        include_duplicates: bool = False,
+    ):
         """
         Initializes the TransactionRecordQuery with the provided transaction ID.
         """
         super().__init__()
         self.transaction_id : Optional[TransactionId] = transaction_id
-        self.include_duplicates: bool = bool(include_duplicates)
+        self.include_duplicates: bool = bool(include_duplicates)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

@gangulysiddhartha22-cmyk gangulysiddhartha22-cmyk force-pushed the feature/1635-transaction-record-query-include-duplicates branch from 903c0a0 to 7a9de74 Compare January 30, 2026 09:26
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/hiero_sdk_python/transaction/transaction_record.py (1)

113-141: 🧹 Nitpick | 🔵 Trivial

Update docstring to document the duplicates parameter.

The _from_proto method now accepts a duplicates parameter, but the docstring's Args section doesn't document it. This could cause confusion for future maintainers.

📝 Proposed docstring update
         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.
+            duplicates (Optional[list['TransactionRecord']]): Optional list of
+                duplicate transaction records to associate with this record. 
+                Defaults to an empty list if not provided.
tests/integration/transaction_record_query_e2e_test.py (1)

103-103: ⚠️ Potential issue | 🟡 Minor

Missing @pytest.mark.integration decorator.

The test_transaction_record_query_can_execute_fungible_transfer test is missing the @pytest.mark.integration marker, which may cause it to run in unit test suites unexpectedly.

🔧 Proposed fix
+@pytest.mark.integration
 def test_transaction_record_query_can_execute_fungible_transfer():

@gangulysiddhartha22-cmyk gangulysiddhartha22-cmyk force-pushed the feature/1635-transaction-record-query-include-duplicates branch from 7a9de74 to 6a947f0 Compare January 30, 2026 10:13
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/unit/transaction_record_test.py (1)

70-83: 🧹 Nitpick | 🔵 Trivial

Add assertion for duplicates in default initialization test.

Per coding guidelines, tests should protect against breaking changes by asserting public attributes exist. Add an assertion for the new duplicates field.

♻️ Proposed addition
 def test_transaction_record_default_initialization():
     """Test the default initialization of the TransactionRecord class"""
     record = TransactionRecord()
     assert record.transaction_id is None
     assert record.transaction_hash is None
     assert record.transaction_memo is None
     assert record.transaction_fee is None
     assert record.receipt is None
     assert record.token_transfers == defaultdict(lambda: defaultdict(int))
     assert record.nft_transfers == defaultdict(list[TokenNftTransfer])
     assert record.transfers == defaultdict(int)
     assert record.new_pending_airdrops == []
     assert record.prng_number is None
     assert record.prng_bytes is None
+    assert hasattr(record, 'duplicates'), "TransactionRecord should have duplicates attribute"
+    assert record.duplicates == []

As per coding guidelines: "Assert public attributes exist (e.g., assert hasattr(obj, 'account_id'))."

@gangulysiddhartha22-cmyk gangulysiddhartha22-cmyk force-pushed the feature/1635-transaction-record-query-include-duplicates branch from 6a947f0 to b67b068 Compare January 30, 2026 10:41
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Signed-off-by: Siddhartha Ganguly <gangulysiddhartha22@gmail.com>
@gangulysiddhartha22-cmyk gangulysiddhartha22-cmyk force-pushed the feature/1635-transaction-record-query-include-duplicates branch from b67b068 to 4b9b909 Compare January 30, 2026 10:53
@gangulysiddhartha22-cmyk
Copy link
Contributor Author

@manishdait Ready for review. Thanks!

Copy link
Contributor

@manishdait manishdait left a comment

Choose a reason for hiding this comment

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

@gangulysiddhartha22-cmyk, thanks, I’ve added the requested changes.

@exploreriii exploreriii requested a review from a team January 30, 2026 16:15
Signed-off-by: Siddhartha Ganguly <gangulysiddhartha22@gmail.com>
@gangulysiddhartha22-cmyk
Copy link
Contributor Author

@manishdait all your suggestions have been implemented.

Copy link
Contributor

@prajeeta15 prajeeta15 left a comment

Choose a reason for hiding this comment

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

this looks good to me . but I would ask @manishdait to review it too for confirmation and additional changes.

Copy link
Contributor

@aceppaluni aceppaluni left a comment

Choose a reason for hiding this comment

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

This LGTM!

Further feedback from @manishdait

Copy link
Member

@AntonioCeppellini AntonioCeppellini left a comment

Choose a reason for hiding this comment

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

Nice work! 🚀

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

manishdait
manishdait previously approved these changes Jan 31, 2026
Copy link
Contributor

@manishdait manishdait left a comment

Choose a reason for hiding this comment

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

lgtm, but the unit test can be extend for TransactionRecordQuery

@codecov
Copy link

codecov bot commented Jan 31, 2026

Codecov Report

❌ Patch coverage is 68.57143% with 11 lines in your changes missing coverage. Please review.

❌ Your patch check has failed because the patch coverage (68.57%) is below the target coverage (92.00%). You can increase the patch coverage or adjust the target coverage.

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1646      +/-   ##
==========================================
- Coverage   92.89%   92.79%   -0.11%     
==========================================
  Files         140      140              
  Lines        8765     8781      +16     
==========================================
+ Hits         8142     8148       +6     
- Misses        623      633      +10     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@gangulysiddhartha22-cmyk gangulysiddhartha22-cmyk force-pushed the feature/1635-transaction-record-query-include-duplicates branch from c843904 to 181c26b Compare January 31, 2026 14:41
@gangulysiddhartha22-cmyk
Copy link
Contributor Author

@manishdait Added unit tests. Ready for another look. Thanks!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

@gangulysiddhartha22-cmyk gangulysiddhartha22-cmyk force-pushed the feature/1635-transaction-record-query-include-duplicates branch from 181c26b to 5142de6 Compare January 31, 2026 15:00
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

@gangulysiddhartha22-cmyk gangulysiddhartha22-cmyk force-pushed the feature/1635-transaction-record-query-include-duplicates branch from 5142de6 to 8310306 Compare January 31, 2026 15:22
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

@gangulysiddhartha22-cmyk gangulysiddhartha22-cmyk force-pushed the feature/1635-transaction-record-query-include-duplicates branch from 8310306 to ea8a064 Compare January 31, 2026 15:44
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

@gangulysiddhartha22-cmyk gangulysiddhartha22-cmyk force-pushed the feature/1635-transaction-record-query-include-duplicates branch from 6808467 to 425fa27 Compare January 31, 2026 19:28
@gangulysiddhartha22-cmyk
Copy link
Contributor Author

@manishdait

Fixed the QueryHeader protobuf crash in the unit test.
Workflows passing.

Requesting re-assessment

Thanks!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/unit/transaction_record_query_test.py (1)

118-125: 🧹 Nitpick | 🔵 Trivial

Remove redundant try/except—let pytest handle exceptions natively.

The try/except with pytest.fail() obscures the original traceback and provides less debugging information. If an unexpected exception occurs, pytest will fail the test with the full stack trace automatically.

🔧 Suggested fix
-        try:
-            # Get the cost of executing the query - should be 2 tinybars based on the mock response
-            cost = query.get_cost(client)
-            assert cost.to_tinybars() == 2
-            
-            result = query.execute(client)
-        except Exception as e:
-            pytest.fail(f"Unexpected exception raised: {e}")
+        # Get the cost of executing the query - should be 2 tinybars based on the mock response
+        cost = query.get_cost(client)
+        assert cost.to_tinybars() == 2
+        
+        result = query.execute(client)

@gangulysiddhartha22-cmyk gangulysiddhartha22-cmyk force-pushed the feature/1635-transaction-record-query-include-duplicates branch from 425fa27 to d8a8f93 Compare January 31, 2026 19:49
Copy link
Member

@AntonioCeppellini AntonioCeppellini left a comment

Choose a reason for hiding this comment

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

HI @gangulysiddhartha22-cmyk nice work so far

"""

def __init__(self, transaction_id: Optional[TransactionId] = None):
def __init__(self, transaction_id: Optional[TransactionId] = None, include_duplicates: bool = False)->None:
Copy link
Member

Choose a reason for hiding this comment

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

False)->None: should become False) -> None:

"include_duplicates must be a boolean (True or False)"
)
self.include_duplicates = include_duplicates
return self
Copy link
Member

Choose a reason for hiding this comment

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

are you using balck for formatting?

@github-actions
Copy link

github-actions bot commented Feb 1, 2026

Hi, this is MergeConflictBot.
Your pull request cannot be merged because it contains merge conflicts.

Please resolve these conflicts locally and push the changes.

Quick Fix for CHANGELOG.md Conflicts

If your conflict is only in CHANGELOG.md, you can resolve it easily using the GitHub web editor:

  1. Click on the "Resolve conflicts" button in the PR
  2. Accept both changes (keep both changelog entries)
  3. Click "Mark as resolved"
  4. Commit the merge

For all other merge conflicts, please read:

Thank you for contributing!

@gangulysiddhartha22-cmyk gangulysiddhartha22-cmyk force-pushed the feature/1635-transaction-record-query-include-duplicates branch from 8da712f to 453af5e Compare February 1, 2026 07:02
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 21

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (30)
src/hiero_sdk_python/channels.py (1)

12-213: 🧹 Nitpick | 🔵 Trivial

LGTM! Formatting improvements enhance readability.

The formatting changes (blank lines in docstrings and multi-line wrapping at lines 91-93 and 211-213) improve code readability without altering behavior.


Optional: Consider adding type annotations.

Static analysis flags missing return type annotations for __init__ and property methods. While these are pre-existing issues and outside this PR's scope, adding them would improve type safety.

📝 Optional type annotation improvements
-    def __init__(self, grpc_channel=None):
+    def __init__(self, grpc_channel=None) -> None:
     `@property`
-    def file(self):
+    def file(self) -> file_service_pb2_grpc.FileServiceStub | None:

Similarly for other property methods (smart_contract, topic, freeze, network, token, schedule, util, address_book).

src/hiero_sdk_python/tokens/token_fee_schedule_update_transaction.py (1)

41-45: ⚠️ Potential issue | 🟠 Major

Public API return type change in set_token_id.
Returning self changes the public method’s return type; please confirm this is intentional and document the change (or add deprecation guidance if needed) to preserve API stability expectations.

As per coding guidelines, public token APIs must not change return types without explicit stability considerations.

src/hiero_sdk_python/query/account_info_query.py (1)

58-74: 🧹 Nitpick | 🔵 Trivial

Consider simplifying exception handling.

The broad try/except Exception block catches all exceptions just to re-raise them. Since the ValueError for missing account_id is already explicit and there's no recovery logic, the try/except wrapper adds complexity without benefit.

♻️ Suggested simplification
     def _make_request(self):
-        try:
-            if not self.account_id:
-                raise ValueError("Account ID must be set before making the request.")
-
-            query_header = self._make_request_header()
-
-            crypto_info_query = crypto_get_info_pb2.CryptoGetInfoQuery()
-            crypto_info_query.header.CopyFrom(query_header)
-            crypto_info_query.accountID.CopyFrom(self.account_id._to_proto())
-
-            query = query_pb2.Query()
-            query.cryptoGetInfo.CopyFrom(crypto_info_query)
-
-            return query
-        except Exception as e:
-            print(f"Exception in _make_request: {e}")
-            raise
+        if not self.account_id:
+            raise ValueError("Account ID must be set before making the request.")
+
+        query_header = self._make_request_header()
+
+        crypto_info_query = crypto_get_info_pb2.CryptoGetInfoQuery()
+        crypto_info_query.header.CopyFrom(query_header)
+        crypto_info_query.accountID.CopyFrom(self.account_id._to_proto())
+
+        query = query_pb2.Query()
+        query.cryptoGetInfo.CopyFrom(crypto_info_query)
+
+        return query
src/hiero_sdk_python/tokens/token_freeze_status.py (1)

34-39: 🧹 Nitpick | 🔵 Trivial

Note: Custom __eq__ may break hashability contract.

While this code isn't part of the current PR's changed lines, the custom __eq__ allowing integer comparison creates a subtle issue: TokenFreezeStatus.FROZEN == 1 returns True, but hash(TokenFreezeStatus.FROZEN) != hash(1). This violates Python's contract that equal objects must have equal hashes, which can cause unexpected behavior when enum values are used in sets or as dictionary keys alongside integers.

If integer comparison is intentional, consider also overriding __hash__ to return hash(self.value).

♻️ Optional fix to preserve hashability contract
     def __eq__(self, other: Any) -> bool:
         """Checks equality with another TokenFreezeStatus or an integer."""
         if isinstance(other, TokenFreezeStatus):
             return self.value == other.value
         if isinstance(other, int):
             return self.value == other
         return False
+
+    def __hash__(self) -> int:
+        """Returns hash based on value to maintain hashability contract."""
+        return hash(self.value)
src/hiero_sdk_python/consensus/topic_update_transaction.py (1)

245-301: 🧹 Nitpick | 🔵 Trivial

Reduce _build_proto_body cyclomatic complexity to satisfy Codacy.

Codacy flags Line 245 for complexity > 12. Consider extracting helper builders (e.g., _build_custom_fees, _build_fee_exempt_key_list, _build_memo) to lower complexity and isolate optional-field handling.

src/hiero_sdk_python/crypto/public_key.py (1)

52-71: ⚠️ Potential issue | 🟡 Minor

Avoid misleading ambiguity warnings for non‑Ed25519 inputs.

_warn_ed25519_ambiguity() runs before length checks, so ECDSA/DER inputs also warn even though they’re not ambiguous. Consider warning only when the decoded input is 32 bytes (the actual ambiguity case).

💡 Suggested fix
-        _warn_ed25519_ambiguity("PublicKey.from_bytes")
-
-        # 1) 32-byte raw ⇒ Ed25519
-        if len(pub) == 32:
-            return cls._from_bytes_ed25519(pub)
+        # 1) 32-byte raw ⇒ Ed25519 (ambiguous with Ed25519 seed)
+        if len(pub) == 32:
+            _warn_ed25519_ambiguity("PublicKey.from_bytes")
+            return cls._from_bytes_ed25519(pub)
@@
-        _warn_ed25519_ambiguity("PublicKey.from_string")
-
         # 1) Remove the unecessary prefix and decode the hex
         hex_str = hex_str.removeprefix("0x")
         try:
             data = bytes.fromhex(hex_str)
         except ValueError as exc:
             raise ValueError(
                 f"Invalid hex-encoded public key string: {hex_str!r}"
             ) from exc
+        if len(data) == 32:
+            _warn_ed25519_ambiguity("PublicKey.from_string")

Also applies to: 276-315

src/hiero_sdk_python/file/file_append_transaction.py (1)

330-375: ⚠️ Potential issue | 🔴 Critical

Fix early return that stops multi-chunk execution.
Line 373 returns inside the loop, so only the first chunk is executed. Move the return after the loop to execute all chunks.

🛠️ Proposed fix
-            # Return the first response (as per JavaScript implementation)
-            return responses[0] if responses else None
+        # Return the first response (as per JavaScript implementation)
+        return responses[0] if responses else None
src/hiero_sdk_python/executable.py (2)

199-292: ⚠️ Potential issue | 🟡 Minor

Stabilize requestId across a single execution attempt.
_get_request_id() is called multiple times, generating different IDs for the same attempt, which breaks log correlation across retries and delay logs. Consider generating it once and reusing it.

🛠️ Proposed fix
@@
-        logger = client.logger
+        logger = client.logger
+        request_id = self._get_request_id()
@@
-            logger.trace(
+            logger.trace(
                 "Executing",
-                "requestId",
-                self._get_request_id(),
+                "requestId",
+                request_id,
@@
-                logger.trace("Executing gRPC call", "requestId", self._get_request_id())
+                logger.trace("Executing gRPC call", "requestId", request_id)
@@
-                        _delay_for_attempt(
-                            self._get_request_id(),
+                        _delay_for_attempt(
+                            request_id,
@@
-                _delay_for_attempt(
-                    self._get_request_id(),
+                _delay_for_attempt(
+                    request_id,
@@
-        logger.error(
+        logger.error(
             "Exceeded maximum attempts for request",
             "requestId",
-            self._get_request_id(),
+            request_id,

381-385: 🧹 Nitpick | 🔵 Trivial

Create a dedicated ExecutableMethodError exception for better error handling clarity.

Raising generic Exception is flagged by Ruff (TRY002/TRY003) and makes error handling less precise. Replace it with a custom exception that follows the existing pattern in your codebase.

Add to src/hiero_sdk_python/exceptions.py:

class ExecutableMethodError(Exception):
    """Raised when no transaction or query method is available to execute."""

Then update src/hiero_sdk_python/executable.py:

  • Import the exception: from hiero_sdk_python.exceptions import MaxAttemptsError, ExecutableMethodError
  • Update the docstring to document ExecutableMethodError instead of generic Exception
  • Replace the generic raise with raise ExecutableMethodError("No method to execute")
src/hiero_sdk_python/tokens/token_kyc_status.py (1)

38-44: ⚠️ Potential issue | 🟠 Major

Define __hash__ when overriding __eq__ to restore set/dict compatibility.

Overriding __eq__ without defining __hash__ violates Python's equality/hash contract, making instances unhashable. This breaks any code that stores these enums in sets or uses them as dictionary keys.

Add a __hash__ method and return NotImplemented instead of False for incompatible types:

Suggested fix
     def __eq__(self, other: Any) -> bool:
         """Checks equality with another TokenKycStatus or an integer."""
         if isinstance(other, TokenKycStatus):
             return self.value == other.value
         if isinstance(other, int):
             return self.value == other
-        return False
+        return NotImplemented
+
+    def __hash__(self) -> int:
+        return hash(self.value)

Note: This same issue exists in TokenFreezeStatus and TokenPauseStatus.

src/hiero_sdk_python/tokens/token_pause_status.py (1)

47-59: ⚠️ Potential issue | 🟠 Major

Custom __eq__ breaks hashability for public API enum

TokenPauseStatus instances are unhashable due to custom __eq__ without corresponding __hash__, preventing valid Python patterns like set operations or dict keys. This violates the hash contract (equal objects must hash equally) and breaks user expectations for a public token enum. Add __hash__ returning hash(self.value) to align equality and hashability semantics.

Note: TokenFreezeStatus has identical issue.

src/hiero_sdk_python/tokens/token_key_validation.py (1)

44-50: ⚠️ Potential issue | 🟠 Major

Add __hash__ to restore hashability when overriding __eq__

The custom __eq__ method disables hashing (Python sets __hash__ = None), preventing enum members from being used in sets or as dict keys. This violates standard Enum behavior and creates a latent API stability issue for users who may attempt to use these enums in collections.

Add a compatible __hash__ implementation:

Fix
     def __eq__(self, other: Any) -> bool:
         """Checks equality with another TokenKeyValidation or an integer."""
         if isinstance(other, TokenKeyValidation):
             return self.value == other.value
         if isinstance(other, int):
             return self.value == other
         return False
+
+    def __hash__(self) -> int:
+        return hash(self.value)

Note: This issue also affects TokenFreezeStatus, TokenPauseStatus, and TokenKycStatus—all should be updated consistently.

src/hiero_sdk_python/tokens/token_airdrop_pending_id.py (1)

46-47: ⚠️ Potential issue | 🟡 Minor

Fix grammatical error in validation message.

The error message uses awkward phrasing. "must be required" should be "must be provided".

✏️ Suggested fix
         if (token_id is None) == (nft_id is None):
-            raise ValueError("Exactly one of 'token_id' or 'nft_id' must be required.")
+            raise ValueError("Exactly one of 'token_id' or 'nft_id' must be provided.")
src/hiero_sdk_python/hbar.py (1)

177-185: 🧹 Nitpick | 🔵 Trivial

Potential parsing inconsistency between regex and string split.

The regex FROM_STRING_PATTERN validates the format but from_string then uses amount.split(" ") to extract parts. If the input has multiple spaces (e.g., "10 ℏ"), the regex would reject it, but if validation is bypassed, split could produce unexpected results.

Currently safe since the regex check precedes the split, but consider extracting values directly from the match groups for consistency:

♻️ Suggested improvement using match groups
     def from_string(cls, amount: str, unit: HbarUnit = HbarUnit.HBAR) -> "Hbar":
         match = FROM_STRING_PATTERN.match(amount)
 
         if not match:
             raise ValueError(f"Invalid Hbar format: '{amount}'")
 
-        parts = amount.split(" ")
-        value = Decimal(parts[0])
-        unit = HbarUnit.from_string(parts[1]) if len(parts) == 2 else unit
+        value = Decimal(match.group(1))
+        unit_str = match.group(3)  # Group 3 captures the unit symbol
+        unit = HbarUnit.from_string(unit_str) if unit_str else unit
         return cls(value, unit=unit)
src/hiero_sdk_python/tokens/token_nft_allowance.py (1)

194-216: 🧹 Nitpick | 🔵 Trivial

Consider extracting the duplicated _from_proto_field helper.

This helper method is duplicated verbatim in TokenAllowance, HbarAllowance, and TokenNftAllowance (see src/hiero_sdk_python/tokens/token_allowance.py:116-137 and src/hiero_sdk_python/tokens/hbar_allowance.py:119-140). Consider extracting it to a shared utility module.

src/hiero_sdk_python/query/topic_info_query.py (1)

91-109: ⚠️ Potential issue | 🟡 Minor

Remove debug print statements from _make_request.

The print() and traceback.print_exc() calls are debug artifacts that should not be in production code. The exception is already re-raised, so the caller can handle it appropriately.

🧹 Proposed fix to remove debug output
         try:
             if not self.topic_id:
                 raise ValueError("Topic ID must be set before making the request.")
 
             query_header = self._make_request_header()
 
             topic_info_query = consensus_get_topic_info_pb2.ConsensusGetTopicInfoQuery()
             topic_info_query.header.CopyFrom(query_header)
             topic_info_query.topicID.CopyFrom(self.topic_id._to_proto())
 
             query = query_pb2.Query()
             query.consensusGetTopicInfo.CopyFrom(topic_info_query)
 
             return query
 
-        except Exception as e:
-            print(f"Exception in _make_request: {e}")
-            traceback.print_exc()
-            raise

The entire try/except block can be removed since the exceptions (ValueError for missing topic_id, or any protobuf errors) will naturally propagate to the caller.

src/hiero_sdk_python/tokens/token_update_transaction.py (1)

93-115: ⚠️ Potential issue | 🟡 Minor

Docstring param name doesn’t match the signature.

The docstring references token_key_validation, but the parameter is token_key_verification_mode. Please align the docstring to avoid confusion.

✏️ Suggested docstring fix
-            token_key_validation (TokenKeyValidation, optional): The validation mode for token keys.
+            token_key_verification_mode (TokenKeyValidation, optional): The validation mode for token keys.
src/hiero_sdk_python/tokens/token_update_nfts_transaction.py (1)

106-119: ⚠️ Potential issue | 🟠 Major

Ensure metadata field is only present when explicitly provided.

When self.metadata is None, BytesValue(value=None) produces an empty wrapper instead of leaving the field absent. This causes the metadata field to always be present in the protobuf message, breaking field optionality semantics. Match the correct pattern already used in TokenUpdateTransaction:

Proposed fix
         return TokenUpdateNftsTransactionBody(
             token=self.token_id._to_proto(),
             serial_numbers=self.serial_numbers,
-            metadata=BytesValue(value=self.metadata),
+            metadata=BytesValue(value=self.metadata) if self.metadata else None,
         )
src/hiero_sdk_python/query/transaction_get_receipt_query.py (1)

147-178: ⚠️ Potential issue | 🟠 Major

Avoid stdout/traceback in _make_request.
Printing exceptions introduces noisy side effects and can leak sensitive data. Let the exception propagate or use structured logging if needed.

Proposed fix (remove print/traceback wrapper)
-        try:
-            if not self.transaction_id:
-                raise ValueError(
-                    "Transaction ID must be set before making the request."
-                )
-
-            query_header = query_header_pb2.QueryHeader()
-            query_header.responseType = query_header_pb2.ResponseType.ANSWER_ONLY
-
-            transaction_get_receipt = (
-                transaction_get_receipt_pb2.TransactionGetReceiptQuery()
-            )
-            transaction_get_receipt.header.CopyFrom(query_header)
-            transaction_get_receipt.transactionID.CopyFrom(
-                self.transaction_id._to_proto()
-            )
-
-            transaction_get_receipt.include_child_receipts = self.include_children
-            transaction_get_receipt.includeDuplicates = self.include_duplicates
-
-            query = query_pb2.Query()
-            if not hasattr(query, "transactionGetReceipt"):
-                raise AttributeError(
-                    "Query object has no attribute 'transactionGetReceipt'"
-                )
-            query.transactionGetReceipt.CopyFrom(transaction_get_receipt)
-
-            return query
-        except Exception as e:
-            print(f"Exception in _make_request: {e}")
-            traceback.print_exc()
-            raise
+        if not self.transaction_id:
+            raise ValueError(
+                "Transaction ID must be set before making the request."
+            )
+
+        query_header = query_header_pb2.QueryHeader()
+        query_header.responseType = query_header_pb2.ResponseType.ANSWER_ONLY
+
+        transaction_get_receipt = (
+            transaction_get_receipt_pb2.TransactionGetReceiptQuery()
+        )
+        transaction_get_receipt.header.CopyFrom(query_header)
+        transaction_get_receipt.transactionID.CopyFrom(
+            self.transaction_id._to_proto()
+        )
+
+        transaction_get_receipt.include_child_receipts = self.include_children
+        transaction_get_receipt.includeDuplicates = self.include_duplicates
+
+        query = query_pb2.Query()
+        if not hasattr(query, "transactionGetReceipt"):
+            raise AttributeError(
+                "Query object has no attribute 'transactionGetReceipt'"
+            )
+        query.transactionGetReceipt.CopyFrom(transaction_get_receipt)
+
+        return query
src/hiero_sdk_python/consensus/topic_message_submit_transaction.py (1)

26-52: ⚠️ Potential issue | 🟠 Major

Initialize custom_fee_limits to avoid AttributeError.
add_custom_fee_limit() assumes the list exists, but it isn’t initialized in __init__.

🛠️ Suggested fix
         self._transaction_ids: List[TransactionId] = []
         self._signing_keys: List["PrivateKey"] = []
+        self.custom_fee_limits: List[CustomFeeLimit] = []
src/hiero_sdk_python/account/account_create_transaction.py (1)

68-82: ⚠️ Potential issue | 🟡 Minor

Type annotations mislead that Optional values are safe, but _build_proto_body crashes if None is explicitly passed.

auto_renew_period and decline_staking_reward are typed as Optional[Duration] and Optional[bool] respectively, but have non-None defaults (AUTO_RENEW_PERIOD and False). However, if a caller explicitly passes None, the code will fail: auto_renew_period.seconds will raise AttributeError when accessed in _build_proto_body. The type hint conflicts with the implementation and should either be corrected (remove Optional) or the code should validate/normalize.

src/hiero_sdk_python/consensus/topic_id.py (1)

36-40: ⚠️ Potential issue | 🟠 Major

Checksum field changes TopicId equality semantics

Adding checksum to the dataclass means equality now depends on checksum, which is a backward-incompatible behavioral change for comparisons and set/dict keys. If equality should remain based on shard/realm/num, mark the checksum field as non-comparable.

Proposed fix
-    checksum: str | None = field(default=None, init=False)
+    checksum: str | None = field(default=None, init=False, compare=False)
src/hiero_sdk_python/tokens/token_id.py (1)

38-42: ⚠️ Potential issue | 🟠 Major

Checksum inclusion changes TokenId equality behavior

With checksum as a dataclass field, TokenId(0,0,1) and TokenId.from_string("0.0.1-xxxx") are no longer equal, which is a backward-incompatible behavior change. If equality should remain based on shard/realm/num, mark checksum as non-comparable.

Proposed fix
-    checksum: str | None = field(default=None, init=False)
+    checksum: str | None = field(default=None, init=False, compare=False)

As per coding guidelines, default values and behaviors of token APIs must remain stable.

src/hiero_sdk_python/tokens/custom_fee.py (1)

75-126: ⚠️ Potential issue | 🟠 Major

Preserve backward compatibility for _from_protobuf / _to_protobuf

Renaming these methods is a breaking change for external subclasses or callers. Consider adding deprecated aliases to keep the old names working while encouraging migration.

Proposed fix
+import warnings
@@
     `@staticmethod`
     def _from_proto(
         custom_fee: "CustomFeeProto",
     ) -> "CustomFee":  # Changed from _from_protobuf
@@
         raise ValueError(f"Unrecognized fee case: {fee_case}")
+
+    `@staticmethod`
+    def _from_protobuf(custom_fee: "CustomFeeProto") -> "CustomFee":
+        warnings.warn(
+            "_from_protobuf() is deprecated; use _from_proto()",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return CustomFee._from_proto(custom_fee)
@@
     `@abstractmethod`
     def _to_proto(self) -> "CustomFeeProto":  # Changed from _to_protobuf
         """Converts this CustomFee to its protobuf representation.
@@
         ...
+
+    def _to_protobuf(self) -> "CustomFeeProto":
+        warnings.warn(
+            "_to_protobuf() is deprecated; use _to_proto()",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self._to_proto()

As per coding guidelines, token APIs must not rename or remove public methods without deprecation.

src/hiero_sdk_python/transaction/transaction.py (1)

816-823: 🧹 Nitpick | 🔵 Trivial

Use exception chaining for better error traceability.

When re-raising exceptions within an except block, use raise ... from e to preserve the original traceback and improve debugging.

♻️ Proposed fix
         except (ImportError, AttributeError) as e:
-            raise ValueError(
+            raise ValueError(
                 f"Failed to import transaction class for type '{transaction_type}': {e}"
-            )
+            ) from e
src/hiero_sdk_python/tokens/nft_id.py (1)

28-59: 🧹 Nitpick | 🔵 Trivial

Consider removing redundant validation in __post_init__.

The __init__ method already validates that token_id and serial_number are not None (lines 34-37), so the None checks in __post_init__ (lines 48-49) are redundant and will never trigger. The type checks (lines 50-57) are still valuable.

♻️ Suggested simplification
     def __post_init__(self) -> None:
         """Validate the NftId after initialization."""
-        if self.token_id is None:
-            raise TypeError("token_id is required")
         if not isinstance(self.token_id, TokenId):
             raise TypeError(
                 f"token_id must be of type TokenId, got {type(self.token_id)}"
             )
         if not isinstance(self.serial_number, int):
             raise TypeError(
                 f"serial_number must be an integer, got {type(self.serial_number)}"
             )
         if self.serial_number < 0:
             raise ValueError("serial_number must be non-negative")
src/hiero_sdk_python/query/token_nft_info_query.py (1)

74-77: ⚠️ Potential issue | 🟡 Minor

Remove debug print statements and traceback from production code.

Using print() and traceback.print_exc() in production code is not ideal. These should either be replaced with proper logging or removed entirely, as the exception is already being re-raised. This introduces side effects (console output) that are inconsistent with other query implementations.

🛠️ Proposed fix
         except Exception as e:
-            print(f"Exception in _make_request: {e}")
-            traceback.print_exc()
             raise

Or if logging is desired:

+import logging
+
+logger = logging.getLogger(__name__)
+
 ...
         except Exception as e:
-            print(f"Exception in _make_request: {e}")
-            traceback.print_exc()
+            logger.exception("Exception in _make_request")
             raise
src/hiero_sdk_python/tokens/token_transfer_list.py (1)

22-47: ⚠️ Potential issue | 🟡 Minor

Fix typo in docstring.

Line 33 has a typo: "Thhe" should be "The".

📝 Proposed fix
         """
         Initializes a new TokenTransferList instance.

         Args:
-            token (TokenId): Thhe ID of the token being transferred.
+            token (TokenId): The ID of the token being transferred.
             transfers (optional, list[TokenTransfer]): A list of fungible token transfers.
src/hiero_sdk_python/tokens/token_transfer.py (1)

58-88: ⚠️ Potential issue | 🟡 Minor

Fix unclosed parenthesis in docstring.

Line 66 has an unclosed parenthesis in the Args section: proto (basic_types_pb2.TokenTransferList: should be proto (basic_types_pb2.TokenTransferList):.

📝 Proposed fix
         """
         Construct a list of TokenTransfer from the protobuf of TokenTransferList.

         Args:
-        proto (basic_types_pb2.TokenTransferList:
+            proto (basic_types_pb2.TokenTransferList):
             The protobuf representation of a TokenTransferList
         """
src/hiero_sdk_python/client/client.py (1)

149-164: 🧹 Nitpick | 🔵 Trivial

Consider merging endswith calls for cleaner code.

The static analyzer suggests combining the two endswith checks into a single call with a tuple.

♻️ Suggested improvement
         mirror_address = self.network.get_mirror_address()
-        if mirror_address.endswith(":50212") or mirror_address.endswith(":443"):
+        if mirror_address.endswith((":50212", ":443")):
             self.mirror_channel = grpc.secure_channel(
                 mirror_address, grpc.ssl_channel_credentials()
             )

Comment on lines 354 to 360
cryptoAllowances=[
allowance._to_proto() for allowance in self.hbar_allowances
],
tokenAllowances=[
allowance._to_proto() for allowance in self.token_allowances
],
nftAllowances=[allowance._to_proto() for allowance in self.nft_allowances],
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Inconsistent formatting across similar list comprehensions.

Lines 354-359 format cryptoAllowances and tokenAllowances as multi-line, but line 360 keeps nftAllowances as single-line. For consistency, consider applying the same multi-line formatting to all three list comprehensions.

✨ Proposed consistent formatting
         body = CryptoApproveAllowanceTransactionBody(
             cryptoAllowances=[
                 allowance._to_proto() for allowance in self.hbar_allowances
             ],
             tokenAllowances=[
                 allowance._to_proto() for allowance in self.token_allowances
             ],
-            nftAllowances=[allowance._to_proto() for allowance in self.nft_allowances],
+            nftAllowances=[
+                allowance._to_proto() for allowance in self.nft_allowances
+            ],
         )

"""
Returns a human-friendly string representation of the account balance.

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

These docstring formatting changes are unrelated to the PR objectives.

The PR objectives focus on adding include_duplicates support to TransactionRecordQuery, but this file modifies only blank lines in docstrings for AccountBalance.__str__ and AccountBalance.__repr__. Including unrelated formatting changes increases review noise and makes it harder to understand the PR's scope.

Please consider removing these changes and keeping the PR focused on the transaction record duplicate functionality.

Also applies to: 73-73

staked_account_id (Optional[AccountId]): The account to which this account is staked.
staked_node_id (Optional[int]): The node to which this account is staked.
decline_staking_reward (bool): Whether this account declines receiving staking rewards.
decline_staking_reward (bool): Whether this account declines receiving staking rewards.
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

Minor docstring inconsistency with field type.

The docstring on line 43 declares decline_staking_reward as bool, but the actual field declaration on line 61 is Optional[bool]. The docstring should match the field type.

📝 Suggested fix
-        decline_staking_reward (bool): Whether this account declines receiving staking rewards.
+        decline_staking_reward (Optional[bool]): Whether this account declines receiving staking rewards.

Also applies to: 61-61

Comment on lines 270 to 280
chunk_valid_start = timestamp_pb2.Timestamp(
seconds=base_timestamp.seconds,
nanos=base_timestamp.nanos + i
seconds=base_timestamp.seconds, nanos=base_timestamp.nanos + i
)
chunk_transaction_id = TransactionId(
account_id=self.transaction_id.account_id,
valid_start=chunk_valid_start
valid_start=chunk_valid_start,
)
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

🧩 Analysis chain

🏁 Script executed:

cd src && find . -name "topic_message_submit_transaction.py" -type f | head -5

Repository: hiero-ledger/hiero-sdk-python

Length of output: 137


🏁 Script executed:

cd src && cat -n hiero_sdk_python/consensus/topic_message_submit_transaction.py | sed -n '250,290p'

Repository: hiero-ledger/hiero-sdk-python

Length of output: 1849


🏁 Script executed:

rg "Timestamp" src/hiero_sdk_python/consensus/topic_message_submit_transaction.py -B 2 -A 2 | head -30

Repository: hiero-ledger/hiero-sdk-python

Length of output: 331


🏁 Script executed:

rg "timestamp_pb2|from google.protobuf|import.*timestamp" src/hiero_sdk_python/ -A 1 | head -20

Repository: hiero-ledger/hiero-sdk-python

Length of output: 1558


🏁 Script executed:

rg "def get_required_chunks" src/hiero_sdk_python/ -A 5

Repository: hiero-ledger/hiero-sdk-python

Length of output: 1171


🏁 Script executed:

rg "get_required_chunks" src/hiero_sdk_python/consensus/topic_message_submit_transaction.py -A 10

Repository: hiero-ledger/hiero-sdk-python

Length of output: 2748


🏁 Script executed:

rg "1_?000_?000_?000|1e9|nanos.*%" src/hiero_sdk_python/ --type py

Repository: hiero-ledger/hiero-sdk-python

Length of output: 747


🏁 Script executed:

cat -n src/hiero_sdk_python/transaction/transaction_id.py | sed -n '1,100p'

Repository: hiero-ledger/hiero-sdk-python

Length of output: 4658


🏁 Script executed:

cat -n src/hiero_sdk_python/timestamp.py | head -50

Repository: hiero-ledger/hiero-sdk-python

Length of output: 1884


🌐 Web query:

protobuf Timestamp nanos field constraints 0 999999999

💡 Result:

In google.protobuf.Timestamp, the nanos field is constrained to 0 through 999,999,999 inclusive (nanosecond fraction of the second). Negative seconds values are allowed, but if there is a fractional part it must still be represented with a non-negative nanos that counts forward in time. [1], [2], [3]

(Contrast: google.protobuf.Duration allows nanos to be -999,999,999 through +999,999,999 with sign rules.) [4]


Normalize chunk timestamp nanos to stay within protobuf bounds.

Adding i to nanos can exceed 999,999,999, producing an invalid Timestamp. The protobuf Timestamp.nanos field must remain in the range [0, 999,999,999].

🛠️ Suggested fix (carry nanos into seconds)
-                    chunk_valid_start = timestamp_pb2.Timestamp(
-                        seconds=base_timestamp.seconds, nanos=base_timestamp.nanos + i
-                    )
+                    nanos = base_timestamp.nanos + i
+                    seconds = base_timestamp.seconds + nanos // 1_000_000_000
+                    chunk_valid_start = timestamp_pb2.Timestamp(
+                        seconds=seconds, nanos=nanos % 1_000_000_000
+                    )

Comment on lines 126 to 128
def set_auto_renew_period(
self, seconds: Union[Duration, int]
) -> "TopicUpdateTransaction":
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for declared Python version requirements in common configuration files
echo "=== Checking setup files ==="
for file in pyproject.toml setup.py setup.cfg .python-version requirements.txt poetry.lock; do
  if [ -f "$file" ]; then
    echo "Found: $file"
    case "$file" in
      pyproject.toml)
        grep -n "python" "$file" | head -20
        ;;
      setup.py)
        grep -n "python_requires" "$file"
        ;;
      setup.cfg)
        grep -n "python_requires" "$file"
        ;;
      .python-version)
        cat "$file"
        ;;
    esac
    echo ""
  fi
done

# Also check for any Ruff configuration
if [ -f "pyproject.toml" ] || [ -f "ruff.toml" ] || [ -f ".ruff.toml" ]; then
  echo "=== Ruff configuration ==="
  rg -A 5 "tool.ruff|target-version" 2>/dev/null || echo "No ruff config found"
fi

Repository: hiero-ledger/hiero-sdk-python

Length of output: 876


🏁 Script executed:

#!/bin/bash
# Check the imports and the specific lines 126-128
echo "=== Checking imports at top of file ==="
head -30 src/hiero_sdk_python/consensus/topic_update_transaction.py | grep -n "Union\|from typing"

echo ""
echo "=== Checking lines 120-135 ==="
sed -n '120,135p' src/hiero_sdk_python/consensus/topic_update_transaction.py | cat -n

Repository: hiero-ledger/hiero-sdk-python

Length of output: 735


Use PEP 604 union syntax to align with project standards.

The project targets Python ≥3.10 and Ruff is configured to flag Union usage (UP007). Replace Union[Duration, int] with the modern Duration | int syntax.

Change
-from typing import Union, Optional, List
+from typing import Optional, List
@@
-    def set_auto_renew_period(
-        self, seconds: Union[Duration, int]
-    ) -> "TopicUpdateTransaction":
+    def set_auto_renew_period(
+        self, seconds: Duration | int
+    ) -> "TopicUpdateTransaction":
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 127-127: Use X | Y for type annotations

Convert to X | Y

(UP007)

Comment on lines 19 to 21
• PAUSE_NOT_APPLICABLE – pause not relevant
• PAUSED – token is paused
• UNPAUSED – token is active
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

Replace ambiguous EN DASH characters in docstring

Ruff flags the characters as ambiguous (RUF002). Replace with - to silence the lint and avoid encoding ambiguity.

🧹 Suggested cleanup
-      • PAUSE_NOT_APPLICABLE – pause not relevant
-      • PAUSED              – token is paused
-      • UNPAUSED            – token is active
+      • PAUSE_NOT_APPLICABLE - pause not relevant
+      • PAUSED              - token is paused
+      • UNPAUSED            - token is active
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
PAUSE_NOT_APPLICABLE pause not relevant
PAUSED token is paused
UNPAUSED token is active
PAUSE_NOT_APPLICABLE - pause not relevant
PAUSED - token is paused
UNPAUSED - token is active
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 19-19: Docstring contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF002)


[warning] 20-20: Docstring contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF002)


[warning] 21-21: Docstring contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF002)

Comment on lines 9 to 21
from typing import List, Optional
from google.protobuf.wrappers_pb2 import BytesValue

from hiero_sdk_python.tokens.token_id import TokenId
from hiero_sdk_python.transaction.transaction import Transaction
from hiero_sdk_python.channels import _Channel
from hiero_sdk_python.executable import _Method
from hiero_sdk_python.hapi.services.token_update_nfts_pb2 import TokenUpdateNftsTransactionBody
from hiero_sdk_python.hapi.services.token_update_nfts_pb2 import (
TokenUpdateNftsTransactionBody,
)
from hiero_sdk_python.hapi.services import transaction_pb2
from hiero_sdk_python.hapi.services.schedulable_transaction_body_pb2 import (
SchedulableTransactionBody,
)
from google.protobuf.wrappers_pb2 import BytesValue
from hiero_sdk_python.hapi.services import token_update_nfts_pb2
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

Remove duplicate BytesValue import to avoid lint failure.

The symbol is imported twice, which triggers redefinition errors in linters.

🧹 Proposed fix
 from hiero_sdk_python.hapi.services.schedulable_transaction_body_pb2 import (
     SchedulableTransactionBody,
 )
-from google.protobuf.wrappers_pb2 import BytesValue
 from hiero_sdk_python.hapi.services import token_update_nfts_pb2
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 9-9: typing.List is deprecated, use list instead

(UP035)


[error] 23-23: Redefinition of unused BytesValue from line 10: BytesValue redefined here

Remove definition: BytesValue

(F811)

Comment on lines 136 to 138
raise ValueError(
f"Expected TransactionResponseProto but got {type(response)}"
)
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider using TypeError for type validation consistency.

The static analyzer suggests using TypeError here since you're validating the type of the response. This would be consistent with the TypeError you raise on line 110 for proto_request type validation.

♻️ Suggested fix
         if not isinstance(response, TransactionResponseProto):
-            raise ValueError(
+            raise TypeError(
                 f"Expected TransactionResponseProto but got {type(response)}"
             )
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 136-138: Prefer TypeError exception for invalid type

(TRY004)


[warning] 136-138: Avoid specifying long messages outside the exception class

(TRY003)

Comment on lines 102 to 104
def add_hbar_transfer(
self, account_id: AccountId, amount: Union[int, Hbar]
) -> "TransferTransaction":
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

# Check for Python version requirements in project config files
find . -maxdepth 2 -type f \( -name "pyproject.toml" -o -name "setup.cfg" -o -name "setup.py" \) | head -5

Repository: hiero-ledger/hiero-sdk-python

Length of output: 89


🏁 Script executed:

cat ./pyproject.toml

Repository: hiero-ledger/hiero-sdk-python

Length of output: 2580


🏁 Script executed:

cat -n src/hiero_sdk_python/transaction/transfer_transaction.py | head -150

Repository: hiero-ledger/hiero-sdk-python

Length of output: 6764


🏁 Script executed:

# Search for PEP 604 pipe syntax usage in the codebase
rg '\w+\s*\|\s*\w+' src/ --type py | head -20

Repository: hiero-ledger/hiero-sdk-python

Length of output: 2524


🏁 Script executed:

# Search for Union usage in the codebase
rg 'Union\[' src/ --type py | head -30

Repository: hiero-ledger/hiero-sdk-python

Length of output: 3538


Use PEP 604 union syntax for consistency with project standards.

The project targets Python 3.10+ and has Ruff's UP rule enabled. Replace Union[int, Hbar] with int | Hbar on lines 103 and 119 (as well as line 69 in the internal _add_hbar_transfer method) to align with PEP 604 syntax already used elsewhere in the codebase.

🧰 Tools
🪛 Ruff (0.14.14)

[warning] 103-103: Use X | Y for type annotations

Convert to X | Y

(UP007)

Comment on lines 16 to 17
if hasattr(key, "_to_proto"):
key = key._to_proto() # Handle PublicKey objects (convert to proto first)
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

LGTM!

The hasattr check correctly identifies PublicKey-like objects and converts them to protobuf before calling HasField(). This aligns with the _to_proto() pattern used throughout the codebase (e.g., PublicKey._to_proto(), TransactionId._to_proto()).

Consider updating the type annotation and docstring to reflect that this function accepts both Key (protobuf) and PublicKey objects:

📝 Optional: Improve type annotation
-def format_key(key: Key) -> str:
+def format_key(key) -> str:
     """
     Converts a protobuf Key into a nicely formatted string:
       - If key is None, return "None"
       - If ed25519, show "ed25519(hex-encoded)"
       - If thresholdKey, keyList, or something else, show a short label.
+
+    Args:
+        key: A protobuf Key or a PublicKey object with a _to_proto() method.
     """

@gangulysiddhartha22-cmyk gangulysiddhartha22-cmyk force-pushed the feature/1635-transaction-record-query-include-duplicates branch from 453af5e to fe923cf Compare February 1, 2026 07:17
…e-duplicates

Signed-off-by: Siddhartha Ganguly <gangulysiddhartha22@gmail.com>
@gangulysiddhartha22-cmyk gangulysiddhartha22-cmyk force-pushed the feature/1635-transaction-record-query-include-duplicates branch from fe923cf to ad6885b Compare February 1, 2026 07:26
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (24)
src/hiero_sdk_python/utils/entity_id_helper.py (2)

17-33: ⚠️ Potential issue | 🟡 Minor

Return type mismatch and inaccurate docstring.

Several issues with this function:

  1. Docstring error: Mentions "An instance of cls" but there's no cls parameter - appears to be copy-paste from a class method.
  2. Missing type annotation: No return type specified.
  3. Returns strings instead of integers: The regex groups return strings, but callers using validate_checksum expect integers for shard, realm, and num (see AccountId.validate_checksum, ContractId.validate_checksum, etc. in the relevant snippets).
🔧 Proposed fix
-def parse_from_string(address: str):
+def parse_from_string(address: str) -> tuple[int, int, int, Optional[str]]:
     """
     Parse an address string of the form: <shard>.<realm>.<num>[-<checksum>]
     Examples:
         "0.0.123"
         "0.0.123-abcde"
 
     Returns:
-        An instance of cls with shard, realm, num, and optional checksum.
+        A tuple of (shard, realm, num, checksum) where shard/realm/num are integers
+        and checksum is an optional string.
     """
     match = ID_REGEX.match(address)
     if not match:
         raise ValueError("Invalid format for entity ID")
 
     shard, realm, num, checksum = match.groups()
 
-    return shard, realm, num, checksum
+    return int(shard), int(realm), int(num), checksum

153-154: 🧹 Nitpick | 🔵 Trivial

Minor: Include exception details in timeout error message for consistency.

The timeout error message omits the exception e, unlike the other exception handlers. While timeout messages are typically self-explanatory, including e maintains consistency and may provide additional context.

♻️ Suggested fix
     except requests.exceptions.Timeout as e:
-        raise RuntimeError(f"Mirror node request timed out for {url}") from e
+        raise RuntimeError(f"Mirror node request timed out for {url}: {e}") from e
src/hiero_sdk_python/hbar.py (1)

34-55: ⚠️ Potential issue | 🟡 Minor

Reject non‑finite Decimal values as well.
Decimal('NaN')/Decimal('Infinity') bypass the current finiteness check and can later break conversion. Add a Decimal.is_finite() guard to match the float path.

🛠️ Suggested fix
         if isinstance(amount, float) and not math.isfinite(amount):
             raise ValueError("Hbar amount must be finite")
+        if isinstance(amount, Decimal) and not amount.is_finite():
+            raise ValueError("Hbar amount must be finite")
src/hiero_sdk_python/tokens/token_freeze_status.py (1)

34-39: 🧹 Nitpick | 🔵 Trivial

Custom __eq__ breaks hash invariant - consider adding __hash__.

Overriding __eq__ to allow integer comparison (status == 1) breaks Python's invariant that a == b implies hash(a) == hash(b). This can cause subtle bugs when enums are used as dictionary keys or in sets alongside integers.

If integer comparison is intentional, explicitly define __hash__ to restore hashability:

♻️ Proposed fix
     def __eq__(self, other: Any) -> bool:
         """Checks equality with another TokenFreezeStatus or an integer."""
         if isinstance(other, TokenFreezeStatus):
             return self.value == other.value
         if isinstance(other, int):
             return self.value == other
         return False
+
+    def __hash__(self) -> int:
+        """Returns hash based on enum value."""
+        return hash(self.value)
src/hiero_sdk_python/tokens/token_pause_status.py (1)

47-59: 🧹 Nitpick | 🔵 Trivial

Custom __eq__ breaks hash invariant - consider adding __hash__.

Same issue as in TokenFreezeStatus: overriding __eq__ to compare with integers breaks the hash contract. Consider adding an explicit __hash__ method.

♻️ Proposed fix
         if isinstance(other, int):
             return self.value == other
         return False
+
+    def __hash__(self) -> int:
+        """Returns hash based on enum value."""
+        return hash(self.value)
src/hiero_sdk_python/tokens/token_kyc_status.py (1)

38-44: 🧹 Nitpick | 🔵 Trivial

Custom __eq__ breaks hash invariant - consider adding __hash__.

Same issue as the other status enums: overriding __eq__ to compare with integers breaks the hash contract. Consider adding an explicit __hash__ method for consistency.

♻️ Proposed fix
         if isinstance(other, int):
             return self.value == other
         return False
+
+    def __hash__(self) -> int:
+        """Returns hash based on enum value."""
+        return hash(self.value)
src/hiero_sdk_python/tokens/token_key_validation.py (1)

44-50: ⚠️ Potential issue | 🟠 Major

Overriding Enum.__eq__ breaks hashability—a breaking change for public token APIs.

Custom __eq__ makes TokenKeyValidation unhashable, preventing use as dict keys or set members. This breaks the enum contract and may violate existing user code. Per REVIEW FOCUS 1 (API Stability): "Token APIs are public and user-facing. Default values and behaviors MUST remain stable."

Choose one of:

  1. Remove __eq__ and add __int__: Let standard enum equality work; offer explicit int conversion where needed.
  2. Keep __eq__ but restore __hash__: Explicitly restore hashability with __hash__ = Enum.__hash__ and return NotImplemented for unsupported types.
Option 1: Replace with explicit `__int__`
-    def __eq__(self, other: Any) -> bool:
-        """Checks equality with another TokenKeyValidation or an integer."""
-        if isinstance(other, TokenKeyValidation):
-            return self.value == other.value
-        if isinstance(other, int):
-            return self.value == other
-        return False
+    def __int__(self) -> int:
+        """Returns the integer value for explicit comparisons."""
+        return int(self.value)
Option 2: Restore hashability if int comparison is essential
     def __eq__(self, other: Any) -> bool:
         """Checks equality with another TokenKeyValidation or an integer."""
         if isinstance(other, TokenKeyValidation):
             return self.value == other.value
         if isinstance(other, int):
             return self.value == other
-        return False
+        return NotImplemented
+
+    __hash__ = Enum.__hash__
src/hiero_sdk_python/tokens/custom_fee.py (1)

75-126: ⚠️ Potential issue | 🟠 Major

Preserve backward compatibility for _to_protobuf/_from_protobuf.
Renaming these hooks can break downstream subclasses or callers relying on the previous method names. Please keep deprecated wrappers to maintain API stability.

🔧 Suggested compatibility shim
     `@staticmethod`
     def _from_proto(
         custom_fee: "CustomFeeProto",
     ) -> "CustomFee":  # Changed from _from_protobuf
@@
         raise ValueError(f"Unrecognized fee case: {fee_case}")
+
+    `@staticmethod`
+    def _from_protobuf(custom_fee: "CustomFeeProto") -> "CustomFee":
+        import warnings
+        warnings.warn(
+            "CustomFee._from_protobuf is deprecated; use _from_proto instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return CustomFee._from_proto(custom_fee)
@@
     `@abstractmethod`
     def _to_proto(self) -> "CustomFeeProto":  # Changed from _to_protobuf
@@
         ...
+
+    def _to_protobuf(self) -> "CustomFeeProto":
+        import warnings
+        warnings.warn(
+            "CustomFee._to_protobuf is deprecated; use _to_proto instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self._to_proto()

As per coding guidelines: Token APIs are public and user-facing; method names and signatures must remain stable.

src/hiero_sdk_python/account/account_info.py (1)

142-173: ⚠️ Potential issue | 🟡 Minor

Preserve node id 0 when serializing staking_info.

Line 170 uses a truthiness check, so a valid staked_node_id=0 is dropped. Use an explicit is not None guard.

✅ Suggested fix
-                staked_node_id=self.staked_node_id if self.staked_node_id else None,
+                staked_node_id=(
+                    self.staked_node_id
+                    if self.staked_node_id is not None
+                    else None
+                ),
src/hiero_sdk_python/tokens/token_airdrop_pending_id.py (1)

46-47: 🧹 Nitpick | 🔵 Trivial

Validation aligns with protobuf oneof semantics, but error message has a grammatical issue.

The XOR logic (token_id is None) == (nft_id is None) correctly enforces mutual exclusivity matching the protobuf oneof structure. However, the error message "must be required" is grammatically awkward.

📝 Suggested message fix
         if (token_id is None) == (nft_id is None):
-            raise ValueError("Exactly one of 'token_id' or 'nft_id' must be required.")
+            raise ValueError("Exactly one of 'token_id' or 'nft_id' must be provided.")
src/hiero_sdk_python/file/file_append_transaction.py (2)

299-313: ⚠️ Potential issue | 🟠 Major

Normalize nanoseconds when offsetting timestamps.

Line 307 adds i directly to nanos. If base_timestamp.nanos is near 1e9, this produces an invalid protobuf Timestamp and can break multi‑chunk submissions.

Suggested fix
-                chunk_valid_start = timestamp_pb2.Timestamp(
-                    seconds=base_timestamp.seconds, nanos=base_timestamp.nanos + i
-                )
+                total_nanos = base_timestamp.nanos + i
+                extra_seconds, nanos = divmod(total_nanos, 1_000_000_000)
+                chunk_valid_start = timestamp_pb2.Timestamp(
+                    seconds=base_timestamp.seconds + extra_seconds, nanos=nanos
+                )

51-81: ⚠️ Potential issue | 🟡 Minor

Remove quoted forward reference on _signing_keys.

The file has from __future__ import annotations enabled, and Ruff's UP037 rule is configured (as part of the "UP" ruleset in pyproject.toml). Line 79 uses a quoted forward reference List["PrivateKey"] which will trigger UP037. Remove the quotes to use List[PrivateKey] directly.

Note: The missing -> None return annotation on __init__ is not enforced by the current Ruff configuration (ANN204 is not selected) and does not need to be added.

Suggested fix
-        self._signing_keys: List["PrivateKey"] = (
-            []
-        )  # Use string annotation to avoid import issues
+        self._signing_keys: List[PrivateKey] = []
src/hiero_sdk_python/consensus/topic_message_submit_transaction.py (1)

18-52: ⚠️ Potential issue | 🟠 Major

Validate constructor inputs for chunk_size / max_chunks.
Negative values bypass setter validation and can yield invalid chunk counts (including no-op execution). Align constructor behavior with the setters.

🛠️ Proposed validation
-        self.chunk_size: int = chunk_size or 1024
-        self.max_chunks: int = max_chunks or 20
+        if chunk_size is not None and chunk_size <= 0:
+            raise ValueError("chunk_size must be positive")
+        if max_chunks is not None and max_chunks <= 0:
+            raise ValueError("max_chunks must be positive")
+        self.chunk_size = chunk_size or 1024
+        self.max_chunks = max_chunks or 20
src/hiero_sdk_python/consensus/topic_message.py (1)

150-180: 🧹 Nitpick | 🔵 Trivial

Make chunking_enabled keyword-only to avoid positional-bool ambiguity.

This internal method is unused in the codebase, making the refactoring safe with no call sites to update. The change aligns with boolean-arg best practices and prevents accidental misuse.

♻️ Proposed signature change
-    def _from_proto(
-        cls,
-        response_or_responses: Union[
-            mirror_proto.ConsensusTopicResponse,
-            List[mirror_proto.ConsensusTopicResponse],
-        ],
-        chunking_enabled: bool = False,
-    ) -> "TopicMessage":
+    def _from_proto(
+        cls,
+        response_or_responses: Union[
+            mirror_proto.ConsensusTopicResponse,
+            List[mirror_proto.ConsensusTopicResponse],
+        ],
+        *,
+        chunking_enabled: bool = False,
+    ) -> "TopicMessage":
src/hiero_sdk_python/tokens/fee_assessment_method.py (1)

33-34: ⚠️ Potential issue | 🔴 Critical

CRITICAL: FeeAssessmentMethod enum values are semantically inverted relative to Hedera protobuf.

The enum currently defines INCLUSIVE = 0 and EXCLUSIVE = 1, but this is backwards. According to Hedera's protobuf definition, the net_of_transfers field is a boolean where:

  • true (1) means fee is assessed net of transfersINCLUSIVE
  • false (0) means fee is assessed not net of transfersEXCLUSIVE

This inversion causes a critical semantic bug: developers setting assessment_method=FeeAssessmentMethod.INCLUSIVE will have their fees assessed as EXCLUSIVE on Hedera, and vice versa.

Required fix: Swap the enum values:

  • INCLUSIVE = 1
  • EXCLUSIVE = 0

This must be treated as a breaking change requiring deprecation warnings and migration guidance, as existing code relying on the inverted semantics will need updates.

src/hiero_sdk_python/query/topic_info_query.py (1)

106-109: ⚠️ Potential issue | 🟡 Minor

Remove debug print statements from production code.

The print() and traceback.print_exc() calls introduce side effects that should not be in production code. These appear to be debug artifacts. Per coding guidelines, side effects like logging and prints should be flagged if they could affect execution flow or user experience.

Proposed fix — remove debug statements
         except Exception as e:
-            print(f"Exception in _make_request: {e}")
-            traceback.print_exc()
             raise

If logging is needed, consider using the standard logging module with appropriate log levels instead.

src/hiero_sdk_python/client/network.py (1)

19-71: 🧹 Nitpick | 🔵 Trivial

Consider annotating mutable class attributes with ClassVar.

The static analysis correctly identifies that mutable class attributes (MIRROR_ADDRESS_DEFAULT, MIRROR_NODE_URLS, DEFAULT_NODES, LEDGER_ID) should be annotated with typing.ClassVar to indicate they are class-level and not instance-level attributes. This is a recommended practice but not blocking.

♻️ Proposed fix
+from typing import ClassVar, Dict, List, Optional, Any, Tuple
-from typing import Dict, List, Optional, Any, Tuple

 class Network:
     # Mirror node gRPC addresses (always use TLS, port 443 for HTTPS)
-    MIRROR_ADDRESS_DEFAULT: Dict[str, str] = {
+    MIRROR_ADDRESS_DEFAULT: ClassVar[Dict[str, str]] = {
         "mainnet": "mainnet.mirrornode.hedera.com:443",
         ...
     }
src/hiero_sdk_python/tokens/token_create_transaction.py (1)

115-125: ⚠️ Potential issue | 🔴 Critical

Bug: Instance method called as static method.

The _validate_token_freeze_status method is defined without the @staticmethod decorator (unlike _validate_token_params at line 104), making it an instance method. However, at line 517 it's called as TokenCreateValidator._validate_token_freeze_status(self._keys, self._token_params) which is a class-level call without instantiation.

This will cause a TypeError at runtime because the self parameter won't be bound.

🐛 Proposed fix - Add `@staticmethod` decorator
+    `@staticmethod`
     def _validate_token_freeze_status(
-        keys: TokenKeys, token_params: TokenParams
+        keys: TokenKeys, token_params: TokenParams
     ) -> None:
         """Ensure account is not frozen for this token."""
src/hiero_sdk_python/query/transaction_get_receipt_query.py (1)

175-178: ⚠️ Potential issue | 🟡 Minor

Remove debug print statements from production code.

The print() and traceback.print_exc() calls are debug artifacts that should not be in production code. They bypass the SDK's logging infrastructure and clutter stdout. If error context is needed, use the SDK's Logger or let the exception propagate naturally with its traceback.

🧹 Suggested fix
-        except Exception as e:
-            print(f"Exception in _make_request: {e}")
-            traceback.print_exc()
-            raise
+        except Exception:
+            raise

Or remove the try/except entirely since it just re-raises:

-        try:
             if not self.transaction_id:
                 raise ValueError(
                     "Transaction ID must be set before making the request."
                 )
             # ... rest of method ...
             return query
-        except Exception as e:
-            print(f"Exception in _make_request: {e}")
-            traceback.print_exc()
-            raise
src/hiero_sdk_python/__init__.py (1)

215-215: ⚠️ Potential issue | 🟡 Minor

Duplicate ResponseCode entry in __all__.

ResponseCode appears twice in the exports list — at line 215 (under "Response") and line 240 (under "HBAR"). This redundancy is harmless but should be cleaned up.

🧹 Suggested fix

Remove the duplicate on line 240:

     # HBAR
     "Hbar",
     "HbarUnit",
-    "ResponseCode",
     "Timestamp",
     "Duration",
     "StakingInfo",

Also applies to: 240-240

src/hiero_sdk_python/tokens/token_nft_info.py (1)

1-14: 🧹 Nitpick | 🔵 Trivial

Module docstring should precede all imports.

The module docstring (lines 3-9) is placed after the dataclass import on line 1. Per Python conventions, module-level docstrings should be the first statement in the file.

🧹 Suggested fix
+"""
+hiero_sdk_python.tokens.token_nft_info.py
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Defines TokenNftInfo, a class representing Non-Fungible Token details (ID, owner,
+creation time, metadata, spender) on the Hedera network, with protobuf conversion.
+"""
 from dataclasses import dataclass
-
-"""
-hiero_sdk_python.tokens.token_nft_info.py
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Defines TokenNftInfo, a class representing Non-Fungible Token details (ID, owner,
-creation time, metadata, spender) on the Hedera network, with protobuf conversion.
-"""
 from typing import Optional
src/hiero_sdk_python/tokens/token_id.py (1)

41-42: ⚠️ Potential issue | 🔴 Critical

TokenId checksum must be excluded from equality comparison.

Adding checksum to the dataclass without compare=False creates a Python hash/equality contract violation. TokenId defines a custom __hash__ based only on (shard, realm, num), but the generated __eq__ would include checksum. This means two TokenIds with identical shard/realm/num but different checksums would have the same hash yet not be equal—breaking dict/set behavior and violating Python's equality contract.

Since checksums are validation metadata (not part of identity), compare=False is the correct fix and aligns with the semantic intent of token identity.

Proposed fix
-    checksum: str | None = field(default=None, init=False)
+    checksum: str | None = field(default=None, init=False, compare=False)
src/hiero_sdk_python/query/token_nft_info_query.py (1)

60-77: ⚠️ Potential issue | 🟡 Minor

Remove print/traceback side effects from _make_request.

Query construction shouldn’t emit console output or stack traces; it changes execution side effects and can leak details. Let the exception propagate naturally.

🔧 Suggested change
-        try:
-            if not self.nft_id:
-                raise ValueError("NFT ID must be set before making the request.")
-
-            query_header = self._make_request_header()
-
-            nft_info_query = token_get_nft_info_pb2.TokenGetNftInfoQuery()
-            nft_info_query.header.CopyFrom(query_header)
-            nft_info_query.nftID.CopyFrom(self.nft_id._to_proto())
-
-            query = query_pb2.Query()
-            query.tokenGetNftInfo.CopyFrom(nft_info_query)
-
-            return query
-        except Exception as e:
-            print(f"Exception in _make_request: {e}")
-            traceback.print_exc()
-            raise
+        if not self.nft_id:
+            raise ValueError("NFT ID must be set before making the request.")
+
+        query_header = self._make_request_header()
+
+        nft_info_query = token_get_nft_info_pb2.TokenGetNftInfoQuery()
+        nft_info_query.header.CopyFrom(query_header)
+        nft_info_query.nftID.CopyFrom(self.nft_id._to_proto())
+
+        query = query_pb2.Query()
+        query.tokenGetNftInfo.CopyFrom(nft_info_query)
+
+        return query
🧹 Follow-up cleanup
-import traceback
src/hiero_sdk_python/tokens/token_update_transaction.py (1)

492-515: ⚠️ Potential issue | 🔴 Critical

Remove reference to non-existent metadata_key field.

The protobuf definition for TokenUpdateTransactionBody does not include a metadata_key field. Attempting to access token_update_body.metadata_key will raise an AttributeError at runtime.

The mixed casing (camelCase for adminKey/kycKey vs snake_case for pause_key/fee_schedule_key) is correct and matches the official hedera-protobufs definition. Remove the metadata_key assignment or clarify if this field should map to a different protobuf field.

staked_account_id: Optional[AccountId] = None,
staked_node_id: Optional[int] = None,
decline_staking_reward: Optional[bool] = False
decline_staking_reward: Optional[bool] = False,
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Minor type hint inconsistency: Optional[bool] with non-None default.

The type Optional[bool] implies None is valid, but the default is False and the setter at line 318 rejects non-bool values (including None). Consider using bool = False for consistency, or if None is intentionally allowed in the constructor, update the setter accordingly.

💡 Suggested fix for type consistency
-        decline_staking_reward: Optional[bool] = False,
+        decline_staking_reward: bool = False,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
decline_staking_reward: Optional[bool] = False,
decline_staking_reward: bool = False,
🧰 Tools
🪛 Ruff (0.14.14)

[error] 48-48: Boolean-typed positional argument in function definition

(FBT001)


[error] 48-48: Boolean default positional argument in function definition

(FBT002)

Comment on lines 156 to 159
if mirror_address.endswith(":50212") or mirror_address.endswith(":443"):
self.mirror_channel = grpc.secure_channel(
mirror_address, grpc.ssl_channel_credentials()
)
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Merge endswith calls into a single tuple check.

The static analysis correctly identifies that multiple endswith calls can be merged for cleaner code.

♻️ Proposed fix
-        if mirror_address.endswith(":50212") or mirror_address.endswith(":443"):
+        if mirror_address.endswith((":50212", ":443")):
             self.mirror_channel = grpc.secure_channel(
                 mirror_address, grpc.ssl_channel_credentials()
             )
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 156-156: Call endswith once with a tuple

Merge into a single endswith call

(PIE810)

Comment on lines 263 to 271
# ← Extract the many ternaries here (this flattens control flow)
admin_key_proto = self.admin_key._to_proto() if self.admin_key else None
submit_key_proto = self.submit_key._to_proto() if self.submit_key else None
auto_renew_period = duration_pb2.Duration(seconds=self.auto_renew_period.seconds) if self.auto_renew_period else None
auto_renew_account = self.auto_renew_account._to_proto() if self.auto_renew_account else None
expiration_time = self.expiration_time._to_protobuf() if self.expiration_time else None
memo_value = _wrappers_pb2.StringValue(value=self.memo) if self.memo is not None else None
fee_schedule_key_proto = self.fee_schedule_key._to_proto() if self.fee_schedule_key else None

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

Remove stray code review comment.

Line 263 contains a leftover comment # ← Extract the many ternaries here that appears to be a code review artifact or development note. This should be removed before merging.

Proposed fix
-    # ← Extract the many ternaries here
         admin_key_proto       = self.admin_key._to_proto()       if self.admin_key       else None
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# ← Extract the many ternaries here (this flattens control flow)
admin_key_proto = self.admin_key._to_proto() if self.admin_key else None
submit_key_proto = self.submit_key._to_proto() if self.submit_key else None
auto_renew_period = duration_pb2.Duration(seconds=self.auto_renew_period.seconds) if self.auto_renew_period else None
auto_renew_account = self.auto_renew_account._to_proto() if self.auto_renew_account else None
expiration_time = self.expiration_time._to_protobuf() if self.expiration_time else None
memo_value = _wrappers_pb2.StringValue(value=self.memo) if self.memo is not None else None
fee_schedule_key_proto = self.fee_schedule_key._to_proto() if self.fee_schedule_key else None
admin_key_proto = self.admin_key._to_proto() if self.admin_key else None
submit_key_proto = self.submit_key._to_proto() if self.submit_key else None
auto_renew_period = duration_pb2.Duration(seconds=self.auto_renew_period.seconds) if self.auto_renew_period else None
auto_renew_account = self.auto_renew_account._to_proto() if self.auto_renew_account else None
expiration_time = self.expiration_time._to_protobuf() if self.expiration_time else None
memo_value = _wrappers_pb2.StringValue(value=self.memo) if self.memo is not None else None
fee_schedule_key_proto = self.fee_schedule_key._to_proto() if self.fee_schedule_key else None

Comment on lines 215 to 217
raise ValueError(
"to_string_with_checksum cannot be applied to ContractId with evm_address"
)
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Address Ruff TRY003 (long message outside exception class).

If the rule is enforced, either suppress it or use a custom exception with a built-in message to comply.

🛠️ Minimal suppression option
         if self.evm_address is not None:
             raise ValueError(
                 "to_string_with_checksum cannot be applied to ContractId with evm_address"
-            )
+            )  # noqa: TRY003
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 215-217: Avoid specifying long messages outside the exception class

(TRY003)

Comment on lines 330 to 341
def execute(self, client: "Client") -> Any:
"""
Executes the file append transaction.

For multi-chunk transactions, this method will execute all chunks sequentially.

Args:
client: The client to execute the transaction with.

Returns:
TransactionReceipt: The receipt from the first chunk execution.
"""
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:

# Inspect base Transaction.execute signature/return type
rg -nP 'class Transaction\b' src/hiero_sdk_python/transaction/transaction.py -C2
rg -nP 'def execute\(' src/hiero_sdk_python/transaction/transaction.py -C3

Repository: hiero-ledger/hiero-sdk-python

Length of output: 349


🏁 Script executed:

# Get more context around Transaction.execute
sed -n '351,370p' src/hiero_sdk_python/transaction/transaction.py

# Check for type stub files
find . -name "*.pyi" | head -20

# Check ruff configuration
cat pyproject.toml | grep -A 20 "\[tool.ruff"

Repository: hiero-ledger/hiero-sdk-python

Length of output: 1723


🏁 Script executed:

# Check FileAppendTransaction.execute() full implementation
sed -n '330,360p' src/hiero_sdk_python/file/file_append_transaction.py

# Check imports in FileAppendTransaction
head -50 src/hiero_sdk_python/file/file_append_transaction.py | grep -E "^import|^from"

# Verify ANN401 rule status in ruff
grep -i "ANN" pyproject.toml

Repository: hiero-ledger/hiero-sdk-python

Length of output: 1663


🏁 Script executed:

# Check if Client is in TYPE_CHECKING block in FileAppendTransaction
grep -n "TYPE_CHECKING" src/hiero_sdk_python/file/file_append_transaction.py -A 20

# Also check what the multi-chunk execute() actually returns at the end
sed -n '330,380p' src/hiero_sdk_python/file/file_append_transaction.py | tail -30

Repository: hiero-ledger/hiero-sdk-python

Length of output: 2884


Correct the return type annotation to match the implementation.

The return type is annotated as Any, but the docstring and implementation indicate it returns TransactionReceipt. For single-chunk transactions it returns the base class result (TransactionReceipt), and for multi-chunk it returns responses[0] if responses else None. Update the signature to TransactionReceipt | None to accurately reflect the actual return type. The quoted Client type is necessary and should remain unchanged (it's correctly placed in the TYPE_CHECKING block to avoid circular imports).

🧰 Tools
🪛 Ruff (0.14.14)

[warning] 330-330: Remove quotes from type annotation

Remove quotes

(UP037)


[warning] 330-330: Dynamically typed expressions (typing.Any) are disallowed in execute

(ANN401)

Comment on lines 155 to 161
if status == ResponseCode.OK:
if response.transactionGetRecord.header.responseType == query_header_pb2.ResponseType.COST_ANSWER:
if (
response.transactionGetRecord.header.responseType
== query_header_pb2.ResponseType.COST_ANSWER
):
return _ExecutionState.FINISHED
pass
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Remove unnecessary pass statement.

The pass on line 161 serves no purpose and can be safely removed.

♻️ Proposed fix
         if status == ResponseCode.OK:
             if (
                 response.transactionGetRecord.header.responseType
                 == query_header_pb2.ResponseType.COST_ANSWER
             ):
                 return _ExecutionState.FINISHED
-            pass
         elif (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if status == ResponseCode.OK:
if response.transactionGetRecord.header.responseType == query_header_pb2.ResponseType.COST_ANSWER:
if (
response.transactionGetRecord.header.responseType
== query_header_pb2.ResponseType.COST_ANSWER
):
return _ExecutionState.FINISHED
pass
if status == ResponseCode.OK:
if (
response.transactionGetRecord.header.responseType
== query_header_pb2.ResponseType.COST_ANSWER
):
return _ExecutionState.FINISHED
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 161-161: Unnecessary pass statement

Remove unnecessary pass

(PIE790)

Comment on lines 130 to 133
def set_denominating_token_id(
self, token_id: typing.Optional["TokenId"]
) -> "CustomFixedFee":
"""Sets the fungible token used to pay the fee.
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:

# Check if the file exists and read its contents
cat -n src/hiero_sdk_python/tokens/custom_fixed_fee.py | head -20

Repository: hiero-ledger/hiero-sdk-python

Length of output: 995


🏁 Script executed:

# Get file info and check lines 130-133 and 160-163
wc -l src/hiero_sdk_python/tokens/custom_fixed_fee.py

Repository: hiero-ledger/hiero-sdk-python

Length of output: 124


🏁 Script executed:

# Read the specific sections
sed -n '1,15p' src/hiero_sdk_python/tokens/custom_fixed_fee.py
sed -n '125,140p' src/hiero_sdk_python/tokens/custom_fixed_fee.py
sed -n '155,170p' src/hiero_sdk_python/tokens/custom_fixed_fee.py

Repository: hiero-ledger/hiero-sdk-python

Length of output: 1597


Address Ruff UP037: remove quoted annotations.

With from __future__ import annotations at line 1, annotations are already deferred and implicitly stringified. Remove unnecessary quotes from TokenId, CustomFixedFee, and custom_fees_pb2.FixedFee annotations at lines 130–133 and 160–163 to keep the codebase lint-clean.

Suggested changes
     def set_denominating_token_id(
-        self, token_id: typing.Optional["TokenId"]
-    ) -> "CustomFixedFee":
+        self, token_id: typing.Optional[TokenId]
+    ) -> CustomFixedFee:
     `@staticmethod`
     def _from_fixed_fee_proto(
-        fixed_fee: "custom_fees_pb2.FixedFee",
-    ) -> "CustomFixedFee":
+        fixed_fee: custom_fees_pb2.FixedFee,
+    ) -> CustomFixedFee:
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 131-131: Remove quotes from type annotation

Remove quotes

(UP037)


[warning] 132-132: Remove quotes from type annotation

Remove quotes

(UP037)

raise ValueError("Pending airdrops list must contain mininum 1 and maximum 10 pendingAirdrop.")
if len(pending_airdrops_proto) < 1 or len(pending_airdrops_proto) > 10:
raise ValueError(
"Pending airdrops list must contain mininum 1 and maximum 10 pendingAirdrop."
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

Fix typo in error message.

The error message contains a typo: "mininum" should be "minimum".

📝 Proposed fix
-                "Pending airdrops list must contain mininum 1 and maximum 10 pendingAirdrop."
+                "Pending airdrops list must contain minimum 1 and maximum 10 pendingAirdrop."
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"Pending airdrops list must contain mininum 1 and maximum 10 pendingAirdrop."
"Pending airdrops list must contain minimum 1 and maximum 10 pendingAirdrop."

Comment on lines 18 to 20
• KYC_NOT_APPLICABLE – not applicable
• GRANTED – KYC granted
• REVOKED – KYC revoked
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Replace ambiguous EN DASH characters in docstring.

Ruff flags the (EN DASH) characters as ambiguous (RUF002). Replace with standard hyphen-minus - to avoid encoding ambiguity.

🧹 Suggested cleanup
-      • KYC_NOT_APPLICABLE – not applicable
-      • GRANTED           – KYC granted
+      • KYC_NOT_APPLICABLE - not applicable
+      • GRANTED           - KYC granted
       • REVOKED           – KYC revoked
+      • REVOKED           - KYC revoked
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
KYC_NOT_APPLICABLE not applicable
GRANTED KYC granted
REVOKED KYC revoked
KYC_NOT_APPLICABLE - not applicable
GRANTED - KYC granted
REVOKED - KYC revoked
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 18-18: Docstring contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF002)


[warning] 19-19: Docstring contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF002)


[warning] 20-20: Docstring contains ambiguous (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF002)

Comment on lines 76 to 78
expected_decimals=(
{"value": self.expected_decimals} if self.expected_decimals else None
),
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

Falsy check incorrectly excludes expected_decimals=0.

The condition if self.expected_decimals evaluates to False when expected_decimals is 0, which may be a valid decimal value. This would cause expected_decimals=0 to be silently omitted from the protobuf message.

🐛 Proposed fix
         proto = basic_types_pb2.TokenTransferList(
             token=self.token._to_proto(),
             expected_decimals=(
-                {"value": self.expected_decimals} if self.expected_decimals else None
+                {"value": self.expected_decimals} if self.expected_decimals is not None else None
             ),
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expected_decimals=(
{"value": self.expected_decimals} if self.expected_decimals else None
),
expected_decimals=(
{"value": self.expected_decimals} if self.expected_decimals is not None else None
),

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/hiero_sdk_python/transaction/transaction_record.py (1)

62-76: 🧹 Nitpick | 🔵 Trivial

Document the new duplicates field in the class docstring.

duplicates is now a public field but isn’t listed in the Attributes section, which makes the public API docs incomplete.

📝 Suggested doc update
         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]):             Duplicate/child transaction records returned by include-duplicates queries.
src/hiero_sdk_python/query/transaction_record_query.py (1)

177-212: ⚠️ Potential issue | 🟠 Major

Use receipt status when constructing ReceiptStatusError.

ReceiptStatusError should reflect the receipt’s status; currently it uses the header precheck status (often OK), which can mask the actual failure code.

🛠️ Proposed fix
-        receipt = response.transactionGetRecord.transactionRecord.receipt
-
-        return ReceiptStatusError(
-            status,
-            self.transaction_id,
-            TransactionReceipt._from_proto(receipt, self.transaction_id),
-        )
+        receipt = response.transactionGetRecord.transactionRecord.receipt
+        receipt_status = receipt.status
+
+        return ReceiptStatusError(
+            receipt_status,
+            self.transaction_id,
+            TransactionReceipt._from_proto(receipt, self.transaction_id),
+        )

@gangulysiddhartha22-cmyk
Copy link
Contributor Author

Closing this PR to open a fresh one with all feedback incorporated. Thank you!

@gangulysiddhartha22-cmyk
Copy link
Contributor Author

New PR for the same issue #1671

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for include_duplicates to the TransactionRecordQuery class

5 participants