Skip to content

Commit 05719bd

Browse files
authored
feat: implement endpoint for createTopic method (#2215)
Signed-off-by: MonaaEid <monaa_eid@hotmail.com>
1 parent 3a5600c commit 05719bd

7 files changed

Lines changed: 225 additions & 6 deletions

File tree

tck/handlers/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
account,
77
key,
88
sdk, # setup, reset
9+
topic,
910
)
1011
from .registry import (
1112
get_all_handlers,
@@ -18,7 +19,4 @@
1819
"get_handler",
1920
"get_all_handlers",
2021
"safe_dispatch",
21-
"account",
22-
"key",
23-
"sdk",
2422
]

tck/handlers/key.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
from tck.util.key_utils import KeyType, get_key_from_string
1111

1212

13+
def _format_generated_public_key(public_key: PublicKey) -> str:
14+
"""Return DER-hex output expected by TCK for generated public keys."""
15+
if public_key.is_ecdsa():
16+
return public_key.to_string_der_ecdsa_compressed()
17+
return public_key.to_string_der()
18+
19+
1320
@rpc_method("generateKey")
1421
def generate_key(params: KeyGenerationParams) -> KeyGenerationResponse:
1522
if params.fromKey and params.type not in {
@@ -62,7 +69,7 @@ def _handle_private_key(params: KeyGenerationParams, response: KeyGenerationResp
6269

6370
def _handle_public_key(params: KeyGenerationParams, response: KeyGenerationResponse, is_list: bool) -> str:
6471
if params.fromKey:
65-
return PrivateKey.from_string(params.fromKey).public_key().to_string_der()
72+
return _format_generated_public_key(PrivateKey.from_string(params.fromKey).public_key())
6673

6774
if params.type == KeyType.ED25519_PUBLIC_KEY:
6875
private_key = PrivateKey.generate_ed25519()
@@ -72,7 +79,7 @@ def _handle_public_key(params: KeyGenerationParams, response: KeyGenerationRespo
7279
if is_list:
7380
response.privateKeys.append(private_key.to_string_der())
7481

75-
return private_key.public_key().to_string_der()
82+
return _format_generated_public_key(private_key.public_key())
7683

7784

7885
def _handle_key_list(params: KeyGenerationParams, response: KeyGenerationResponse) -> str:

tck/handlers/topic.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from __future__ import annotations
2+
3+
from hiero_sdk_python.account.account_id import AccountId
4+
from hiero_sdk_python.consensus.topic_create_transaction import TopicCreateTransaction
5+
from hiero_sdk_python.response_code import ResponseCode
6+
from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee
7+
from hiero_sdk_python.tokens.token_id import TokenId
8+
from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt
9+
from tck.handlers.registry import rpc_method
10+
from tck.param.topic import CreateTopicCustomFeeParams, CreateTopicParams
11+
from tck.response.topic import CreateTopicResponse
12+
from tck.util.client_utils import get_client
13+
from tck.util.constants import DEFAULT_GRPC_TIMEOUT
14+
from tck.util.key_utils import get_key_from_string
15+
16+
17+
def _build_custom_fee(custom_fee_params: CreateTopicCustomFeeParams) -> CustomFixedFee:
18+
custom_fee = CustomFixedFee()
19+
20+
if custom_fee_params.feeCollectorAccountId == "":
21+
# Keep TCK behavior: explicitly empty collector should surface as internal error.
22+
raise ValueError("feeCollectorAccountId cannot be empty")
23+
24+
if custom_fee_params.feeCollectorAccountId is not None:
25+
custom_fee.set_fee_collector_account_id(AccountId.from_string(custom_fee_params.feeCollectorAccountId))
26+
27+
if custom_fee_params.feeCollectorsExempt:
28+
custom_fee.set_all_collectors_are_exempt(custom_fee_params.feeCollectorsExempt)
29+
30+
if custom_fee_params.fixedFee is not None:
31+
if custom_fee_params.fixedFee.amount is not None:
32+
custom_fee.amount = custom_fee_params.fixedFee.amount
33+
34+
if custom_fee_params.fixedFee.denominatingTokenId:
35+
custom_fee.set_denominating_token_id(TokenId.from_string(custom_fee_params.fixedFee.denominatingTokenId))
36+
37+
return custom_fee
38+
39+
40+
def _build_create_topic_transaction(params: CreateTopicParams) -> TopicCreateTransaction:
41+
transaction = TopicCreateTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT)
42+
43+
if params.memo is not None:
44+
transaction.set_memo(params.memo)
45+
46+
if params.adminKey is not None:
47+
transaction.set_admin_key(get_key_from_string(params.adminKey))
48+
49+
if params.submitKey is not None:
50+
transaction.set_submit_key(get_key_from_string(params.submitKey))
51+
52+
if params.autoRenewPeriod is not None:
53+
transaction.set_auto_renew_period(params.autoRenewPeriod)
54+
55+
if params.autoRenewAccountId is not None:
56+
transaction.set_auto_renew_account(AccountId.from_string(params.autoRenewAccountId))
57+
58+
if params.feeScheduleKey is not None:
59+
transaction.set_fee_schedule_key(get_key_from_string(params.feeScheduleKey))
60+
61+
if params.feeExemptKeys is not None:
62+
transaction.set_fee_exempt_keys([get_key_from_string(key) for key in params.feeExemptKeys])
63+
64+
if params.customFees is not None:
65+
transaction.set_custom_fees([_build_custom_fee(custom_fee_params) for custom_fee_params in params.customFees])
66+
67+
return transaction
68+
69+
70+
@rpc_method("createTopic")
71+
def create_topic(params: CreateTopicParams) -> CreateTopicResponse:
72+
client = get_client(params.sessionId)
73+
74+
transaction = _build_create_topic_transaction(params)
75+
76+
if params.autoRenewAccountId is None and client is not None and client.operator_account_id is not None:
77+
transaction.set_auto_renew_account(client.operator_account_id)
78+
79+
if params.commonTransactionParams is not None:
80+
params.commonTransactionParams.apply_common_params(transaction, client)
81+
82+
response = transaction.execute(client, wait_for_receipt=False)
83+
receipt: TransactionReceipt = response.get_receipt(client, validate_status=True)
84+
85+
topic_id = ""
86+
if receipt.status == ResponseCode.SUCCESS and receipt.topic_id is not None:
87+
topic_id = str(receipt.topic_id)
88+
89+
return CreateTopicResponse(topic_id, ResponseCode(receipt.status).name)

tck/param/common.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ def apply_common_params(self, transaction: Transaction, client: Client) -> None:
3838
except Exception:
3939
transaction.set_transaction_id(TransactionId.generate(AccountId.from_string(self.transactionId)))
4040

41-
# TODO add a max_transaction_fee sdk missing func
41+
if self.maxTransactionFee is not None:
42+
transaction.transaction_fee = int(self.maxTransactionFee)
4243

4344
if self.validTransactionDuration is not None:
4445
transaction.set_transaction_valid_duration(self.validTransactionDuration)

tck/param/topic.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
5+
from tck.param.base import BaseTransactionParams
6+
from tck.util.param_utils import (
7+
non_empty_string_list,
8+
non_empty_string_or_none,
9+
parse_common_transaction_params,
10+
parse_session_id,
11+
to_bool,
12+
to_int,
13+
)
14+
15+
16+
@dataclass
17+
class CreateTopicFixedFeeParams:
18+
"""Parameters for a fixed fee custom fee in topic creation."""
19+
20+
amount: int | None = None
21+
denominatingTokenId: str | None = None
22+
23+
@classmethod
24+
def parse_json_params(cls, params: dict) -> CreateTopicFixedFeeParams:
25+
return cls(
26+
amount=to_int(params.get("amount")),
27+
denominatingTokenId=non_empty_string_or_none(params.get("denominatingTokenId")),
28+
)
29+
30+
31+
@dataclass
32+
class CreateTopicCustomFeeParams:
33+
"""Parameters for a custom fee in topic creation."""
34+
35+
feeCollectorAccountId: str | None = None
36+
feeCollectorsExempt: bool | None = None
37+
fixedFee: CreateTopicFixedFeeParams | None = None
38+
39+
@classmethod
40+
def parse_json_params(cls, params: dict) -> CreateTopicCustomFeeParams:
41+
fixed_fee = params.get("fixedFee")
42+
43+
fee_collector_account_id = params.get("feeCollectorAccountId")
44+
if isinstance(fee_collector_account_id, str):
45+
fee_collector_account_id = fee_collector_account_id.strip()
46+
47+
return cls(
48+
feeCollectorAccountId=fee_collector_account_id,
49+
feeCollectorsExempt=to_bool(params.get("feeCollectorsExempt")),
50+
fixedFee=(CreateTopicFixedFeeParams.parse_json_params(fixed_fee) if isinstance(fixed_fee, dict) else None),
51+
)
52+
53+
54+
@dataclass
55+
class CreateTopicParams(BaseTransactionParams):
56+
"""Parameters for creating a topic. Extends BaseTransactionParams to include common transaction parameters."""
57+
58+
memo: str | None = None
59+
adminKey: str | None = None
60+
submitKey: str | None = None
61+
autoRenewPeriod: int | None = None
62+
autoRenewAccountId: str | None = None
63+
feeScheduleKey: str | None = None
64+
feeExemptKeys: list[str] | None = None
65+
customFees: list[CreateTopicCustomFeeParams] | None = None
66+
67+
@classmethod
68+
def parse_json_params(cls, params: dict) -> CreateTopicParams:
69+
fee_exempt_keys = params.get("feeExemptKeys")
70+
if fee_exempt_keys is not None and not isinstance(fee_exempt_keys, list):
71+
raise ValueError("feeExemptKeys must be a list")
72+
73+
custom_fees = params.get("customFees")
74+
if custom_fees is not None and not isinstance(custom_fees, list):
75+
raise ValueError("customFees must be a list")
76+
if custom_fees is not None and any(not isinstance(custom_fee, dict) for custom_fee in custom_fees):
77+
raise ValueError("each customFees item must be an object")
78+
return cls(
79+
memo=params.get("memo"),
80+
adminKey=non_empty_string_or_none(params.get("adminKey")),
81+
submitKey=non_empty_string_or_none(params.get("submitKey")),
82+
autoRenewPeriod=to_int(params.get("autoRenewPeriod")),
83+
autoRenewAccountId=non_empty_string_or_none(params.get("autoRenewAccountId")),
84+
feeScheduleKey=non_empty_string_or_none(params.get("feeScheduleKey")),
85+
feeExemptKeys=non_empty_string_list(fee_exempt_keys),
86+
customFees=(
87+
[CreateTopicCustomFeeParams.parse_json_params(custom_fee) for custom_fee in custom_fees]
88+
if custom_fees is not None
89+
else None
90+
),
91+
sessionId=parse_session_id(params),
92+
commonTransactionParams=parse_common_transaction_params(params),
93+
)

tck/response/topic.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
5+
6+
@dataclass
7+
class CreateTopicResponse:
8+
topicId: str | None = None
9+
status: str | None = None

tck/util/param_utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,28 @@ def to_int(value) -> int | None:
3030
return None
3131

3232

33+
def non_empty_string_or_none(value: str | None) -> str | None:
34+
"""Trim string values; convert blank strings to None."""
35+
if not isinstance(value, str):
36+
return value
37+
cleaned = value.strip()
38+
return cleaned if cleaned else None
39+
40+
41+
def non_empty_string_list(values) -> list[str] | None:
42+
"""Trim list entries and remove empty-string items."""
43+
if values is None:
44+
return None
45+
46+
cleaned_values: list[str] = []
47+
for value in values:
48+
cleaned = non_empty_string_or_none(value)
49+
if cleaned is not None:
50+
cleaned_values.append(cleaned)
51+
52+
return cleaned_values
53+
54+
3355
def to_bool(value) -> bool | None:
3456
"""Helper to convert value to bool."""
3557
if isinstance(value, str):

0 commit comments

Comments
 (0)