Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions tck/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
account,
key,
sdk, # setup, reset
topic,
)
from .registry import (
get_all_handlers,
Expand All @@ -18,7 +19,4 @@
"get_handler",
"get_all_handlers",
"safe_dispatch",
"account",
"key",
"sdk",
]
11 changes: 9 additions & 2 deletions tck/handlers/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,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 {
Expand Down Expand Up @@ -62,7 +69,7 @@ def _handle_private_key(params: KeyGenerationParams, response: KeyGenerationResp

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()
Expand All @@ -72,7 +79,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:
Expand Down
89 changes: 89 additions & 0 deletions tck/handlers/topic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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.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.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
Comment thread
MonaaEid marked this conversation as resolved.


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 is not None:
transaction.set_admin_key(get_key_from_string(params.adminKey))

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 is not None:
transaction.set_auto_renew_account(AccountId.from_string(params.autoRenewAccountId))

if params.feeScheduleKey is not None:
transaction.set_fee_schedule_key(get_key_from_string(params.feeScheduleKey))

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:
transaction.set_custom_fees([_build_custom_fee(custom_fee_params) for custom_fee_params in params.customFees])

return transaction
Comment thread
MonaaEid marked this conversation as resolved.


@rpc_method("createTopic")
def create_topic(params: CreateTopicParams) -> CreateTopicResponse:
client = get_client(params.sessionId)

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)
Comment thread
MonaaEid marked this conversation as resolved.

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)

return CreateTopicResponse(topic_id, ResponseCode(receipt.status).name)
3 changes: 2 additions & 1 deletion tck/param/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ def apply_common_params(self, transaction: Transaction, client: Client) -> None:
except Exception:
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)
Expand Down
93 changes: 93 additions & 0 deletions tck/param/topic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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")),
)
Comment thread
MonaaEid marked this conversation as resolved.


@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),
)
9 changes: 9 additions & 0 deletions tck/response/topic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from __future__ import annotations

from dataclasses import dataclass


@dataclass
class CreateTopicResponse:
topicId: str | None = None
status: str | None = None
22 changes: 22 additions & 0 deletions tck/util/param_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,28 @@ def to_int(value) -> int | None:
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
Comment thread
manishdait marked this conversation as resolved.


def to_bool(value) -> bool | None:
"""Helper to convert value to bool."""
if isinstance(value, str):
Expand Down
Loading