From 5326e178686bd8fd295c5d2b7773b1d8e2cba2cb Mon Sep 17 00:00:00 2001 From: tech0priyanshu Date: Thu, 4 Jun 2026 01:26:57 +0530 Subject: [PATCH 1/9] feat: set_max_transaction to client Signed-off-by: tech0priyanshu removed print Signed-off-by: tech0priyanshu fixed magicMock Signed-off-by: tech0priyanshu fixed precheckerror Signed-off-by: tech0priyanshu fixes Signed-off-by: tech0priyanshu fixing magicmock Signed-off-by: tech0priyanshu fix: callback for _coerce_fee Signed-off-by: tech0priyanshu fix: comment moved Signed-off-by: tech0priyanshu fix: max_transaction_fee to default_max_transaction_fee Signed-off-by: tech0priyanshu --- src/hiero_sdk_python/client/client.py | 14 +++ src/hiero_sdk_python/hbar.py | 15 +++ .../transaction/transaction.py | 28 ++++- .../account_update_transaction_e2e_test.py | 14 ++- tests/unit/client_test.py | 43 +++++++ tests/unit/fee_estimate_query_test.py | 5 + tests/unit/file_append_transaction_test.py | 2 + .../unit/transaction_freeze_and_bytes_test.py | 105 ++++++++++++++++++ 8 files changed, 219 insertions(+), 7 deletions(-) diff --git a/src/hiero_sdk_python/client/client.py b/src/hiero_sdk_python/client/client.py index 361324d06..3a9329223 100644 --- a/src/hiero_sdk_python/client/client.py +++ b/src/hiero_sdk_python/client/client.py @@ -59,6 +59,7 @@ def __init__(self, network: Network = None) -> None: self.max_attempts: int = 10 self.default_max_query_payment: Hbar = DEFAULT_MAX_QUERY_PAYMENT + self.default_max_transaction_fee: Hbar | None = None self._min_backoff: float = DEFAULT_MIN_BACKOFF self._max_backoff: float = DEFAULT_MAX_BACKOFF @@ -290,6 +291,19 @@ def set_default_max_query_payment(self, max_query_payment: int | float | Decimal self.default_max_query_payment = value return self + def set_default_max_transaction_fee( + self, + max_transaction_fee: int | float | Decimal | Hbar, + ) -> Client: + """ + Sets the default maximum Hbar fee allowed for any transaction executed by this client. + """ + value = Hbar._coerce_fee(max_transaction_fee) + if value < Hbar.ZERO: + raise ValueError("max_transaction_fee must be non-negative") + self.default_max_transaction_fee = value + return self + def set_max_attempts(self, max_attempts: int) -> Client: """ Set the maximum number of execution attempts for all transactions and queries diff --git a/src/hiero_sdk_python/hbar.py b/src/hiero_sdk_python/hbar.py index 22e3d03a6..cc58b5d2a 100644 --- a/src/hiero_sdk_python/hbar.py +++ b/src/hiero_sdk_python/hbar.py @@ -83,6 +83,21 @@ def negated(self) -> Hbar: """ return Hbar.from_tinybars(-self._amount_in_tinybar) + @staticmethod + def _coerce_fee(value: int | float | Decimal | Hbar) -> Hbar: + """ + Coerce a value to an Hbar instance. Accepts int, float, Decimal, or Hbar (but not bool). + Args: + value: The value to coerce. + Returns: + Hbar: An Hbar instance. + Raises: + TypeError: If value is not a valid type. + """ + if isinstance(value, bool) or not isinstance(value, (int, float, Decimal, Hbar)): + raise TypeError(f"value must be int, float, Decimal, or Hbar, got {type(value).__name__}") + return value if isinstance(value, Hbar) else Hbar(value) + @classmethod def of(cls, amount: int | float | Decimal, unit: HbarUnit) -> Hbar: """ diff --git a/src/hiero_sdk_python/transaction/transaction.py b/src/hiero_sdk_python/transaction/transaction.py index f83298bf9..99a3ec254 100644 --- a/src/hiero_sdk_python/transaction/transaction.py +++ b/src/hiero_sdk_python/transaction/transaction.py @@ -509,7 +509,7 @@ def build_base_transaction_body(self) -> transaction_pb2.TransactionBody: """ transaction_body = transaction_pb2.TransactionBody() - fee = self._transaction_fee or self._default_transaction_fee + fee = self._transaction_fee if self._transaction_fee is not None else self._default_transaction_fee if hasattr(fee, "to_tinybars"): transaction_body.transactionFee = int(fee.to_tinybars()) else: @@ -837,6 +837,32 @@ def from_bytes(transaction_bytes: bytes): transaction_body, signed_transaction.bodyBytes, signed_transaction.sigMap ) + def set_default_max_transaction_fee(self, max_transaction_fee): + """ + Sets the maximum transaction fee for this transaction. + + The maximum transaction fee specifies the highest fee that can be + charged when the transaction is executed. The value must be + non-negative. + + Args: + max_transaction_fee (Hbar | int | str): The maximum transaction + fee. Accepted types are those supported by ``Hbar._coerce_fee``. + + Returns: + Self: This transaction instance, allowing method chaining. + + Raises: + ValueError: If ``max_transaction_fee`` is negative. + RuntimeError: If the transaction has been frozen. + """ + self._require_not_frozen() + value = Hbar._coerce_fee(max_transaction_fee) + if value < Hbar.ZERO: + raise ValueError("max_transaction_fee must be non-negative") + self.transaction_fee = value + return self + @staticmethod def _get_transaction_class(transaction_type: str): """ diff --git a/tests/integration/account_update_transaction_e2e_test.py b/tests/integration/account_update_transaction_e2e_test.py index 88f0ed8cf..ff2b046a2 100644 --- a/tests/integration/account_update_transaction_e2e_test.py +++ b/tests/integration/account_update_transaction_e2e_test.py @@ -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 @@ -241,14 +242,14 @@ def test_integration_account_update_transaction_invalid_auto_renew_period(env): def _apply_tiny_max_fee_if_supported(tx, client) -> bool: # Try tx-level setters - for attr in ("set_max_transaction_fee", "set_max_fee", "set_transaction_fee"): + for attr in ("set_default_max_transaction_fee", "set_max_fee", "set_transaction_fee"): if hasattr(tx, attr): getattr(tx, attr)(Hbar.from_tinybars(1)) return True # Try client-level default for attr in ( "set_default_max_transaction_fee", - "set_max_transaction_fee", + "set_default_max_transaction_fee", "set_default_max_fee", "setMaxTransactionFee", ): @@ -283,10 +284,11 @@ 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) diff --git a/tests/unit/client_test.py b/tests/unit/client_test.py index 3cdc79c1f..15737a222 100644 --- a/tests/unit/client_test.py +++ b/tests/unit/client_test.py @@ -231,6 +231,49 @@ def test_set_default_max_query_payment_valid_param(valid_amount, expected): assert client.default_max_query_payment == expected +def test_default_max_transaction_fee_is_none(): + """Default `default_max_transaction_fee` should be None.""" + client = Client.for_testnet() + assert client.default_max_transaction_fee is None + + +@pytest.mark.parametrize( + "valid_amount,expected", + [ + (1, Hbar(1)), + (0.1, Hbar(0.1)), + (Decimal("0.1"), Hbar(Decimal("0.1"))), + (Hbar(1), Hbar(1)), + (Hbar(0), Hbar(0)), + ], +) +def test_set_default_max_transaction_fee_valid_param(valid_amount, expected): + """Test set_default_max_transaction_fee converts inputs to Hbar and stores them.""" + client = Client.for_testnet() + + returned = client.set_default_max_transaction_fee(valid_amount) + assert client.default_max_transaction_fee == expected + assert returned is client + + +@pytest.mark.parametrize("invalid_amount", ["1", True, False, None, object()]) +def test_set_default_max_transaction_fee_invalid_param(invalid_amount): + """Test set_default_max_transaction_fee rejects invalid types.""" + client = Client.for_testnet() + + with pytest.raises(TypeError): + client.set_default_max_transaction_fee(invalid_amount) + + +@pytest.mark.parametrize("negative_amount", [-1, -0.1, Decimal("-0.1"), Hbar(-1)]) +def test_set_default_max_transaction_fee_negative_value(negative_amount): + """Test set_default_max_transaction_fee rejects negative values.""" + client = Client.for_testnet() + + with pytest.raises(ValueError): + client.set_default_max_transaction_fee(negative_amount) + + @pytest.mark.parametrize("negative_amount", [-1, -0.1, Decimal("-0.1"), Decimal("-1"), Hbar(-1)]) def test_set_default_max_query_payment_negative_value(negative_amount): """Test set_default_max_query_payment for negative amount values.""" diff --git a/tests/unit/fee_estimate_query_test.py b/tests/unit/fee_estimate_query_test.py index 6b6d253af..582bcef81 100644 --- a/tests/unit/fee_estimate_query_test.py +++ b/tests/unit/fee_estimate_query_test.py @@ -32,6 +32,7 @@ def mock_client(): client.mirror_network = "https://testnet.mirrornode.hedera.com" client.max_retries = 3 + client.default_max_transaction_fee = Hbar(2) client.generate_transaction_id.return_value = TransactionId.generate(AccountId(0, 0, 1001)) client.operator_account_id._to_proto.return_value = AccountId(0, 0, 1)._to_proto() @@ -337,6 +338,7 @@ def test_port_replacement_for_localhost_execute_single(): """Test localhost:38081 is replaced with :8084.""" client_1 = MagicMock() client_1.network.get_mirror_rest_url.return_value = "http://localhost:38081/api/v1" + client_1.default_max_transaction_fee = Hbar(1) tx = ( TransferTransaction() @@ -354,6 +356,7 @@ def test_port_replacement_for_localhost_execute_single(): client_2 = MagicMock() client_2.network.get_mirror_rest_url.return_value = "http://127.0.0.1:38081/api/v1" + client_2.default_max_transaction_fee = Hbar(1) with patch.object(query, "_execute_single") as mock_execute_single: query.execute(client_2) @@ -366,6 +369,7 @@ def test_port_replacement_for_localhost_execute_multiple(): """Test localhost:38081 is replaced with :8084 for chunked tx.""" client_1 = MagicMock() client_1.network.get_mirror_rest_url.return_value = "http://localhost:38081/api/v1" + client_1.default_max_transaction_fee = Hbar(1) tx = ( TopicMessageSubmitTransaction() @@ -388,6 +392,7 @@ def test_port_replacement_for_localhost_execute_multiple(): client_2 = MagicMock() client_2.network.get_mirror_rest_url.return_value = "http://127.0.0.1:38081/api/v1" + client_2.default_max_transaction_fee = Hbar(1) with patch.object(query, "_execute_chunked", return_value=MagicMock()) as mock_execute_chunked: query.execute(client_2) diff --git a/tests/unit/file_append_transaction_test.py b/tests/unit/file_append_transaction_test.py index 4aa83a186..54d9fe93e 100644 --- a/tests/unit/file_append_transaction_test.py +++ b/tests/unit/file_append_transaction_test.py @@ -99,6 +99,7 @@ def test_freeze_with_generates_transaction_ids(): # Mock client and transaction_id mock_client = MagicMock() + mock_client.default_max_transaction_fee = Hbar(1) mock_transaction_id = TransactionId(account_id=MagicMock(), valid_start=Timestamp(0, 1)) file_tx.transaction_id = mock_transaction_id @@ -141,6 +142,7 @@ def test_multi_chunk_execution(): # Mock client and responses mock_client = MagicMock() + mock_client.default_max_transaction_fee = Hbar(1) mock_receipt = MagicMock(spec=TransactionReceipt) mock_receipt.status = ResponseCode.SUCCESS diff --git a/tests/unit/transaction_freeze_and_bytes_test.py b/tests/unit/transaction_freeze_and_bytes_test.py index 6916cb27e..f1aae0bce 100644 --- a/tests/unit/transaction_freeze_and_bytes_test.py +++ b/tests/unit/transaction_freeze_and_bytes_test.py @@ -8,6 +8,8 @@ from __future__ import annotations +from decimal import Decimal + import pytest from hiero_sdk_python.account.account_id import AccountId @@ -15,6 +17,7 @@ from hiero_sdk_python.hapi.services.transaction_response_pb2 import ( TransactionResponse as TransactionResponseProto, ) +from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.transaction.transaction_id import TransactionId from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction @@ -67,6 +70,45 @@ def test_freeze_with_valid_parameters(): assert node_id in transaction._transaction_body_bytes[transaction_id] +@pytest.mark.parametrize( + "valid_amount,expected", + [ + (1, 100_000_000), + (0.1, 10_000_000), + (Decimal("0.1"), 10_000_000), + (Hbar(1), 100_000_000), + (Hbar(0), 0), + ], +) +def test_set_max_transaction_fee_valid_param(valid_amount, expected): + """Transaction.set_default_max_transaction_fee should accept various numeric types and Hbar.""" + tx = TransferTransaction() + + returned = tx.set_default_max_transaction_fee(valid_amount) + assert tx.transaction_fee == expected + assert returned is tx + + +@pytest.mark.parametrize("invalid_amount", ["1", True, False, None, object()]) +def test_set_max_transaction_fee_invalid_param(invalid_amount): + """Transaction.set_default_max_transaction_fee should reject invalid types.""" + tx = TransferTransaction() + + with pytest.raises(TypeError): + tx.set_default_max_transaction_fee(invalid_amount) + + +@pytest.mark.parametrize("negative_amount", [-1, -0.1, Decimal("-0.1"), Hbar(-1)]) +def test_set_max_transaction_fee_negative_value(negative_amount): + """Transaction.set_default_max_transaction_fee should reject negative values.""" + tx = TransferTransaction() + + with pytest.raises(ValueError): + tx.set_default_max_transaction_fee(negative_amount) + # checking state un modified + assert len(tx._transaction_body_bytes) == 0 + + def test_freeze_is_idempotent(): """Test that calling freeze() multiple times doesn't cause issues.""" operator_id = AccountId.from_string("0.0.1234") @@ -715,3 +757,66 @@ def test_map_response_raises_if_proto_request_is_not_transaction(): node_id=mock_node_id, proto_request=invalid_proto_request, ) + + +def test_fee_resolution_transaction_precedence(mock_client): + """Transaction fee explicitly set should take precedence over client default.""" + tx = TransferTransaction() + tx.set_default_max_transaction_fee(Hbar(10)) + + # client has different default + mock_client.set_default_max_transaction_fee(Hbar(5)) + + before = tx.transaction_fee + tx.freeze_with(mock_client) + + assert tx.transaction_fee == 1_000_000_000 + assert tx.transaction_fee == before + + +def test_fee_resolution_client_default_used_when_transaction_missing(mock_client): + """When transaction fee is not set, client.default_max_transaction_fee should be used.""" + tx = TransferTransaction() + # leave tx.transaction_fee as None + + mock_client.set_default_max_transaction_fee(Hbar(7)) + + tx.freeze_with(mock_client) + + assert tx.transaction_fee == 700_000_000 + + +def test_fee_resolution_falls_back_to_transaction_default(mock_client): + """When neither transaction nor client provide a fee, fallback to transaction default Hbar(1).""" + tx = TransferTransaction() + tx.set_transaction_id(TransactionId.generate(AccountId.from_string("0.0.1234"))) + # Ensure client default is None + mock_client.default_max_transaction_fee = None + + tx.freeze_with(mock_client) + + assert tx.transaction_fee == 100_000_000 # Default fee for TransferTransaction + + +def test_resolved_fee_serialized_into_transaction_body(mock_client): + """The resolved fee must reach the serialized proto transactionFee.""" + tx = TransferTransaction() + + tx.set_default_max_transaction_fee(Hbar(2)) + + body = tx.build_base_scheduled_body() + + assert body.transactionFee == Hbar(2).to_tinybars() + + +def test_max_transaction_fee_survives_to_bytes_round_trip(mock_client): + """An explicitly set max fee must survive a to_bytes -> from_bytes round trip.""" + tx = TransferTransaction() + + tx.set_default_max_transaction_fee(Hbar(2)) + + tx.freeze_with(mock_client) # Serlize to_bytes() + data = tx.to_bytes() + + restored = TransferTransaction.from_bytes(data) # Deserialize from_bytes(). + assert restored._transaction_fee == Hbar(2).to_tinybars() From a5d1bfcac52c42f0f54e685a4e315b1207703f7d Mon Sep 17 00:00:00 2001 From: tech0priyanshu Date: Tue, 25 Aug 2026 11:36:24 +0530 Subject: [PATCH 2/9] add client to set max transaction fee in Transaction class Signed-off-by: tech0priyanshu --- src/hiero_sdk_python/transaction/transaction.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/hiero_sdk_python/transaction/transaction.py b/src/hiero_sdk_python/transaction/transaction.py index 99a3ec254..83486235b 100644 --- a/src/hiero_sdk_python/transaction/transaction.py +++ b/src/hiero_sdk_python/transaction/transaction.py @@ -65,6 +65,7 @@ def __init__(self) -> None: self._default_transaction_fee = Hbar(2) self.operator_account_id = None self.batch_key: Key | None = None + self._client: Client | None = None def _make_request(self): """ @@ -302,6 +303,7 @@ def freeze_with(self, client: Client): return self # Resolve transaction_id and node_accountids to be set when using freeze() + self._client = client self._resolve_transaction_id(client) self._resolve_node_ids(client) @@ -508,8 +510,16 @@ def build_base_transaction_body(self) -> transaction_pb2.TransactionBody: ValueError: If required IDs are not set. """ transaction_body = transaction_pb2.TransactionBody() + transaction_body.transactionID.CopyFrom(transaction_id_proto) + transaction_body.nodeAccountID.CopyFrom(selected_node._to_proto()) - fee = self._transaction_fee if self._transaction_fee is not None else self._default_transaction_fee + if self._transaction_fee is None: + if self._client is not None and self._client.default_max_transaction_fee is not None: + self.transaction_fee = self._client.default_max_transaction_fee + else: + self.transaction_fee = self._default_transaction_fee + + fee = self._transaction_fee if hasattr(fee, "to_tinybars"): transaction_body.transactionFee = int(fee.to_tinybars()) else: From c490cdd15801293aad36a08824f5aa3822087272 Mon Sep 17 00:00:00 2001 From: exploreriii <133720349+exploreriii@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:36:20 +0100 Subject: [PATCH 3/9] fix: remove default transaction fee from client freeze with, instead locate it in resolve transaction fee Signed-off-by: exploreriii <133720349+exploreriii@users.noreply.github.com> Signed-off-by: tech0priyanshu --- .../transaction/transaction.py | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/hiero_sdk_python/transaction/transaction.py b/src/hiero_sdk_python/transaction/transaction.py index 83486235b..c03d25c9c 100644 --- a/src/hiero_sdk_python/transaction/transaction.py +++ b/src/hiero_sdk_python/transaction/transaction.py @@ -65,7 +65,6 @@ def __init__(self) -> None: self._default_transaction_fee = Hbar(2) self.operator_account_id = None self.batch_key: Key | None = None - self._client: Client | None = None def _make_request(self): """ @@ -266,6 +265,17 @@ 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 + + self.transaction_fee = default if default is not None else self._default_transaction_fee + def freeze(self): """ Freezes the transaction by building the transaction body and setting necessary IDs. @@ -303,9 +313,9 @@ def freeze_with(self, client: Client): return self # Resolve transaction_id and node_accountids to be set when using freeze() - self._client = client 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) @@ -513,14 +523,8 @@ def build_base_transaction_body(self) -> transaction_pb2.TransactionBody: transaction_body.transactionID.CopyFrom(transaction_id_proto) transaction_body.nodeAccountID.CopyFrom(selected_node._to_proto()) - if self._transaction_fee is None: - if self._client is not None and self._client.default_max_transaction_fee is not None: - self.transaction_fee = self._client.default_max_transaction_fee - else: - self.transaction_fee = self._default_transaction_fee - - fee = self._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) From 907d9b0c9ef0ca4bd48b05191a03bff457af28a8 Mon Sep 17 00:00:00 2001 From: exploreriii <133720349+exploreriii@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:36:53 +0100 Subject: [PATCH 4/9] fix: fallback to true defaults max transaction fee, which doesn't need to be specified in tests Signed-off-by: exploreriii <133720349+exploreriii@users.noreply.github.com> Signed-off-by: tech0priyanshu --- tests/unit/fee_estimate_query_test.py | 5 ----- tests/unit/file_append_transaction_test.py | 2 -- tests/unit/transaction_freeze_and_bytes_test.py | 12 ++++++++++++ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/unit/fee_estimate_query_test.py b/tests/unit/fee_estimate_query_test.py index 582bcef81..6b6d253af 100644 --- a/tests/unit/fee_estimate_query_test.py +++ b/tests/unit/fee_estimate_query_test.py @@ -32,7 +32,6 @@ def mock_client(): client.mirror_network = "https://testnet.mirrornode.hedera.com" client.max_retries = 3 - client.default_max_transaction_fee = Hbar(2) client.generate_transaction_id.return_value = TransactionId.generate(AccountId(0, 0, 1001)) client.operator_account_id._to_proto.return_value = AccountId(0, 0, 1)._to_proto() @@ -338,7 +337,6 @@ def test_port_replacement_for_localhost_execute_single(): """Test localhost:38081 is replaced with :8084.""" client_1 = MagicMock() client_1.network.get_mirror_rest_url.return_value = "http://localhost:38081/api/v1" - client_1.default_max_transaction_fee = Hbar(1) tx = ( TransferTransaction() @@ -356,7 +354,6 @@ def test_port_replacement_for_localhost_execute_single(): client_2 = MagicMock() client_2.network.get_mirror_rest_url.return_value = "http://127.0.0.1:38081/api/v1" - client_2.default_max_transaction_fee = Hbar(1) with patch.object(query, "_execute_single") as mock_execute_single: query.execute(client_2) @@ -369,7 +366,6 @@ def test_port_replacement_for_localhost_execute_multiple(): """Test localhost:38081 is replaced with :8084 for chunked tx.""" client_1 = MagicMock() client_1.network.get_mirror_rest_url.return_value = "http://localhost:38081/api/v1" - client_1.default_max_transaction_fee = Hbar(1) tx = ( TopicMessageSubmitTransaction() @@ -392,7 +388,6 @@ def test_port_replacement_for_localhost_execute_multiple(): client_2 = MagicMock() client_2.network.get_mirror_rest_url.return_value = "http://127.0.0.1:38081/api/v1" - client_2.default_max_transaction_fee = Hbar(1) with patch.object(query, "_execute_chunked", return_value=MagicMock()) as mock_execute_chunked: query.execute(client_2) diff --git a/tests/unit/file_append_transaction_test.py b/tests/unit/file_append_transaction_test.py index 54d9fe93e..4aa83a186 100644 --- a/tests/unit/file_append_transaction_test.py +++ b/tests/unit/file_append_transaction_test.py @@ -99,7 +99,6 @@ def test_freeze_with_generates_transaction_ids(): # Mock client and transaction_id mock_client = MagicMock() - mock_client.default_max_transaction_fee = Hbar(1) mock_transaction_id = TransactionId(account_id=MagicMock(), valid_start=Timestamp(0, 1)) file_tx.transaction_id = mock_transaction_id @@ -142,7 +141,6 @@ def test_multi_chunk_execution(): # Mock client and responses mock_client = MagicMock() - mock_client.default_max_transaction_fee = Hbar(1) mock_receipt = MagicMock(spec=TransactionReceipt) mock_receipt.status = ResponseCode.SUCCESS diff --git a/tests/unit/transaction_freeze_and_bytes_test.py b/tests/unit/transaction_freeze_and_bytes_test.py index f1aae0bce..55f615474 100644 --- a/tests/unit/transaction_freeze_and_bytes_test.py +++ b/tests/unit/transaction_freeze_and_bytes_test.py @@ -9,6 +9,7 @@ from __future__ import annotations from decimal import Decimal +from unittest.mock import MagicMock import pytest @@ -820,3 +821,14 @@ def test_max_transaction_fee_survives_to_bytes_round_trip(mock_client): restored = TransferTransaction.from_bytes(data) # Deserialize from_bytes(). assert restored._transaction_fee == Hbar(2).to_tinybars() + + +def test_freeze_with_bare_magicmock_client_resolves_default_fee(): + """Freezing against a bare MagicMock client must not raise and must fall back to the per-type default fee.""" + tx = TransferTransaction() + tx.set_transaction_id(TransactionId.generate(AccountId.from_string("0.0.1234"))) + tx.set_node_account_ids([AccountId.from_string("0.0.3")]) + + tx.freeze_with(MagicMock()) + + assert tx.transaction_fee == 100_000_000 # TransferTransaction per-type default, Hbar(1) From 671a0545e9bd53703c6ab1114b90eb8e00421dbe Mon Sep 17 00:00:00 2001 From: exploreriii <133720349+exploreriii@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:42:39 +0100 Subject: [PATCH 5/9] fix: rename set_default_max_transaction_fee to set_max_transaction_fee as it sets max transaction fee for a transaction, not its default Signed-off-by: exploreriii <133720349+exploreriii@users.noreply.github.com> Signed-off-by: tech0priyanshu --- .../transaction/transaction.py | 25 ++++++++----------- .../account_update_transaction_e2e_test.py | 2 +- .../unit/transaction_freeze_and_bytes_test.py | 18 ++++++------- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/src/hiero_sdk_python/transaction/transaction.py b/src/hiero_sdk_python/transaction/transaction.py index c03d25c9c..545c80217 100644 --- a/src/hiero_sdk_python/transaction/transaction.py +++ b/src/hiero_sdk_python/transaction/transaction.py @@ -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 @@ -851,30 +852,24 @@ def from_bytes(transaction_bytes: bytes): transaction_body, signed_transaction.bodyBytes, signed_transaction.sigMap ) - def set_default_max_transaction_fee(self, max_transaction_fee): + def set_max_transaction_fee(self, max_transaction_fee: int | float | Decimal | Hbar) -> Transaction: """ - Sets the maximum transaction fee for this transaction. - - The maximum transaction fee specifies the highest fee that can be - charged when the transaction is executed. The value must be - non-negative. + Set the maximum transaction fee the payer is willing to pay for this transaction. Args: - max_transaction_fee (Hbar | int | str): The maximum transaction - fee. Accepted types are those supported by ``Hbar._coerce_fee``. + max_transaction_fee (int | float | Decimal | Hbar): The maximum fee. + Numeric values are interpreted as Hbar. Returns: - Self: This transaction instance, allowing method chaining. + Transaction: This transaction instance for method chaining. Raises: - ValueError: If ``max_transaction_fee`` is negative. - RuntimeError: If the transaction has been frozen. + 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() - value = Hbar._coerce_fee(max_transaction_fee) - if value < Hbar.ZERO: - raise ValueError("max_transaction_fee must be non-negative") - self.transaction_fee = value + self.transaction_fee = Hbar._coerce_non_negative(max_transaction_fee, "max_transaction_fee") return self @staticmethod diff --git a/tests/integration/account_update_transaction_e2e_test.py b/tests/integration/account_update_transaction_e2e_test.py index ff2b046a2..3e7d32d1e 100644 --- a/tests/integration/account_update_transaction_e2e_test.py +++ b/tests/integration/account_update_transaction_e2e_test.py @@ -242,7 +242,7 @@ def test_integration_account_update_transaction_invalid_auto_renew_period(env): def _apply_tiny_max_fee_if_supported(tx, client) -> bool: # Try tx-level setters - for attr in ("set_default_max_transaction_fee", "set_max_fee", "set_transaction_fee"): + for attr in ("set_max_transaction_fee", "set_max_fee", "set_transaction_fee"): if hasattr(tx, attr): getattr(tx, attr)(Hbar.from_tinybars(1)) return True diff --git a/tests/unit/transaction_freeze_and_bytes_test.py b/tests/unit/transaction_freeze_and_bytes_test.py index 55f615474..c1b2589dd 100644 --- a/tests/unit/transaction_freeze_and_bytes_test.py +++ b/tests/unit/transaction_freeze_and_bytes_test.py @@ -82,30 +82,30 @@ def test_freeze_with_valid_parameters(): ], ) def test_set_max_transaction_fee_valid_param(valid_amount, expected): - """Transaction.set_default_max_transaction_fee should accept various numeric types and Hbar.""" + """Transaction.set_max_transaction_fee should accept various numeric types and Hbar.""" tx = TransferTransaction() - returned = tx.set_default_max_transaction_fee(valid_amount) + returned = tx.set_max_transaction_fee(valid_amount) assert tx.transaction_fee == expected assert returned is tx @pytest.mark.parametrize("invalid_amount", ["1", True, False, None, object()]) def test_set_max_transaction_fee_invalid_param(invalid_amount): - """Transaction.set_default_max_transaction_fee should reject invalid types.""" + """Transaction.set_max_transaction_fee should reject invalid types.""" tx = TransferTransaction() with pytest.raises(TypeError): - tx.set_default_max_transaction_fee(invalid_amount) + tx.set_max_transaction_fee(invalid_amount) @pytest.mark.parametrize("negative_amount", [-1, -0.1, Decimal("-0.1"), Hbar(-1)]) def test_set_max_transaction_fee_negative_value(negative_amount): - """Transaction.set_default_max_transaction_fee should reject negative values.""" + """Transaction.set_max_transaction_fee should reject negative values.""" tx = TransferTransaction() with pytest.raises(ValueError): - tx.set_default_max_transaction_fee(negative_amount) + tx.set_max_transaction_fee(negative_amount) # checking state un modified assert len(tx._transaction_body_bytes) == 0 @@ -763,7 +763,7 @@ def test_map_response_raises_if_proto_request_is_not_transaction(): def test_fee_resolution_transaction_precedence(mock_client): """Transaction fee explicitly set should take precedence over client default.""" tx = TransferTransaction() - tx.set_default_max_transaction_fee(Hbar(10)) + tx.set_max_transaction_fee(Hbar(10)) # client has different default mock_client.set_default_max_transaction_fee(Hbar(5)) @@ -803,7 +803,7 @@ def test_resolved_fee_serialized_into_transaction_body(mock_client): """The resolved fee must reach the serialized proto transactionFee.""" tx = TransferTransaction() - tx.set_default_max_transaction_fee(Hbar(2)) + tx.set_max_transaction_fee(Hbar(2)) body = tx.build_base_scheduled_body() @@ -814,7 +814,7 @@ def test_max_transaction_fee_survives_to_bytes_round_trip(mock_client): """An explicitly set max fee must survive a to_bytes -> from_bytes round trip.""" tx = TransferTransaction() - tx.set_default_max_transaction_fee(Hbar(2)) + tx.set_max_transaction_fee(Hbar(2)) tx.freeze_with(mock_client) # Serlize to_bytes() data = tx.to_bytes() From e2469636cba7a6bed2502fb06993a3aa2d789dae Mon Sep 17 00:00:00 2001 From: exploreriii <133720349+exploreriii@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:43:33 +0100 Subject: [PATCH 6/9] refactor: use _coerce_non_negative instead of duplicating in client Signed-off-by: exploreriii <133720349+exploreriii@users.noreply.github.com> Signed-off-by: tech0priyanshu --- src/hiero_sdk_python/client/client.py | 17 +++-------------- src/hiero_sdk_python/hbar.py | 20 ++++++++++++++------ 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/hiero_sdk_python/client/client.py b/src/hiero_sdk_python/client/client.py index 3a9329223..29e9f3895 100644 --- a/src/hiero_sdk_python/client/client.py +++ b/src/hiero_sdk_python/client/client.py @@ -278,16 +278,7 @@ def set_default_max_query_payment(self, max_query_payment: int | float | Decimal Returns: Client: The current client instance for method chaining. """ - if isinstance(max_query_payment, bool) or not isinstance(max_query_payment, (int, float, Decimal, Hbar)): - raise TypeError( - f"max_query_payment must be int, float, Decimal, or Hbar, got {type(max_query_payment).__name__}" - ) - - value = max_query_payment if isinstance(max_query_payment, Hbar) else Hbar(max_query_payment) - - if value < Hbar(0): - raise ValueError("max_query_payment must be non-negative") - + value = Hbar._coerce_non_negative(max_query_payment, "max_query_payment") self.default_max_query_payment = value return self @@ -296,11 +287,9 @@ def set_default_max_transaction_fee( max_transaction_fee: int | float | Decimal | Hbar, ) -> Client: """ - Sets the default maximum Hbar fee allowed for any transaction executed by this client. + Sets the default maximum Hbar fee allowed for any transaction executed by this client. """ - value = Hbar._coerce_fee(max_transaction_fee) - if value < Hbar.ZERO: - raise ValueError("max_transaction_fee must be non-negative") + value = Hbar._coerce_non_negative(max_transaction_fee, "max_transaction_fee") self.default_max_transaction_fee = value return self diff --git a/src/hiero_sdk_python/hbar.py b/src/hiero_sdk_python/hbar.py index cc58b5d2a..18edf3d20 100644 --- a/src/hiero_sdk_python/hbar.py +++ b/src/hiero_sdk_python/hbar.py @@ -84,19 +84,27 @@ def negated(self) -> Hbar: return Hbar.from_tinybars(-self._amount_in_tinybar) @staticmethod - def _coerce_fee(value: int | float | Decimal | Hbar) -> Hbar: + def _coerce_non_negative(value: int | float | Decimal | Hbar, name: str = "value") -> Hbar: """ - Coerce a value to an Hbar instance. Accepts int, float, Decimal, or Hbar (but not bool). + Coerce a value to a non-negative Hbar. Accepts int, float, Decimal, or Hbar (but not bool). + Args: value: The value to coerce. + name: The field name used in error messages. Defaults to "value". + Returns: - Hbar: An Hbar instance. + Hbar: A non-negative Hbar instance. + Raises: - TypeError: If value is not a valid type. + TypeError: If value is not int, float, Decimal, or Hbar. + ValueError: If value is negative. """ if isinstance(value, bool) or not isinstance(value, (int, float, Decimal, Hbar)): - raise TypeError(f"value must be int, float, Decimal, or Hbar, got {type(value).__name__}") - return value if isinstance(value, Hbar) else Hbar(value) + raise TypeError(f"{name} must be int, float, Decimal, or Hbar, got {type(value).__name__}") + result = value if isinstance(value, Hbar) else Hbar(value) + if result < Hbar.ZERO: + raise ValueError(f"{name} must be non-negative") + return result @classmethod def of(cls, amount: int | float | Decimal, unit: HbarUnit) -> Hbar: From 4b44a3eed8d062d75d4583eb635be27b6214a36c Mon Sep 17 00:00:00 2001 From: exploreriii <133720349+exploreriii@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:47:35 +0100 Subject: [PATCH 7/9] fix: honor explicit zero fee in scheduled bodies Signed-off-by: exploreriii <133720349+exploreriii@users.noreply.github.com> Signed-off-by: tech0priyanshu --- src/hiero_sdk_python/transaction/transaction.py | 4 ++-- tests/unit/transaction_freeze_and_bytes_test.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/hiero_sdk_python/transaction/transaction.py b/src/hiero_sdk_python/transaction/transaction.py index 545c80217..2da12b56a 100644 --- a/src/hiero_sdk_python/transaction/transaction.py +++ b/src/hiero_sdk_python/transaction/transaction.py @@ -552,8 +552,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) diff --git a/tests/unit/transaction_freeze_and_bytes_test.py b/tests/unit/transaction_freeze_and_bytes_test.py index c1b2589dd..41f7a3970 100644 --- a/tests/unit/transaction_freeze_and_bytes_test.py +++ b/tests/unit/transaction_freeze_and_bytes_test.py @@ -15,6 +15,7 @@ from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.hapi.services import transaction_pb2 from hiero_sdk_python.hapi.services.transaction_response_pb2 import ( TransactionResponse as TransactionResponseProto, ) @@ -823,6 +824,20 @@ def test_max_transaction_fee_survives_to_bytes_round_trip(mock_client): assert restored._transaction_fee == Hbar(2).to_tinybars() +def test_explicit_zero_fee_is_honored_on_direct_and_scheduled_paths(mock_client): + """An explicit zero fee must reach both the direct and the scheduled body unchanged.""" + tx = TransferTransaction() + tx.set_max_transaction_fee(Hbar(0)) + + scheduled_body = tx.build_base_scheduled_body() + assert scheduled_body.transactionFee == 0 + + tx.freeze_with(mock_client) + body = transaction_pb2.TransactionBody() + body.ParseFromString(next(iter(tx._transaction_body_bytes.values()))) + assert body.transactionFee == 0 + + def test_freeze_with_bare_magicmock_client_resolves_default_fee(): """Freezing against a bare MagicMock client must not raise and must fall back to the per-type default fee.""" tx = TransferTransaction() From e0122c270dc4733b228c648c2d7e3d166aff0751 Mon Sep 17 00:00:00 2001 From: exploreriii <133720349+exploreriii@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:49:53 +0100 Subject: [PATCH 8/9] fix: duplicate set_default_max_transaction_fee in e2e test and improve test surface area to check default is applied if not set Signed-off-by: exploreriii <133720349+exploreriii@users.noreply.github.com> Signed-off-by: tech0priyanshu --- .../account_update_transaction_e2e_test.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/integration/account_update_transaction_e2e_test.py b/tests/integration/account_update_transaction_e2e_test.py index 3e7d32d1e..80dc4d536 100644 --- a/tests/integration/account_update_transaction_e2e_test.py +++ b/tests/integration/account_update_transaction_e2e_test.py @@ -248,7 +248,6 @@ def _apply_tiny_max_fee_if_supported(tx, client) -> bool: return True # Try client-level default for attr in ( - "set_default_max_transaction_fee", "set_default_max_transaction_fee", "set_default_max_fee", "setMaxTransactionFee", @@ -295,6 +294,29 @@ def test_account_update_insufficient_fee_with_valid_expiration_bump(env): 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.""" From afe0bc0aab7fe9731ac04d26ea26763fdda81a38 Mon Sep 17 00:00:00 2001 From: tech0priyanshu Date: Tue, 29 Sep 2026 23:26:49 +0530 Subject: [PATCH 9/9] fix: update transaction fee handling and improve error messages for invalid types Signed-off-by: tech0priyanshu --- .../transaction/transaction.py | 35 ++++++------------- .../unit/transaction_freeze_and_bytes_test.py | 12 +++++-- tests/unit/transaction_test.py | 6 ++-- 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/src/hiero_sdk_python/transaction/transaction.py b/src/hiero_sdk_python/transaction/transaction.py index 2da12b56a..baddc7d25 100644 --- a/src/hiero_sdk_python/transaction/transaction.py +++ b/src/hiero_sdk_python/transaction/transaction.py @@ -275,7 +275,10 @@ def _resolve_transaction_fee(self, client: Client | None) -> None: if not isinstance(default, Hbar): default = None - self.transaction_fee = default if default is not None else self._default_transaction_fee + if default is not None: + self.transaction_fee = default + else: + self._transaction_fee = self._default_transaction_fee def freeze(self): """ @@ -521,8 +524,6 @@ def build_base_transaction_body(self) -> transaction_pb2.TransactionBody: ValueError: If required IDs are not set. """ transaction_body = transaction_pb2.TransactionBody() - transaction_body.transactionID.CopyFrom(transaction_id_proto) - transaction_body.nodeAccountID.CopyFrom(selected_node._to_proto()) fee = self._transaction_fee if self._transaction_fee is not None else self._default_transaction_fee if isinstance(fee, Hbar): @@ -696,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) def to_bytes(self) -> bytes: """ @@ -861,15 +850,14 @@ def set_max_transaction_fee(self, max_transaction_fee: int | float | Decimal | H Numeric values are interpreted as Hbar. Returns: - Transaction: This transaction instance for method chaining. - + 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. + 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") + self._transaction_fee = Hbar._coerce_non_negative(max_transaction_fee, "max_transaction_fee").to_tinybars() return self @staticmethod @@ -977,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 @@ -1002,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): diff --git a/tests/unit/transaction_freeze_and_bytes_test.py b/tests/unit/transaction_freeze_and_bytes_test.py index 41f7a3970..ebf10b6d0 100644 --- a/tests/unit/transaction_freeze_and_bytes_test.py +++ b/tests/unit/transaction_freeze_and_bytes_test.py @@ -414,7 +414,7 @@ def test_from_bytes_preserves_all_common_fields(): transaction.transaction_id = TransactionId.generate(operator_id) transaction.set_node_account_ids([node_id]) - transaction.transaction_fee = 5_000_000 # Custom fee + transaction._transaction_fee = 5_000_000 # Custom fee assert transaction.set_transaction_valid_duration(180) is transaction # 3 minutes - using setter transaction.generate_record = True @@ -825,7 +825,7 @@ def test_max_transaction_fee_survives_to_bytes_round_trip(mock_client): def test_explicit_zero_fee_is_honored_on_direct_and_scheduled_paths(mock_client): - """An explicit zero fee must reach both the direct and the scheduled body unchanged.""" + """An explicit zero fee must reach both the direct and scheduled body unchanged.""" tx = TransferTransaction() tx.set_max_transaction_fee(Hbar(0)) @@ -833,8 +833,14 @@ def test_explicit_zero_fee_is_honored_on_direct_and_scheduled_paths(mock_client) assert scheduled_body.transactionFee == 0 tx.freeze_with(mock_client) + + transaction_id = tx._transaction_ids.current + node_account_id = tx._node_account_ids.current + body_bytes = tx._transaction_body_bytes[transaction_id][node_account_id] + body = transaction_pb2.TransactionBody() - body.ParseFromString(next(iter(tx._transaction_body_bytes.values()))) + body.ParseFromString(body_bytes) + assert body.transactionFee == 0 diff --git a/tests/unit/transaction_test.py b/tests/unit/transaction_test.py index 59205563b..595a8946b 100644 --- a/tests/unit/transaction_test.py +++ b/tests/unit/transaction_test.py @@ -564,7 +564,7 @@ def test_transaction_fee_rejects_bool(): """Test transaction_fee rejects boolean values.""" tx = AccountCreateTransaction() - with pytest.raises(TypeError, match="fee must be of type Hbar or int"): + with pytest.raises(TypeError, match="transaction_fee must be int, float, Decimal, or Hbar, got bool"): tx.transaction_fee = True @@ -572,7 +572,7 @@ def test_transaction_fee_rejects_invalid_type(): """Test transaction_fee rejects invalid types.""" tx = AccountCreateTransaction() - with pytest.raises(TypeError, match="fee must be of type Hbar or int"): + with pytest.raises(TypeError, match="transaction_fee must be int, float, Decimal, or Hbar, got str"): tx.transaction_fee = "100" @@ -580,7 +580,7 @@ def test_transaction_fee_rejects_negative_int(): """Test transaction_fee rejects negative integer values.""" tx = AccountCreateTransaction() - with pytest.raises(ValueError, match="fee must be greater than or equal to 0"): + with pytest.raises(ValueError, match="transaction_fee must be non-negative"): tx.transaction_fee = -1