Skip to content

Commit e99b2ee

Browse files
committed
feat(tck): implement getScheduleInfo JSON-RPC method
Adds GetScheduleInfoParams and ScheduleInfoResponse following the getTopicInfo/getTokenInfo query-handler pattern, plus a getCost path that returns only the tinybars cost without executing the query. adminKey, executedAt and deletedAt stay nullable so a pending or non-deleted schedule serializes them as null rather than dropping them. Signed-off-by: tannaya7 <234580934+tannaya7@users.noreply.github.com>
1 parent c973743 commit e99b2ee

4 files changed

Lines changed: 277 additions & 4 deletions

File tree

tck/handlers/schedule.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
from typing import Any, cast
55

66
from hiero_sdk_python.account.account_id import AccountId
7+
from hiero_sdk_python.hbar import Hbar
78
from hiero_sdk_python.response_code import ResponseCode
89
from hiero_sdk_python.schedule.schedule_create_transaction import ScheduleCreateTransaction
910
from hiero_sdk_python.schedule.schedule_delete_transaction import ScheduleDeleteTransaction
1011
from hiero_sdk_python.schedule.schedule_id import ScheduleId
12+
from hiero_sdk_python.schedule.schedule_info import ScheduleInfo
13+
from hiero_sdk_python.schedule.schedule_info_query import ScheduleInfoQuery
1114
from hiero_sdk_python.schedule.schedule_sign_transaction import ScheduleSignTransaction
1215
from hiero_sdk_python.timestamp import Timestamp
1316
from hiero_sdk_python.transaction.transaction import Transaction
@@ -26,16 +29,22 @@
2629
from tck.param.schedule import (
2730
CreateScheduleParams,
2831
DeleteScheduleParams,
32+
GetScheduleInfoParams,
2933
ScheduledTransactionParams,
3034
SignScheduleParams,
3135
)
3236
from tck.param.token import BurnTokenParams, MintTokenParams
3337
from tck.param.topic import CreateTopicParams, TopicMessageSubmitParams
3438
from tck.param.transfer import TransferCryptoParams
35-
from tck.response.schedule import CreateScheduleResponse, DeleteScheduleResponse, SignScheduleResponse
39+
from tck.response.schedule import (
40+
CreateScheduleResponse,
41+
DeleteScheduleResponse,
42+
ScheduleInfoResponse,
43+
SignScheduleResponse,
44+
)
3645
from tck.util.client_utils import get_client
3746
from tck.util.constants import DEFAULT_GRPC_TIMEOUT
38-
from tck.util.key_utils import get_key_from_string
47+
from tck.util.key_utils import get_key_from_string, key_to_string
3948
from tck.util.param_utils import to_int
4049

4150

@@ -208,3 +217,50 @@ def delete_schedule(params: DeleteScheduleParams) -> DeleteScheduleResponse:
208217
receipt = response.get_receipt(client, validate_status=True)
209218

210219
return DeleteScheduleResponse(status=ResponseCode(receipt.status).name)
220+
221+
222+
@rpc_method("getScheduleInfo")
223+
def get_schedule_info(params: GetScheduleInfoParams) -> ScheduleInfoResponse:
224+
"""Get schedule info."""
225+
client = get_client(params.sessionId)
226+
227+
query = ScheduleInfoQuery().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT)
228+
229+
if params.scheduleId is not None:
230+
query.set_schedule_id(ScheduleId.from_string(params.scheduleId))
231+
232+
if params.queryPayment is not None:
233+
query.set_query_payment(Hbar.from_tinybars(int(params.queryPayment)))
234+
235+
if params.maxQueryPayment is not None:
236+
query.set_max_query_payment(Hbar.from_tinybars(int(params.maxQueryPayment)))
237+
238+
if params.getCost:
239+
cost = query.get_cost(client)
240+
return ScheduleInfoResponse(cost=str(cost.to_tinybars()))
241+
242+
schedule_info = query.execute(client)
243+
return _map_schedule_info_response(schedule_info)
244+
245+
246+
def _map_schedule_info_response(schedule_info: ScheduleInfo) -> ScheduleInfoResponse:
247+
"""Map ScheduleInfo to JSON-RPC ScheduleInfoResponse."""
248+
return ScheduleInfoResponse(
249+
scheduleId=str(schedule_info.schedule_id) if schedule_info.schedule_id is not None else None,
250+
creatorAccountId=(
251+
str(schedule_info.creator_account_id) if schedule_info.creator_account_id is not None else None
252+
),
253+
payerAccountId=str(schedule_info.payer_account_id) if schedule_info.payer_account_id is not None else None,
254+
scheduledTransactionId=(
255+
str(schedule_info.scheduled_transaction_id) if schedule_info.scheduled_transaction_id is not None else None
256+
),
257+
signers=[key_to_string(signer) for signer in schedule_info.signers],
258+
adminKey=key_to_string(schedule_info.admin_key) if schedule_info.admin_key is not None else None,
259+
expirationTime=(
260+
str(schedule_info.expiration_time.seconds) if schedule_info.expiration_time is not None else None
261+
),
262+
executedAt=str(schedule_info.executed_at.seconds) if schedule_info.executed_at is not None else None,
263+
deletedAt=str(schedule_info.deleted_at.seconds) if schedule_info.deleted_at is not None else None,
264+
scheduleMemo=schedule_info.schedule_memo,
265+
waitForExpiry=schedule_info.wait_for_expiry,
266+
)

tck/param/schedule.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from dataclasses import dataclass, field
44

5-
from tck.param.base import BaseTransactionParams
5+
from tck.param.base import BaseParams, BaseTransactionParams
66
from tck.util.param_utils import parse_common_transaction_params, parse_session_id, to_bool
77

88

@@ -93,3 +93,24 @@ def parse_json_params(cls, params: dict) -> DeleteScheduleParams:
9393
sessionId=parse_session_id(params),
9494
commonTransactionParams=parse_common_transaction_params(params),
9595
)
96+
97+
98+
@dataclass
99+
class GetScheduleInfoParams(BaseParams):
100+
"""Request parameters for the getScheduleInfo endpoint."""
101+
102+
scheduleId: str | None = None
103+
queryPayment: str | None = None
104+
maxQueryPayment: str | None = None
105+
getCost: bool | None = None
106+
107+
@classmethod
108+
def parse_json_params(cls, params: dict) -> GetScheduleInfoParams:
109+
"""Parse JSON-RPC params into a GetScheduleInfoParams instance."""
110+
return cls(
111+
scheduleId=params.get("scheduleId"),
112+
queryPayment=params.get("queryPayment"),
113+
maxQueryPayment=params.get("maxQueryPayment"),
114+
getCost=to_bool(params.get("getCost")),
115+
sessionId=parse_session_id(params),
116+
)

tck/response/schedule.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from dataclasses import dataclass
3+
from dataclasses import dataclass, field
44

55
from tck.response.base import StatusOnlyResponse
66

@@ -22,3 +22,21 @@ class SignScheduleResponse(StatusOnlyResponse):
2222
@dataclass
2323
class DeleteScheduleResponse(StatusOnlyResponse):
2424
"""Response payload for deleteSchedule."""
25+
26+
27+
@dataclass
28+
class ScheduleInfoResponse:
29+
"""Response payload for getScheduleInfo."""
30+
31+
scheduleId: str | None = None
32+
creatorAccountId: str | None = None
33+
payerAccountId: str | None = None
34+
scheduledTransactionId: str | None = None
35+
signers: list[str] = field(default_factory=list)
36+
adminKey: str | None = field(metadata={"nullable": True}, default=None)
37+
expirationTime: str | None = None
38+
executedAt: str | None = field(metadata={"nullable": True}, default=None)
39+
deletedAt: str | None = field(metadata={"nullable": True}, default=None)
40+
scheduleMemo: str | None = None
41+
waitForExpiry: bool | None = None
42+
cost: str | None = None

tests/tck/schedule_test.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
"""Test cases for the getScheduleInfo TCK handler and its parameter parsing."""
2+
3+
from __future__ import annotations
4+
5+
import importlib
6+
from unittest.mock import MagicMock, patch
7+
8+
import pytest
9+
10+
from hiero_sdk_python.account.account_id import AccountId
11+
from hiero_sdk_python.exceptions import PrecheckError
12+
from hiero_sdk_python.hbar import Hbar
13+
from hiero_sdk_python.schedule.schedule_id import ScheduleId
14+
from hiero_sdk_python.transaction.transaction_id import TransactionId
15+
from tck.handlers import registry, schedule as schedule_handlers
16+
from tck.handlers.schedule import get_schedule_info
17+
from tck.param.schedule import GetScheduleInfoParams
18+
from tck.response.schedule import ScheduleInfoResponse
19+
20+
21+
pytestmark = pytest.mark.unit
22+
23+
24+
def _mock_schedule_info(**overrides):
25+
"""A MagicMock standing in for a populated ScheduleInfo."""
26+
defaults = {
27+
"schedule_id": ScheduleId(0, 0, 100),
28+
"creator_account_id": AccountId(0, 0, 10),
29+
"payer_account_id": AccountId(0, 0, 20),
30+
"scheduled_transaction_id": TransactionId(AccountId(0, 0, 20), MagicMock(seconds=1700000000, nanos=0)),
31+
"signers": [],
32+
"admin_key": None,
33+
"expiration_time": MagicMock(seconds=1700000500),
34+
"executed_at": None,
35+
"deleted_at": None,
36+
"schedule_memo": "a memo",
37+
"wait_for_expiry": True,
38+
}
39+
defaults.update(overrides)
40+
return MagicMock(**defaults)
41+
42+
43+
def _mock_query():
44+
"""A MagicMock standing in for ScheduleInfoQuery's fluent setter chain."""
45+
query = MagicMock()
46+
query.set_grpc_deadline.return_value = query
47+
query.set_schedule_id.return_value = query
48+
query.set_query_payment.return_value = query
49+
query.set_max_query_payment.return_value = query
50+
return query
51+
52+
53+
def test_get_schedule_info_is_registered():
54+
# Re-import to re-run the @rpc_method decorator, in case another test module's
55+
# teardown (e.g. handlers_test.py) cleared the shared registry after this module
56+
# was first imported.
57+
importlib.reload(schedule_handlers)
58+
handler = registry.get_handler("getScheduleInfo")
59+
assert handler is not None and callable(handler)
60+
61+
62+
def test_parse_json_params():
63+
params = GetScheduleInfoParams.parse_json_params(
64+
{
65+
"sessionId": "session-1",
66+
"scheduleId": "0.0.100",
67+
"queryPayment": "1000",
68+
"maxQueryPayment": "5000",
69+
"getCost": True,
70+
}
71+
)
72+
73+
assert params.scheduleId == "0.0.100"
74+
assert params.queryPayment == "1000"
75+
assert params.maxQueryPayment == "5000"
76+
assert params.getCost is True
77+
78+
minimal = GetScheduleInfoParams.parse_json_params({"sessionId": "session-1"})
79+
assert minimal.scheduleId is None
80+
assert minimal.queryPayment is None
81+
assert minimal.maxQueryPayment is None
82+
assert minimal.getCost is None
83+
84+
85+
def test_get_schedule_info_maps_fields():
86+
params = GetScheduleInfoParams(sessionId="session-1", scheduleId="0.0.100")
87+
query = _mock_query()
88+
query.execute.return_value = _mock_schedule_info()
89+
90+
with (
91+
patch("tck.handlers.schedule.get_client", return_value=MagicMock()),
92+
patch("tck.handlers.schedule.ScheduleInfoQuery", return_value=query),
93+
):
94+
result = get_schedule_info(params)
95+
96+
query.set_schedule_id.assert_called_once_with(ScheduleId.from_string("0.0.100"))
97+
assert isinstance(result, ScheduleInfoResponse)
98+
assert result.scheduleId == "0.0.100"
99+
assert result.creatorAccountId == "0.0.10"
100+
assert result.payerAccountId == "0.0.20"
101+
assert result.scheduledTransactionId == "0.0.20@1700000000.0"
102+
assert result.expirationTime == "1700000500"
103+
assert result.scheduleMemo == "a memo"
104+
assert result.waitForExpiry is True
105+
assert result.cost is None
106+
107+
108+
def test_get_schedule_info_keeps_nullable_fields_as_none():
109+
"""A pending, non-deleted schedule must keep executedAt/deletedAt as None, not empty strings."""
110+
params = GetScheduleInfoParams(sessionId="session-1", scheduleId="0.0.100")
111+
query = _mock_query()
112+
query.execute.return_value = _mock_schedule_info(executed_at=None, deleted_at=None, admin_key=None)
113+
114+
with (
115+
patch("tck.handlers.schedule.get_client", return_value=MagicMock()),
116+
patch("tck.handlers.schedule.ScheduleInfoQuery", return_value=query),
117+
):
118+
result = get_schedule_info(params)
119+
120+
assert result.executedAt is None
121+
assert result.deletedAt is None
122+
assert result.adminKey is None
123+
124+
125+
def test_get_schedule_info_applies_query_payments():
126+
params = GetScheduleInfoParams(
127+
sessionId="session-1", scheduleId="0.0.100", queryPayment="1000", maxQueryPayment="5000"
128+
)
129+
query = _mock_query()
130+
query.execute.return_value = _mock_schedule_info()
131+
132+
with (
133+
patch("tck.handlers.schedule.get_client", return_value=MagicMock()),
134+
patch("tck.handlers.schedule.ScheduleInfoQuery", return_value=query),
135+
):
136+
get_schedule_info(params)
137+
138+
query.set_query_payment.assert_called_once_with(Hbar.from_tinybars(1000))
139+
query.set_max_query_payment.assert_called_once_with(Hbar.from_tinybars(5000))
140+
141+
142+
def test_get_schedule_info_get_cost_returns_cost_only():
143+
"""getCost=true must return only the cost, without executing the full query."""
144+
params = GetScheduleInfoParams(sessionId="session-1", scheduleId="0.0.100", getCost=True)
145+
query = _mock_query()
146+
query.get_cost.return_value = Hbar.from_tinybars(250)
147+
148+
with (
149+
patch("tck.handlers.schedule.get_client", return_value=MagicMock()),
150+
patch("tck.handlers.schedule.ScheduleInfoQuery", return_value=query),
151+
):
152+
result = get_schedule_info(params)
153+
154+
query.get_cost.assert_called_once()
155+
query.execute.assert_not_called()
156+
assert result == ScheduleInfoResponse(cost="250")
157+
158+
159+
def test_get_schedule_info_propagates_query_failure():
160+
"""A non-existent scheduleId (INVALID_SCHEDULE_ID) must surface, not be swallowed."""
161+
params = GetScheduleInfoParams(sessionId="session-1", scheduleId="1000000.0.0")
162+
query = _mock_query()
163+
query.execute.side_effect = PrecheckError(status=1, transaction_id="0.0.1@1.1", message="INVALID_SCHEDULE_ID")
164+
165+
with (
166+
patch("tck.handlers.schedule.get_client", return_value=MagicMock()),
167+
patch("tck.handlers.schedule.ScheduleInfoQuery", return_value=query),
168+
pytest.raises(PrecheckError),
169+
):
170+
get_schedule_info(params)
171+
172+
173+
def test_get_schedule_info_invalid_schedule_id_raises():
174+
"""A malformed scheduleId should fail via ScheduleId.from_string before hitting the network."""
175+
params = GetScheduleInfoParams(sessionId="session-1", scheduleId="not-a-schedule-id")
176+
177+
with patch("tck.handlers.schedule.get_client", return_value=MagicMock()), pytest.raises(ValueError):
178+
get_schedule_info(params)

0 commit comments

Comments
 (0)