Skip to content

Commit 7ccf5e3

Browse files
authored
chore: enhance exception type safety and context (hiero-ledger#1594)
Signed-off-by: Adityarya11 <arya050411@gmail.com>
1 parent b5744a3 commit 7ccf5e3

8 files changed

Lines changed: 312 additions & 69 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
146146
- Added MirrorNode based population for `ContractId`, including parsing and serialization support.
147147
- Added `/working` command to reset the inactivity timer on issues and PRs. ([#1552](https://github.com/hiero-ledger/hiero-sdk-python/issues/1552))
148148
- Added `grpc_deadline` support for transaction and query execution.
149+
- Type hints to exception classes (`PrecheckError`, `MaxAttemptsError`, `ReceiptStatusError`) constructors and string methods.
149150

150151
### Documentation
151152
- Added comprehensive docstring to `compress_with_cryptography` function (#1626)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Example demonstrating how to handle MaxAttemptsError in the Hiero SDK.
4+
5+
run:
6+
uv run examples/errors/max_attempts_error.py
7+
python examples/errors/max_attempts_error.py
8+
"""
9+
10+
from hiero_sdk_python import (
11+
Client,
12+
TransactionGetReceiptQuery,
13+
TransactionId,
14+
)
15+
from hiero_sdk_python.exceptions import MaxAttemptsError
16+
17+
def main() -> None:
18+
# Initialize the client
19+
client = Client.from_env()
20+
operator_id = client.operator_account_id
21+
22+
# Configure client to fail quickly
23+
# This sets the maximum number of attempts for any request to 1
24+
client.set_max_attempts(1)
25+
26+
print("Attempting to fetch receipt with restricted max attempts...")
27+
28+
# We generate a random transaction ID that definitely doesn't exist.
29+
# The network would normally return RECEIPT_NOT_FOUND, but depending on the
30+
# node's state or if we simulate a network blip, the SDK's retry mechanism kicks in.
31+
# By forcing max_attempts=1, we prevent retries.
32+
# Note: Triggering a pure MaxAttemptsError usually requires a timeout or busy node.
33+
# This example demonstrates the structure of handling the error.
34+
35+
36+
# Using a generated TransactionId
37+
tx_id = TransactionId.generate(operator_id)
38+
39+
try:
40+
TransactionGetReceiptQuery().set_transaction_id(tx_id).execute(client)
41+
print("Query finished (unexpected for this example test).")
42+
43+
except MaxAttemptsError as e:
44+
print("\nCaught MaxAttemptsError!")
45+
print(f"Node ID: {e.node_id}")
46+
print(f"Message: {e.message}")
47+
print("This error means the SDK gave up after reaching the maximum number of retry attempts.")
48+
49+
except Exception as e:
50+
# Note: In a real network test with a made-up ID, we might get ReceiptStatusError
51+
# or PrecheckError (RECEIPT_NOT_FOUND). MaxAttemptsError typically happens
52+
# on network timeouts or BUSY responses.
53+
print(f"\nCaught unexpected error (expected for this specific simulation): {type(e).__name__}")
54+
print(f"Details: {e}")
55+
print("\n(To verify MaxAttemptsError logic, this example relies on the client's retry configuration)")
56+
57+
58+
if __name__ == "__main__":
59+
main()

examples/errors/precheck_error.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Example demonstrating how to handle PrecheckError in the Hiero SDK.
4+
5+
run:
6+
uv run examples/errors/precheck_error.py
7+
python examples/errors/precheck_error.py
8+
"""
9+
10+
from hiero_sdk_python.exceptions import PrecheckError
11+
from hiero_sdk_python import (
12+
Client,
13+
TransferTransaction,
14+
AccountId
15+
)
16+
17+
def main() -> None:
18+
# Initialize the client
19+
client = Client.from_env()
20+
operator_id = client.operator_account_id
21+
operator_key = client.operator_private_key
22+
23+
print("Creating transaction with invalid parameters to force PrecheckError...")
24+
25+
# Create a simple transfer transaction
26+
# To trigger a PrecheckError, we set the transaction valid duration to 0.
27+
# The node's precheck validation requires a valid duration, so this will fail immediately.
28+
transaction = (
29+
TransferTransaction()
30+
.add_hbar_transfer(operator_id, -1)
31+
.add_hbar_transfer(AccountId(0, 0, 3), 1)
32+
.set_transaction_valid_duration(1)
33+
.freeze_with(client)
34+
.sign(operator_key)
35+
)
36+
37+
try:
38+
print("Executing transaction...")
39+
transaction.execute(client)
40+
print("Transaction unexpectedly succeeded (this should not happen).")
41+
42+
except PrecheckError as e:
43+
print("\nCaught PrecheckError!")
44+
print(f"Status: {e.status.name} ({e.status})")
45+
print(f"Transaction ID: {e.transaction_id}")
46+
print("This error means the transaction failed validation at the node *before* reaching consensus.")
47+
48+
except Exception as e:
49+
print(f"\nAn unexpected error occurred: {type(e).__name__}: {e}")
50+
51+
if __name__ == "__main__":
52+
main()

examples/errors/receipt_status_error.py

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,19 @@
66
uv run examples/errors/receipt_status_error.py
77
python examples/errors/receipt_status_error.py
88
"""
9-
import os
10-
import dotenv
11-
12-
from hiero_sdk_python.client.client import Client
13-
from hiero_sdk_python.account.account_id import AccountId
14-
from hiero_sdk_python.crypto.private_key import PrivateKey
15-
from hiero_sdk_python.response_code import ResponseCode
16-
from hiero_sdk_python.tokens.token_associate_transaction import (
9+
from hiero_sdk_python import (
10+
Client,
11+
ResponseCode,
1712
TokenAssociateTransaction,
13+
TokenId
1814
)
19-
from hiero_sdk_python.tokens.token_id import TokenId
2015
from hiero_sdk_python.exceptions import ReceiptStatusError
2116

22-
dotenv.load_dotenv()
23-
24-
25-
def main():
26-
# Initialize the client
27-
# For this example, we assume we are running against a local node or testnet
28-
# You would typically load these from environment variables
29-
operator_id_str = os.environ.get("OPERATOR_ID", "")
30-
operator_key_str = os.environ.get("OPERATOR_KEY", "")
31-
32-
try:
33-
operator_id = AccountId.from_string(operator_id_str)
34-
operator_key = PrivateKey.from_string(operator_key_str)
35-
except Exception as e:
36-
print(f"Error parsing operator credentials: {e}")
37-
return
38-
39-
client = Client()
40-
client.set_operator(operator_id, operator_key)
41-
42-
# Create a transaction that is likely to fail post-consensus
43-
# Here we try to associate a non-existent token
17+
def main() -> None:
18+
client = Client.from_env()
19+
20+
operator_id = client.operator_account_id
21+
operator_key = client.operator_private_key
4422

4523
print("Creating transaction...")
4624
transaction = (
@@ -53,22 +31,21 @@ def main():
5331

5432
try:
5533
print("Executing transaction...")
56-
# execute() submits the transaction to the network and returns a receipt
57-
# Note: this does NOT automatically raise an exception if the transaction fails post-consensus.
5834
receipt = transaction.execute(client)
5935
print(f"Transaction submitted. ID: {receipt.transaction_id}")
6036

6137
# Check if the execution raised something other than SUCCESS
62-
# If not, we raise our custom ReceiptStatusError for handling.
38+
if receipt.status is None:
39+
raise ValueError("Receipt missing status")
6340
if receipt.status != ResponseCode.SUCCESS:
6441
raise ReceiptStatusError(receipt.status, receipt.transaction_id, receipt)
65-
# If we reach here, the transaction succeeded
42+
6643
print("Transaction successful!")
6744

6845
# This exception is raised when the transaction raised something other than SUCCESS
6946
except ReceiptStatusError as e:
7047
print("\nCaught ReceiptStatusError!")
71-
print(f"Status: {e.status} ({ResponseCode(e.status).name})")
48+
print(f"Status: {e.status.name} ({e.status})")
7249
print(f"Transaction ID: {e.transaction_id}")
7350
print(
7451
"This error means the transaction reached consensus but failed logic execution."
@@ -80,4 +57,4 @@ def main():
8057

8158

8259
if __name__ == "__main__":
83-
main()
60+
main()

src/hiero_sdk_python/exceptions.py

Lines changed: 56 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1+
from __future__ import annotations
2+
from typing import TYPE_CHECKING, Optional, Union
3+
14
from hiero_sdk_python.response_code import ResponseCode
25

6+
if TYPE_CHECKING:
7+
from hiero_sdk_python import (
8+
TransactionId,
9+
TransactionReceipt
10+
)
11+
312
class PrecheckError(Exception):
413
"""
514
Exception thrown when a transaction fails its precheck validation.
@@ -11,81 +20,97 @@ class PrecheckError(Exception):
1120
transaction_id (TransactionId): The ID of the transaction that failed.
1221
message (str): The message of the error. If not provided, a default message is generated.
1322
"""
14-
def __init__(self, status, transaction_id=None, message=None):
15-
self.status = status
23+
24+
def __init__(
25+
self,
26+
status: Union[ResponseCode, int],
27+
transaction_id: Optional[TransactionId] = None,
28+
message: Optional[str] = None,
29+
) -> None:
30+
self.status = ResponseCode(status)
1631
self.transaction_id = transaction_id
17-
32+
1833
# Build a default message if none provided
1934
if message is None:
20-
status_name = ResponseCode(status).name
21-
message = f"Transaction failed precheck with status: {status_name} ({status})"
35+
status_name = self.status.name
36+
message = f"Transaction failed precheck with status: {status_name} ({int(self.status)})"
2237
if transaction_id:
2338
message += f", transaction ID: {transaction_id}"
24-
39+
2540
self.message = message
2641
super().__init__(self.message)
27-
28-
def __str__(self):
42+
43+
def __str__(self) -> str:
2944
return self.message
30-
31-
def __repr__(self):
45+
46+
def __repr__(self) -> str:
3247
return f"PrecheckError(status={self.status}, transaction_id={self.transaction_id})"
33-
34-
48+
49+
3550
class MaxAttemptsError(Exception):
3651
"""
3752
Exception raised when the maximum number of attempts for a request has been reached.
38-
53+
3954
Attributes:
4055
message (str): The error message explaining why the maximum attempts were reached
4156
node_id (str): The ID of the node that was being contacted when the max attempts were reached
42-
last_error (Exception): The last error that occurred during the final attempt
57+
last_error (BaseException): The last error that occurred during the final attempt
4358
"""
44-
45-
def __init__(self, message, node_id, last_error=None):
59+
60+
def __init__(self, message: str, node_id: str, last_error: Optional[BaseException] = None) -> None:
4661
self.node_id = node_id
4762
self.last_error = last_error
48-
63+
4964
# Build a comprehensive error message
5065
error_message = message
5166
if last_error is not None:
5267
error_message += f"; last error: {str(last_error)}"
53-
68+
5469
self.message = error_message
5570
super().__init__(self.message)
56-
57-
def __str__(self):
71+
72+
def __str__(self) -> str:
5873
return self.message
59-
60-
def __repr__(self):
74+
75+
def __repr__(self) -> str:
6176
return f"MaxAttemptsError(message='{self.message}', node_id='{self.node_id}')"
62-
77+
78+
6379
class ReceiptStatusError(Exception):
6480
"""
6581
Exception raised when a transaction receipt contains an error status.
6682
6783
Attributes:
6884
status (ResponseCode): The error status code from the receipt
69-
transaction_id (TransactionId): The ID of the transaction that failed
85+
transaction_id (TransactionId): The ID of the transaction that failed (Optional)
7086
transaction_receipt (TransactionReceipt): The receipt containing the error status
7187
message (str): The error message describing the failure
7288
"""
7389

74-
def __init__(self, status, transaction_id, transaction_receipt, message=None):
75-
self.status = status
90+
def __init__(
91+
self,
92+
status: Union[ResponseCode, int],
93+
transaction_id: Optional[TransactionId],
94+
transaction_receipt: TransactionReceipt,
95+
message: Optional[str] = None
96+
) -> None:
97+
self.status = ResponseCode(status)
7698
self.transaction_id = transaction_id
7799
self.transaction_receipt = transaction_receipt
78100

79101
# Build a default message if none provided
80102
if message is None:
81-
status_name = ResponseCode(status).name
82-
message = f"Receipt for transaction {transaction_id} contained error status: {status_name} ({status})"
103+
status_name = self.status.name
104+
if transaction_id:
105+
message = f"Receipt for transaction {transaction_id} contained error status: {status_name} ({int(self.status)})"
106+
else:
107+
message = f"Receipt contained error status: {status_name} ({int(self.status)})"
83108

84109
self.message = message
85110
super().__init__(self.message)
86111

87-
def __str__(self):
112+
def __str__(self) -> str:
88113
return self.message
89114

90-
def __repr__(self):
91-
return f"ReceiptStatusError(status={self.status}, transaction_id={self.transaction_id})"
115+
def __repr__(self) -> str:
116+
return f"ReceiptStatusError(status={self.status}, transaction_id={self.transaction_id})"

src/hiero_sdk_python/transaction/transaction.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,22 @@ def set_transaction_memo(self, memo):
551551
self.memo = memo
552552
return self
553553

554+
def set_transaction_valid_duration(self, duration: int) -> "Transaction":
555+
"""
556+
Sets the valid duration for the transaction.
557+
558+
Args:
559+
duration (int): The duration in seconds.
560+
561+
Returns:
562+
Transaction: The current transaction instance for method chaining.
563+
"""
564+
self._require_not_frozen()
565+
if duration <= 0:
566+
raise ValueError("duration must be greater than 0")
567+
self.transaction_valid_duration = duration
568+
return self
569+
554570
def set_transaction_id(self, transaction_id: TransactionId):
555571
"""
556572
Sets the transaction ID for the transaction.

0 commit comments

Comments
 (0)