From 376553ee3e6269d80c8c9f8f860fee6ffa700cbe Mon Sep 17 00:00:00 2001 From: MonaaEid Date: Fri, 1 May 2026 06:52:22 +0300 Subject: [PATCH 1/6] feat: implement createTopic functionality and related parameters in TCK handlers Signed-off-by: MonaaEid --- tck/handlers/__init__.py | 16 ++-- tck/handlers/key.py | 21 +++-- tck/handlers/topic.py | 134 +++++++++++++++++++++++++++++++ tck/param/common.py | 11 ++- tck/param/topic.py | 97 ++++++++++++++++++++++ tck/response/topic.py | 9 +++ tck/util/param_utils.py | 27 ++++++- tests/tck/topic_handlers_test.py | 119 +++++++++++++++++++++++++++ 8 files changed, 411 insertions(+), 23 deletions(-) create mode 100644 tck/handlers/topic.py create mode 100644 tck/param/topic.py create mode 100644 tck/response/topic.py create mode 100644 tests/tck/topic_handlers_test.py diff --git a/tck/handlers/__init__.py b/tck/handlers/__init__.py index 5b76afd46..3058bbda3 100644 --- a/tck/handlers/__init__.py +++ b/tck/handlers/__init__.py @@ -1,24 +1,20 @@ """TCK handlers - auto-import all handler modules.""" # Import registry functions first to make them available -# Import all handler modules to trigger @rpc_method decorators -from . import ( - account, - key, - sdk, # setup, reset -) from .registry import ( - get_all_handlers, get_handler, + get_all_handlers, safe_dispatch, ) +# Import all handler modules to trigger @rpc_method decorators +from . import sdk # setup, reset +from . import key +from . import account +from . import topic __all__ = [ "get_handler", "get_all_handlers", "safe_dispatch", - "account", - "key", - "sdk", ] diff --git a/tck/handlers/key.py b/tck/handlers/key.py index c198110b0..4e226d900 100644 --- a/tck/handlers/key.py +++ b/tck/handlers/key.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from hiero_sdk_python.crypto.key_list import KeyList from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.crypto.public_key import PublicKey @@ -10,6 +8,13 @@ from tck.util.key_utils import KeyType, get_key_from_string +def _format_generated_public_key(public_key: PublicKey) -> str: + """Return DER-hex output expected by TCK for generated public keys.""" + if public_key.is_ecdsa(): + return public_key.to_string_der_ecdsa_compressed() + return public_key.to_string_der() + + @rpc_method("generateKey") def generate_key(params: KeyGenerationParams) -> KeyGenerationResponse: if params.fromKey and params.type not in { @@ -23,7 +28,9 @@ def generate_key(params: KeyGenerationParams) -> KeyGenerationResponse: ) if params.threshold is not None and params.type != KeyType.THRESHOLD_KEY: - raise JsonRpcError.invalid_params_error("invalid parameters: threshold is only allowed for thresholdKey types.") + raise JsonRpcError.invalid_params_error( + "invalid parameters: threshold is only allowed for thresholdKey types." + ) if params.type == KeyType.THRESHOLD_KEY and params.threshold is None: raise JsonRpcError.invalid_params_error( @@ -60,9 +67,11 @@ def _handle_private_key(params: KeyGenerationParams, response: KeyGenerationResp return private_key_string -def _handle_public_key(params: KeyGenerationParams, response: KeyGenerationResponse, is_list: bool) -> str: +def _handle_public_key( + params: KeyGenerationParams, response: KeyGenerationResponse, is_list: bool +) -> str: if params.fromKey: - return PrivateKey.from_string(params.fromKey).public_key().to_string_der() + return _format_generated_public_key(PrivateKey.from_string(params.fromKey).public_key()) if params.type == KeyType.ED25519_PUBLIC_KEY: private_key = PrivateKey.generate_ed25519() @@ -72,7 +81,7 @@ def _handle_public_key(params: KeyGenerationParams, response: KeyGenerationRespo if is_list: response.privateKeys.append(private_key.to_string_der()) - return private_key.public_key().to_string_der() + return _format_generated_public_key(private_key.public_key()) def _handle_key_list(params: KeyGenerationParams, response: KeyGenerationResponse) -> str: diff --git a/tck/handlers/topic.py b/tck/handlers/topic.py new file mode 100644 index 000000000..fdbe96136 --- /dev/null +++ b/tck/handlers/topic.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.consensus.topic_create_transaction import TopicCreateTransaction +from hiero_sdk_python.exceptions import PrecheckError, ReceiptStatusError +from hiero_sdk_python.query.account_info_query import AccountInfoQuery +from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee +from hiero_sdk_python.tokens.token_id import TokenId +from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt +from tck.errors import JsonRpcError +from tck.handlers.registry import rpc_method +from tck.param.topic import CreateTopicCustomFeeParams, CreateTopicParams +from tck.response.topic import CreateTopicResponse +from tck.util.client_utils import get_client +from tck.util.constants import DEFAULT_GRPC_TIMEOUT +from tck.util.key_utils import get_key_from_string + + +def _build_custom_fee(custom_fee_params: CreateTopicCustomFeeParams) -> CustomFixedFee: + custom_fee = CustomFixedFee() + + if custom_fee_params.feeCollectorAccountId == "": + # Keep TCK behavior: explicitly empty collector should surface as internal error. + raise ValueError("feeCollectorAccountId cannot be empty") + + if custom_fee_params.feeCollectorAccountId is not None: + custom_fee.set_fee_collector_account_id( + AccountId.from_string(custom_fee_params.feeCollectorAccountId) + ) + + if custom_fee_params.feeCollectorsExempt: + custom_fee.set_all_collectors_are_exempt(custom_fee_params.feeCollectorsExempt) + + if custom_fee_params.fixedFee is not None: + if custom_fee_params.fixedFee.amount is not None: + custom_fee.amount = custom_fee_params.fixedFee.amount + + if custom_fee_params.fixedFee.denominatingTokenId: + custom_fee.set_denominating_token_id( + TokenId.from_string(custom_fee_params.fixedFee.denominatingTokenId) + ) + + return custom_fee + + +def _build_create_topic_transaction(params: CreateTopicParams) -> TopicCreateTransaction: + transaction = TopicCreateTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT) + + if params.memo is not None: + transaction.set_memo(params.memo) + + if params.adminKey: + transaction.set_admin_key(get_key_from_string(params.adminKey)) + + if params.submitKey: + transaction.set_submit_key(get_key_from_string(params.submitKey)) + + if params.autoRenewPeriod is not None: + transaction.set_auto_renew_period(params.autoRenewPeriod) + + if params.autoRenewAccountId: + transaction.set_auto_renew_account( + AccountId.from_string(params.autoRenewAccountId) + ) + + if params.feeScheduleKey: + transaction.set_fee_schedule_key(get_key_from_string(params.feeScheduleKey)) + + if params.feeExemptKeys: + transaction.set_fee_exempt_keys( + [get_key_from_string(key) for key in params.feeExemptKeys] + ) + + if params.customFees is not None: + transaction.set_custom_fees( + [_build_custom_fee(custom_fee_params) for custom_fee_params in params.customFees] + ) + + return transaction + + +def _get_auto_renew_account_state(client, auto_renew_account_id: str | None) -> str: + if not auto_renew_account_id: + return "none" + + account_id = AccountId.from_string(auto_renew_account_id) + try: + AccountInfoQuery().set_account_id(account_id).execute(client) + return "exists" + except (PrecheckError, ReceiptStatusError) as exc: + status = ResponseCode(exc.status).name + if status == "INVALID_ACCOUNT_ID": + return "missing" + if status == "ACCOUNT_DELETED": + return "deleted" + return "unknown" + +@rpc_method("createTopic") +def create_topic(params: CreateTopicParams) -> CreateTopicResponse: + client = get_client(params.sessionId) + + auto_renew_account_state = _get_auto_renew_account_state(client, params.autoRenewAccountId) + if auto_renew_account_state == "missing": + raise JsonRpcError.hiero_error({"status": "INVALID_AUTORENEW_ACCOUNT"}) + + transaction = _build_create_topic_transaction(params) + + # Align with TCK expectation: if no explicit auto-renew account is provided, + # default to the transaction payer/operator account. + if params.autoRenewAccountId is None and client is not None and client.operator_account_id is not None: + transaction.set_auto_renew_account(client.operator_account_id) + + if params.commonTransactionParams is not None: + params.commonTransactionParams.apply_common_params(transaction, client) + + try: + response = transaction.execute(client, wait_for_receipt=False) + receipt: TransactionReceipt = response.get_receipt(client, validate_status=True) + except (PrecheckError, ReceiptStatusError) as exc: + status = ResponseCode(exc.status).name + if ( + params.autoRenewAccountId + and status == "INVALID_SIGNATURE" + and auto_renew_account_state != "deleted" + ): + raise JsonRpcError.hiero_error({"status": "INVALID_AUTORENEW_ACCOUNT"}) + raise + + topic_id = "" + if receipt.status == ResponseCode.SUCCESS and receipt.topic_id is not None: + topic_id = str(receipt.topic_id) + + return CreateTopicResponse(topic_id, ResponseCode(receipt.status).name) diff --git a/tck/param/common.py b/tck/param/common.py index 7e90038d5..9f63075b0 100644 --- a/tck/param/common.py +++ b/tck/param/common.py @@ -34,11 +34,16 @@ def apply_common_params(self, transaction: Transaction, client: Client) -> None: """Apply commonTransactionParams to a given transaction.""" if self.transactionId is not None: try: - transaction.set_transaction_id(TransactionId.from_string(self.transactionId)) + transaction.set_transaction_id( + TransactionId.from_string(self.transactionId) + ) except Exception: - transaction.set_transaction_id(TransactionId.generate(AccountId.from_string(self.transactionId))) + transaction.set_transaction_id( + TransactionId.generate(AccountId.from_string(self.transactionId)) + ) - # TODO add a max_transaction_fee sdk missing func + if self.maxTransactionFee is not None: + transaction.transaction_fee = int(self.maxTransactionFee) if self.validTransactionDuration is not None: transaction.set_transaction_valid_duration(self.validTransactionDuration) diff --git a/tck/param/topic.py b/tck/param/topic.py new file mode 100644 index 000000000..d17825966 --- /dev/null +++ b/tck/param/topic.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from tck.param.base import BaseTransactionParams +from tck.util.param_utils import ( + non_empty_string_list, + non_empty_string_or_none, + parse_common_transaction_params, + parse_session_id, + to_bool, + to_int, +) + + +@dataclass +class CreateTopicFixedFeeParams: + """Parameters for a fixed fee custom fee in topic creation.""" + amount: int | None = None + denominatingTokenId: str | None = None + + @classmethod + def parse_json_params(cls, params: dict) -> "CreateTopicFixedFeeParams": + return cls( + amount=to_int(params.get("amount")), + denominatingTokenId=non_empty_string_or_none(params.get("denominatingTokenId")), + ) + + +@dataclass +class CreateTopicCustomFeeParams: + """Parameters for a custom fee in topic creation.""" + feeCollectorAccountId: str | None = None + feeCollectorsExempt: bool | None = None + fixedFee: CreateTopicFixedFeeParams | None = None + + @classmethod + def parse_json_params(cls, params: dict) -> "CreateTopicCustomFeeParams": + fixed_fee = params.get("fixedFee") + + fee_collector_account_id = params.get("feeCollectorAccountId") + if isinstance(fee_collector_account_id, str): + fee_collector_account_id = fee_collector_account_id.strip() + + return cls( + feeCollectorAccountId=fee_collector_account_id, + feeCollectorsExempt=to_bool(params.get("feeCollectorsExempt")), + fixedFee=( + CreateTopicFixedFeeParams.parse_json_params(fixed_fee) + if isinstance(fixed_fee, dict) + else None + ), + ) + + +@dataclass +class CreateTopicParams(BaseTransactionParams): + """Parameters for creating a topic. Extends BaseTransactionParams to include common transaction parameters.""" + memo: str | None = None + adminKey: str | None = None + submitKey: str | None = None + autoRenewPeriod: int | None = None + autoRenewAccountId: str | None = None + feeScheduleKey: str | None = None + feeExemptKeys: list[str] | None = None + customFees: list[CreateTopicCustomFeeParams] | None = None + + @classmethod + def parse_json_params(cls, params: dict) -> "CreateTopicParams": + fee_exempt_keys = params.get("feeExemptKeys") + if fee_exempt_keys is not None and not isinstance(fee_exempt_keys, list): + raise ValueError("feeExemptKeys must be a list") + + custom_fees = params.get("customFees") + if custom_fees is not None and not isinstance(custom_fees, list): + raise ValueError("customFees must be a list") + if custom_fees is not None and any(not isinstance(custom_fee, dict) for custom_fee in custom_fees): + raise ValueError("each customFees item must be an object") + return cls( + memo=params.get("memo"), + adminKey=non_empty_string_or_none(params.get("adminKey")), + submitKey=non_empty_string_or_none(params.get("submitKey")), + autoRenewPeriod=to_int(params.get("autoRenewPeriod")), + autoRenewAccountId=non_empty_string_or_none(params.get("autoRenewAccountId")), + feeScheduleKey=non_empty_string_or_none(params.get("feeScheduleKey")), + feeExemptKeys=non_empty_string_list(fee_exempt_keys), + customFees=( + [ + CreateTopicCustomFeeParams.parse_json_params(custom_fee) + for custom_fee in custom_fees + ] + if custom_fees is not None + else None + ), + sessionId=parse_session_id(params), + commonTransactionParams=parse_common_transaction_params(params), + ) diff --git a/tck/response/topic.py b/tck/response/topic.py new file mode 100644 index 000000000..015a8ba74 --- /dev/null +++ b/tck/response/topic.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class CreateTopicResponse: + topicId: str | None = None + status: str | None = None diff --git a/tck/util/param_utils.py b/tck/util/param_utils.py index 49a12b4db..476d133f7 100644 --- a/tck/util/param_utils.py +++ b/tck/util/param_utils.py @@ -1,6 +1,3 @@ -from __future__ import annotations - - def parse_session_id(params: dict) -> str: """Parse sessionId from the json rpc params.""" session_id = params.get("sessionId") @@ -19,7 +16,9 @@ def parse_common_transaction_params(params: dict): if common_params is None: return None - return CommonTransactionParams.parse_json_params(params.get("commonTransactionParams")) + return CommonTransactionParams.parse_json_params( + params.get("commonTransactionParams") + ) def to_int(value) -> int | None: @@ -29,6 +28,26 @@ def to_int(value) -> int | None: except (TypeError, ValueError): return None +def non_empty_string_or_none(value: str | None) -> str | None: + """Trim string values; convert blank strings to None.""" + if not isinstance(value, str): + return value + cleaned = value.strip() + return cleaned if cleaned else None + + +def non_empty_string_list(values) -> list[str] | None: + """Trim list entries and remove empty-string items.""" + if values is None: + return None + + cleaned_values: list[str] = [] + for value in values: + cleaned = non_empty_string_or_none(value) + if cleaned is not None: + cleaned_values.append(cleaned) + + return cleaned_values def to_bool(value) -> bool | None: """Helper to convert value to bool.""" diff --git a/tests/tck/topic_handlers_test.py b/tests/tck/topic_handlers_test.py new file mode 100644 index 000000000..fc1f88a34 --- /dev/null +++ b/tests/tck/topic_handlers_test.py @@ -0,0 +1,119 @@ +"""Focused createTopic tests for TCK handlers.""" +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +import pytest + +from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.consensus.topic_create_transaction import TopicCreateTransaction +from hiero_sdk_python.exceptions import PrecheckError, ReceiptStatusError +from hiero_sdk_python.query.account_info_query import AccountInfoQuery +from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.tokens.token_id import TokenId +from tck.errors import JsonRpcError +from tck.handlers.topic import ( + _build_create_topic_transaction, + _build_custom_fee, + _get_auto_renew_account_state, + create_topic, +) +from tck.param.topic import CreateTopicCustomFeeParams, CreateTopicFixedFeeParams, CreateTopicParams + + +pytestmark = pytest.mark.unit + +TEST_KEY = "302e020100300506032b6570042204203e3e6e76b8f1ea10c1d7aaada2fcc088c08a82b65aa3299f66a1c7289ea3fcd2" + + +def _params(**overrides): + base = dict(sessionId="test", memo=None, adminKey=None, submitKey=None, autoRenewPeriod=None, autoRenewAccountId=None, feeScheduleKey=None, feeExemptKeys=None, customFees=None, commonTransactionParams=None) + base.update(overrides) + return CreateTopicParams(**base) + + +def _success_response(topic_num: int): + response = MagicMock() + receipt = MagicMock() + receipt.status = ResponseCode.SUCCESS + receipt.topic_id = AccountId(0, 0, topic_num) + response.get_receipt.return_value = receipt + return response + + +@pytest.fixture +def client_mock(): + client = MagicMock() + client.operator_account_id = AccountId(0, 0, 3) + return client + + +def test_parse_json_and_validation(): + assert CreateTopicParams.parse_json_params({"sessionId": "s", "memo": "Topic"}).memo == "Topic" + with pytest.raises(ValueError, match="customFees must be a list"): + CreateTopicParams.parse_json_params({"sessionId": "s", "customFees": "bad"}) + + +def test_build_custom_fee_and_reject_empty_collector(): + fee = _build_custom_fee(CreateTopicCustomFeeParams(feeCollectorAccountId="0.0.98", feeCollectorsExempt=True, fixedFee=CreateTopicFixedFeeParams(amount=100, denominatingTokenId="0.0.500"))) + assert fee.amount == 100 + assert fee.fee_collector_account_id == AccountId(0, 0, 98) + assert fee.denominating_token_id == TokenId(0, 0, 500) + assert fee.all_collectors_are_exempt is True + with pytest.raises(ValueError, match="feeCollectorAccountId cannot be empty"): + _build_custom_fee(CreateTopicCustomFeeParams(feeCollectorAccountId="", fixedFee=CreateTopicFixedFeeParams(amount=1))) + + +def test_build_transaction_core_fields(): + tx = _build_create_topic_transaction(_params(memo="m", adminKey=TEST_KEY, submitKey=TEST_KEY, autoRenewPeriod=7776000, autoRenewAccountId="0.0.98", feeScheduleKey=TEST_KEY, feeExemptKeys=[TEST_KEY], customFees=[CreateTopicCustomFeeParams(feeCollectorAccountId="0.0.98", fixedFee=CreateTopicFixedFeeParams(amount=100))])) + assert tx.memo == "m" + assert getattr(tx.auto_renew_period, "seconds", tx.auto_renew_period) == 7776000 + assert len(tx.custom_fees) == 1 + + +@pytest.mark.parametrize( + "side_effect,account_id,expected", + [ + (None, None, "none"), + (None, "0.0.98", "exists"), + (PrecheckError(status=ResponseCode.INVALID_ACCOUNT_ID), "0.0.999", "missing"), + ( + ReceiptStatusError(status=ResponseCode.ACCOUNT_DELETED, transaction_id=None, transaction_receipt=MagicMock()), + "0.0.98", + "deleted", + ), + ], +) +def test_auto_renew_account_state_mapping(client_mock, side_effect, account_id, expected): + if account_id is None: + assert _get_auto_renew_account_state(client_mock, account_id) == expected + return + with patch.object(AccountInfoQuery, "execute", side_effect=side_effect): + assert _get_auto_renew_account_state(client_mock, account_id) == expected + + +@patch("tck.handlers.topic.get_client") +def test_create_topic_success_minimal(mock_get_client, client_mock): + mock_get_client.return_value = client_mock + with patch.object(TopicCreateTransaction, "execute", return_value=_success_response(1000)): + result = create_topic(_params(memo="Test Topic")) + assert result.status == "SUCCESS" + assert result.topicId == "0.0.1000" + + +@patch("tck.handlers.topic.get_client") +@patch("tck.handlers.topic._get_auto_renew_account_state", return_value="missing") +def test_create_topic_rejects_missing_auto_renew(_, mock_get_client, client_mock): + mock_get_client.return_value = client_mock + with pytest.raises(JsonRpcError): + create_topic(_params(autoRenewAccountId="0.0.999")) + + +@patch("tck.handlers.topic.get_client") +@patch("tck.handlers.topic._get_auto_renew_account_state", return_value="exists") +def test_create_topic_with_all_parameters(_, mock_get_client, client_mock): + mock_get_client.return_value = client_mock + with patch.object(TopicCreateTransaction, "execute", return_value=_success_response(2000)): + result = create_topic(_params(memo="Full", adminKey=TEST_KEY, submitKey=TEST_KEY, autoRenewPeriod=7776000, autoRenewAccountId="0.0.98", feeScheduleKey=TEST_KEY, feeExemptKeys=[TEST_KEY], customFees=[CreateTopicCustomFeeParams(feeCollectorAccountId="0.0.98", feeCollectorsExempt=True, fixedFee=CreateTopicFixedFeeParams(amount=500, denominatingTokenId="0.0.500"))])) + assert result.status == "SUCCESS" + assert result.topicId == "0.0.2000" From 86e567cae783f7c36eb3333f3ae2f2b85dca570a Mon Sep 17 00:00:00 2001 From: MonaaEid Date: Fri, 1 May 2026 17:19:44 +0300 Subject: [PATCH 2/6] feat: implement createTopic functionality and related parameters in TCK handlers Signed-off-by: MonaaEid --- tck/handlers/__init__.py | 14 ++++--- tck/handlers/key.py | 10 ++--- tck/handlers/topic.py | 32 +++++----------- tck/param/common.py | 8 +--- tck/param/topic.py | 20 ++++------ tck/util/param_utils.py | 9 +++-- tests/tck/topic_handlers_test.py | 66 +++++++++++++++++++++++++++++--- 7 files changed, 97 insertions(+), 62 deletions(-) diff --git a/tck/handlers/__init__.py b/tck/handlers/__init__.py index 3058bbda3..e0469fbc0 100644 --- a/tck/handlers/__init__.py +++ b/tck/handlers/__init__.py @@ -1,17 +1,19 @@ """TCK handlers - auto-import all handler modules.""" # Import registry functions first to make them available +# Import all handler modules to trigger @rpc_method decorators +from . import ( + account, + key, + sdk, # setup, reset + topic, +) from .registry import ( - get_handler, get_all_handlers, + get_handler, safe_dispatch, ) -# Import all handler modules to trigger @rpc_method decorators -from . import sdk # setup, reset -from . import key -from . import account -from . import topic __all__ = [ "get_handler", diff --git a/tck/handlers/key.py b/tck/handlers/key.py index 4e226d900..e67aa85f8 100644 --- a/tck/handlers/key.py +++ b/tck/handlers/key.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from hiero_sdk_python.crypto.key_list import KeyList from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.crypto.public_key import PublicKey @@ -28,9 +30,7 @@ def generate_key(params: KeyGenerationParams) -> KeyGenerationResponse: ) if params.threshold is not None and params.type != KeyType.THRESHOLD_KEY: - raise JsonRpcError.invalid_params_error( - "invalid parameters: threshold is only allowed for thresholdKey types." - ) + raise JsonRpcError.invalid_params_error("invalid parameters: threshold is only allowed for thresholdKey types.") if params.type == KeyType.THRESHOLD_KEY and params.threshold is None: raise JsonRpcError.invalid_params_error( @@ -67,9 +67,7 @@ def _handle_private_key(params: KeyGenerationParams, response: KeyGenerationResp return private_key_string -def _handle_public_key( - params: KeyGenerationParams, response: KeyGenerationResponse, is_list: bool -) -> str: +def _handle_public_key(params: KeyGenerationParams, response: KeyGenerationResponse, is_list: bool) -> str: if params.fromKey: return _format_generated_public_key(PrivateKey.from_string(params.fromKey).public_key()) diff --git a/tck/handlers/topic.py b/tck/handlers/topic.py index fdbe96136..2326b8bd9 100644 --- a/tck/handlers/topic.py +++ b/tck/handlers/topic.py @@ -25,9 +25,7 @@ def _build_custom_fee(custom_fee_params: CreateTopicCustomFeeParams) -> CustomFi raise ValueError("feeCollectorAccountId cannot be empty") if custom_fee_params.feeCollectorAccountId is not None: - custom_fee.set_fee_collector_account_id( - AccountId.from_string(custom_fee_params.feeCollectorAccountId) - ) + custom_fee.set_fee_collector_account_id(AccountId.from_string(custom_fee_params.feeCollectorAccountId)) if custom_fee_params.feeCollectorsExempt: custom_fee.set_all_collectors_are_exempt(custom_fee_params.feeCollectorsExempt) @@ -37,9 +35,7 @@ def _build_custom_fee(custom_fee_params: CreateTopicCustomFeeParams) -> CustomFi custom_fee.amount = custom_fee_params.fixedFee.amount if custom_fee_params.fixedFee.denominatingTokenId: - custom_fee.set_denominating_token_id( - TokenId.from_string(custom_fee_params.fixedFee.denominatingTokenId) - ) + custom_fee.set_denominating_token_id(TokenId.from_string(custom_fee_params.fixedFee.denominatingTokenId)) return custom_fee @@ -60,22 +56,16 @@ def _build_create_topic_transaction(params: CreateTopicParams) -> TopicCreateTra transaction.set_auto_renew_period(params.autoRenewPeriod) if params.autoRenewAccountId: - transaction.set_auto_renew_account( - AccountId.from_string(params.autoRenewAccountId) - ) + transaction.set_auto_renew_account(AccountId.from_string(params.autoRenewAccountId)) if params.feeScheduleKey: transaction.set_fee_schedule_key(get_key_from_string(params.feeScheduleKey)) if params.feeExemptKeys: - transaction.set_fee_exempt_keys( - [get_key_from_string(key) for key in params.feeExemptKeys] - ) + transaction.set_fee_exempt_keys([get_key_from_string(key) for key in params.feeExemptKeys]) if params.customFees is not None: - transaction.set_custom_fees( - [_build_custom_fee(custom_fee_params) for custom_fee_params in params.customFees] - ) + transaction.set_custom_fees([_build_custom_fee(custom_fee_params) for custom_fee_params in params.customFees]) return transaction @@ -96,6 +86,7 @@ def _get_auto_renew_account_state(client, auto_renew_account_id: str | None) -> return "deleted" return "unknown" + @rpc_method("createTopic") def create_topic(params: CreateTopicParams) -> CreateTopicResponse: client = get_client(params.sessionId) @@ -119,14 +110,9 @@ def create_topic(params: CreateTopicParams) -> CreateTopicResponse: receipt: TransactionReceipt = response.get_receipt(client, validate_status=True) except (PrecheckError, ReceiptStatusError) as exc: status = ResponseCode(exc.status).name - if ( - params.autoRenewAccountId - and status == "INVALID_SIGNATURE" - and auto_renew_account_state != "deleted" - ): - raise JsonRpcError.hiero_error({"status": "INVALID_AUTORENEW_ACCOUNT"}) - raise - + if params.autoRenewAccountId and status == "INVALID_SIGNATURE" and auto_renew_account_state != "deleted": + raise JsonRpcError.hiero_error({"status": "INVALID_AUTORENEW_ACCOUNT"}) from exc + raise JsonRpcError.hiero_error({"status": status}) from exc topic_id = "" if receipt.status == ResponseCode.SUCCESS and receipt.topic_id is not None: topic_id = str(receipt.topic_id) diff --git a/tck/param/common.py b/tck/param/common.py index 9f63075b0..31423b865 100644 --- a/tck/param/common.py +++ b/tck/param/common.py @@ -34,13 +34,9 @@ def apply_common_params(self, transaction: Transaction, client: Client) -> None: """Apply commonTransactionParams to a given transaction.""" if self.transactionId is not None: try: - transaction.set_transaction_id( - TransactionId.from_string(self.transactionId) - ) + transaction.set_transaction_id(TransactionId.from_string(self.transactionId)) except Exception: - transaction.set_transaction_id( - TransactionId.generate(AccountId.from_string(self.transactionId)) - ) + transaction.set_transaction_id(TransactionId.generate(AccountId.from_string(self.transactionId))) if self.maxTransactionFee is not None: transaction.transaction_fee = int(self.maxTransactionFee) diff --git a/tck/param/topic.py b/tck/param/topic.py index d17825966..17aea4c81 100644 --- a/tck/param/topic.py +++ b/tck/param/topic.py @@ -16,11 +16,12 @@ @dataclass class CreateTopicFixedFeeParams: """Parameters for a fixed fee custom fee in topic creation.""" + amount: int | None = None denominatingTokenId: str | None = None @classmethod - def parse_json_params(cls, params: dict) -> "CreateTopicFixedFeeParams": + def parse_json_params(cls, params: dict) -> CreateTopicFixedFeeParams: return cls( amount=to_int(params.get("amount")), denominatingTokenId=non_empty_string_or_none(params.get("denominatingTokenId")), @@ -30,12 +31,13 @@ def parse_json_params(cls, params: dict) -> "CreateTopicFixedFeeParams": @dataclass class CreateTopicCustomFeeParams: """Parameters for a custom fee in topic creation.""" + feeCollectorAccountId: str | None = None feeCollectorsExempt: bool | None = None fixedFee: CreateTopicFixedFeeParams | None = None @classmethod - def parse_json_params(cls, params: dict) -> "CreateTopicCustomFeeParams": + def parse_json_params(cls, params: dict) -> CreateTopicCustomFeeParams: fixed_fee = params.get("fixedFee") fee_collector_account_id = params.get("feeCollectorAccountId") @@ -45,17 +47,14 @@ def parse_json_params(cls, params: dict) -> "CreateTopicCustomFeeParams": return cls( feeCollectorAccountId=fee_collector_account_id, feeCollectorsExempt=to_bool(params.get("feeCollectorsExempt")), - fixedFee=( - CreateTopicFixedFeeParams.parse_json_params(fixed_fee) - if isinstance(fixed_fee, dict) - else None - ), + fixedFee=(CreateTopicFixedFeeParams.parse_json_params(fixed_fee) if isinstance(fixed_fee, dict) else None), ) @dataclass class CreateTopicParams(BaseTransactionParams): """Parameters for creating a topic. Extends BaseTransactionParams to include common transaction parameters.""" + memo: str | None = None adminKey: str | None = None submitKey: str | None = None @@ -66,7 +65,7 @@ class CreateTopicParams(BaseTransactionParams): customFees: list[CreateTopicCustomFeeParams] | None = None @classmethod - def parse_json_params(cls, params: dict) -> "CreateTopicParams": + def parse_json_params(cls, params: dict) -> CreateTopicParams: fee_exempt_keys = params.get("feeExemptKeys") if fee_exempt_keys is not None and not isinstance(fee_exempt_keys, list): raise ValueError("feeExemptKeys must be a list") @@ -85,10 +84,7 @@ def parse_json_params(cls, params: dict) -> "CreateTopicParams": feeScheduleKey=non_empty_string_or_none(params.get("feeScheduleKey")), feeExemptKeys=non_empty_string_list(fee_exempt_keys), customFees=( - [ - CreateTopicCustomFeeParams.parse_json_params(custom_fee) - for custom_fee in custom_fees - ] + [CreateTopicCustomFeeParams.parse_json_params(custom_fee) for custom_fee in custom_fees] if custom_fees is not None else None ), diff --git a/tck/util/param_utils.py b/tck/util/param_utils.py index 476d133f7..4e687ab6d 100644 --- a/tck/util/param_utils.py +++ b/tck/util/param_utils.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + def parse_session_id(params: dict) -> str: """Parse sessionId from the json rpc params.""" session_id = params.get("sessionId") @@ -16,9 +19,7 @@ def parse_common_transaction_params(params: dict): if common_params is None: return None - return CommonTransactionParams.parse_json_params( - params.get("commonTransactionParams") - ) + return CommonTransactionParams.parse_json_params(params.get("commonTransactionParams")) def to_int(value) -> int | None: @@ -28,6 +29,7 @@ def to_int(value) -> int | None: except (TypeError, ValueError): return None + def non_empty_string_or_none(value: str | None) -> str | None: """Trim string values; convert blank strings to None.""" if not isinstance(value, str): @@ -49,6 +51,7 @@ def non_empty_string_list(values) -> list[str] | None: return cleaned_values + def to_bool(value) -> bool | None: """Helper to convert value to bool.""" if isinstance(value, str): diff --git a/tests/tck/topic_handlers_test.py b/tests/tck/topic_handlers_test.py index fc1f88a34..b6591091f 100644 --- a/tests/tck/topic_handlers_test.py +++ b/tests/tck/topic_handlers_test.py @@ -1,4 +1,5 @@ """Focused createTopic tests for TCK handlers.""" + from __future__ import annotations from unittest.mock import MagicMock, patch @@ -27,7 +28,18 @@ def _params(**overrides): - base = dict(sessionId="test", memo=None, adminKey=None, submitKey=None, autoRenewPeriod=None, autoRenewAccountId=None, feeScheduleKey=None, feeExemptKeys=None, customFees=None, commonTransactionParams=None) + base = dict( + sessionId="test", + memo=None, + adminKey=None, + submitKey=None, + autoRenewPeriod=None, + autoRenewAccountId=None, + feeScheduleKey=None, + feeExemptKeys=None, + customFees=None, + commonTransactionParams=None, + ) base.update(overrides) return CreateTopicParams(**base) @@ -55,17 +67,40 @@ def test_parse_json_and_validation(): def test_build_custom_fee_and_reject_empty_collector(): - fee = _build_custom_fee(CreateTopicCustomFeeParams(feeCollectorAccountId="0.0.98", feeCollectorsExempt=True, fixedFee=CreateTopicFixedFeeParams(amount=100, denominatingTokenId="0.0.500"))) + fee = _build_custom_fee( + CreateTopicCustomFeeParams( + feeCollectorAccountId="0.0.98", + feeCollectorsExempt=True, + fixedFee=CreateTopicFixedFeeParams(amount=100, denominatingTokenId="0.0.500"), + ) + ) assert fee.amount == 100 assert fee.fee_collector_account_id == AccountId(0, 0, 98) assert fee.denominating_token_id == TokenId(0, 0, 500) assert fee.all_collectors_are_exempt is True with pytest.raises(ValueError, match="feeCollectorAccountId cannot be empty"): - _build_custom_fee(CreateTopicCustomFeeParams(feeCollectorAccountId="", fixedFee=CreateTopicFixedFeeParams(amount=1))) + _build_custom_fee( + CreateTopicCustomFeeParams(feeCollectorAccountId="", fixedFee=CreateTopicFixedFeeParams(amount=1)) + ) def test_build_transaction_core_fields(): - tx = _build_create_topic_transaction(_params(memo="m", adminKey=TEST_KEY, submitKey=TEST_KEY, autoRenewPeriod=7776000, autoRenewAccountId="0.0.98", feeScheduleKey=TEST_KEY, feeExemptKeys=[TEST_KEY], customFees=[CreateTopicCustomFeeParams(feeCollectorAccountId="0.0.98", fixedFee=CreateTopicFixedFeeParams(amount=100))])) + tx = _build_create_topic_transaction( + _params( + memo="m", + adminKey=TEST_KEY, + submitKey=TEST_KEY, + autoRenewPeriod=7776000, + autoRenewAccountId="0.0.98", + feeScheduleKey=TEST_KEY, + feeExemptKeys=[TEST_KEY], + customFees=[ + CreateTopicCustomFeeParams( + feeCollectorAccountId="0.0.98", fixedFee=CreateTopicFixedFeeParams(amount=100) + ) + ], + ) + ) assert tx.memo == "m" assert getattr(tx.auto_renew_period, "seconds", tx.auto_renew_period) == 7776000 assert len(tx.custom_fees) == 1 @@ -78,7 +113,9 @@ def test_build_transaction_core_fields(): (None, "0.0.98", "exists"), (PrecheckError(status=ResponseCode.INVALID_ACCOUNT_ID), "0.0.999", "missing"), ( - ReceiptStatusError(status=ResponseCode.ACCOUNT_DELETED, transaction_id=None, transaction_receipt=MagicMock()), + ReceiptStatusError( + status=ResponseCode.ACCOUNT_DELETED, transaction_id=None, transaction_receipt=MagicMock() + ), "0.0.98", "deleted", ), @@ -114,6 +151,23 @@ def test_create_topic_rejects_missing_auto_renew(_, mock_get_client, client_mock def test_create_topic_with_all_parameters(_, mock_get_client, client_mock): mock_get_client.return_value = client_mock with patch.object(TopicCreateTransaction, "execute", return_value=_success_response(2000)): - result = create_topic(_params(memo="Full", adminKey=TEST_KEY, submitKey=TEST_KEY, autoRenewPeriod=7776000, autoRenewAccountId="0.0.98", feeScheduleKey=TEST_KEY, feeExemptKeys=[TEST_KEY], customFees=[CreateTopicCustomFeeParams(feeCollectorAccountId="0.0.98", feeCollectorsExempt=True, fixedFee=CreateTopicFixedFeeParams(amount=500, denominatingTokenId="0.0.500"))])) + result = create_topic( + _params( + memo="Full", + adminKey=TEST_KEY, + submitKey=TEST_KEY, + autoRenewPeriod=7776000, + autoRenewAccountId="0.0.98", + feeScheduleKey=TEST_KEY, + feeExemptKeys=[TEST_KEY], + customFees=[ + CreateTopicCustomFeeParams( + feeCollectorAccountId="0.0.98", + feeCollectorsExempt=True, + fixedFee=CreateTopicFixedFeeParams(amount=500, denominatingTokenId="0.0.500"), + ) + ], + ) + ) assert result.status == "SUCCESS" assert result.topicId == "0.0.2000" From d7211e48f52906d3515df1f79f31cced5c93fdcf Mon Sep 17 00:00:00 2001 From: MonaaEid Date: Fri, 1 May 2026 17:33:06 +0300 Subject: [PATCH 3/6] feat: implement createTopic functionality and related parameters in TCK handlers Signed-off-by: MonaaEid --- tck/param/topic.py | 2 +- tests/tck/topic_handlers_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tck/param/topic.py b/tck/param/topic.py index 17aea4c81..c708d65e0 100644 --- a/tck/param/topic.py +++ b/tck/param/topic.py @@ -54,7 +54,7 @@ def parse_json_params(cls, params: dict) -> CreateTopicCustomFeeParams: @dataclass class CreateTopicParams(BaseTransactionParams): """Parameters for creating a topic. Extends BaseTransactionParams to include common transaction parameters.""" - + memo: str | None = None adminKey: str | None = None submitKey: str | None = None diff --git a/tests/tck/topic_handlers_test.py b/tests/tck/topic_handlers_test.py index b6591091f..3749b8228 100644 --- a/tests/tck/topic_handlers_test.py +++ b/tests/tck/topic_handlers_test.py @@ -81,7 +81,7 @@ def test_build_custom_fee_and_reject_empty_collector(): with pytest.raises(ValueError, match="feeCollectorAccountId cannot be empty"): _build_custom_fee( CreateTopicCustomFeeParams(feeCollectorAccountId="", fixedFee=CreateTopicFixedFeeParams(amount=1)) - ) + ) def test_build_transaction_core_fields(): From 1e8aa1d0400cce0716df8045072c2b2e125b736d Mon Sep 17 00:00:00 2001 From: MonaaEid Date: Mon, 11 May 2026 22:27:47 +0300 Subject: [PATCH 4/6] feat: implement createTopic functionality and related parameters in TCK handlers Signed-off-by: MonaaEid --- tck/handlers/topic.py | 43 +------- tck/param/topic.py | 2 +- tests/tck/topic_handlers_test.py | 173 ------------------------------- 3 files changed, 6 insertions(+), 212 deletions(-) delete mode 100644 tests/tck/topic_handlers_test.py diff --git a/tck/handlers/topic.py b/tck/handlers/topic.py index 2326b8bd9..ce6585b4b 100644 --- a/tck/handlers/topic.py +++ b/tck/handlers/topic.py @@ -2,13 +2,10 @@ from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.consensus.topic_create_transaction import TopicCreateTransaction -from hiero_sdk_python.exceptions import PrecheckError, ReceiptStatusError -from hiero_sdk_python.query.account_info_query import AccountInfoQuery from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee from hiero_sdk_python.tokens.token_id import TokenId from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt -from tck.errors import JsonRpcError from tck.handlers.registry import rpc_method from tck.param.topic import CreateTopicCustomFeeParams, CreateTopicParams from tck.response.topic import CreateTopicResponse @@ -56,7 +53,7 @@ def _build_create_topic_transaction(params: CreateTopicParams) -> TopicCreateTra transaction.set_auto_renew_period(params.autoRenewPeriod) if params.autoRenewAccountId: - transaction.set_auto_renew_account(AccountId.from_string(params.autoRenewAccountId)) + transaction.set_auto_renew_account(params.autoRenewAccountId) if params.feeScheduleKey: transaction.set_fee_schedule_key(get_key_from_string(params.feeScheduleKey)) @@ -70,49 +67,19 @@ def _build_create_topic_transaction(params: CreateTopicParams) -> TopicCreateTra return transaction -def _get_auto_renew_account_state(client, auto_renew_account_id: str | None) -> str: - if not auto_renew_account_id: - return "none" - - account_id = AccountId.from_string(auto_renew_account_id) - try: - AccountInfoQuery().set_account_id(account_id).execute(client) - return "exists" - except (PrecheckError, ReceiptStatusError) as exc: - status = ResponseCode(exc.status).name - if status == "INVALID_ACCOUNT_ID": - return "missing" - if status == "ACCOUNT_DELETED": - return "deleted" - return "unknown" - - @rpc_method("createTopic") def create_topic(params: CreateTopicParams) -> CreateTopicResponse: client = get_client(params.sessionId) - auto_renew_account_state = _get_auto_renew_account_state(client, params.autoRenewAccountId) - if auto_renew_account_state == "missing": - raise JsonRpcError.hiero_error({"status": "INVALID_AUTORENEW_ACCOUNT"}) - transaction = _build_create_topic_transaction(params) - # Align with TCK expectation: if no explicit auto-renew account is provided, - # default to the transaction payer/operator account. - if params.autoRenewAccountId is None and client is not None and client.operator_account_id is not None: - transaction.set_auto_renew_account(client.operator_account_id) - if params.commonTransactionParams is not None: params.commonTransactionParams.apply_common_params(transaction, client) - try: - response = transaction.execute(client, wait_for_receipt=False) - receipt: TransactionReceipt = response.get_receipt(client, validate_status=True) - except (PrecheckError, ReceiptStatusError) as exc: - status = ResponseCode(exc.status).name - if params.autoRenewAccountId and status == "INVALID_SIGNATURE" and auto_renew_account_state != "deleted": - raise JsonRpcError.hiero_error({"status": "INVALID_AUTORENEW_ACCOUNT"}) from exc - raise JsonRpcError.hiero_error({"status": status}) from exc + + response = transaction.execute(client, wait_for_receipt=False) + receipt: TransactionReceipt = response.get_receipt(client, validate_status=True) + topic_id = "" if receipt.status == ResponseCode.SUCCESS and receipt.topic_id is not None: topic_id = str(receipt.topic_id) diff --git a/tck/param/topic.py b/tck/param/topic.py index c708d65e0..6a73958b5 100644 --- a/tck/param/topic.py +++ b/tck/param/topic.py @@ -80,7 +80,7 @@ def parse_json_params(cls, params: dict) -> CreateTopicParams: adminKey=non_empty_string_or_none(params.get("adminKey")), submitKey=non_empty_string_or_none(params.get("submitKey")), autoRenewPeriod=to_int(params.get("autoRenewPeriod")), - autoRenewAccountId=non_empty_string_or_none(params.get("autoRenewAccountId")), + autoRenewAccountId=params.get("autoRenewAccountId"), feeScheduleKey=non_empty_string_or_none(params.get("feeScheduleKey")), feeExemptKeys=non_empty_string_list(fee_exempt_keys), customFees=( diff --git a/tests/tck/topic_handlers_test.py b/tests/tck/topic_handlers_test.py deleted file mode 100644 index 3749b8228..000000000 --- a/tests/tck/topic_handlers_test.py +++ /dev/null @@ -1,173 +0,0 @@ -"""Focused createTopic tests for TCK handlers.""" - -from __future__ import annotations - -from unittest.mock import MagicMock, patch - -import pytest - -from hiero_sdk_python.account.account_id import AccountId -from hiero_sdk_python.consensus.topic_create_transaction import TopicCreateTransaction -from hiero_sdk_python.exceptions import PrecheckError, ReceiptStatusError -from hiero_sdk_python.query.account_info_query import AccountInfoQuery -from hiero_sdk_python.response_code import ResponseCode -from hiero_sdk_python.tokens.token_id import TokenId -from tck.errors import JsonRpcError -from tck.handlers.topic import ( - _build_create_topic_transaction, - _build_custom_fee, - _get_auto_renew_account_state, - create_topic, -) -from tck.param.topic import CreateTopicCustomFeeParams, CreateTopicFixedFeeParams, CreateTopicParams - - -pytestmark = pytest.mark.unit - -TEST_KEY = "302e020100300506032b6570042204203e3e6e76b8f1ea10c1d7aaada2fcc088c08a82b65aa3299f66a1c7289ea3fcd2" - - -def _params(**overrides): - base = dict( - sessionId="test", - memo=None, - adminKey=None, - submitKey=None, - autoRenewPeriod=None, - autoRenewAccountId=None, - feeScheduleKey=None, - feeExemptKeys=None, - customFees=None, - commonTransactionParams=None, - ) - base.update(overrides) - return CreateTopicParams(**base) - - -def _success_response(topic_num: int): - response = MagicMock() - receipt = MagicMock() - receipt.status = ResponseCode.SUCCESS - receipt.topic_id = AccountId(0, 0, topic_num) - response.get_receipt.return_value = receipt - return response - - -@pytest.fixture -def client_mock(): - client = MagicMock() - client.operator_account_id = AccountId(0, 0, 3) - return client - - -def test_parse_json_and_validation(): - assert CreateTopicParams.parse_json_params({"sessionId": "s", "memo": "Topic"}).memo == "Topic" - with pytest.raises(ValueError, match="customFees must be a list"): - CreateTopicParams.parse_json_params({"sessionId": "s", "customFees": "bad"}) - - -def test_build_custom_fee_and_reject_empty_collector(): - fee = _build_custom_fee( - CreateTopicCustomFeeParams( - feeCollectorAccountId="0.0.98", - feeCollectorsExempt=True, - fixedFee=CreateTopicFixedFeeParams(amount=100, denominatingTokenId="0.0.500"), - ) - ) - assert fee.amount == 100 - assert fee.fee_collector_account_id == AccountId(0, 0, 98) - assert fee.denominating_token_id == TokenId(0, 0, 500) - assert fee.all_collectors_are_exempt is True - with pytest.raises(ValueError, match="feeCollectorAccountId cannot be empty"): - _build_custom_fee( - CreateTopicCustomFeeParams(feeCollectorAccountId="", fixedFee=CreateTopicFixedFeeParams(amount=1)) - ) - - -def test_build_transaction_core_fields(): - tx = _build_create_topic_transaction( - _params( - memo="m", - adminKey=TEST_KEY, - submitKey=TEST_KEY, - autoRenewPeriod=7776000, - autoRenewAccountId="0.0.98", - feeScheduleKey=TEST_KEY, - feeExemptKeys=[TEST_KEY], - customFees=[ - CreateTopicCustomFeeParams( - feeCollectorAccountId="0.0.98", fixedFee=CreateTopicFixedFeeParams(amount=100) - ) - ], - ) - ) - assert tx.memo == "m" - assert getattr(tx.auto_renew_period, "seconds", tx.auto_renew_period) == 7776000 - assert len(tx.custom_fees) == 1 - - -@pytest.mark.parametrize( - "side_effect,account_id,expected", - [ - (None, None, "none"), - (None, "0.0.98", "exists"), - (PrecheckError(status=ResponseCode.INVALID_ACCOUNT_ID), "0.0.999", "missing"), - ( - ReceiptStatusError( - status=ResponseCode.ACCOUNT_DELETED, transaction_id=None, transaction_receipt=MagicMock() - ), - "0.0.98", - "deleted", - ), - ], -) -def test_auto_renew_account_state_mapping(client_mock, side_effect, account_id, expected): - if account_id is None: - assert _get_auto_renew_account_state(client_mock, account_id) == expected - return - with patch.object(AccountInfoQuery, "execute", side_effect=side_effect): - assert _get_auto_renew_account_state(client_mock, account_id) == expected - - -@patch("tck.handlers.topic.get_client") -def test_create_topic_success_minimal(mock_get_client, client_mock): - mock_get_client.return_value = client_mock - with patch.object(TopicCreateTransaction, "execute", return_value=_success_response(1000)): - result = create_topic(_params(memo="Test Topic")) - assert result.status == "SUCCESS" - assert result.topicId == "0.0.1000" - - -@patch("tck.handlers.topic.get_client") -@patch("tck.handlers.topic._get_auto_renew_account_state", return_value="missing") -def test_create_topic_rejects_missing_auto_renew(_, mock_get_client, client_mock): - mock_get_client.return_value = client_mock - with pytest.raises(JsonRpcError): - create_topic(_params(autoRenewAccountId="0.0.999")) - - -@patch("tck.handlers.topic.get_client") -@patch("tck.handlers.topic._get_auto_renew_account_state", return_value="exists") -def test_create_topic_with_all_parameters(_, mock_get_client, client_mock): - mock_get_client.return_value = client_mock - with patch.object(TopicCreateTransaction, "execute", return_value=_success_response(2000)): - result = create_topic( - _params( - memo="Full", - adminKey=TEST_KEY, - submitKey=TEST_KEY, - autoRenewPeriod=7776000, - autoRenewAccountId="0.0.98", - feeScheduleKey=TEST_KEY, - feeExemptKeys=[TEST_KEY], - customFees=[ - CreateTopicCustomFeeParams( - feeCollectorAccountId="0.0.98", - feeCollectorsExempt=True, - fixedFee=CreateTopicFixedFeeParams(amount=500, denominatingTokenId="0.0.500"), - ) - ], - ) - ) - assert result.status == "SUCCESS" - assert result.topicId == "0.0.2000" From 8d3938ee2d2b7d43864aea6b34115658e221b006 Mon Sep 17 00:00:00 2001 From: MonaaEid Date: Tue, 12 May 2026 01:55:41 +0300 Subject: [PATCH 5/6] feat: implement createTopic functionality and related parameters in TCK handlers Signed-off-by: MonaaEid --- tck/handlers/topic.py | 16 +++++++++------- tck/param/topic.py | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/tck/handlers/topic.py b/tck/handlers/topic.py index ce6585b4b..77aca2742 100644 --- a/tck/handlers/topic.py +++ b/tck/handlers/topic.py @@ -43,22 +43,22 @@ def _build_create_topic_transaction(params: CreateTopicParams) -> TopicCreateTra if params.memo is not None: transaction.set_memo(params.memo) - if params.adminKey: + if params.adminKey is not None: transaction.set_admin_key(get_key_from_string(params.adminKey)) - if params.submitKey: + if params.submitKey is not None: transaction.set_submit_key(get_key_from_string(params.submitKey)) if params.autoRenewPeriod is not None: transaction.set_auto_renew_period(params.autoRenewPeriod) - if params.autoRenewAccountId: - transaction.set_auto_renew_account(params.autoRenewAccountId) + if params.autoRenewAccountId is not None: + transaction.set_auto_renew_account(AccountId.from_string(params.autoRenewAccountId)) - if params.feeScheduleKey: + if params.feeScheduleKey is not None: transaction.set_fee_schedule_key(get_key_from_string(params.feeScheduleKey)) - if params.feeExemptKeys: + if params.feeExemptKeys is not None: transaction.set_fee_exempt_keys([get_key_from_string(key) for key in params.feeExemptKeys]) if params.customFees is not None: @@ -73,10 +73,12 @@ def create_topic(params: CreateTopicParams) -> CreateTopicResponse: transaction = _build_create_topic_transaction(params) + if params.autoRenewAccountId is None and client is not None and client.operator_account_id is not None: + transaction.set_auto_renew_account(client.operator_account_id) + if params.commonTransactionParams is not None: params.commonTransactionParams.apply_common_params(transaction, client) - response = transaction.execute(client, wait_for_receipt=False) receipt: TransactionReceipt = response.get_receipt(client, validate_status=True) diff --git a/tck/param/topic.py b/tck/param/topic.py index 6a73958b5..2133ace87 100644 --- a/tck/param/topic.py +++ b/tck/param/topic.py @@ -4,8 +4,8 @@ from tck.param.base import BaseTransactionParams from tck.util.param_utils import ( - non_empty_string_list, non_empty_string_or_none, + non_empty_string_list, parse_common_transaction_params, parse_session_id, to_bool, @@ -80,7 +80,7 @@ def parse_json_params(cls, params: dict) -> CreateTopicParams: adminKey=non_empty_string_or_none(params.get("adminKey")), submitKey=non_empty_string_or_none(params.get("submitKey")), autoRenewPeriod=to_int(params.get("autoRenewPeriod")), - autoRenewAccountId=params.get("autoRenewAccountId"), + autoRenewAccountId=non_empty_string_or_none(params.get("autoRenewAccountId")), feeScheduleKey=non_empty_string_or_none(params.get("feeScheduleKey")), feeExemptKeys=non_empty_string_list(fee_exempt_keys), customFees=( From a621641fcf62b049497cd97211f0b6e0e863a106 Mon Sep 17 00:00:00 2001 From: MonaaEid Date: Tue, 12 May 2026 02:02:21 +0300 Subject: [PATCH 6/6] feat: implement createTopic functionality and related parameters in TCK handlers Signed-off-by: MonaaEid --- tck/param/topic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tck/param/topic.py b/tck/param/topic.py index 2133ace87..c708d65e0 100644 --- a/tck/param/topic.py +++ b/tck/param/topic.py @@ -4,8 +4,8 @@ from tck.param.base import BaseTransactionParams from tck.util.param_utils import ( - non_empty_string_or_none, non_empty_string_list, + non_empty_string_or_none, parse_common_transaction_params, parse_session_id, to_bool,