-
Notifications
You must be signed in to change notification settings - Fork 301
feat(client): add global max transaction fee configuration #2332
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
5326e17
a5d1bfc
c490cdd
907d9b0
671a054
e246963
4b44a3e
e0122c2
afe0bc0
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 |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import hashlib | ||
| from decimal import Decimal | ||
| from typing import TYPE_CHECKING, Literal, overload | ||
|
|
||
| from hiero_sdk_python.account.account_id import AccountId | ||
|
|
@@ -265,6 +266,20 @@ def _resolve_node_ids(self, client: Client): | |
| if self._node_account_ids.is_empty: | ||
| self._node_account_ids.set_list([node._account_id for node in client.network.nodes]) | ||
|
|
||
| def _resolve_transaction_fee(self, client: Client | None) -> None: | ||
| """Resolve the max transaction fee: explicit fee, else client default, else per-type default.""" | ||
| if self._transaction_fee is not None: | ||
| return | ||
|
|
||
| default = client.default_max_transaction_fee if client is not None else None | ||
| if not isinstance(default, Hbar): | ||
| default = None | ||
|
|
||
| if default is not None: | ||
| self.transaction_fee = default | ||
| else: | ||
| self._transaction_fee = self._default_transaction_fee | ||
|
|
||
| def freeze(self): | ||
| """ | ||
| Freezes the transaction by building the transaction body and setting necessary IDs. | ||
|
|
@@ -304,6 +319,7 @@ def freeze_with(self, client: Client): | |
| # Resolve transaction_id and node_accountids to be set when using freeze() | ||
| self._resolve_transaction_id(client) | ||
| self._resolve_node_ids(client) | ||
| self._resolve_transaction_fee(client) | ||
|
|
||
| required_chunks = self.get_required_chunks() | ||
| self._generate_transaction_ids(self._transaction_ids.get(0), required_chunks) | ||
|
|
@@ -509,8 +525,8 @@ def build_base_transaction_body(self) -> transaction_pb2.TransactionBody: | |
| """ | ||
| transaction_body = transaction_pb2.TransactionBody() | ||
|
|
||
| fee = self._transaction_fee or self._default_transaction_fee | ||
| if hasattr(fee, "to_tinybars"): | ||
| fee = self._transaction_fee if self._transaction_fee is not None else self._default_transaction_fee | ||
| if isinstance(fee, Hbar): | ||
| transaction_body.transactionFee = int(fee.to_tinybars()) | ||
| else: | ||
| transaction_body.transactionFee = int(fee) | ||
|
|
@@ -537,8 +553,8 @@ def build_base_scheduled_body(self) -> SchedulableTransactionBody: | |
| """ | ||
| schedulable_body = SchedulableTransactionBody() | ||
|
|
||
| fee = self._transaction_fee or self._default_transaction_fee | ||
| if hasattr(fee, "to_tinybars"): | ||
| fee = self._transaction_fee if self._transaction_fee is not None else self._default_transaction_fee | ||
| if isinstance(fee, Hbar): | ||
| schedulable_body.transactionFee = int(fee.to_tinybars()) | ||
| else: | ||
| schedulable_body.transactionFee = int(fee) | ||
|
|
@@ -681,19 +697,7 @@ def transaction_fee(self, fee: Hbar | int): | |
| """ | ||
| Set the maximum transaction fee for this transaction. | ||
| """ | ||
| self._require_not_frozen() | ||
|
|
||
| if isinstance(fee, Hbar): | ||
| tinybars = fee.to_tinybars() | ||
| elif isinstance(fee, bool) or not isinstance(fee, int): | ||
| raise TypeError("fee must be of type Hbar or int") | ||
| else: | ||
| tinybars = fee | ||
|
|
||
| if tinybars < 0: | ||
| raise ValueError("fee must be greater than or equal to 0") | ||
|
|
||
| self._transaction_fee = tinybars | ||
| self.set_max_transaction_fee(fee) | ||
|
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. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: git show b9cfe74e266e03705fb082ced217961f9e6b9c65:src/hiero_sdk_python/transaction/transaction.py | grep -n -A25 'transaction_fee.setter'
sed -n '680,710p;840,870p' src/hiero_sdk_python/transaction/transaction.py
sed -n '80,110p' src/hiero_sdk_python/hbar.py
sed -n '560,590p' tests/unit/transaction_test.pyRepository: hiero-ledger/hiero-sdk-python Length of output: 5715 Preserve tinybar units in At the merge base, integer assignments to Keep integer assignments in tinybars. Preserve the property-specific |
||
|
|
||
| def to_bytes(self) -> bytes: | ||
| """ | ||
|
|
@@ -837,6 +841,25 @@ def from_bytes(transaction_bytes: bytes): | |
| transaction_body, signed_transaction.bodyBytes, signed_transaction.sigMap | ||
| ) | ||
|
|
||
| def set_max_transaction_fee(self, max_transaction_fee: int | float | Decimal | Hbar) -> Transaction: | ||
| """ | ||
| Set the maximum transaction fee the payer is willing to pay for this transaction. | ||
|
|
||
| Args: | ||
| max_transaction_fee (int | float | Decimal | Hbar): The maximum fee. | ||
| Numeric values are interpreted as Hbar. | ||
|
|
||
| Returns: | ||
| Transaction: This transaction instance for method chaining. | ||
| Raises: | ||
| TypeError: If the value is not int, float, Decimal, or Hbar. | ||
| ValueError: If the value is negative. | ||
| Exception: If the transaction has already been frozen. | ||
| """ | ||
| self._require_not_frozen() | ||
| self._transaction_fee = Hbar._coerce_non_negative(max_transaction_fee, "max_transaction_fee").to_tinybars() | ||
| return self | ||
|
|
||
| @staticmethod | ||
| def _get_transaction_class(transaction_type: str): | ||
| """ | ||
|
|
@@ -942,7 +965,7 @@ def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): | |
| if transaction_body.HasField("nodeAccountID"): | ||
| transaction._node_account_ids.set_list([AccountId._from_proto(transaction_body.nodeAccountID)]) | ||
|
|
||
| transaction.transaction_fee = transaction_body.transactionFee | ||
| transaction._transaction_fee = transaction_body.transactionFee | ||
| transaction.transaction_valid_duration = transaction_body.transactionValidDuration.seconds | ||
| transaction.generate_record = transaction_body.generateRecord | ||
| transaction._high_volume = transaction_body.high_volume | ||
|
|
@@ -967,7 +990,6 @@ def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): | |
|
|
||
| if sig_map and sig_map.sigPair: | ||
| transaction._signature_map[body_bytes] = sig_map | ||
|
|
||
| return transaction | ||
|
|
||
| def set_batch_key(self, key: Key): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ | |
| from hiero_sdk_python.crypto.key_list import KeyList | ||
| from hiero_sdk_python.crypto.private_key import PrivateKey | ||
| from hiero_sdk_python.Duration import Duration | ||
| from hiero_sdk_python.exceptions import PrecheckError | ||
| from hiero_sdk_python.hbar import Hbar | ||
| from hiero_sdk_python.query.account_info_query import AccountInfoQuery | ||
| from hiero_sdk_python.response_code import ResponseCode | ||
|
|
@@ -248,7 +249,6 @@ def _apply_tiny_max_fee_if_supported(tx, client) -> bool: | |
| # Try client-level default | ||
| for attr in ( | ||
|
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. same for this one we can directly do |
||
| "set_default_max_transaction_fee", | ||
| "set_max_transaction_fee", | ||
| "set_default_max_fee", | ||
| "setMaxTransactionFee", | ||
| ): | ||
|
|
@@ -283,16 +283,40 @@ def test_account_update_insufficient_fee_with_valid_expiration_bump(env): | |
| if not _apply_tiny_max_fee_if_supported(tx, env.client): | ||
| pytest.skip("SDK lacks a max-fee API; cannot deterministically trigger INSUFFICIENT_TX_FEE.") | ||
|
|
||
| receipt = tx.execute(env.client) | ||
| assert receipt.status == ResponseCode.INSUFFICIENT_TX_FEE, ( | ||
| f"Expected INSUFFICIENT_TX_FEE but got {ResponseCode(receipt.status).name}" | ||
| ) | ||
| # If it succeeds or raises a different error, the test will fail. | ||
| with pytest.raises(PrecheckError) as exc_info: | ||
| tx.execute(env.client) | ||
|
|
||
| assert exc_info.value.status == ResponseCode.INSUFFICIENT_TX_FEE | ||
|
|
||
| # Confirm expiration time did not change | ||
| info_after = AccountInfoQuery(account_id).execute(env.client) | ||
| assert int(info_after.expiration_time.seconds) == base_expiry_secs | ||
|
|
||
|
|
||
| @pytest.mark.integration | ||
| def test_account_update_insufficient_fee_via_client_default(env): | ||
| """A client-level default max fee must apply to transactions that set no explicit fee.""" | ||
| receipt = ( | ||
| AccountCreateTransaction() | ||
| .set_key(env.operator_key.public_key()) | ||
| .set_initial_balance(Hbar(1)) | ||
| .execute(env.client) | ||
| ) | ||
| assert receipt.status == ResponseCode.SUCCESS | ||
| account_id = receipt.account_id | ||
|
|
||
| env.client.set_default_max_transaction_fee(Hbar.from_tinybars(1)) | ||
|
|
||
| # No tx-level fee: the 1-tinybar client default must be resolved at freeze and rejected at precheck. | ||
| tx = AccountUpdateTransaction().set_account_id(account_id).set_account_memo("client default fee test") | ||
|
|
||
| with pytest.raises(PrecheckError) as exc_info: | ||
| tx.execute(env.client) | ||
|
|
||
| assert exc_info.value.status == ResponseCode.INSUFFICIENT_TX_FEE | ||
|
|
||
|
|
||
| @pytest.mark.integration | ||
| def test_integration_account_update_transaction_with_only_account_id(env): | ||
| """Test that AccountUpdateTransaction can execute with only account ID set.""" | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.