Skip to content

Conversation

@manishdait
Copy link
Contributor

Description:
This PR adds support for populating the ContractId numeric value using a Mirror Node query, similar to the existing AccountId.populate_account_num() functionality introduced in PR #1009.

Chnaged Made

  • Added from_evm_address method to ContractId.
  • Added populate_contract_num(client) method to ContractId to resolve the contract number via Mirror Node.
  • Updated _from_proto() to correctly handle evm_address when present in the protobuf representation.
  • Update to_evm_address() to use the method form EntityHelper utils.
  • Added byte serialization support via to_bytes() and from_bytes().
  • Updated related AccountId methods for consistency with ContractId, returning new immutable instances to ensure correct hashing behavior.

Related issue(s):

Fixes #1564

Checklist

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

@codecov
Copy link

codecov bot commented Jan 22, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1565      +/-   ##
==========================================
+ Coverage   92.89%   92.93%   +0.04%     
==========================================
  Files         140      140              
  Lines        8765     8805      +40     
==========================================
+ Hits         8142     8183      +41     
+ Misses        623      622       -1     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@manishdait manishdait marked this pull request as ready for review January 23, 2026 13:45
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 23, 2026

Walkthrough

Adds Mirror Node population and protobuf (de)serialization for ContractId via EVM-address, and changes AccountId population methods to return new instances instead of mutating in place. Tests, examples, and a wait-for-mirror-node utility were added/updated to reflect the new behavior.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Documented ContractId mirror-node population and AccountId/utility updates.
Examples
examples/account/account_id_populate_from_mirror.py
Updated to use returned AccountId values from populate_account_num() and populate_evm_address(); formatting/EOF fixes.
AccountId core
src/hiero_sdk_python/account/account_id.py
populate_account_num() and populate_evm_address() now return new AccountId instances instead of mutating the caller; minor error/message and formatting tweaks—call sites must capture return values.
ContractId core
src/hiero_sdk_python/contract/contract_id.py
New EVM-address support and mirror population: from_evm_address(), from_bytes(), to_bytes(), populate_contract_num(); _from_proto handles evm_address; to_evm_address() now delegates to to_solidity_address(); added validation and mirror-node query logic.
Utilities
src/hiero_sdk_python/utils/entity_id_helper.py
Added parse_from_string(); strengthened perform_query_to_mirror_node() input validation and timeout/error handling; minor declaration/formatting changes in checksum helpers.
Integration tests & utils
tests/integration/..., tests/integration/utils.py
Added wait_for_mirror_node() utility and updated integration tests: account population uses returned AccountId; new E2E test for ContractId.populate_contract_num() via mirror node.
Unit tests
tests/unit/account_id_test.py, tests/unit/contract_id_test.py
AccountId unit tests updated for return-value semantics; extensive new ContractId tests covering EVM-address parsing, proto/bytes, mirror-node population, and many negative/error cases.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ContractId
    participant MirrorNode

    Client->>ContractId: from_evm_address(shard, realm, evm_address)
    Note over ContractId: construct ContractId (evm_address set, numeric contract unset)
    Client->>ContractId: populate_contract_num(client)
    ContractId->>MirrorNode: HTTP query by EVM address (perform_query_to_mirror_node)
    MirrorNode-->>ContractId: response with contract id (or error)
    Note over ContractId: create new ContractId with populated contract number
    ContractId-->>Client: return new ContractId instance
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the main change: adding MirrorNode-based population support for ContractId numeric values, which aligns with the primary objective of this changeset.
Description check ✅ Passed The PR description is related to the changeset, listing the key changes made including from_evm_address, populate_contract_num, byte serialization, and AccountId consistency updates.
Linked Issues check ✅ Passed The PR successfully implements all coding requirements from issue #1564: from_evm_address method, populate_contract_num method with Mirror Node queries returning new instances, byte serialization (to_bytes/from_bytes), and AccountId consistency updates with immutable return values.
Out of Scope Changes check ✅ Passed All code changes are directly related to the linked issue #1564 objectives: ContractId Mirror Node population, byte serialization, AccountId consistency updates, and utility function additions for polling Mirror Node.
Docstring Coverage ✅ Passed Docstring coverage is 95.40% 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

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: 10

Caution

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

⚠️ Outside diff range comments (1)
tests/integration/account_id_population_e2e_test.py (1)

27-65: Test validates immutability and population correctness well.

The test properly validates:

  1. Original mirror_account_id.num remains 0 (immutability check on line 60)
  2. Returned new_account_id has populated values matching the receipt's account_id
  3. EVM address is preserved in the returned instance

One minor observation: the test could benefit from an explicit type assertion to protect against breaking changes.

Consider adding a type assertion for stronger contract protection
     new_account_id = mirror_account_id.populate_account_num(env.client)
 
     assert mirror_account_id.num == 0
+    assert isinstance(new_account_id, AccountId), "populate_account_num must return AccountId"
 
     assert new_account_id.evm_address == mirror_account_id.evm_address

import requests

from typing import TYPE_CHECKING, Any, Dict, Optional
from typing import TYPE_CHECKING, Any, Dict
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

Use built-in dict instead of typing.Dict.

typing.Dict is deprecated since Python 3.9. Use the built-in dict for type hints.

Suggested fix
-from typing import TYPE_CHECKING, Any, Dict
+from typing import TYPE_CHECKING, Any

Then update the return type on line 139:

-def perform_query_to_mirror_node(url: str, timeout: float = 10) -> Dict[str, Any]:
+def perform_query_to_mirror_node(url: str, timeout: float = 10) -> dict[str, Any]:
🧰 Tools
🪛 Ruff (0.14.13)

5-5: typing.Dict is deprecated, use dict instead

(UP035)

Comment on lines 25 to 73
@pytest.mark.integration
def test_populate_contract_id_num(env):
"""Test populate ContractId num from mirror node."""
# Create a contract transaction
file_receipt = (
FileCreateTransaction()
.set_keys(env.operator_key.public_key())
.set_contents(STATEFUL_CONTRACT_BYTECODE)
.set_file_memo("integration test contract")
.execute(env.client)
)
assert file_receipt.status == ResponseCode.SUCCESS
file_id = file_receipt.file_id
assert file_id is not None

constructor_params = ContractFunctionParameters().add_bytes32(
b"Initial message from constructor"
)
contract_receipt = (
ContractCreateTransaction()
.set_admin_key(env.operator_key.public_key())
.set_gas(CONTRACT_DEPLOY_GAS)
.set_constructor_parameters(constructor_params)
.set_bytecode_file_id(file_id)
.execute(env.client)
)
assert contract_receipt.status == ResponseCode.SUCCESS
contract_id_with_num = contract_receipt.contract_id
assert contract_id_with_num is not None

# Query contract info to get evm_address
info = ContractInfoQuery().set_contract_id(contract_id_with_num).execute(env.client)
contract_id_with_evm_addr = ContractId.from_evm_address(
0, 0, info.contract_account_id
)

# Wait for mirrornode to update
time.sleep(5)

final_contract_id = contract_id_with_evm_addr.populate_contract_num(env.client)

assert final_contract_id.shard == contract_id_with_num.shard
assert final_contract_id.realm == contract_id_with_num.realm
assert final_contract_id.contract == contract_id_with_num.contract
assert final_contract_id.evm_address == contract_id_with_evm_addr.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

Consider adding type assertion for returned ContractId.

To protect against breaking changes and ensure the method contract is maintained, add a type assertion on the returned value.

Add type assertion
     final_contract_id = contract_id_with_evm_addr.populate_contract_num(env.client)
 
+    assert isinstance(final_contract_id, ContractId), "populate_contract_num must return ContractId"
+
     assert final_contract_id.shard == contract_id_with_num.shard
🧰 Tools
🪛 Ruff (0.14.13)

26-26: Redefinition of unused env from line 22: env redefined here

(F811)

Comment on lines +795 to +798
new_account_id = account_id.populate_evm_address(mock_client)

assert account_id.evm_address == evm_address
assert account_id.evm_address == None
assert new_account_id.evm_address == 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

Use is None instead of == None for None comparison.

Per Python conventions (PEP 8), comparisons to None should use is operator rather than ==.

Fix None comparison
     new_account_id = account_id.populate_evm_address(mock_client)
 
-    assert account_id.evm_address == None
+    assert account_id.evm_address is None
     assert new_account_id.evm_address == evm_address
📝 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
new_account_id = account_id.populate_evm_address(mock_client)
assert account_id.evm_address == evm_address
assert account_id.evm_address == None
assert new_account_id.evm_address == evm_address
new_account_id = account_id.populate_evm_address(mock_client)
assert account_id.evm_address is None
assert new_account_id.evm_address == evm_address
🧰 Tools
🪛 Ruff (0.14.13)

797-797: Comparison to None should be cond is None

Replace with cond is None

(E711)

Comment on lines +61 to +73
@pytest.mark.parametrize(
"contract_str, expected",
[
("1.2.101", (1, 2, 101, None, None)),
("0.0.0", (0, 0, 0, None, None)),
("1.2.3-abcde", (1, 2, 3, None, "abcde")),
(
"1.2.abcdef0123456789abcdef0123456789abcdef01",
(1, 2, 0, bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01"), None),
),
],
)
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 tuple for parametrize argument names.

Ruff suggests using a tuple instead of a string for the first argument of pytest.mark.parametrize for consistency.

Suggested fix
 `@pytest.mark.parametrize`(
-    "contract_str, expected",
+    ("contract_str", "expected"),
     [
         ("1.2.101", (1, 2, 101, None, 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
@pytest.mark.parametrize(
"contract_str, expected",
[
("1.2.101", (1, 2, 101, None, None)),
("0.0.0", (0, 0, 0, None, None)),
("1.2.3-abcde", (1, 2, 3, None, "abcde")),
(
"1.2.abcdef0123456789abcdef0123456789abcdef01",
(1, 2, 0, bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01"), None),
),
],
)
`@pytest.mark.parametrize`(
("contract_str", "expected"),
[
("1.2.101", (1, 2, 101, None, None)),
("0.0.0", (0, 0, 0, None, None)),
("1.2.3-abcde", (1, 2, 3, None, "abcde")),
(
"1.2.abcdef0123456789abcdef0123456789abcdef01",
(1, 2, 0, bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01"), None),
),
],
)
🧰 Tools
🪛 Ruff (0.14.13)

62-62: Wrong type passed to first argument of pytest.mark.parametrize; expected tuple

Use a tuple for the first argument

(PT006)

Comment on lines 339 to 352
contract_id = ContractId.from_string("0.0.1-wronx")

with pytest.raises(ValueError, match="Checksum mismatch for 0.0.1"):
contract_id.validate_checksum(client)
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

Escape regex metacharacters in match pattern.

The pattern "Checksum mismatch for 0.0.1" contains dots (.) which are regex metacharacters matching any character. Use a raw string with escaped dots or re.escape() to ensure exact matching.

Proposed fix
-    with pytest.raises(ValueError, match="Checksum mismatch for 0.0.1"):
+    with pytest.raises(ValueError, match=r"Checksum mismatch for 0\.0\.1"):
         contract_id.validate_checksum(client)
📝 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
contract_id = ContractId.from_string("0.0.1-wronx")
with pytest.raises(ValueError, match="Checksum mismatch for 0.0.1"):
contract_id.validate_checksum(client)
contract_id = ContractId.from_string("0.0.1-wronx")
with pytest.raises(ValueError, match=r"Checksum mismatch for 0\.0\.1"):
contract_id.validate_checksum(client)
🧰 Tools
🪛 Ruff (0.14.13)

341-341: Pattern passed to match= contains metacharacters but is neither escaped nor raw

(RUF043)

Comment on lines +500 to +516
):
with pytest.raises(
ValueError,
match="Invalid contract_id format received: invalid.account.format",
):
contract_id.populate_contract_num(client)
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

Escape regex metacharacters in match pattern.

The pattern contains dots which are regex metacharacters. This could cause false positives if the error message format changes.

Proposed fix
         with pytest.raises(
             ValueError,
-            match="Invalid contract_id format received: invalid.account.format",
+            match=r"Invalid contract_id format received: invalid\.account\.format",
         ):
📝 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
):
with pytest.raises(
ValueError,
match="Invalid contract_id format received: invalid.account.format",
):
contract_id.populate_contract_num(client)
):
with pytest.raises(
ValueError,
match=r"Invalid contract_id format received: invalid\.account\.format",
):
contract_id.populate_contract_num(client)
🧰 Tools
🪛 Ruff (0.14.13)

503-503: Pattern passed to match= contains metacharacters but is neither escaped nor raw

(RUF043)

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

@github-actions
Copy link

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

Please resolve these conflicts locally and push the changes.

To assist you, please read:

Thank you for contributing!

1 similar comment
@github-actions
Copy link

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

Please resolve these conflicts locally and push the changes.

To assist you, please read:

Thank you for contributing!

@exploreriii exploreriii requested a review from a team January 24, 2026 00:35
Copy link
Contributor

@MonaaEid MonaaEid left a comment

Choose a reason for hiding this comment

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

LGTM!

@Mounil2005
Copy link
Contributor

Mounil2005 commented Jan 24, 2026

Please take a look at the CodeRabbit review suggestions.
(Need to solve conflicts and rebase too)
Everything else LGTM! 👍

@manishdait manishdait force-pushed the feat/contract-id-mirror-population branch from a043739 to 500229a Compare January 25, 2026 13:57
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 (2)
examples/account/account_id_populate_from_mirror.py (1)

17-66: Check receipt.status against ResponseCode in the auto‑create flow.
Examples should validate receipt status explicitly before continuing, otherwise users may proceed with a failed transfer and get confusing downstream errors.

✅ Proposed fix
 from hiero_sdk_python import (
     Client,
     AccountId,
     PrivateKey,
     TransferTransaction,
     Hbar,
     TransactionGetReceiptQuery,
+    ResponseCode,
 )
@@
     if not receipt.children:
         print("Auto account creation failed: no child receipts found")
         sys.exit(1)
+
+    if receipt.status != ResponseCode.SUCCESS:
+        print(
+            "Auto account creation failed: "
+            f"{ResponseCode(receipt.status).name}"
+        )
+        sys.exit(1)
src/hiero_sdk_python/contract/contract_id.py (1)

79-92: Potential inconsistency: _to_proto sets both contractNum and evm_address.

When evm_address is set, the protobuf should ideally have contractNum=0 rather than whatever value is in self.contract. The test at line 243 of contract_id_test.py expects proto.contractNum == 0 when evm_address is set, but the current implementation passes self.contract regardless.

Proposed fix
     def _to_proto(self):
         """
         Converts the ContractId instance to a protobuf ContractID object.

         Returns:
             basic_types_pb2.ContractID: The corresponding protobuf
             ContractID object.
         """
+        if self.evm_address is not None:
+            return basic_types_pb2.ContractID(
+                shardNum=self.shard,
+                realmNum=self.realm,
+                contractNum=0,
+                evm_address=self.evm_address,
+            )
         return basic_types_pb2.ContractID(
             shardNum=self.shard,
             realmNum=self.realm,
             contractNum=self.contract,
-            evm_address=self.evm_address,
         )
♻️ Duplicate comments (4)
src/hiero_sdk_python/account/account_id.py (1)

326-333: Parse shard/realm from mirror response to avoid incorrect IDs.
If the mirror returns a non‑zero shard/realm, reusing self.shard/self.realm yields a wrong AccountId. Use parse_from_string() to preserve the full response.

🐛 Proposed fix
-            num = int(account_id.split(".")[-1])
-            return AccountId(
-                shard=self.shard,
-                realm=self.realm,
-                num=num,
-                evm_address=self.evm_address,
-            )
+            shard, realm, num, _ = parse_from_string(account_id)
+            return AccountId(
+                shard=int(shard),
+                realm=int(realm),
+                num=int(num),
+                evm_address=self.evm_address,
+            )
tests/unit/contract_id_test.py (1)

504-517: Typo in test function name.

The function name has a typo: test_populate_contract_num_invlid_response should be test_populate_contract_num_invalid_response.

Proposed fix
-def test_populate_contract_num_invlid_response(client):
+def test_populate_contract_num_invalid_response(client):
     """Should raise error when populating contract number invalid response."""
tests/unit/account_id_test.py (1)

795-798: Use is None instead of == None for None comparison.

Per Python conventions (PEP 8), comparisons to None should use the is operator rather than ==.

Proposed fix
     new_account_id = account_id.populate_evm_address(mock_client)

-    assert account_id.evm_address == None
+    assert account_id.evm_address is None
     assert new_account_id.evm_address == evm_address
src/hiero_sdk_python/contract/contract_id.py (1)

247-256: Consider parsing full shard/realm/contract from mirror response.

The current implementation only extracts the contract number from the mirror node response while preserving self.shard and self.realm. If the mirror node returns a different shard/realm than expected (e.g., "1.2.123" when self has shard=0, realm=0), this could create an inconsistent ContractId.

Consider using parse_from_string(contract_id) to extract all three components from the authoritative mirror node response, as was suggested in a previous review.

Suggested fix
         try:
-            contract = int(contract_id.split(".")[-1])
+            shard, realm, contract, _ = parse_from_string(contract_id)
             return ContractId(
-                shard=self.shard,
-                realm=self.realm,
-                contract=contract,
+                shard=int(shard),
+                realm=int(realm),
+                contract=int(contract),
                 evm_address=self.evm_address,
             )
         except (ValueError, AttributeError) as e:
             raise ValueError(f"Invalid contract_id format received: {contract_id}") from e

@manishdait manishdait force-pushed the feat/contract-id-mirror-population branch from e6c6733 to 9ae865c Compare January 25, 2026 14:09
@github-actions
Copy link

Hi, this is WorkflowBot.
Your pull request cannot be merged as it is not passing all our workflow checks.
Please click on each check to review the logs and resolve issues so all checks pass.
To help you:

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

♻️ Duplicate comments (4)
tests/unit/contract_id_test.py (4)

343-348: Escape dots in regex match for checksum mismatch.

This avoids accidental matches when the message changes.

♻️ Proposed fix
-    with pytest.raises(ValueError, match="Checksum mismatch for 0.0.1"):
+    with pytest.raises(ValueError, match=r"Checksum mismatch for 0\.0\.1"):
         contract_id.validate_checksum(client)

62-73: Use a tuple for pytest.mark.parametrize argument names (PT006).

Keeps Ruff happy and aligns with pytest style.

♻️ Proposed fix
 `@pytest.mark.parametrize`(
-    "contract_str, expected",
+    ("contract_str", "expected"),
     [
         ("1.2.101", (1, 2, 101, None, None)),
         ("0.0.0", (0, 0, 0, None, None)),
         ("1.2.3-abcde", (1, 2, 3, None, "abcde")),
@@
 )
@@
 `@pytest.mark.parametrize`(
-    "evm_address_str, expected",
+    ("evm_address_str", "expected"),
     [
         (
             "abcdef0123456789abcdef0123456789abcdef01",
             (0, 0, 0, bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01")),

Also applies to: 366-378


101-103: Add commas to prevent implicit string concatenation in invalid_id cases.

Without commas, two intended test cases become one, reducing coverage.

🐛 Proposed fix
-        " 1.2.abcdef0123456789abcdef0123456789abcdef01 "
-        "1.2.0xabcdef0123456789abcdef0123456789abcdef01"
+        " 1.2.abcdef0123456789abcdef0123456789abcdef01 ",
+        "1.2.0xabcdef0123456789abcdef0123456789abcdef01",

503-510: Escape dots in regex match for invalid contract_id format.

♻️ Proposed fix
         with pytest.raises(
             ValueError,
-            match="Invalid contract_id format received: invalid.account.format",
+            match=r"Invalid contract_id format received: invalid\.account\.format",
         ):
             contract_id.populate_contract_num(client)

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: 6

Caution

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

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

22-31: Consider adding hasattr assertions to protect against breaking changes.

Per coding guidelines PRIORITY 1, tests should explicitly assert public attributes exist. While the value assertions implicitly check this, explicit hasattr checks provide clearer error messages if attributes are renamed or removed.

Suggested enhancement
 def test_default_initialization():
     """Test ContractId initialization with default values."""
     contract_id = ContractId()
 
+    assert hasattr(contract_id, 'shard'), "ContractId missing 'shard' attribute"
+    assert hasattr(contract_id, 'realm'), "ContractId missing 'realm' attribute"
+    assert hasattr(contract_id, 'contract'), "ContractId missing 'contract' attribute"
+    assert hasattr(contract_id, 'evm_address'), "ContractId missing 'evm_address' attribute"
+    assert hasattr(contract_id, 'checksum'), "ContractId missing 'checksum' attribute"
     assert contract_id.shard == 0
     assert contract_id.realm == 0
     assert contract_id.contract == 0
     assert contract_id.evm_address is None
     assert contract_id.checksum is None
♻️ Duplicate comments (5)
tests/unit/contract_id_test.py (5)

62-73: Use tuple for parametrize argument names.

Ruff PT006 suggests using a tuple instead of a string for the first argument of pytest.mark.parametrize for consistency.


345-348: Escape regex metacharacters in match pattern.

The pattern "Checksum mismatch for 0.0.1" contains dots which are regex metacharacters. Use a raw string with escaped dots for exact matching.


366-378: Use tuple for parametrize argument names.

Ruff PT006 suggests using a tuple instead of a string for the first argument.


506-511: Escape regex metacharacters in match pattern.

The pattern contains dots which are regex metacharacters. This could cause false positives.


483-496: Add assertions to verify immutability contract: return type, new instance creation, and original object unchanged.

Per coding guidelines PRIORITY 1, the test must explicitly verify that populate_contract_num returns a new ContractId instance (not mutating the original). This is critical for the PR objective of returning immutable instances.

Proposed fix
 def test_populate_contract_num_success(client):
     """Should populate contract number using mirror node response."""
     evm_address = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01")
     contract_id = ContractId(shard=0, realm=0, evm_address=evm_address)
 
     with patch(
         "hiero_sdk_python.contract.contract_id.perform_query_to_mirror_node",
         return_value={"contract_id": "0.0.123"},
     ):
         populated = contract_id.populate_contract_num(client)
 
+    assert isinstance(populated, ContractId), "populate_contract_num should return ContractId instance"
+    assert populated is not contract_id, "populate_contract_num should return a new instance"
+    assert contract_id.contract == 0, "Original ContractId should remain unchanged"
     assert populated.contract == 123
     assert populated.evm_address == evm_address

Comment on lines +460 to +473
def test_from_bytes_success():
"""Should deserialize ContractId correctly from protobuf bytes."""
original = ContractId(shard=1, realm=2, contract=3)
data = original.to_bytes()

reconstructed = ContractId.from_bytes(data)

assert reconstructed == original

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

Add return type assertion for from_bytes.

Per coding guidelines PRIORITY 1, explicitly assert the return type to protect against breaking changes.

Proposed fix
 def test_from_bytes_success():
     """Should deserialize ContractId correctly from protobuf bytes."""
     original = ContractId(shard=1, realm=2, contract=3)
     data = original.to_bytes()
 
     reconstructed = ContractId.from_bytes(data)
 
+    assert isinstance(reconstructed, ContractId), "from_bytes should return ContractId instance"
     assert reconstructed == original

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.

@manishdait This is looking great!

Please be sure to address merge conflicts and the suggestions recommended from code rabbit. If you have any questions, please let us know, we are happy to help!

Thank you!!

@manishdait manishdait force-pushed the feat/contract-id-mirror-population branch from 9ae865c to 2f6d1c6 Compare January 26, 2026 07:30
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: 4

♻️ Duplicate comments (10)
src/hiero_sdk_python/utils/entity_id_helper.py (1)

5-5: Replace typing.Dict with built-in dict.
typing.Dict is deprecated in modern Python; use dict[...] for annotations instead.

♻️ Proposed fix
-from typing import TYPE_CHECKING, Any, Dict
+from typing import TYPE_CHECKING, Any
@@
-def perform_query_to_mirror_node(url: str, timeout: float = 10) -> Dict[str, Any]:
+def perform_query_to_mirror_node(url: str, timeout: float = 10) -> dict[str, Any]:

Also applies to: 139-139

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

326-333: Parse shard/realm from the mirror response instead of reusing self.
Using only the last segment can yield an incorrect AccountId if the mirror returns non‑zero shard/realm or a checksum. Parse the full ID string.

🐛 Proposed fix
-            num = int(account_id.split(".")[-1])
-            return AccountId(
-                shard=self.shard,
-                realm=self.realm,
-                num=num,
-                evm_address=self.evm_address,
-            )
+            shard, realm, num, _ = parse_from_string(account_id)
+            return AccountId(
+                shard=int(shard),
+                realm=int(realm),
+                num=int(num),
+                evm_address=self.evm_address,
+            )
src/hiero_sdk_python/contract/contract_id.py (1)

247-255: Parse shard/realm/contract from the mirror response.
Using only the last segment risks incorrect IDs when shard/realm are non‑zero or checksums appear. Mirror response should be treated as authoritative.

🐛 Proposed fix
-            contract = int(contract_id.split(".")[-1])
-            return ContractId(
-                shard=self.shard,
-                realm=self.realm,
-                contract=contract,
-                evm_address=self.evm_address,
-            )
+            shard, realm, contract, _ = parse_from_string(contract_id)
+            return ContractId(
+                shard=int(shard),
+                realm=int(realm),
+                contract=int(contract),
+                evm_address=self.evm_address,
+            )
tests/integration/account_id_population_e2e_test.py (1)

54-58: Replace fixed sleeps with retry/polling to avoid mirror lag flakes.
Hard-coded sleeps are timing-dependent; use a bounded retry with clear failure context.

🧪 Proposed fix
+def _wait_for_mirror(fn, timeout_s: float = 30, interval_s: float = 2):
+    end = time.monotonic() + timeout_s
+    last_exc: Exception | None = None
+    while time.monotonic() < end:
+        try:
+            return fn()
+        except (RuntimeError, ValueError) as exc:
+            last_exc = exc
+            time.sleep(interval_s)
+    raise AssertionError(f"Mirror node did not update within {timeout_s}s") from last_exc
@@
-    # Wait for mirrornode to update
-    time.sleep(5)
-
     mirror_account_id = AccountId.from_evm_address(evm_address, 0, 0)
-    new_account_id = mirror_account_id.populate_account_num(env.client)
+    new_account_id = _wait_for_mirror(
+        lambda: mirror_account_id.populate_account_num(env.client)
+    )
@@
-    # Wait for mirror_node to update
-    time.sleep(5)
-
-    new_account_id = account_id.populate_evm_address(env.client)
+    new_account_id = _wait_for_mirror(
+        lambda: account_id.populate_evm_address(env.client)
+    )

Also applies to: 93-96

tests/unit/contract_id_test.py (4)

347-352: Escape regex metacharacters in match= patterns.

Dots in the expected message should be escaped to avoid regex false positives.

Suggested fix
-    with pytest.raises(ValueError, match="Checksum mismatch for 0.0.1"):
+    with pytest.raises(ValueError, match=r"Checksum mismatch for 0\.0\.1"):
         contract_id.validate_checksum(client)
@@
         with pytest.raises(
             ValueError,
-            match="Invalid contract_id format received: invalid.account.format",
+            match=r"Invalid contract_id format received: invalid\.account\.format",
         ):
             contract_id.populate_contract_num(client)

Also applies to: 512-515


62-64: Use tuples for pytest.mark.parametrize argument names.

Ruff PT006 flags string argument names; use tuples in both parametrizations.

Suggested fix
 `@pytest.mark.parametrize`(
-    "contract_str, expected",
+    ("contract_str", "expected"),
     [
         ("1.2.101", (1, 2, 101, None, None)),
         ("0.0.0", (0, 0, 0, None, None)),
         ("1.2.3-abcde", (1, 2, 3, None, "abcde")),
         (
             "1.2.abcdef0123456789abcdef0123456789abcdef01",
             (1, 2, 0, bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01"), None),
         ),
     ],
 )
@@
 `@pytest.mark.parametrize`(
-    "evm_address_str, expected",
+    ("evm_address_str", "expected"),
     [
         (
             "abcdef0123456789abcdef0123456789abcdef01",
             (0, 0, 0, bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01")),
         ),

Also applies to: 370-372


465-472: Add return-type assertion for from_bytes.

This guards against breaking changes in public factory methods. As per coding guidelines, assert return types in unit tests.

Suggested fix
     reconstructed = ContractId.from_bytes(data)
 
+    assert isinstance(reconstructed, ContractId), "from_bytes should return ContractId instance"
     assert reconstructed == original

488-501: Strengthen populate_contract_num success assertions (type/immutability/shard/realm).

The test should verify the new-instance contract and shard/realm preservation. As per coding guidelines, add these assertions.

Suggested fix
     with patch(
         "hiero_sdk_python.contract.contract_id.perform_query_to_mirror_node",
         return_value={"contract_id": "0.0.123"},
     ):
         populated = contract_id.populate_contract_num(client)
 
+    assert isinstance(populated, ContractId), "populate_contract_num should return ContractId instance"
+    assert populated is not contract_id, "populate_contract_num should return a new instance"
     assert populated.contract == 123
     assert populated.evm_address == evm_address
+    assert populated.shard == 0, "Shard should be preserved"
+    assert populated.realm == 0, "Realm should be preserved"
tests/integration/contract_id_population_e2e_test.py (1)

62-65: Replace fixed sleep with bounded retry to avoid flakes.

Mirror-node propagation is variable; a fixed sleep makes the test flaky. Use a retry loop with a timeout for deterministic behavior. As per coding guidelines, avoid timing-dependent assertions in integration tests.

Suggested fix
-    # Wait for mirror_node to update
-    time.sleep(5)
-
-    final_contract_id = contract_id_with_evm_addr.populate_contract_num(env.client)
+    # Wait for mirror node to update (bounded retry)
+    deadline = time.monotonic() + 30
+    while True:
+        try:
+            final_contract_id = contract_id_with_evm_addr.populate_contract_num(env.client)
+            break
+        except (RuntimeError, ValueError):
+            if time.monotonic() >= deadline:
+                raise AssertionError(
+                    "Mirror node did not return contract_id within 30s"
+                )
+            time.sleep(1)
tests/unit/account_id_test.py (1)

797-797: Use is None for None comparison.

This avoids Ruff E711 and follows Python idioms.

Suggested fix
-    assert account_id.evm_address == None
+    assert account_id.evm_address is None

- Added first-class support for EVM address aliases in `AccountId`, including parsing, serialization, Mirror Node population helpers.
- Add automated bot to recommend next issues to contributors after their first PR merge (#1380)
- Added dry-run support and refactored `.github/workflows/bot-workflows.yml` to use dedicated script `.github/scripts/bot-workflows.js` for improved maintainability and testability. (`#1288`)
- Added MirrorNode based population for `ContractId`, including parsing and serialization support.
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

Hyphenate “MirrorNode-based”.
Minor grammar fix per LanguageTool.

✏️ Proposed fix
-- Added MirrorNode based population for `ContractId`, including parsing and serialization support.
+- Added MirrorNode-based population for `ContractId`, including parsing and serialization support.
📝 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
- Added MirrorNode based population for `ContractId`, including parsing and serialization support.
- Added MirrorNode-based population for `ContractId`, including parsing and serialization support.
🧰 Tools
🪛 LanguageTool

[grammar] ~124-~124: Use a hyphen to join words.
Context: ...estability. (#1288) - Added MirrorNode based population for ContractId, inclu...

(QB_NEW_EN_HYPHEN)

Comment on lines 80 to 84
time.sleep(5)

try:
mirror_account_id.populate_account_num(client)
new_account_id = mirror_account_id.populate_account_num(client)
except Exception as e:
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

Avoid fixed sleeps; poll the mirror node in examples.
Mirror propagation is variable; a retry loop makes the example reliable for users.

💡 Suggested improvement
+def _wait_for_mirror(fn, timeout_s: float = 30, interval_s: float = 2):
+    end = time.time() + timeout_s
+    last_exc: Exception | None = None
+    while time.time() < end:
+        try:
+            return fn()
+        except (RuntimeError, ValueError) as exc:
+            last_exc = exc
+            time.sleep(interval_s)
+    raise RuntimeError(f"Mirror node did not update within {timeout_s}s") from last_exc
@@
-    time.sleep(5)
-
     try:
-        new_account_id = mirror_account_id.populate_account_num(client)
+        new_account_id = _wait_for_mirror(
+            lambda: mirror_account_id.populate_account_num(client)
+        )
@@
-    time.sleep(5)
-
     try:
-        new_account_id = created_account_id.populate_evm_address(client)
+        new_account_id = _wait_for_mirror(
+            lambda: created_account_id.populate_evm_address(client)
+        )

Also applies to: 110-114

🧰 Tools
🪛 Ruff (0.14.13)

84-84: Do not catch blind exception: Exception

(BLE001)

Comment on lines 22 to 26
from tests.integration.utils import env


@pytest.mark.integration
def test_populate_contract_id_num(env):
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:

# Find and read the test file
find . -name "contract_id_population_e2e_test.py" -type f | head -1 | xargs wc -l

Repository: hiero-ledger/hiero-sdk-python

Length of output: 130


🏁 Script executed:

# Read the test file around the specified lines
find . -name "contract_id_population_e2e_test.py" -type f | head -1 | xargs cat -n | head -40

Repository: hiero-ledger/hiero-sdk-python

Length of output: 1643


🏁 Script executed:

# Check the utils module to see how env is defined
find . -path "*/tests/integration/utils.py" -type f | head -1 | xargs cat -n | head -100

Repository: hiero-ledger/hiero-sdk-python

Length of output: 4730


🏁 Script executed:

# Check for sleep calls in the test file
grep -n "sleep\|time\.sleep" ./tests/integration/contract_id_population_e2e_test.py

Repository: hiero-ledger/hiero-sdk-python

Length of output: 93


🏁 Script executed:

# Verify if ruff would actually flag F811 for this pattern
# First check if ruff is available and what it reports
if command -v ruff &> /dev/null; then
  ruff check ./tests/integration/contract_id_population_e2e_test.py --select F811 2>&1 || true
else
  echo "ruff not available, checking with grep for shadowing pattern"
  grep -A 5 "from tests.integration.utils import env" ./tests/integration/contract_id_population_e2e_test.py | head -10
fi

Repository: hiero-ledger/hiero-sdk-python

Length of output: 1117


🏁 Script executed:

# Check how pytest fixtures are typically loaded in conftest.py or other test files
find ./tests -name "conftest.py" -type f | head -1 | xargs cat -n 2>/dev/null | head -50 || echo "No conftest.py found in tests root"

Repository: hiero-ledger/hiero-sdk-python

Length of output: 2171


🏁 Script executed:

# Search for other test files that use the env fixture to see the pattern
rg "from tests.integration.utils import" tests/ -A 3 --context 5 | head -40

Repository: hiero-ledger/hiero-sdk-python

Length of output: 3774


🏁 Script executed:

# Check for conftest.py in the integration tests directory
find ./tests/integration -name "conftest.py" -type f | head -5

Repository: hiero-ledger/hiero-sdk-python

Length of output: 55


🏁 Script executed:

# List all files in tests/integration to understand structure
ls -la ./tests/integration/ | head -20

Repository: hiero-ledger/hiero-sdk-python

Length of output: 1616


🏁 Script executed:

# Check the root tests directory for conftest.py
find ./tests -maxdepth 2 -name "conftest.py" -type f

Repository: hiero-ledger/hiero-sdk-python

Length of output: 97


🏁 Script executed:

# Check pytest configuration to understand fixture discovery
ls -la ./tests/ | grep -E "conftest|pytest"

Repository: hiero-ledger/hiero-sdk-python

Length of output: 55


🏁 Script executed:

# Look at a working test file that correctly uses env as fixture
grep -B5 -A10 "def test_integration_contract_bytecode_query_can_execute" ./tests/integration/contract_bytecode_query_e2e_test.py | head -20

Repository: hiero-ledger/hiero-sdk-python

Length of output: 660


🏁 Script executed:

# Check if there's a way fixtures are being discovered - look at pytest configuration
cat ./pyproject.toml | grep -A 20 "\[tool.pytest" 2>/dev/null || echo "No pytest config found in pyproject.toml"

Repository: hiero-ledger/hiero-sdk-python

Length of output: 585


🏁 Script executed:

# Check if a conftest.py needs to be created in integration tests and what would be needed
head -50 ./tests/unit/conftest.py

Repository: hiero-ledger/hiero-sdk-python

Length of output: 1821


🏁 Script executed:

# Check pytest.ini or setup.cfg
cat ./pytest.ini 2>/dev/null || cat ./setup.cfg 2>/dev/null | grep -A 10 "\[tool:pytest\]" || echo "No pytest config in standard files"

Repository: hiero-ledger/hiero-sdk-python

Length of output: 202


🏁 Script executed:

# Verify the complete implementation needed - what does the unit conftest show?
wc -l ./tests/unit/conftest.py

Repository: hiero-ledger/hiero-sdk-python

Length of output: 101


🏁 Script executed:

# Understand how pytest_plugins works - check if it's only valid in conftest.py
# by examining the pytest documentation pattern and testing scenario
python3 <<'EOF'
import sys
# pytest_plugins is a special variable that must be in conftest.py
# If placed in a regular test file, pytest won't process it
# Let's verify this is indeed how pytest works

print("pytest_plugins variable scope analysis:")
print("- pytest_plugins must be defined in conftest.py (or pytest plugins)")
print("- If defined in regular test modules, pytest will NOT process it")
print("- Placing it in a regular test file would NOT solve the F811 issue")
print("\nThe correct fix requires:")
print("1. Create tests/integration/conftest.py")
print("2. Add: pytest_plugins = ['tests.integration.utils']")
print("3. Remove: from tests.integration.utils import env (from all test files)")
EOF

Repository: hiero-ledger/hiero-sdk-python

Length of output: 159


Create tests/integration/conftest.py to register the env fixture and remove the import from test files.

The F811 shadowing error is real: importing env at line 22 while using it as a fixture parameter at line 26 causes a redefinition conflict. However, the suggested fix in the original review is incomplete. The pytest_plugins variable only works when declared in conftest.py, not in individual test files.

The correct fix:

  1. Create tests/integration/conftest.py with:
pytest_plugins = ["tests.integration.utils"]
  1. Remove the import from this test file (and all integration test files):
-from tests.integration.utils import env

Pytest will then discover the env fixture from tests.integration.utils automatically without shadowing.

🧰 Tools
🪛 Ruff (0.14.13)

26-26: Redefinition of unused env from line 22: env redefined here

(F811)

Copy link
Member

@undefinedIsMyLife undefinedIsMyLife left a comment

Choose a reason for hiding this comment

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

Everything LGTM I think its ready to merge
One remaining concern from the latest CodeRabbit review:

  • fixed sleep(5) in integration tests/examples (potential flakiness)
  • env fixture import vs conftest.py (pytest best practice / F811)

These aren’t correctness blockers, but would be good to address either in this PR or a follow-up.

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.

LGTM!

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.

I concur with @undefinedIsMyLife

If you have any questions about the suggestions please reach out.
We'd be happy to assist.

@manishdait manishdait force-pushed the feat/contract-id-mirror-population branch from 2f6d1c6 to 4755c7b Compare January 28, 2026 06:46
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)
tests/integration/account_id_population_e2e_test.py (1)

40-54: Assert receipt status for clearer diagnostics.

Add explicit status checks before relying on child receipts.

✅ Suggested update
 from hiero_sdk_python.crypto.private_key import PrivateKey
 from hiero_sdk_python.hbar import Hbar
+from hiero_sdk_python.response_code import ResponseCode
 from hiero_sdk_python.query.transaction_get_receipt_query import (
     TransactionGetReceiptQuery,
 )
@@
     transfer_receipt = (
         TransactionGetReceiptQuery()
         .set_transaction_id(transfer_tx.transaction_id)
         .set_include_children(True)
         .execute(env.client)
     )
 
     assert transfer_receipt is not None
+    assert (
+        transfer_receipt.status == ResponseCode.SUCCESS
+    ), f"Transfer failed: {ResponseCode(transfer_receipt.status).name}"
     assert (
         len(transfer_receipt.children) > 0
     ), "Expected child transaction for auto-account creation"
src/hiero_sdk_python/utils/entity_id_helper.py (1)

17-33: Add return type annotation and fix docstring.

The function lacks a return type annotation, and the docstring incorrectly says "Returns: An instance of cls..." which appears to be a copy-paste error. The function returns a tuple of strings.

Suggested fix
-def parse_from_string(address: str):
+def parse_from_string(address: str) -> tuple[str, str, str, str | None]:
     """
     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 checksum may be None.
+
+    Raises:
+        ValueError: If the address format is invalid.
     """

@manishdait
Copy link
Contributor Author

Thanks for the review!
I’ve updated the tests to use wait_for_mirror_node, so the previous sleep(5) calls have been removed.

@manishdait
Copy link
Contributor Author

Regarding the env fixture import:

The current pattern, where env is imported directly from tests.integration.utils, works correctly and all tests continue to pass. The only downside is a linter/pytest warning (F811) Moving env to conftest.py would be a best-practice improvement, but it is not required for correctness. All other integration tests follow the same pattern, which helps maintain consistency across the test suite.

If desired, as a future improvement we could move the env fixture to tests/integration/conftest.py to avoid shadowing warnings and remove the from tests.integration.utils import env lines in all test files. This could be done in a follow-up PR without affecting the current functionality.

@manishdait
Copy link
Contributor Author

@exploreriii @aceppaluni @AntonioCeppellini ,
I’d love to hear your thoughts on whether we should make this change now or leave it for a follow-up.

@exploreriii exploreriii requested a review from a team January 28, 2026 12:38
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.

@manishdait Thank you so much!!

I think this is good for now to keep the PR in scope.
We can pose a follow up issue after to follow best practices.

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 populating ContractId using Mirror Node queries

7 participants