diff --git a/.gitignore b/.gitignore index 900f464..77b0191 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,6 @@ src/hedera_sdk_python/hapi .idea # Pytest -.pytest_cache \ No newline at end of file +.pytest_cache + +uv.lock \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/GitLink.xml b/.idea/GitLink.xml deleted file mode 100644 index 009597c..0000000 --- a/.idea/GitLink.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/hedera_sdk_python.iml b/.idea/hedera_sdk_python.iml deleted file mode 100644 index b9527bf..0000000 --- a/.idea/hedera_sdk_python.iml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 52cb8d5..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index def6f37..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/hedera_sdk_python/consensus/topic_delete_transaction.py b/src/hedera_sdk_python/consensus/topic_delete_transaction.py index 2dd23e7..014c6ef 100644 --- a/src/hedera_sdk_python/consensus/topic_delete_transaction.py +++ b/src/hedera_sdk_python/consensus/topic_delete_transaction.py @@ -18,6 +18,9 @@ def build_transaction_body(self): Raises: ValueError: If required fields are missing. """ + if self.topic_id is None: + raise ValueError("Missing required fields: topic_id") + transaction_body = self.build_base_transaction_body() transaction_body.consensusDeleteTopic.CopyFrom(consensus_delete_topic_pb2.ConsensusDeleteTopicTransactionBody( topicID=self.topic_id.to_proto() diff --git a/src/hedera_sdk_python/consensus/topic_update_transaction.py b/src/hedera_sdk_python/consensus/topic_update_transaction.py index ae0bd5a..6d21552 100644 --- a/src/hedera_sdk_python/consensus/topic_update_transaction.py +++ b/src/hedera_sdk_python/consensus/topic_update_transaction.py @@ -25,6 +25,9 @@ def build_transaction_body(self): Raises: ValueError: If required fields are missing. """ + if self.topic_id is None: + raise ValueError("Missing required fields: topic_id") + transaction_body = self.build_base_transaction_body() transaction_body.consensusUpdateTopic.CopyFrom(consensus_update_topic_pb2.ConsensusUpdateTopicTransactionBody( topicID=self.topic_id.to_proto(), diff --git a/src/hedera_sdk_python/query/transaction_get_receipt_query.py b/src/hedera_sdk_python/query/transaction_get_receipt_query.py index 861924e..dce5525 100644 --- a/src/hedera_sdk_python/query/transaction_get_receipt_query.py +++ b/src/hedera_sdk_python/query/transaction_get_receipt_query.py @@ -1,5 +1,5 @@ from hedera_sdk_python.query.query import Query -from hedera_sdk_python.hapi import transaction_get_receipt_pb2, query_pb2 +from hedera_sdk_python.hapi import transaction_get_receipt_pb2, query_pb2, query_header_pb2 from hedera_sdk_python.response_code import ResponseCode from hedera_sdk_python.transaction.transaction_id import TransactionId from hedera_sdk_python.transaction.transaction_receipt import TransactionReceipt @@ -31,6 +31,13 @@ def set_transaction_id(self, transaction_id: TransactionId): self.transaction_id = transaction_id return self + def _is_payment_required(self): + """ + Override the default in the base Query class: + This particular query does NOT require a payment. + """ + return False + def _make_request(self): """ Constructs the protobuf request for the transaction receipt query. @@ -46,7 +53,9 @@ def _make_request(self): if not self.transaction_id: raise ValueError("Transaction ID must be set before making the request.") - query_header = self._make_request_header() + query_header = query_header_pb2.QueryHeader() + query_header.responseType = query_header_pb2.ResponseType.ANSWER_ONLY + transaction_get_receipt = transaction_get_receipt_pb2.TransactionGetReceiptQuery() transaction_get_receipt.header.CopyFrom(query_header) transaction_get_receipt.transactionID.CopyFrom(self.transaction_id.to_proto()) diff --git a/src/hedera_sdk_python/transaction/transaction.py b/src/hedera_sdk_python/transaction/transaction.py index 5d382f2..621dce9 100644 --- a/src/hedera_sdk_python/transaction/transaction.py +++ b/src/hedera_sdk_python/transaction/transaction.py @@ -185,23 +185,24 @@ def build_base_transaction_body(self): ValueError: If required IDs are not set. """ if self.transaction_id is None: - if self.operator_account_id is None: - raise ValueError("Operator account ID is not set.") - self.transaction_id = TransactionId.generate(self.operator_account_id) + if self.operator_account_id is None: + raise ValueError("Operator account ID is not set.") + self.transaction_id = TransactionId.generate(self.operator_account_id) transaction_id_proto = self.transaction_id.to_proto() if self.node_account_id is None: raise ValueError("Node account ID is not set.") - transaction_body = transaction_body_pb2.TransactionBody( - transactionID=transaction_id_proto, - nodeAccountID=self.node_account_id.to_proto(), - transactionFee=self.transaction_fee or self._default_transaction_fee, - transactionValidDuration=duration_pb2.Duration(seconds=self.transaction_valid_duration), - generateRecord=self.generate_record, - memo=self.memo - ) + transaction_body = transaction_body_pb2.TransactionBody() + transaction_body.transactionID.CopyFrom(transaction_id_proto) + transaction_body.nodeAccountID.CopyFrom(self.node_account_id.to_proto()) + + transaction_body.transactionFee = self.transaction_fee or self._default_transaction_fee + + transaction_body.transactionValidDuration.seconds = self.transaction_valid_duration + transaction_body.generateRecord = self.generate_record + transaction_body.memo = self.memo return transaction_body diff --git a/tests/test_topic_create_transaction.py b/tests/test_topic_create_transaction.py new file mode 100644 index 0000000..71f49e9 --- /dev/null +++ b/tests/test_topic_create_transaction.py @@ -0,0 +1,106 @@ +import pytest +from unittest.mock import MagicMock +from hedera_sdk_python.consensus.topic_create_transaction import TopicCreateTransaction +from hedera_sdk_python.account.account_id import AccountId +from hedera_sdk_python.crypto.private_key import PrivateKey +from hedera_sdk_python.client.client import Client +from hedera_sdk_python.response_code import ResponseCode +from hedera_sdk_python.hapi import ( + transaction_receipt_pb2 +) +from hedera_sdk_python.transaction.transaction_receipt import TransactionReceipt +from hedera_sdk_python.transaction.transaction_id import TransactionId +from hedera_sdk_python.hapi import timestamp_pb2 as hapi_timestamp_pb2 + +@pytest.mark.usefixtures("mock_account_ids") +def test_build_topic_create_transaction_body(mock_account_ids): + """ + Test building a TopicCreateTransaction body with valid memo, admin key. + """ + _, _, node_account_id, _, _ = mock_account_ids + + tx = TopicCreateTransaction(memo="Hello Topic", admin_key=PrivateKey.generate().public_key()) + + tx.operator_account_id = AccountId(0, 0, 2) + tx.node_account_id = node_account_id + + transaction_body = tx.build_transaction_body() + + assert transaction_body.consensusCreateTopic.memo == "Hello Topic" + assert transaction_body.consensusCreateTopic.adminKey.ed25519 + +def test_missing_operator_in_topic_create(mock_account_ids): + """ + Test that building the body fails if no operator ID is set. + """ + _, _, node_account_id, _, _ = mock_account_ids + + tx = TopicCreateTransaction(memo="No Operator") + tx.node_account_id = node_account_id + + with pytest.raises(ValueError, match="Operator account ID is not set."): + tx.build_transaction_body() + +def test_missing_node_in_topic_create(mock_account_ids): + """ + Test that building the body fails if no node account ID is set. + """ + tx = TopicCreateTransaction(memo="No Node") + tx.operator_account_id = AccountId(0, 0, 2) + + with pytest.raises(ValueError, match="Node account ID is not set."): + tx.build_transaction_body() + +def test_sign_topic_create_transaction(mock_account_ids): + """ + Test signing the TopicCreateTransaction with a private key. + """ + _, _, node_account_id, _, _ = mock_account_ids + tx = TopicCreateTransaction(memo="Signing test") + tx.operator_account_id = AccountId(0, 0, 2) + tx.node_account_id = node_account_id + + private_key = PrivateKey.generate() + + body_bytes = tx.build_transaction_body().SerializeToString() + tx.transaction_body_bytes = body_bytes + + tx.sign(private_key) + assert len(tx.signature_map.sigPair) == 1 + +def test_execute_topic_create_transaction(mock_account_ids): + """ + Test executing the TopicCreateTransaction with a mock Client. + """ + _, _, node_account_id, _, _ = mock_account_ids + + tx = TopicCreateTransaction(memo="Execute test") + tx.operator_account_id = AccountId(0, 0, 2) + + client = MagicMock(spec=Client) + client.operator_private_key = PrivateKey.generate() + client.operator_account_id = AccountId(0, 0, 2) + client.node_account_id = node_account_id + + real_tx_id = TransactionId( + account_id=AccountId(0, 0, 2), + valid_start=hapi_timestamp_pb2.Timestamp(seconds=11111, nanos=222) + ) + client.generate_transaction_id.return_value = real_tx_id + + client.topic_stub = MagicMock() + + mock_response = MagicMock() + mock_response.nodeTransactionPrecheckCode = ResponseCode.OK + client.topic_stub.createTopic.return_value = mock_response + + proto_receipt = transaction_receipt_pb2.TransactionReceipt(status=ResponseCode.OK) + real_receipt = TransactionReceipt.from_proto(proto_receipt) + client.get_transaction_receipt.return_value = real_receipt + + receipt = tx.execute(client) + + client.topic_stub.createTopic.assert_called_once() + assert receipt is not None + assert receipt.status == ResponseCode.OK + print("Test passed: TopicCreateTransaction executed successfully.") diff --git a/tests/test_topic_delete_transaction.py b/tests/test_topic_delete_transaction.py new file mode 100644 index 0000000..a4e5c41 --- /dev/null +++ b/tests/test_topic_delete_transaction.py @@ -0,0 +1,94 @@ +import pytest +from unittest.mock import MagicMock +from hedera_sdk_python.consensus.topic_delete_transaction import TopicDeleteTransaction +from hedera_sdk_python.consensus.topic_id import TopicId +from hedera_sdk_python.account.account_id import AccountId +from hedera_sdk_python.crypto.private_key import PrivateKey +from hedera_sdk_python.client.client import Client +from hedera_sdk_python.response_code import ResponseCode +from hedera_sdk_python.hapi import transaction_receipt_pb2 +from hedera_sdk_python.transaction.transaction_receipt import TransactionReceipt +from hedera_sdk_python.transaction.transaction_id import TransactionId +from hedera_sdk_python.hapi import timestamp_pb2 as hapi_timestamp_pb2 + +@pytest.mark.usefixtures("mock_account_ids") +def test_build_topic_delete_transaction_body(mock_account_ids): + """ + Test building a TopicDeleteTransaction body with a valid topic ID. + """ + _, _, node_account_id, _, _ = mock_account_ids + topic_id = TopicId(0,0,1234) + tx = TopicDeleteTransaction(topic_id=topic_id) + + tx.operator_account_id = AccountId(0, 0, 2) + tx.node_account_id = node_account_id + + transaction_body = tx.build_transaction_body() + assert transaction_body.consensusDeleteTopic.topicID.topicNum == 1234 + +def test_missing_topic_id_in_delete(mock_account_ids): + """ + Test that building fails if no topic ID is provided. + """ + _, _, node_account_id, _, _ = mock_account_ids + tx = TopicDeleteTransaction(topic_id=None) + tx.operator_account_id = AccountId(0, 0, 2) + tx.node_account_id = node_account_id + + with pytest.raises(ValueError, match="Missing required fields"): + tx.build_transaction_body() + +def test_sign_topic_delete_transaction(mock_account_ids): + """ + Test signing the TopicDeleteTransaction with a private key. + """ + _, _, node_account_id, _, _ = mock_account_ids + tx = TopicDeleteTransaction(topic_id=TopicId(0,0,9876)) + tx.operator_account_id = AccountId(0, 0, 2) + tx.node_account_id = node_account_id + + private_key = PrivateKey.generate() + + body_bytes = tx.build_transaction_body().SerializeToString() + tx.transaction_body_bytes = body_bytes + + tx.sign(private_key) + assert len(tx.signature_map.sigPair) == 1 + +def test_execute_topic_delete_transaction(mock_account_ids): + """ + Test executing the TopicDeleteTransaction with a mock Client. + """ + _, _, node_account_id, _, _ = mock_account_ids + topic_id = TopicId(0,0,9876) + tx = TopicDeleteTransaction(topic_id=topic_id) + tx.operator_account_id = AccountId(0, 0, 2) + + client = MagicMock(spec=Client) + client.operator_private_key = PrivateKey.generate() + client.operator_account_id = AccountId(0, 0, 2) + client.node_account_id = node_account_id + + real_tx_id = TransactionId( + account_id=AccountId(0, 0, 2), + valid_start=hapi_timestamp_pb2.Timestamp(seconds=20000, nanos=3333) + ) + client.generate_transaction_id.return_value = real_tx_id + + client.topic_stub = MagicMock() + mock_response = MagicMock() + mock_response.nodeTransactionPrecheckCode = ResponseCode.OK + client.topic_stub.deleteTopic.return_value = mock_response + + proto_receipt = transaction_receipt_pb2.TransactionReceipt(status=ResponseCode.OK) + real_receipt = TransactionReceipt.from_proto(proto_receipt) + client.get_transaction_receipt.return_value = real_receipt + + # Act + receipt = tx.execute(client) + + # Assert + client.topic_stub.deleteTopic.assert_called_once() + assert receipt is not None + assert receipt.status == ResponseCode.OK + print("Test passed: TopicDeleteTransaction executed successfully.") diff --git a/tests/test_topic_message_submit_transaction.py b/tests/test_topic_message_submit_transaction.py new file mode 100644 index 0000000..567e2a6 --- /dev/null +++ b/tests/test_topic_message_submit_transaction.py @@ -0,0 +1,60 @@ +import pytest +from unittest.mock import MagicMock +from hedera_sdk_python.consensus.topic_id import TopicId +from hedera_sdk_python.consensus.topic_message_submit_transaction import TopicMessageSubmitTransaction +from hedera_sdk_python.transaction.transaction_id import TransactionId +from hedera_sdk_python.transaction.transaction_receipt import TransactionReceipt +from hedera_sdk_python.account.account_id import AccountId +from hedera_sdk_python.crypto.private_key import PrivateKey +from hedera_sdk_python.client.client import Client +from hedera_sdk_python.response_code import ResponseCode +from hedera_sdk_python.hapi import ( + response_pb2, + transaction_receipt_pb2, + timestamp_pb2 as hapi_timestamp_pb2 +) + +def test_execute_topic_submit_message(): + """ + Test executing the TopicMessageSubmitTransaction with a mock Client. + When calling tx.execute(client), freeze_with() checks client.node_account_id if tx.node_account_id is None. + """ + topic_id = TopicId(0, 0, 1234) + message = "Hello from topic submit!" + tx = TopicMessageSubmitTransaction(topic_id, message) + + tx.operator_account_id = AccountId(0, 0, 2) + + client = MagicMock(spec=Client) + client.operator_private_key = PrivateKey.generate() + client.operator_account_id = AccountId(0, 0, 2) + client.node_account_id = AccountId(0, 0, 3) + + real_tx_id = TransactionId( + account_id=AccountId(0, 0, 2), + valid_start=hapi_timestamp_pb2.Timestamp(seconds=12345, nanos=6789) + ) + client.generate_transaction_id.return_value = real_tx_id + + client.topic_stub = MagicMock() + + mock_response = MagicMock() + mock_response.nodeTransactionPrecheckCode = ResponseCode.OK + client.topic_stub.submitMessage.return_value = mock_response + + real_receipt_proto = transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.OK + ) + real_receipt = TransactionReceipt.from_proto(real_receipt_proto) + + client.get_transaction_receipt.return_value = real_receipt + + try: + receipt = tx.execute(client) + except Exception as e: + pytest.fail(f"TopicMessageSubmitTransaction execution failed with: {e}") + + client.topic_stub.submitMessage.assert_called_once() + assert receipt is not None + assert receipt.status == ResponseCode.OK + print("Test passed: TopicMessageSubmitTransaction executed successfully.") diff --git a/tests/test_topic_update_transaction.py b/tests/test_topic_update_transaction.py new file mode 100644 index 0000000..5fab690 --- /dev/null +++ b/tests/test_topic_update_transaction.py @@ -0,0 +1,95 @@ +import pytest +from unittest.mock import MagicMock +from hedera_sdk_python.consensus.topic_update_transaction import TopicUpdateTransaction +from hedera_sdk_python.consensus.topic_id import TopicId +from hedera_sdk_python.account.account_id import AccountId +from hedera_sdk_python.crypto.private_key import PrivateKey +from hedera_sdk_python.client.client import Client +from hedera_sdk_python.response_code import ResponseCode +from hedera_sdk_python.hapi import transaction_receipt_pb2 +from hedera_sdk_python.transaction.transaction_receipt import TransactionReceipt +from hedera_sdk_python.transaction.transaction_id import TransactionId +from hedera_sdk_python.hapi import timestamp_pb2 as hapi_timestamp_pb2 + +@pytest.mark.usefixtures("mock_account_ids") +def test_build_topic_update_transaction_body(mock_account_ids): + """ + Test building a TopicUpdateTransaction body with valid topic ID and memo. + """ + _, _, node_account_id, _, _ = mock_account_ids + topic_id = TopicId(0,0,1234) + tx = TopicUpdateTransaction(topic_id=topic_id, memo="Updated Memo") + + tx.operator_account_id = AccountId(0, 0, 2) + tx.node_account_id = node_account_id + + transaction_body = tx.build_transaction_body() + assert transaction_body.consensusUpdateTopic.topicID.topicNum == 1234 + assert transaction_body.consensusUpdateTopic.memo.value == "Updated Memo" + +def test_missing_topic_id_in_update(mock_account_ids): + """ + Test that building fails if no topic ID is provided. + """ + _, _, node_account_id, _, _ = mock_account_ids + + tx = TopicUpdateTransaction(topic_id=None, memo="No ID") + tx.operator_account_id = AccountId(0, 0, 2) + tx.node_account_id = node_account_id + + with pytest.raises(ValueError, match="Missing required fields"): + tx.build_transaction_body() + +def test_sign_topic_update_transaction(mock_account_ids): + """ + Test signing the TopicUpdateTransaction with a private key. + """ + _, _, node_account_id, _, _ = mock_account_ids + topic_id = TopicId(0,0,9999) + tx = TopicUpdateTransaction(topic_id=topic_id, memo="Signature test") + tx.operator_account_id = AccountId(0, 0, 2) + tx.node_account_id = node_account_id + + private_key = PrivateKey.generate() + + body_bytes = tx.build_transaction_body().SerializeToString() + tx.transaction_body_bytes = body_bytes + + tx.sign(private_key) + assert len(tx.signature_map.sigPair) == 1 + +def test_execute_topic_update_transaction(mock_account_ids): + """ + Test executing the TopicUpdateTransaction with a mock Client. + """ + _, _, node_account_id, _, _ = mock_account_ids + topic_id = TopicId(0,0,9999) + tx = TopicUpdateTransaction(topic_id=topic_id, memo="Exec update") + tx.operator_account_id = AccountId(0, 0, 2) + + client = MagicMock(spec=Client) + client.operator_private_key = PrivateKey.generate() + client.operator_account_id = AccountId(0, 0, 2) + client.node_account_id = node_account_id + + real_tx_id = TransactionId( + account_id=AccountId(0, 0, 2), + valid_start=hapi_timestamp_pb2.Timestamp(seconds=12345, nanos=6789) + ) + client.generate_transaction_id.return_value = real_tx_id + + client.topic_stub = MagicMock() + mock_response = MagicMock() + mock_response.nodeTransactionPrecheckCode = ResponseCode.OK + client.topic_stub.updateTopic.return_value = mock_response + + proto_receipt = transaction_receipt_pb2.TransactionReceipt(status=ResponseCode.OK) + real_receipt = TransactionReceipt.from_proto(proto_receipt) + client.get_transaction_receipt.return_value = real_receipt + + receipt = tx.execute(client) + + client.topic_stub.updateTopic.assert_called_once() + assert receipt is not None + assert receipt.status == ResponseCode.OK + print("Test passed: TopicUpdateTransaction executed successfully.")