Skip to content

Commit 9f5b724

Browse files
authored
feat(tck): Add submitTopicMessage method to TCK (hiero-ledger#2352)
Signed-off-by: Manish Dait <daitmanish88@gmail.com>
1 parent 3016392 commit 9f5b724

10 files changed

Lines changed: 283 additions & 160 deletions

File tree

src/hiero_sdk_python/consensus/topic_message_submit_transaction.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,9 @@ def _build_proto_body(self) -> consensus_submit_message_pb2.ConsensusSubmitMessa
183183
ConsensusSubmitMessageTransactionBody: The protobuf body for this transaction.
184184
185185
Raises:
186-
ValueError: If required fields (topic_id, message) are missing.
186+
ValueError: If required fields (message) are missing.
187187
"""
188-
if self.topic_id is None:
189-
raise ValueError("Missing required fields: topic_id.")
190-
if self.message is None:
188+
if self.message is None or self.message == "":
191189
raise ValueError("Missing required fields: message.")
192190

193191
content = self.message.encode("utf-8")
@@ -197,7 +195,7 @@ def _build_proto_body(self) -> consensus_submit_message_pb2.ConsensusSubmitMessa
197195
chunk_content = content[start_index:end_index]
198196

199197
body = consensus_submit_message_pb2.ConsensusSubmitMessageTransactionBody(
200-
topicID=self.topic_id._to_proto(), message=chunk_content
198+
topicID=self.topic_id._to_proto() if self.topic_id else None, message=chunk_content
201199
)
202200

203201
# Multi-chunk metadata

src/hiero_sdk_python/transaction/transaction.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ def __init__(self) -> None:
6464
# This allows us to maintain the signatures for each unique transaction
6565
# and ensures that the correct signatures are used when submitting transactions
6666
self._signature_map: dict[bytes, basic_types_pb2.SignatureMap] = {}
67-
# changed from int: 2_000_000 to Hbar: 0.02
68-
self._default_transaction_fee = Hbar(0.02)
67+
# changed from int: 2_000_000 to Hbar: 2
68+
self._default_transaction_fee = Hbar(2)
6969
self.operator_account_id = None
7070
self.batch_key: Key | None = None
7171

tck/handlers/token.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,12 @@
2424
from hiero_sdk_python.tokens.token_type import TokenType
2525
from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt
2626
from tck.handlers.registry import rpc_method
27+
from tck.param.custom_fee import CustomFeeParams, FixedFeeParams
2728
from tck.param.token import (
2829
AirdropTokenParams,
2930
AssociateTokenParams,
3031
CreateTokenParams,
31-
CustomFeeParams,
3232
DeleteTokenParams,
33-
FixedFeeParams,
3433
FreezeTokenParams,
3534
MintTokenParams,
3635
PauseTokenParams,

tck/handlers/topic.py

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,25 @@
22

33
from hiero_sdk_python.account.account_id import AccountId
44
from hiero_sdk_python.consensus.topic_create_transaction import TopicCreateTransaction
5+
from hiero_sdk_python.consensus.topic_id import TopicId
6+
from hiero_sdk_python.consensus.topic_message_submit_transaction import TopicMessageSubmitTransaction
57
from hiero_sdk_python.response_code import ResponseCode
68
from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee
79
from hiero_sdk_python.tokens.token_id import TokenId
10+
from hiero_sdk_python.transaction.custom_fee_limit import CustomFeeLimit
811
from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt
912
from tck.handlers.registry import rpc_method
10-
from tck.param.topic import CreateTopicCustomFeeParams, CreateTopicParams
11-
from tck.response.topic import CreateTopicResponse
13+
from tck.param.custom_fee import CustomFeeLimitParams, CustomFeeParams
14+
from tck.param.topic import CreateTopicParams, TopicMessageSubmitParams
15+
from tck.response.topic import CreateTopicResponse, TopicMessageSubmitResponse
1216
from tck.util.client_utils import get_client
1317
from tck.util.constants import DEFAULT_GRPC_TIMEOUT
1418
from tck.util.key_utils import get_key_from_string
1519

1620

17-
def _build_custom_fee(custom_fee_params: CreateTopicCustomFeeParams) -> CustomFixedFee:
21+
def _build_custom_fee(custom_fee_params: CustomFeeParams) -> CustomFixedFee:
1822
custom_fee = CustomFixedFee()
1923

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-
2424
if custom_fee_params.feeCollectorAccountId is not None:
2525
custom_fee.set_fee_collector_account_id(AccountId.from_string(custom_fee_params.feeCollectorAccountId))
2626

@@ -29,7 +29,7 @@ def _build_custom_fee(custom_fee_params: CreateTopicCustomFeeParams) -> CustomFi
2929

3030
if custom_fee_params.fixedFee is not None:
3131
if custom_fee_params.fixedFee.amount is not None:
32-
custom_fee.amount = custom_fee_params.fixedFee.amount
32+
custom_fee.amount = int(custom_fee_params.fixedFee.amount)
3333

3434
if custom_fee_params.fixedFee.denominatingTokenId:
3535
custom_fee.set_denominating_token_id(TokenId.from_string(custom_fee_params.fixedFee.denominatingTokenId))
@@ -84,3 +84,65 @@ def create_topic(params: CreateTopicParams) -> CreateTopicResponse:
8484
topic_id = str(receipt.topic_id)
8585

8686
return CreateTopicResponse(topic_id, ResponseCode(receipt.status).name)
87+
88+
89+
def _build_custom_fee_limit(params: CustomFeeLimitParams) -> CustomFeeLimit:
90+
"""Build custom fee limit from params."""
91+
custom_fee_limit = CustomFeeLimit()
92+
93+
if params.payerId is not None:
94+
custom_fee_limit.set_payer_id(AccountId.from_string(params.payerId))
95+
if params.fixedFees is not None:
96+
fixed_fees = []
97+
for fee in params.fixedFees:
98+
fixed_fee = CustomFixedFee()
99+
if fee.amount is not None:
100+
fixed_fee.set_amount_in_tinybars(int(fee.amount))
101+
102+
if fee.denominatingTokenId is not None:
103+
fixed_fee.set_denominating_token_id(TokenId.from_string(fee.denominatingTokenId))
104+
105+
fixed_fees.append(fixed_fee)
106+
107+
custom_fee_limit.set_custom_fees(fixed_fees)
108+
return custom_fee_limit
109+
110+
111+
def _build_topic_message_submit_transaction(params: TopicMessageSubmitParams) -> TopicMessageSubmitTransaction:
112+
"""Build topic message submit transaction from params."""
113+
transaction = TopicMessageSubmitTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT)
114+
115+
if params.topicId is not None:
116+
transaction.set_topic_id(TopicId.from_string(params.topicId))
117+
118+
if params.message is not None:
119+
transaction.set_message(params.message)
120+
121+
if params.maxChunks is not None:
122+
transaction.set_max_chunks(params.maxChunks)
123+
124+
if params.chunkSize is not None:
125+
transaction.set_chunk_size(params.chunkSize)
126+
127+
if params.customFeeLimits is not None:
128+
custom_fee_limits = [_build_custom_fee_limit(fee) for fee in params.customFeeLimits]
129+
130+
transaction.set_custom_fee_limits(custom_fee_limits)
131+
132+
return transaction
133+
134+
135+
@rpc_method("submitTopicMessage")
136+
def submit_topic_message(params: TopicMessageSubmitParams) -> TopicMessageSubmitResponse:
137+
"""Submit message to a topic."""
138+
client = get_client(params.sessionId)
139+
140+
transaction = _build_topic_message_submit_transaction(params)
141+
142+
if params.commonTransactionParams is not None:
143+
params.commonTransactionParams.apply_common_params(transaction, client)
144+
145+
response = transaction.execute(client, wait_for_receipt=False)
146+
receipt: TransactionReceipt = response.get_receipt(client, validate_status=True)
147+
148+
return TopicMessageSubmitResponse(ResponseCode(receipt.status).name)

tck/param/custom_fee.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
5+
from tck.util.param_utils import to_bool
6+
7+
8+
@dataclass
9+
class CustomFeeParams:
10+
"""Request parameters for CustomFee."""
11+
12+
feeCollectorAccountId: str | None = None
13+
feeCollectorsExempt: bool | None = None
14+
fixedFee: FixedFeeParams | None = None
15+
fractionalFee: FractionalFeeParams | None = None
16+
royaltyFee: RoyaltyFeeParams | None = None
17+
18+
@classmethod
19+
def parse_json_params(cls, params: dict) -> CustomFeeParams:
20+
"""Parse JSON-RPC params into a CustomFeeParams instance."""
21+
fixed_fee = params.get("fixedFee")
22+
fractional_fee = params.get("fractionalFee")
23+
royalty_fee = params.get("royaltyFee")
24+
25+
fee_collector_account_id = params.get("feeCollectorAccountId")
26+
27+
return cls(
28+
feeCollectorAccountId=fee_collector_account_id,
29+
feeCollectorsExempt=to_bool(params.get("feeCollectorsExempt")),
30+
fixedFee=(FixedFeeParams.parse_json_params(fixed_fee) if isinstance(fixed_fee, dict) else None),
31+
fractionalFee=(
32+
FractionalFeeParams.parse_json_params(fractional_fee) if isinstance(fractional_fee, dict) else None
33+
),
34+
royaltyFee=(RoyaltyFeeParams.parse_json_params(royalty_fee) if isinstance(royalty_fee, dict) else None),
35+
)
36+
37+
38+
@dataclass
39+
class FixedFeeParams:
40+
"""Request parameters for FixedFee."""
41+
42+
amount: str | None = None
43+
denominatingTokenId: str | None = None
44+
45+
@classmethod
46+
def parse_json_params(cls, params: dict) -> FixedFeeParams:
47+
"""Parse JSON-RPC params into a FixedFeeParams instance."""
48+
return cls(
49+
amount=params.get("amount"),
50+
denominatingTokenId=params.get("denominatingTokenId"),
51+
)
52+
53+
54+
@dataclass
55+
class FractionalFeeParams:
56+
"""Request parameters for FractionalFee."""
57+
58+
numerator: str | None = None
59+
denominator: str | None = None
60+
minimumAmount: str | None = None
61+
maximumAmount: str | None = None
62+
assessmentMethod: str | None = None
63+
64+
@classmethod
65+
def parse_json_params(cls, params: dict) -> FractionalFeeParams:
66+
"""Parse JSON-RPC params into a FractionalFeeParams instance."""
67+
return cls(
68+
numerator=params.get("numerator"),
69+
denominator=params.get("denominator"),
70+
minimumAmount=params.get("minimumAmount"),
71+
maximumAmount=params.get("maximumAmount"),
72+
assessmentMethod=params.get("assessmentMethod"),
73+
)
74+
75+
76+
@dataclass
77+
class RoyaltyFeeParams:
78+
"""Royalty custom fee parameters."""
79+
80+
numerator: str | None = None
81+
denominator: str | None = None
82+
fallbackFee: FixedFeeParams | None = None
83+
84+
@classmethod
85+
def parse_json_params(cls, params: dict) -> RoyaltyFeeParams:
86+
fallback_fee = params.get("fallbackFee")
87+
if fallback_fee is not None and not isinstance(fallback_fee, dict):
88+
raise ValueError("fallbackFee must be an object")
89+
90+
return cls(
91+
numerator=params.get("numerator"),
92+
denominator=params.get("denominator"),
93+
fallbackFee=FixedFeeParams.parse_json_params(fallback_fee) if fallback_fee is not None else None,
94+
)
95+
96+
97+
@dataclass
98+
class CustomFeeLimitParams:
99+
"""Request parameters for CustomFeeLimit."""
100+
101+
payerId: str | None = None
102+
fixedFees: list[FixedFeeParams] | None = None
103+
104+
@classmethod
105+
def parse_json_params(cls, params: dict) -> CustomFeeLimitParams:
106+
"""Parse JSON-RPC params into a CustomFeeLimitParams instance."""
107+
fixed_fees = params.get("fixedFees")
108+
109+
return cls(
110+
payerId=params.get("payerId"),
111+
fixedFees=(
112+
[
113+
FixedFeeParams.parse_json_params(fixed_fee) if isinstance(fixed_fee, dict) else None
114+
for fixed_fee in fixed_fees
115+
]
116+
if fixed_fees is not None
117+
else None
118+
),
119+
)

tck/param/token.py

Lines changed: 1 addition & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from dataclasses import dataclass
66

77
from tck.param.base import BaseTransactionParams
8+
from tck.param.custom_fee import CustomFeeParams
89
from tck.util.param_utils import (
910
parse_common_transaction_params,
1011
parse_session_id,
@@ -13,108 +14,6 @@
1314
)
1415

1516

16-
@dataclass
17-
class FixedFeeParams:
18-
"""Fixed custom fee parameters."""
19-
20-
amount: str | None = None
21-
denominatingTokenId: str | None = None
22-
23-
@classmethod
24-
def parse_json_params(cls, params: dict) -> FixedFeeParams:
25-
return cls(
26-
amount=params.get("amount"),
27-
denominatingTokenId=params.get("denominatingTokenId"),
28-
)
29-
30-
31-
@dataclass
32-
class FractionalFeeParams:
33-
"""Fractional custom fee parameters."""
34-
35-
numerator: str | None = None
36-
denominator: str | None = None
37-
minimumAmount: str | None = None
38-
maximumAmount: str | None = None
39-
assessmentMethod: str | None = None
40-
41-
@classmethod
42-
def parse_json_params(cls, params: dict) -> FractionalFeeParams:
43-
return cls(
44-
numerator=params.get("numerator"),
45-
denominator=params.get("denominator"),
46-
minimumAmount=params.get("minimumAmount"),
47-
maximumAmount=params.get("maximumAmount"),
48-
assessmentMethod=params.get("assessmentMethod"),
49-
)
50-
51-
52-
@dataclass
53-
class RoyaltyFeeParams:
54-
"""Royalty custom fee parameters."""
55-
56-
numerator: str | None = None
57-
denominator: str | None = None
58-
fallbackFee: FixedFeeParams | None = None
59-
60-
@classmethod
61-
def parse_json_params(cls, params: dict) -> RoyaltyFeeParams:
62-
fallback_fee = params.get("fallbackFee")
63-
if fallback_fee is not None and not isinstance(fallback_fee, dict):
64-
raise ValueError("fallbackFee must be an object")
65-
66-
return cls(
67-
numerator=params.get("numerator"),
68-
denominator=params.get("denominator"),
69-
fallbackFee=FixedFeeParams.parse_json_params(fallback_fee) if fallback_fee is not None else None,
70-
)
71-
72-
73-
@dataclass
74-
class CustomFeeParams:
75-
"""Token custom fee parameters."""
76-
77-
feeCollectorAccountId: str | None = None
78-
feeCollectorsExempt: bool | None = None
79-
fixedFee: FixedFeeParams | None = None
80-
fractionalFee: FractionalFeeParams | None = None
81-
royaltyFee: RoyaltyFeeParams | None = None
82-
83-
@classmethod
84-
def parse_json_params(cls, params: dict) -> CustomFeeParams:
85-
if not isinstance(params, dict):
86-
raise ValueError("each customFees item must be an object")
87-
88-
fee_values = {
89-
"fixedFee": params.get("fixedFee"),
90-
"fractionalFee": params.get("fractionalFee"),
91-
"royaltyFee": params.get("royaltyFee"),
92-
}
93-
present_fees = [name for name, value in fee_values.items() if value is not None]
94-
if len(present_fees) != 1:
95-
raise ValueError("custom fee must contain exactly one fee type")
96-
if not isinstance(fee_values[present_fees[0]], dict):
97-
raise ValueError(f"{present_fees[0]} must be an object")
98-
99-
return cls(
100-
feeCollectorAccountId=params.get("feeCollectorAccountId"),
101-
feeCollectorsExempt=to_bool(params.get("feeCollectorsExempt")),
102-
fixedFee=(
103-
FixedFeeParams.parse_json_params(fee_values["fixedFee"]) if fee_values["fixedFee"] is not None else None
104-
),
105-
fractionalFee=(
106-
FractionalFeeParams.parse_json_params(fee_values["fractionalFee"])
107-
if fee_values["fractionalFee"] is not None
108-
else None
109-
),
110-
royaltyFee=(
111-
RoyaltyFeeParams.parse_json_params(fee_values["royaltyFee"])
112-
if fee_values["royaltyFee"] is not None
113-
else None
114-
),
115-
)
116-
117-
11817
@dataclass
11918
class CreateTokenParams(BaseTransactionParams):
12019
"""Request parameters for the createToken endpoint."""

0 commit comments

Comments
 (0)