Skip to content

Commit 212178b

Browse files
authored
feat(tck): add createSchedule method (#2536)
Signed-off-by: Ntege Daniel <danientege785@gmail.com>
1 parent 969a859 commit 212178b

4 files changed

Lines changed: 217 additions & 0 deletions

File tree

tck/handlers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
allowance,
88
file,
99
key,
10+
schedule,
1011
sdk, # setup, reset
1112
token,
1213
topic,

tck/handlers/schedule.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Callable
4+
from typing import Any, cast
5+
6+
from hiero_sdk_python.account.account_id import AccountId
7+
from hiero_sdk_python.response_code import ResponseCode
8+
from hiero_sdk_python.schedule.schedule_create_transaction import ScheduleCreateTransaction
9+
from hiero_sdk_python.timestamp import Timestamp
10+
from hiero_sdk_python.transaction.transaction import Transaction
11+
from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt
12+
from tck.errors import JsonRpcError
13+
from tck.handlers.account import _build_create_account_transaction
14+
from tck.handlers.allowance import _build_approve_allowance_transaction
15+
from tck.handlers.registry import rpc_method
16+
from tck.handlers.token import _build_burn_token_transaction, _build_mint_token_transaction
17+
from tck.handlers.topic import _build_create_topic_transaction, _build_topic_message_submit_transaction
18+
from tck.handlers.transfer import _build_transfer_transaction
19+
from tck.param.account import CreateAccountParams
20+
from tck.param.allowance import ApproveAllowanceParams
21+
from tck.param.base import BaseTransactionParams
22+
from tck.param.common import CommonTransactionParams
23+
from tck.param.schedule import CreateScheduleParams, ScheduledTransactionParams
24+
from tck.param.token import BurnTokenParams, MintTokenParams
25+
from tck.param.topic import CreateTopicParams, TopicMessageSubmitParams
26+
from tck.param.transfer import TransferCryptoParams
27+
from tck.response.schedule import CreateScheduleResponse
28+
from tck.util.client_utils import get_client
29+
from tck.util.constants import DEFAULT_GRPC_TIMEOUT
30+
from tck.util.key_utils import get_key_from_string
31+
from tck.util.param_utils import to_int
32+
33+
34+
# Maps a scheduled transaction method name to its params class and builder.
35+
# "submitMessage" is the name used by the TCK inside scheduledTransaction, while
36+
# "submitTopicMessage" is the top-level JSON-RPC method name; both are accepted.
37+
_SCHEDULABLE: dict[str, tuple[type[BaseTransactionParams], Callable[[Any], Transaction]]] = {
38+
"createAccount": (CreateAccountParams, _build_create_account_transaction),
39+
"transferCrypto": (TransferCryptoParams, _build_transfer_transaction),
40+
"submitMessage": (TopicMessageSubmitParams, _build_topic_message_submit_transaction),
41+
"submitTopicMessage": (TopicMessageSubmitParams, _build_topic_message_submit_transaction),
42+
"burnToken": (BurnTokenParams, _build_burn_token_transaction),
43+
"mintToken": (MintTokenParams, _build_mint_token_transaction),
44+
"approveAllowance": (ApproveAllowanceParams, _build_approve_allowance_transaction),
45+
"createTopic": (CreateTopicParams, _build_create_topic_transaction),
46+
}
47+
48+
49+
def _apply_schedulable_common_params(transaction: Transaction, common: CommonTransactionParams | None) -> None:
50+
"""Apply the common params that survive into a SchedulableTransactionBody.
51+
52+
A SchedulableTransactionBody carries only transactionFee and memo, so the rest of
53+
apply_common_params() would be silently discarded: transactionId and
54+
validTransactionDuration are not part of the body, and signers would freeze and sign a
55+
transaction that is never submitted. Schedule signers belong on the outer
56+
ScheduleCreateTransaction, which create_schedule() already handles.
57+
"""
58+
if common is None:
59+
return
60+
61+
if common.maxTransactionFee is not None:
62+
transaction.transaction_fee = int(common.maxTransactionFee)
63+
64+
if common.memo is not None:
65+
transaction.set_transaction_memo(common.memo)
66+
67+
68+
def _build_scheduled_transaction(params: ScheduledTransactionParams, session_id: str) -> Transaction:
69+
"""Build the inner transaction that a schedule wraps."""
70+
schedulable = _SCHEDULABLE.get(params.method)
71+
if schedulable is None:
72+
raise JsonRpcError.invalid_params_error(f"Unsupported scheduled transaction method: {params.method}")
73+
74+
params_class, build_transaction = schedulable
75+
76+
# The inner params object carries no sessionId of its own, so inherit the outer one.
77+
inner_json = dict(params.params)
78+
inner_json["sessionId"] = session_id
79+
80+
try:
81+
inner_params = cast(BaseTransactionParams, params_class.parse_json_params(inner_json))
82+
except (TypeError, ValueError) as e:
83+
raise JsonRpcError.invalid_params_error(str(e)) from e
84+
85+
transaction = build_transaction(inner_params)
86+
_apply_schedulable_common_params(transaction, inner_params.commonTransactionParams)
87+
88+
return transaction
89+
90+
91+
def _build_create_schedule_transaction(params: CreateScheduleParams) -> ScheduleCreateTransaction:
92+
"""Build a ScheduleCreateTransaction from TCK params."""
93+
transaction = ScheduleCreateTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT)
94+
95+
if params.scheduledTransaction is not None:
96+
transaction.set_scheduled_transaction(
97+
_build_scheduled_transaction(params.scheduledTransaction, params.sessionId)
98+
)
99+
100+
if params.memo is not None:
101+
transaction.set_schedule_memo(params.memo)
102+
103+
if params.adminKey is not None:
104+
transaction.set_admin_key(get_key_from_string(params.adminKey))
105+
106+
if params.payerAccountId is not None:
107+
# Passed through unchecked so an empty string surfaces as an SDK error.
108+
transaction.set_payer_account_id(AccountId.from_string(params.payerAccountId))
109+
110+
if params.expirationTime is not None:
111+
transaction.set_expiration_time(Timestamp(seconds=to_int(params.expirationTime), nanos=0))
112+
113+
if params.waitForExpiry is not None:
114+
transaction.set_wait_for_expiry(params.waitForExpiry)
115+
116+
return transaction
117+
118+
119+
@rpc_method("createSchedule")
120+
def create_schedule(params: CreateScheduleParams) -> CreateScheduleResponse:
121+
"""Create a schedule."""
122+
client = get_client(params.sessionId)
123+
124+
transaction = _build_create_schedule_transaction(params)
125+
126+
if params.commonTransactionParams is not None:
127+
params.commonTransactionParams.apply_common_params(transaction, client)
128+
129+
response = transaction.execute(client, wait_for_receipt=False)
130+
receipt: TransactionReceipt = response.get_receipt(client, validate_status=True)
131+
132+
schedule_id = ""
133+
scheduled_transaction_id = None
134+
if receipt.status == ResponseCode.SUCCESS:
135+
if receipt.schedule_id is not None:
136+
schedule_id = str(receipt.schedule_id)
137+
if receipt.scheduled_transaction_id is not None:
138+
scheduled_transaction_id = str(receipt.scheduled_transaction_id)
139+
140+
return CreateScheduleResponse(schedule_id, scheduled_transaction_id, ResponseCode(receipt.status).name)

tck/param/schedule.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass, field
4+
5+
from tck.param.base import BaseTransactionParams
6+
from tck.util.param_utils import parse_common_transaction_params, parse_session_id, to_bool
7+
8+
9+
@dataclass
10+
class ScheduledTransactionParams:
11+
"""The inner (schedulable) transaction of a createSchedule request."""
12+
13+
method: str | None = None
14+
params: dict = field(default_factory=dict)
15+
16+
@classmethod
17+
def parse_json_params(cls, params: dict) -> ScheduledTransactionParams:
18+
"""Parse JSON-RPC params into a ScheduledTransactionParams instance."""
19+
if not isinstance(params, dict):
20+
raise ValueError("scheduledTransaction must be an object")
21+
22+
method = params.get("method")
23+
if not isinstance(method, str) or not method.strip():
24+
raise ValueError("scheduledTransaction.method is required and must be a non-empty string")
25+
26+
inner_params = params.get("params")
27+
if inner_params is None:
28+
inner_params = {}
29+
if not isinstance(inner_params, dict):
30+
raise ValueError("scheduledTransaction.params must be an object")
31+
32+
return cls(method=method, params=inner_params)
33+
34+
35+
@dataclass
36+
class CreateScheduleParams(BaseTransactionParams):
37+
"""Request parameters for the createSchedule endpoint."""
38+
39+
scheduledTransaction: ScheduledTransactionParams | None = None
40+
memo: str | None = None
41+
adminKey: str | None = None
42+
payerAccountId: str | None = None
43+
expirationTime: str | None = None
44+
waitForExpiry: bool | None = None
45+
46+
@classmethod
47+
def parse_json_params(cls, params: dict) -> CreateScheduleParams:
48+
"""Parse JSON-RPC params into a CreateScheduleParams instance."""
49+
scheduled_transaction = params.get("scheduledTransaction")
50+
51+
return cls(
52+
scheduledTransaction=(
53+
ScheduledTransactionParams.parse_json_params(scheduled_transaction)
54+
if scheduled_transaction is not None
55+
else None
56+
),
57+
memo=params.get("memo"),
58+
adminKey=params.get("adminKey"),
59+
payerAccountId=params.get("payerAccountId"),
60+
expirationTime=params.get("expirationTime"),
61+
waitForExpiry=to_bool(params.get("waitForExpiry")),
62+
sessionId=parse_session_id(params),
63+
commonTransactionParams=parse_common_transaction_params(params),
64+
)

tck/response/schedule.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
5+
6+
@dataclass
7+
class CreateScheduleResponse:
8+
"""Response payload for createSchedule."""
9+
10+
scheduleId: str | None = None
11+
transactionId: str | None = None
12+
status: str | None = None

0 commit comments

Comments
 (0)