Skip to content

feat: Add Hip 1261 Implementation#2019

Draft
aceppaluni wants to merge 4 commits intohiero-ledger:mainfrom
aceppaluni:Hip1261
Draft

feat: Add Hip 1261 Implementation#2019
aceppaluni wants to merge 4 commits intohiero-ledger:mainfrom
aceppaluni:Hip1261

Conversation

@aceppaluni
Copy link
Copy Markdown
Contributor

Description:

This PR aims to add in the implementation for HIP-1261.

Related issue(s):

Fixes #1633

Notes for reviewer:

  • Please let me know if there is anything missing or if something needs adjusting

Checklist

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

Signed-off-by: aceppaluni <aceppaluni@gmail.com>
@github-actions
Copy link
Copy Markdown

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:

@aceppaluni
Copy link
Copy Markdown
Contributor Author

I have a version of execute where the tests do pass. I am happy to share this version if needed. :)

Signed-off-by: aceppaluni <aceppaluni@gmail.com>
Copy link
Copy Markdown
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.

Great work @aceppaluni, left a few suggestions.

mode: FeeEstimateMode
network_fee: NetworkFee
node_fee: FeeEstimate
service_fee: FeeEstimate
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

these fields may be absent in the response better to mark them as the Optional[None]:

network_fee: Optional[NetworkFee] = None
node_fee: Optional[FeeEstimate] = None
service_fee: Optional[FeeEstimate] = None


@dataclass(frozen=True)
class FeeExtra:
name: str
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This field may not always be present in the response

Suggested change
name: str
name: Optional[str] = None

def __init__(self):
self._mode: Optional[FeeEstimateMode] = None
self._transaction: Optional["Transaction"] = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

consider adding query level max_attempts and max_backoff configuration and make them configurable by adding setters

network_multiplier = None
notes = []

max_retries = getattr(client, "max_retries", 3)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we can remove this and use the max_attempts instead

Suggested change
max_retries = getattr(client, "max_retries", 3)
if self.attempts is None:
self.max_attempts = client.max_attempts


tx_bytes = tx.to_bytes()

for attempt in range(max_retries):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
for attempt in range(max_retries):
for attempt in range(self.max_attempts):

except Exception as e:

if "UNAVAILABLE" in str(e) or "DEADLINE_EXCEEDED" in str(e):
if attempt == max_retries - 1:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
if attempt == max_retries - 1:
if attempt == self.max_attempts - 1:

except Exception as e:

if "UNAVAILABLE" in str(e) or "DEADLINE_EXCEEDED" in str(e):
if attempt == max_retries - 1:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we can add backoff if we want to retry similar to what we do in the execute() function of the a normal Query

def backoff(self, attempt: int):
        delay = min(0.5 * (2 ** attempt), self._max_backoff)
        time.sleep(delay)

def get_transaction(self) -> Optional["Transaction"]:
return self._transaction

def execute(self, client) -> FeeEstimateResponse:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

consider splitting into smaller helper methods to reduce complexity.


max_retries = getattr(client, "max_retries", 3)

import requests
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can be remove duplicate import


if response.status_code == 400:
raise ValueError("INVALID_ARGUMENT")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can impose retry logic on stautus code like:

408, 429, 5xx

Signed-off-by: aceppaluni <aceppaluni@gmail.com>
@aceppaluni aceppaluni marked this pull request as ready for review March 24, 2026 15:44
@aceppaluni aceppaluni requested review from a team as code owners March 24, 2026 15:44
@aceppaluni aceppaluni marked this pull request as draft March 24, 2026 15:46
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 24, 2026

Walkthrough

This PR implements HIP-1261 fee estimation support for the Hiero Python SDK. It introduces fee data models, a FeeEstimateMode enum, a FeeEstimateQuery class that integrates with the Mirror Node API endpoint, a convenience method on Transaction, updates to handle string account IDs, and comprehensive unit tests.

Changes

Cohort / File(s) Summary
Fee Data Models
src/hiero_sdk_python/fees/fee_extra.py, src/hiero_sdk_python/fees/fee_estimate.py, src/hiero_sdk_python/fees/network_fee.py, src/hiero_sdk_python/fees/fee_estimate_response.py
Added immutable dataclasses representing fee components: FeeExtra (individual fee item with name, included count, charged count, and per-unit fees), FeeEstimate (base fee plus extras with computed subtotal), NetworkFee (multiplier and subtotal), and FeeEstimateResponse (aggregated response with mode, all fee types, notes, and total).
Fee Estimation Mode
src/hiero_sdk_python/fees/fee_estimate_mode.py
Added FeeEstimateMode enum with two string-valued members: STATE (default, uses mirror node's latest state) and INTRINSIC (payload-based estimation).
Fee Estimation Query
src/hiero_sdk_python/query/fee_estimate_query.py
Added FeeEstimateQuery class that communicates with Mirror Node's /api/v1/network/fees endpoint. Supports fluent configuration of mode and transaction. execute() method performs HTTP POST with retry logic (on UNAVAILABLE or DEADLINE_EXCEEDED), parses JSON response, aggregates fee subtotals, applies network multiplier, and returns FeeEstimateResponse.
Transaction Integration
src/hiero_sdk_python/transaction/transaction.py, src/hiero_sdk_python/transaction/transfer_transaction.py
Added estimate_fee() method to Transaction for fee estimation convenience. Modified _add_hbar_transfer() in TransferTransaction to accept string account IDs by converting them via AccountId.from_string() before validation.
Testing
tests/unit/test_fee_estimate_query.py
Added comprehensive unit test suite covering fee calculation formulas, mode selection (STATE/INTRINSIC), transaction requirement validation, successful execution across multiple transaction types, error handling (HTTP 400), retry behavior (transient errors), and chunked transaction scenarios.
Documentation
CHANGELOG.md
Added changelog entry documenting HIP-1261 implementation.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant SDK as SDK<br/>(Python)
    participant MirrorNode as Mirror Node<br/>(/api/v1/network/fees)

    User->>SDK: transaction.estimate_fee()<br/>.set_mode(mode).execute(client)
    SDK->>SDK: Validate transaction is set
    SDK->>SDK: Serialize transaction to protobuf
    SDK->>MirrorNode: POST /api/v1/network/fees<br/>?mode=STATE or INTRINSIC<br/>[protobuf binary]
    
    alt Transient Error (UNAVAILABLE/DEADLINE_EXCEEDED)
        MirrorNode-->>SDK: Exception
        SDK->>SDK: Retry (up to max_retries)
        SDK->>MirrorNode: POST /api/v1/network/fees [retry]
    else Permanent Error
        MirrorNode-->>SDK: HTTP 400 or other error
        SDK->>SDK: Raise ValueError
        SDK-->>User: Exception
    else Success
        MirrorNode-->>SDK: 200 OK + JSON response<br/>{node, service, network, mode, notes}
        SDK->>SDK: Parse JSON into fee components
        SDK->>SDK: Compute network_total = node.subtotal × network.multiplier
        SDK->>SDK: Compute total = node + network + service
        SDK->>SDK: Construct FeeEstimateResponse
        SDK-->>User: FeeEstimateResponse<br/>(mode, node_fee, service_fee,<br/>network_fee, total, notes)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Add Hip 1261 Implementation' clearly summarizes the main change—adding HIP-1261 fee estimation support—matching the PR's primary objective.
Description check ✅ Passed The description states the PR adds implementation for HIP-1261 and references issue #1633, directly relating to the changeset which includes fee estimation classes and query logic.
Linked Issues check ✅ Passed The PR implements all key requirements from issue #1633: FeeEstimateMode enum, data model classes (FeeExtra, FeeEstimate, NetworkFee, FeeEstimateResponse), FeeEstimateQuery with Mirror Node integration and retry logic, Transaction.estimate_fee() method, and comprehensive unit tests covering multiple scenarios.
Out of Scope Changes check ✅ Passed All changes are directly aligned with HIP-1261 implementation requirements. The modification to TransferTransaction's _add_hbar_transfer (accepting str account_id) is a supporting enhancement for broader transaction compatibility, not out-of-scope.

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

📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #1633

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch Hip1261

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
Copy Markdown
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

♻️ Duplicate comments (1)
src/hiero_sdk_python/query/fee_estimate_query.py (1)

48-60: ⚠️ Potential issue | 🔴 Critical

Use the actual Client REST/retry API here.

The concrete client exposes network.get_mirror_rest_url() and max_attempts, not mirror_network / max_retries. With a real Client, Line 48 raises AttributeError, and the retry loop ignores the caller’s configured attempt budget.

🐛 Suggested fix
-        url = f"{client.mirror_network}/api/v1/network/fees?mode={mode.value}"
+        url = f"{client.network.get_mirror_rest_url()}/network/fees?mode={mode.value}"
@@
-        max_retries = getattr(client, "max_retries", 3)
+        max_retries = client.max_attempts

Verify against the actual client/network definitions:

#!/bin/bash
set -euo pipefail
rg -n '\bmirror_network\b|\bmax_retries\b|\bmax_attempts\b|get_mirror_rest_url' \
  src/hiero_sdk_python/query/fee_estimate_query.py \
  src/hiero_sdk_python/client/client.py \
  src/hiero_sdk_python/client/network.py

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8f0c538d-669e-4bc6-a84f-971b8df7fb8e

📥 Commits

Reviewing files that changed from the base of the PR and between 18768b7 and 65427d9.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • src/hiero_sdk_python/fees/fee_estimate.py
  • src/hiero_sdk_python/fees/fee_estimate_mode.py
  • src/hiero_sdk_python/fees/fee_estimate_response.py
  • src/hiero_sdk_python/fees/fee_extra.py
  • src/hiero_sdk_python/fees/network_fee.py
  • src/hiero_sdk_python/query/fee_estimate_query.py
  • src/hiero_sdk_python/transaction/transaction.py
  • src/hiero_sdk_python/transaction/transfer_transaction.py
  • tests/unit/test_fee_estimate_query.py

node_fee: FeeEstimate
service_fee: FeeEstimate
notes: List[str] = field(default_factory=list)
total: int = 0
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid storing total as a mutable constructor value; derive it from components.

Line 14 defaults total to 0, which can drift from node_fee/network_fee/service_fee and violate the fee formula invariant. Make total a computed property.

Suggested patch
 `@dataclass`(frozen=True)
 class FeeEstimateResponse:
@@
     service_fee: FeeEstimate
     notes: List[str] = field(default_factory=list)
-    total: int = 0
+
+    `@property`
+    def total(self) -> int:
+        return self.node_fee.subtotal + self.network_fee.subtotal + self.service_fee.subtotal

Comment on lines +28 to +37
def set_transaction(self, transaction: "Transaction") -> "FeeEstimateQuery":

#if hasattr(transaction, "freeze") and not transaction.is_frozen:
#transaction.freeze()

#if hasattr(transaction, "freeze") and not getattr(transaction, "is_frozen", False):
#transaction.freeze()

self._transaction = transaction
return self
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

The supplied transaction is never serialized or chunked.

set_transaction() only stores the object, while execute() always iterates over [b"dummy"] and posts b"dummy". That makes every transaction type produce the same estimate and prevents chunked transactions from ever aggregating per-chunk fees. The auto-freeze behavior called out in the PR objective is also effectively absent because the transaction bytes are never derived from self._transaction.

Also applies to: 50-65

Comment on lines +83 to +114
parsed = self._parse_response(data)

node_total += parsed.node_fee.subtotal
service_total += parsed.service_fee.subtotal

network_multiplier = parsed.network_fee.multiplier
notes.extend(parsed.notes)

break
except Exception as e:

if "UNAVAILABLE" in str(e) or "DEADLINE_EXCEEDED" in str(e):
if attempt == max_retries - 1:
raise
continue

raise

network_total = node_total * network_multiplier
total = node_total + service_total + network_total

return FeeEstimateResponse(
mode=mode,
node_fee=FeeEstimate(base=node_total, extras=[]),
service_fee=FeeEstimate(base=service_total, extras=[]),
network_fee=NetworkFee(
multiplier=network_multiplier,
subtotal=network_total
),
notes=notes,
total=total
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve the parsed response instead of rebuilding it from request state.

_parse_response() already deserializes the Mirror Node payload, but execute() throws that object away and returns a new FeeEstimateResponse built from aggregated subtotals plus mode=mode from the request. That can misreport what the service actually returned and drops any detail _parse_response() captured. Aggregate the parsed responses instead of reconstructing a new one from request-side values.

Also applies to: 116-140

Comment on lines +63 to +71
def _add_hbar_transfer(
self, account_id: AccountId, amount: Union[int, Hbar], is_approved: bool = False
) -> "TransferTransaction":
"""
Internal method to add a HBAR transfer to the transaction.

Args:
account_id (AccountId): The account ID of the sender or receiver.
amount (Union[int, Hbar]): The amount of the HBAR to transfer.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Align public type hints/docstrings with the new str account-id behavior.

At Line 63 and Lines 100-116, runtime now accepts str, but method signatures/docs still advertise AccountId only. This creates a public API contract mismatch (and static typing friction) for the newly supported path.

Suggested patch
-    def _init_hbar_transfers(self, hbar_transfers: Dict[AccountId, int]) -> None:
+    def _init_hbar_transfers(self, hbar_transfers: Dict[AccountId | str, int]) -> None:
@@
-    def _add_hbar_transfer(
-        self, account_id: AccountId, amount: Union[int, Hbar], is_approved: bool = False
+    def _add_hbar_transfer(
+        self, account_id: AccountId | str, amount: Union[int, Hbar], is_approved: bool = False
     ) -> "TransferTransaction":
@@
-            account_id (AccountId): The account ID of the sender or receiver.
+            account_id (AccountId | str): The account ID of the sender or receiver.
@@
-    def add_hbar_transfer(self, account_id: AccountId, amount: Union[int, Hbar]) -> "TransferTransaction":
+    def add_hbar_transfer(self, account_id: AccountId | str, amount: Union[int, Hbar]) -> "TransferTransaction":
@@
-            account_id (AccountId): The account ID of the sender or receiver.
+            account_id (AccountId | str): The account ID of the sender or receiver.
@@
-    def add_approved_hbar_transfer(
-        self, account_id: AccountId, amount: Union[int, Hbar]
+    def add_approved_hbar_transfer(
+        self, account_id: AccountId | str, amount: Union[int, Hbar]
     ) -> "TransferTransaction":
@@
-            account_id (AccountId): The account ID of the sender or receiver.
+            account_id (AccountId | str): The account ID of the sender or receiver.

As per coding guidelines, “Any change to public methods … includes backwards-compatibility analysis.”

Also applies to: 100-116

🧰 Tools
🪛 GitHub Check: Codacy Static Code Analysis

[warning] 63-63: src/hiero_sdk_python/transaction/transfer_transaction.py#L63
Method _add_hbar_transfer has a cyclomatic complexity of 9 (limit is 8)

Comment on lines +63 to +98
def _add_hbar_transfer(
self, account_id: AccountId, amount: Union[int, Hbar], is_approved: bool = False
) -> "TransferTransaction":
"""
Internal method to add a HBAR transfer to the transaction.

Args:
account_id (AccountId): The account ID of the sender or receiver.
amount (Union[int, Hbar]): The amount of the HBAR to transfer.
is_approved (bool, optional): Whether the transfer is approved. Defaults to False.

Returns:
TransferTransaction: The current instance of the transaction for chaining.
"""
self._require_not_frozen()
if isinstance(account_id, str):
account_id = AccountId.from_string(account_id)
if not isinstance(account_id, AccountId):
raise TypeError("account_id must be an AccountId instance.")
if isinstance(amount, Hbar):
amount = amount.to_tinybars()
elif not isinstance(amount, int):
raise TypeError("amount must be an int or Hbar instance.")
if amount == 0:
raise ValueError("Amount must be a non-zero value.")
if not isinstance(is_approved, bool):
raise TypeError("is_approved must be a boolean.")

for transfer in self.hbar_transfers:
if transfer.account_id == account_id:
transfer.amount += amount
return self

self.hbar_transfers.append(
HbarTransfer(account_id, amount, is_approved))
return self
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Reduce _add_hbar_transfer branching to address complexity hotspot.

Line 63 currently combines parsing, validation, normalization, and accumulation. Splitting normalization/validation into helpers will keep this method easier to reason about and maintain (matches the static-analysis warning).

🧰 Tools
🪛 GitHub Check: Codacy Static Code Analysis

[warning] 63-63: src/hiero_sdk_python/transaction/transfer_transaction.py#L63
Method _add_hbar_transfer has a cyclomatic complexity of 9 (limit is 8)

Comment on lines +21 to +25
def mock_client():
client = MagicMock()
client.mirror_network = "https://testnet.mirrornode.hedera.com"
client.max_retries = 3
return client
Copy link
Copy Markdown
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:

#!/bin/bash
set -euo pipefail
rg -n '\bmirror_network\b|\bmax_retries\b|\bmax_attempts\b|get_mirror_rest_url' \
  tests/unit/test_fee_estimate_query.py \
  src/hiero_sdk_python/client/client.py \
  src/hiero_sdk_python/client/network.py

Repository: hiero-ledger/hiero-sdk-python

Length of output: 1166


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find and show FeeEstimateQuery implementation to see what client surface it uses
fd -e py "fee_estimate_query" src/ --type f | head -5

Repository: hiero-ledger/hiero-sdk-python

Length of output: 121


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check what attributes/properties the Client class exposes
cat src/hiero_sdk_python/client/client.py | head -100

Repository: hiero-ledger/hiero-sdk-python

Length of output: 3129


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check what attributes/methods Network class exposes
cat src/hiero_sdk_python/client/network.py | head -100

Repository: hiero-ledger/hiero-sdk-python

Length of output: 4344


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the FeeEstimateQuery implementation - specifically the execute method
cat src/hiero_sdk_python/query/fee_estimate_query.py

Repository: hiero-ledger/hiero-sdk-python

Length of output: 4534


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if Client class actually exposes mirror_network attribute anywhere
rg -n "mirror_network" src/hiero_sdk_python/client/client.py

# Check what Network.get_mirror_rest_url() returns
rg -A 5 "def get_mirror_rest_url" src/hiero_sdk_python/client/network.py

Repository: hiero-ledger/hiero-sdk-python

Length of output: 55


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if there's a mirror_network property on Client
rg -B 2 -A 5 "@property" src/hiero_sdk_python/client/client.py | grep -A 5 "mirror"

# Check what attributes Network has
rg -n "def " src/hiero_sdk_python/client/network.py | head -20

# Check the full get_mirror_rest_url method
rg -B 2 -A 10 "def get_mirror_rest_url" src/hiero_sdk_python/client/network.py

Repository: hiero-ledger/hiero-sdk-python

Length of output: 55


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the specific lines from network.py where get_mirror_rest_url is defined
sed -n '324,335p' src/hiero_sdk_python/client/network.py

# Check if Client has a reference to network.get_mirror_rest_url or mirror_network
grep -n "get_mirror_rest_url\|mirror_network" src/hiero_sdk_python/client/client.py

# Check the full network.py around line 324
sed -n '320,330p' src/hiero_sdk_python/client/network.py

Repository: hiero-ledger/hiero-sdk-python

Length of output: 565


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify the actual FeeEstimateQuery code uses client.mirror_network
grep -n "mirror_network" src/hiero_sdk_python/query/fee_estimate_query.py

# Check if Client class exposes mirror_network as property or attribute
grep -n "mirror_network" src/hiero_sdk_python/client/client.py

# Confirm max_attempts is the real attribute name, not max_retries
grep -n "max_attempts\|max_retries" src/hiero_sdk_python/client/client.py | head -10

Repository: hiero-ledger/hiero-sdk-python

Length of output: 154


Fix the mock to use the actual Client API, and align FeeEstimateQuery.execute() to the real SDK surface.

The mock invents mirror_network and max_retries attributes, but the real Client class exposes max_attempts (not max_retries) and does not have a mirror_network property. The actual Client.network is a Network instance with a get_mirror_rest_url() method.

More critically, FeeEstimateQuery.execute() itself uses client.mirror_network (line 48), which does not exist on the real Client. This test will not catch the production failure when the code runs against a real client.

Update the mock to use Client's real surface (client.network, client.max_attempts), and refactor FeeEstimateQuery.execute() to call client.network.get_mirror_rest_url() and reference client.max_attempts instead of the invented attributes.

Comment on lines +63 to +93
@patch("hiero_sdk_python.query.fee_estimate_query.requests.post")
def test_transfer_transaction_intrinsic_mode(mock_post):

mock_post.return_value = mock_requests_response()

tx = TransferTransaction()
tx.add_hbar_transfer("0.0.1001", Hbar(-1))
tx.add_hbar_transfer("0.0.1002", Hbar(1))

query = (
FeeEstimateQuery()
.set_transaction(tx)
.set_mode(FeeEstimateMode.INTRINSIC)
)

result = query.execute(mock_client())

assert result.mode == FeeEstimateMode.INTRINSIC

@patch("hiero_sdk_python.query.fee_estimate_query.requests.post")
def test_default_mode_is_state(mock_post):

mock_post.return_value = mock_requests_response()

tx = TransferTransaction()

query = FeeEstimateQuery().set_transaction(tx)

result = query.execute(mock_client())

assert result.mode == FeeEstimateMode.STATE
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Assert the outgoing mode parameter, not just the returned enum.

These tests can still pass if the POST URL never includes mode= or always uses the default, because they only inspect result.mode. Check mock_post.call_args so the suite actually protects request construction.

🧪 Suggested assertions
     result = query.execute(mock_client())

     assert result.mode == FeeEstimateMode.INTRINSIC
+    assert (
+        f"mode={FeeEstimateMode.INTRINSIC.value}" in mock_post.call_args[0][0]
+    ), "FeeEstimateQuery must send the selected mode in the Mirror Node URL"
@@
     result = query.execute(mock_client())

     assert result.mode == FeeEstimateMode.STATE
+    assert (
+        f"mode={FeeEstimateMode.STATE.value}" in mock_post.call_args[0][0]
+    ), "FeeEstimateQuery must default the Mirror Node URL to STATE mode"

As per coding guidelines, "Assert backward-compatible defaults are maintained."

Comment on lines +187 to +228
@patch("hiero_sdk_python.query.fee_estimate_query.requests.post")
def test_invalid_argument_error(mock_post):

response = MagicMock()
response.status_code = 400

mock_post.return_value = response

tx = TransferTransaction()

query = FeeEstimateQuery().set_transaction(tx)

with pytest.raises(ValueError):
query.execute(mock_client())

@patch("hiero_sdk_python.query.fee_estimate_query.requests.post")
def test_retry_on_unavailable(mock_post):

mock_post.side_effect = [
Exception("UNAVAILABLE"),
mock_requests_response(),
]

tx = TransferTransaction()

result = FeeEstimateQuery().set_transaction(tx).execute(mock_client())

assert result is not None

@patch("hiero_sdk_python.query.fee_estimate_query.requests.post")
def test_retry_on_timeout(mock_post):

mock_post.side_effect = [
Exception("DEADLINE_EXCEEDED"),
mock_requests_response(),
]

tx = TransferTransaction()

result = FeeEstimateQuery().set_transaction(tx).execute(mock_client())

assert result is not None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Retry-path tests need to assert retry boundaries.

Right now these cases only prove “eventually failed/succeeded”. A regression that retries INVALID_ARGUMENT or stops retrying transient failures would still pass. Assert call counts and the expected error text.

🧪 Suggested assertions
-    with pytest.raises(ValueError):
+    with pytest.raises(ValueError, match="INVALID_ARGUMENT"):
         query.execute(mock_client())
+    assert mock_post.call_count == 1, "INVALID_ARGUMENT must not be retried"
@@
     result = FeeEstimateQuery().set_transaction(tx).execute(mock_client())

     assert result is not None
+    assert mock_post.call_count == 2, "UNAVAILABLE should be retried once before succeeding"
@@
     result = FeeEstimateQuery().set_transaction(tx).execute(mock_client())

     assert result is not None
+    assert mock_post.call_count == 2, "DEADLINE_EXCEEDED should be retried once before succeeding"

As per coding guidelines, "Unit tests should be extensive - test even if we don't expect users to use it currently."

Comment on lines +230 to +252
@patch("hiero_sdk_python.query.fee_estimate_query.requests.post")
def test_topic_message_single_chunk(mock_post):

mock_post.return_value = mock_requests_response()

tx = TopicMessageSubmitTransaction()
tx.set_message("hello")

result = FeeEstimateQuery().set_transaction(tx).execute(mock_client())

assert result is not None

@patch("hiero_sdk_python.query.fee_estimate_query.requests.post")
def test_topic_message_multiple_chunks(mock_post):

mock_post.return_value = mock_requests_response()

tx = TopicMessageSubmitTransaction()
tx.set_message("A" * 5000)

result = FeeEstimateQuery().set_transaction(tx).execute(mock_client())

assert result is not None No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The chunking tests never verify chunking.

Both topic-message tests only assert non-None, so a placeholder implementation that always POSTs once still passes. The large-message case should assert multiple requests and aggregated totals across chunk responses.

🧪 Suggested assertions
     result = FeeEstimateQuery().set_transaction(tx).execute(mock_client())

     assert result is not None
+    assert mock_post.call_count == 1, "Small topic messages should be estimated in a single request"
@@
     result = FeeEstimateQuery().set_transaction(tx).execute(mock_client())

     assert result is not None
+    assert mock_post.call_count > 1, "Large topic messages should be estimated per chunk"
+    single_total = 10 + 20 + (10 * 2)
+    assert (
+        result.total == single_total * mock_post.call_count
+    ), "Chunked fee estimates must aggregate all chunk responses"

As per coding guidelines, "Unit tests should be extensive - test even if we don't expect users to use it currently."

Signed-off-by: aceppaluni <aceppaluni@gmail.com>
@github-actions
Copy link
Copy Markdown

Hello, this is the OfficeHourBot.

This is a reminder that the Hiero Python SDK Office Hours are scheduled in approximately 4 hours (14:00 UTC).

This session provides an opportunity to ask questions regarding this Pull Request.

Details:

Disclaimer: This is an automated reminder. Please verify the schedule here for any changes.

From,
The Python SDK Team

@aceppaluni aceppaluni self-assigned this Mar 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Advanced]: Implement HIP-1261 Fee Estimate Query Support

3 participants