-
Notifications
You must be signed in to change notification settings - Fork 287
feat: Improve to_bytes/from_bytes serialization and deserialization #2390
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
b23daf8
dd7bc92
d19e0e0
2749a85
16f2cd5
4db42f6
06cf3bb
ebba2b9
f120663
54354a5
b94b5e6
2b91e3b
9b40bc6
54cc82e
26c8117
0e802e1
9dd5458
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 | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,7 +4,6 @@ | |||||||||||||||||
|
|
||||||||||||||||||
| from hiero_sdk_python.channels import _Channel | ||||||||||||||||||
| from hiero_sdk_python.consensus.topic_id import TopicId | ||||||||||||||||||
| from hiero_sdk_python.crypto.private_key import PrivateKey | ||||||||||||||||||
| from hiero_sdk_python.executable import _Method | ||||||||||||||||||
| from hiero_sdk_python.hapi.services import consensus_submit_message_pb2, transaction_pb2 | ||||||||||||||||||
| from hiero_sdk_python.hapi.services.schedulable_transaction_body_pb2 import ( | ||||||||||||||||||
|
|
@@ -107,33 +106,6 @@ def set_message(self, message: bytes | str) -> TopicMessageSubmitTransaction: | |||||||||||||||||
| self._total_chunks = self.get_required_chunks() | ||||||||||||||||||
| return self | ||||||||||||||||||
|
|
||||||||||||||||||
| def set_chunk_size(self, chunk_size: int) -> TopicMessageSubmitTransaction: | ||||||||||||||||||
| """ | ||||||||||||||||||
| Set maximum chunk size in bytes. | ||||||||||||||||||
|
|
||||||||||||||||||
| Args: | ||||||||||||||||||
| chunk_size (int): The size of each chunk in bytes. | ||||||||||||||||||
|
|
||||||||||||||||||
| Returns: | ||||||||||||||||||
| TopicMessageSubmitTransaction: This transaction instance (for chaining). | ||||||||||||||||||
| """ | ||||||||||||||||||
| super().set_chunk_size(chunk_size) | ||||||||||||||||||
| self._total_chunks = self.get_required_chunks() | ||||||||||||||||||
| return self | ||||||||||||||||||
|
|
||||||||||||||||||
| def set_max_chunks(self, max_chunks: int) -> TopicMessageSubmitTransaction: | ||||||||||||||||||
| """ | ||||||||||||||||||
| Set maximum allowed chunks. | ||||||||||||||||||
|
|
||||||||||||||||||
| Args: | ||||||||||||||||||
| max_chunks (int): The maximum number of chunks allowed. | ||||||||||||||||||
|
|
||||||||||||||||||
| Returns: | ||||||||||||||||||
| TopicMessageSubmitTransaction: This transaction instance (for chaining). | ||||||||||||||||||
| """ | ||||||||||||||||||
| super().set_max_chunks(max_chunks) | ||||||||||||||||||
| return self | ||||||||||||||||||
|
|
||||||||||||||||||
| def set_custom_fee_limits(self, custom_fee_limits: list[CustomFeeLimit]) -> TopicMessageSubmitTransaction: | ||||||||||||||||||
| """ | ||||||||||||||||||
| Sets the maximum custom fees that the user is willing to pay for the message. | ||||||||||||||||||
|
|
@@ -177,25 +149,28 @@ def _build_proto_body(self) -> consensus_submit_message_pb2.ConsensusSubmitMessa | |||||||||||||||||
|
|
||||||||||||||||||
| content = self._message_as_bytes() | ||||||||||||||||||
|
|
||||||||||||||||||
| start_index = self._current_chunk_index * self.chunk_size | ||||||||||||||||||
| end_index = min(start_index + self.chunk_size, len(content)) | ||||||||||||||||||
| chunk_content = content[start_index:end_index] | ||||||||||||||||||
| if self._current_chunk_index is not None: | ||||||||||||||||||
| chunk_info = consensus_submit_message_pb2.ConsensusMessageChunkInfo( | ||||||||||||||||||
| initialTransactionID=self._initial_transaction_id._to_proto(), | ||||||||||||||||||
| total=self._total_chunks, | ||||||||||||||||||
| number=self._current_chunk_index + 1, | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| body = consensus_submit_message_pb2.ConsensusSubmitMessageTransactionBody( | ||||||||||||||||||
| topicID=self.topic_id._to_proto() if self.topic_id else None, message=chunk_content | ||||||||||||||||||
| ) | ||||||||||||||||||
| start_index = (self._current_chunk_index) * self.chunk_size | ||||||||||||||||||
| end_index = min(start_index + self.chunk_size, len(content)) | ||||||||||||||||||
|
|
||||||||||||||||||
| # Multi-chunk metadata | ||||||||||||||||||
| if self._total_chunks > 1: | ||||||||||||||||||
| body.chunkInfo.CopyFrom( | ||||||||||||||||||
| consensus_submit_message_pb2.ConsensusMessageChunkInfo( | ||||||||||||||||||
| initialTransactionID=self._initial_transaction_id._to_proto(), | ||||||||||||||||||
| total=self._total_chunks, | ||||||||||||||||||
| number=self._current_chunk_index + 1, | ||||||||||||||||||
| ) | ||||||||||||||||||
| chunk_content = content[start_index:end_index] | ||||||||||||||||||
|
|
||||||||||||||||||
| return consensus_submit_message_pb2.ConsensusSubmitMessageTransactionBody( | ||||||||||||||||||
| topicID=self.topic_id._to_proto() if self.topic_id else None, | ||||||||||||||||||
| message=chunk_content, | ||||||||||||||||||
| chunkInfo=chunk_info, | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| return body | ||||||||||||||||||
| return consensus_submit_message_pb2.ConsensusSubmitMessageTransactionBody( | ||||||||||||||||||
| topicID=self.topic_id._to_proto() if self.topic_id else None, | ||||||||||||||||||
| message=content, | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| def build_transaction_body(self) -> transaction_pb2.TransactionBody: | ||||||||||||||||||
| """ | ||||||||||||||||||
|
|
@@ -233,15 +208,16 @@ def _get_method(self, channel: _Channel) -> _Method: | |||||||||||||||||
| """ | ||||||||||||||||||
| return _Method(transaction_func=channel.topic.submitMessage, query_func=None) | ||||||||||||||||||
|
|
||||||||||||||||||
| def sign(self, private_key: PrivateKey) -> TopicMessageSubmitTransaction: | ||||||||||||||||||
| """ | ||||||||||||||||||
| Signs the transaction using the provided private key. | ||||||||||||||||||
| @classmethod | ||||||||||||||||||
| def _from_protobuf(cls, transaction_body): | ||||||||||||||||||
| transaction = super()._from_protobuf(transaction_body) | ||||||||||||||||||
|
|
||||||||||||||||||
| Args: | ||||||||||||||||||
| private_key (PrivateKey): The private key to sign the transaction with. | ||||||||||||||||||
| if transaction_body.HasField("consensusSubmitMessage"): | ||||||||||||||||||
| body = transaction_body.consensusSubmitMessage | ||||||||||||||||||
|
|
||||||||||||||||||
| Returns: | ||||||||||||||||||
| TopicMessageSubmitTransaction: This transaction instance (for chaining). | ||||||||||||||||||
| """ | ||||||||||||||||||
| super().sign(private_key) | ||||||||||||||||||
| return self | ||||||||||||||||||
| if body.HasField("topicID"): | ||||||||||||||||||
| transaction.topic_id = TopicId._from_proto(body.topicID) | ||||||||||||||||||
| transaction.message = body.message.decode("utf-8") if body.message else 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. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🛡️ Proposed change- transaction.message = body.message.decode("utf-8") if body.message else None
+ if not body.message:
+ transaction.message = None
+ else:
+ try:
+ transaction.message = body.message.decode("utf-8")
+ except UnicodeDecodeError:
+ transaction.message = body.message📝 Committable suggestion
Suggested change
Source: Path instructions |
||||||||||||||||||
| transaction._total_chunks = transaction.get_required_chunks() | ||||||||||||||||||
|
|
||||||||||||||||||
| return transaction | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -86,12 +86,28 @@ def __init__(self): | |||||||||||||||||||||||
| self._grpc_deadline: float | None = None | ||||||||||||||||||||||||
| self._request_timeout: float | None = None | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| self.node_account_id: AccountId | None = None | ||||||||||||||||||||||||
| self._node_account_id: AccountId | None = None | ||||||||||||||||||||||||
| self.node_account_ids: list[AccountId] = [] | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| self._used_node_account_id: AccountId | None = None | ||||||||||||||||||||||||
| self._node_account_ids_index: int = 0 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||
| def node_account_id(self) -> AccountId | None: | ||||||||||||||||||||||||
| warnings.warn( | ||||||||||||||||||||||||
| "'node_account_id' is deprecated; use 'node_account_ids' instead.", DeprecationWarning, stacklevel=2 | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return self.node_account_ids[0] if len(self.node_account_ids) > 0 else None | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @node_account_id.setter | ||||||||||||||||||||||||
| def node_account_id(self, account_id: AccountId) -> None: | ||||||||||||||||||||||||
| warnings.warn( | ||||||||||||||||||||||||
| "'node_account_id' is deprecated; use 'node_account_ids' instead.", DeprecationWarning, stacklevel=2 | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| self.node_account_ids = [account_id] if account_id is not None else [] | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def set_node_account_ids(self, node_account_ids: list[AccountId]): | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| Explicitly set the node account IDs to execute against. | ||||||||||||||||||||||||
|
|
@@ -115,6 +131,12 @@ def set_node_account_id(self, node_account_id: AccountId): | |||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||
| The current instance of the class for chaining. | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| warnings.warn( | ||||||||||||||||||||||||
| "Method 'set_node_account_id()' is deprecated; use 'set_node_account_ids()' instead.", | ||||||||||||||||||||||||
| DeprecationWarning, | ||||||||||||||||||||||||
| stacklevel=2, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return self.set_node_account_ids([node_account_id]) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def set_max_attempts(self, max_attempts: int): | ||||||||||||||||||||||||
|
|
@@ -346,9 +368,8 @@ def _resolve_execution_config(self, client: Client, timeout: int | float | None) | |||||||||||||||||||||||
| if getattr(self, attr) is None: | ||||||||||||||||||||||||
| setattr(self, attr, default) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # nodes to which the executaion must be run against, if not provided used nodes from client | ||||||||||||||||||||||||
| if not self.node_account_ids: | ||||||||||||||||||||||||
| self.node_account_ids = [node._account_id for node in client.network._healthy_nodes] | ||||||||||||||||||||||||
| self.node_account_ids = [node._account_id for node in client.network.nodes] | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if not self.node_account_ids: | ||||||||||||||||||||||||
| raise RuntimeError("No healthy nodes available for execution") | ||||||||||||||||||||||||
|
Comment on lines
371
to
375
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. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Node source switched to all nodes, but the error text still says "healthy". Selection now seeds Proposed message fix if not self.node_account_ids:
- raise RuntimeError("No healthy nodes available for execution")
+ raise RuntimeError("No nodes available for execution")📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||
|
|
@@ -429,10 +450,10 @@ def _execute(self, client: Client, timeout: int | float | None = None): | |||||||||||||||||||||||
| node = client.network._get_node(node_id) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if node is None: | ||||||||||||||||||||||||
| raise RuntimeError(f"No node found for node_account_id: {self.node_account_id}") | ||||||||||||||||||||||||
| raise RuntimeError(f"No node found for node_account_id: {self._node_account_id}") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Store for logging and receipts | ||||||||||||||||||||||||
| self.node_account_id = node._account_id | ||||||||||||||||||||||||
| self._node_account_id = node._account_id | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Create a channel wrapper from the client's channel | ||||||||||||||||||||||||
| channel = node._get_channel() | ||||||||||||||||||||||||
|
|
@@ -442,7 +463,7 @@ def _execute(self, client: Client, timeout: int | float | None = None): | |||||||||||||||||||||||
| "requestId", | ||||||||||||||||||||||||
| self._get_request_id(), | ||||||||||||||||||||||||
| "nodeAccountID", | ||||||||||||||||||||||||
| self.node_account_id, | ||||||||||||||||||||||||
| self._node_account_id, | ||||||||||||||||||||||||
| "attempt", | ||||||||||||||||||||||||
| attempt + 1, | ||||||||||||||||||||||||
| "maxAttempts", | ||||||||||||||||||||||||
|
|
@@ -483,7 +504,7 @@ def _execute(self, client: Client, timeout: int | float | None = None): | |||||||||||||||||||||||
| logger.trace( | ||||||||||||||||||||||||
| f"{self.__class__.__name__} status received", | ||||||||||||||||||||||||
| "nodeAccountID", | ||||||||||||||||||||||||
| self.node_account_id, | ||||||||||||||||||||||||
| self._node_account_id, | ||||||||||||||||||||||||
| "network", | ||||||||||||||||||||||||
| client.network.network, | ||||||||||||||||||||||||
| "state", | ||||||||||||||||||||||||
|
|
@@ -518,7 +539,7 @@ def _execute(self, client: Client, timeout: int | float | None = None): | |||||||||||||||||||||||
| case _ExecutionState.FINISHED: | ||||||||||||||||||||||||
| # If the transaction completed successfully, map the response and return it | ||||||||||||||||||||||||
| logger.trace(f"{self.__class__.__name__} finished execution") | ||||||||||||||||||||||||
| return self._map_response(response, self.node_account_id, proto_request) | ||||||||||||||||||||||||
| return self._map_response(response, self._node_account_id, proto_request) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| logger.error( | ||||||||||||||||||||||||
| "Exceeded maximum attempts for request", | ||||||||||||||||||||||||
|
|
@@ -529,7 +550,7 @@ def _execute(self, client: Client, timeout: int | float | None = None): | |||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| raise MaxAttemptsError( | ||||||||||||||||||||||||
| "Exceeded maximum attempts or request timeout", | ||||||||||||||||||||||||
| self.node_account_id, | ||||||||||||||||||||||||
| self._node_account_id, | ||||||||||||||||||||||||
| err_persistant, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
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.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
auto_renew_period = Nonecan make the round-tripped transaction unserializable._build_proto_body()(line 322) doesduration_pb2.Duration(seconds=self.auto_renew_period.seconds), which raisesAttributeErrorwhenauto_renew_periodisNone. If a deserialized proto lacksautoRenewPeriod, this reconstruction sets it toNone, so a subsequentbuild_transaction_body()/execute()on the restored object crashes. Prefer leaving the constructor default in place when the field is absent.🛡️ Proposed change
📝 Committable suggestion