-
Notifications
You must be signed in to change notification settings - Fork 235
feat: Add Hip 1261 Implementation #2019
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| from dataclasses import dataclass, field | ||
| from typing import List | ||
| from .fee_extra import FeeExtra | ||
|
|
||
| @dataclass(frozen=True) | ||
| class FeeEstimate: | ||
| base: int | ||
| extras: List[FeeExtra] = field(default_factory=list) | ||
|
|
||
| @property | ||
| def subtotal(self) -> int: | ||
| return self.base + sum(extra.subtotal for extra in self.extras) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| from enum import Enum | ||
|
|
||
| class FeeEstimateMode(str, Enum): | ||
| STATE = "STATE" | ||
| INTRINSIC = "INTRINSIC" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| from dataclasses import dataclass, field | ||
| from typing import List | ||
| from .fee_estimate_mode import FeeEstimateMode | ||
| from .fee_estimate import FeeEstimate | ||
| from .network_fee import NetworkFee | ||
|
|
||
| @dataclass(frozen=True) | ||
| class FeeEstimateResponse: | ||
| mode: FeeEstimateMode | ||
| network_fee: NetworkFee | ||
| node_fee: FeeEstimate | ||
| service_fee: FeeEstimate | ||
| notes: List[str] = field(default_factory=list) | ||
| total: int = 0 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid storing Line 14 defaults 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 |
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,10 @@ | ||||||
| from dataclasses import dataclass | ||||||
|
|
||||||
| @dataclass(frozen=True) | ||||||
| class FeeExtra: | ||||||
| name: str | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This field may not always be present in the response
Suggested change
|
||||||
| included: int | ||||||
| count: int | ||||||
| charged: int | ||||||
| fee_per_unit: int | ||||||
| subtotal: int | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| from dataclasses import dataclass | ||
|
|
||
| @dataclass(frozen=True) | ||
| class NetworkFee: | ||
| multiplier: int | ||
| subtotal: int |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,140 @@ | ||||||||
| from typing import Optional | ||||||||
| import requests | ||||||||
| from hiero_sdk_python.fees.fee_estimate_mode import FeeEstimateMode | ||||||||
| from hiero_sdk_python.fees.fee_estimate_response import FeeEstimateResponse | ||||||||
| from hiero_sdk_python.fees.fee_extra import FeeExtra | ||||||||
| from hiero_sdk_python.fees.fee_estimate import FeeEstimate | ||||||||
| from hiero_sdk_python.fees.network_fee import NetworkFee | ||||||||
| from hiero_sdk_python.fees.fee_estimate_response import FeeEstimateResponse | ||||||||
|
|
||||||||
| from typing import TYPE_CHECKING | ||||||||
|
|
||||||||
| if TYPE_CHECKING: | ||||||||
| from hiero_sdk_python.transaction.transaction import Transaction | ||||||||
|
|
||||||||
| class FeeEstimateQuery: | ||||||||
|
|
||||||||
| def __init__(self): | ||||||||
manishdait marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
| self._mode: Optional[FeeEstimateMode] = None | ||||||||
| self._transaction: Optional["Transaction"] = None | ||||||||
|
|
||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. consider adding query level |
||||||||
| def set_mode(self, mode: FeeEstimateMode) -> "FeeEstimateQuery": | ||||||||
| self._mode = mode | ||||||||
| return self | ||||||||
|
|
||||||||
| def get_mode(self) -> Optional[FeeEstimateMode]: | ||||||||
| return self._mode | ||||||||
|
|
||||||||
| 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 | ||||||||
|
Comment on lines
+28
to
+37
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The supplied transaction is never serialized or chunked.
Also applies to: 50-65 |
||||||||
|
|
||||||||
| def get_transaction(self) -> Optional["Transaction"]: | ||||||||
| return self._transaction | ||||||||
|
|
||||||||
| def execute(self, client) -> FeeEstimateResponse: | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. consider splitting into smaller helper methods to reduce complexity. |
||||||||
| if self._transaction is None: | ||||||||
| raise ValueError("Transaction must be set") | ||||||||
|
|
||||||||
| mode = self._mode or FeeEstimateMode.STATE | ||||||||
|
|
||||||||
| url = f"{client.mirror_network}/api/v1/network/fees?mode={mode.value}" | ||||||||
|
|
||||||||
| transactions = [b"dummy"] | ||||||||
|
|
||||||||
| if not isinstance(transactions, list): | ||||||||
| transactions = [transactions] | ||||||||
|
|
||||||||
| node_total = 0 | ||||||||
| service_total = 0 | ||||||||
| network_multiplier = None | ||||||||
| notes = [] | ||||||||
|
|
||||||||
| max_retries = getattr(client, "max_retries", 3) | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can remove this and use the
Suggested change
|
||||||||
|
|
||||||||
| for tx in transactions: | ||||||||
|
|
||||||||
| tx_bytes = b"dummy" | ||||||||
| for attempt in range(max_retries): | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
|
|
||||||||
| try: | ||||||||
|
|
||||||||
| response = requests.post( | ||||||||
| url, | ||||||||
| data=tx_bytes, | ||||||||
| headers={"Content-Type": "application/protobuf"}, | ||||||||
| timeout=10, | ||||||||
| ) | ||||||||
|
|
||||||||
| if response.status_code == 400: | ||||||||
| raise ValueError("INVALID_ARGUMENT") | ||||||||
|
|
||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can impose retry logic on stautus code like: |
||||||||
| response.raise_for_status() | ||||||||
|
|
||||||||
| data = response.json() | ||||||||
|
|
||||||||
| 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: | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
||||||||
| 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 | ||||||||
| ) | ||||||||
|
Comment on lines
+83
to
+114
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserve the parsed response instead of rebuilding it from request state.
Also applies to: 116-140 |
||||||||
|
|
||||||||
| def _parse_response(self, data): | ||||||||
|
|
||||||||
| node_fee = FeeEstimate( | ||||||||
| base=data["node"]["subtotal"], | ||||||||
| extras=[] | ||||||||
| ) | ||||||||
|
|
||||||||
| service_fee = FeeEstimate( | ||||||||
| base=data["service"]["subtotal"], | ||||||||
| extras=[] | ||||||||
| ) | ||||||||
|
|
||||||||
| network_fee = NetworkFee( | ||||||||
| multiplier=data["network"]["multiplier"], | ||||||||
| subtotal=0 # computed later | ||||||||
| ) | ||||||||
|
|
||||||||
| return FeeEstimateResponse( | ||||||||
| mode=FeeEstimateMode(data["mode"]), | ||||||||
| network_fee=network_fee, | ||||||||
| node_fee=node_fee, | ||||||||
| service_fee=service_fee, | ||||||||
| notes=data.get("notes", []), | ||||||||
| total=0, # computed later | ||||||||
| ) | ||||||||
There was a problem hiding this comment.
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]: