Skip to content

Commit dd58d74

Browse files
authored
feat(schedule): add signSchedule tck method (hiero-ledger#2630)
Signed-off-by: Ntege Daniel <danientege785@gmail.com> Signed-off-by: Daniel Ntege <danientege785@gmail.com>
1 parent 13ea49d commit dd58d74

6 files changed

Lines changed: 90 additions & 13 deletions

File tree

src/hiero_sdk_python/schedule/schedule_sign_transaction.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,14 @@ def _build_proto_body(self):
6161
"""
6262
Returns the protobuf body for the schedule sign transaction.
6363
64+
An unset schedule ID is omitted from the body rather than rejected locally, so the
65+
network answers with INVALID_SCHEDULE_ID as it does for the other SDKs.
66+
6467
Returns:
6568
ScheduleSignTransactionBody: The protobuf body for this transaction.
66-
67-
Raises:
68-
ValueError: If schedule_id is not set.
6969
"""
70-
if self.schedule_id is None:
71-
raise ValueError("Missing required ScheduleID")
72-
7370
return ScheduleSignTransactionBody(
74-
scheduleID=self.schedule_id._to_proto(),
71+
scheduleID=self.schedule_id._to_proto() if self.schedule_id is not None else None,
7572
)
7673

7774
def build_transaction_body(self):

tck/handlers/schedule.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from hiero_sdk_python.schedule.schedule_create_transaction import ScheduleCreateTransaction
99
from hiero_sdk_python.schedule.schedule_delete_transaction import ScheduleDeleteTransaction
1010
from hiero_sdk_python.schedule.schedule_id import ScheduleId
11+
from hiero_sdk_python.schedule.schedule_sign_transaction import ScheduleSignTransaction
1112
from hiero_sdk_python.timestamp import Timestamp
1213
from hiero_sdk_python.transaction.transaction import Transaction
1314
from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt
@@ -22,11 +23,16 @@
2223
from tck.param.allowance import ApproveAllowanceParams
2324
from tck.param.base import BaseTransactionParams
2425
from tck.param.common import CommonTransactionParams
25-
from tck.param.schedule import CreateScheduleParams, DeleteScheduleParams, ScheduledTransactionParams
26+
from tck.param.schedule import (
27+
CreateScheduleParams,
28+
DeleteScheduleParams,
29+
ScheduledTransactionParams,
30+
SignScheduleParams,
31+
)
2632
from tck.param.token import BurnTokenParams, MintTokenParams
2733
from tck.param.topic import CreateTopicParams, TopicMessageSubmitParams
2834
from tck.param.transfer import TransferCryptoParams
29-
from tck.response.schedule import CreateScheduleResponse, DeleteScheduleResponse
35+
from tck.response.schedule import CreateScheduleResponse, DeleteScheduleResponse, SignScheduleResponse
3036
from tck.util.client_utils import get_client
3137
from tck.util.constants import DEFAULT_GRPC_TIMEOUT
3238
from tck.util.key_utils import get_key_from_string
@@ -118,6 +124,16 @@ def _build_create_schedule_transaction(params: CreateScheduleParams) -> Schedule
118124
return transaction
119125

120126

127+
def _build_sign_schedule_transaction(params: SignScheduleParams) -> ScheduleSignTransaction:
128+
"""Build a ScheduleSignTransaction from TCK params."""
129+
transaction = ScheduleSignTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT)
130+
131+
if params.scheduleId is not None:
132+
transaction.set_schedule_id(ScheduleId.from_string(params.scheduleId))
133+
134+
return transaction
135+
136+
121137
def _build_delete_schedule_transaction(params: DeleteScheduleParams) -> ScheduleDeleteTransaction:
122138
"""Builds a ScheduleDeleteTransaction from the provided parameters."""
123139
transaction = ScheduleDeleteTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT)
@@ -152,6 +168,32 @@ def create_schedule(params: CreateScheduleParams) -> CreateScheduleResponse:
152168
return CreateScheduleResponse(schedule_id, scheduled_transaction_id, ResponseCode(receipt.status).name)
153169

154170

171+
@rpc_method("signSchedule")
172+
def sign_schedule(params: SignScheduleParams) -> SignScheduleResponse:
173+
"""Sign a schedule."""
174+
common_params = params.commonTransactionParams
175+
if (
176+
common_params is not None
177+
and common_params.maxTransactionFee is not None
178+
and common_params.maxTransactionFee < 0
179+
):
180+
# Protobuf transactionFee is unsigned, so a negative TCK boundary value
181+
# cannot reach network precheck in this SDK.
182+
raise JsonRpcError.hiero_error({"status": ResponseCode.INSUFFICIENT_TX_FEE.name})
183+
184+
client = get_client(params.sessionId)
185+
186+
transaction = _build_sign_schedule_transaction(params)
187+
188+
if common_params is not None:
189+
common_params.apply_common_params(transaction, client)
190+
191+
response = transaction.execute(client, wait_for_receipt=False)
192+
receipt: TransactionReceipt = response.get_receipt(client, validate_status=True)
193+
194+
return SignScheduleResponse(status=ResponseCode(receipt.status).name)
195+
196+
155197
@rpc_method("deleteSchedule")
156198
def delete_schedule(params: DeleteScheduleParams) -> DeleteScheduleResponse:
157199
"""Handles the deleteSchedule JSON-RPC request."""

tck/param/schedule.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,22 @@ def parse_json_params(cls, params: dict) -> CreateScheduleParams:
6464
)
6565

6666

67+
@dataclass
68+
class SignScheduleParams(BaseTransactionParams):
69+
"""Request parameters for the signSchedule endpoint."""
70+
71+
scheduleId: str | None = None
72+
73+
@classmethod
74+
def parse_json_params(cls, params: dict) -> SignScheduleParams:
75+
"""Parse JSON-RPC params into a SignScheduleParams instance."""
76+
return cls(
77+
scheduleId=params.get("scheduleId"),
78+
sessionId=parse_session_id(params),
79+
commonTransactionParams=parse_common_transaction_params(params),
80+
)
81+
82+
6783
@dataclass
6884
class DeleteScheduleParams(BaseTransactionParams):
6985
"""Parse JSON-RPC params into a DeleteScheduleParams instance."""

tck/response/schedule.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ class CreateScheduleResponse:
1414
status: str | None = None
1515

1616

17+
@dataclass
18+
class SignScheduleResponse(StatusOnlyResponse):
19+
"""Response payload for signSchedule."""
20+
21+
1722
@dataclass
1823
class DeleteScheduleResponse(StatusOnlyResponse):
1924
"""Response payload for deleteSchedule."""

tests/integration/schedule_sign_transaction_e2e_test.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import pytest
88

9+
from hiero_sdk_python.exceptions import PrecheckError
910
from hiero_sdk_python.response_code import ResponseCode
1011
from hiero_sdk_python.schedule.schedule_id import ScheduleId
1112
from hiero_sdk_python.schedule.schedule_info_query import ScheduleInfoQuery
@@ -187,6 +188,17 @@ def test_integration_schedule_sign_transaction_fails_invalid_schedule_id(env):
187188
)
188189

189190

191+
@pytest.mark.integration
192+
def test_integration_schedule_sign_transaction_fails_without_schedule_id(env):
193+
"""Test that ScheduleSignTransaction fails precheck when no schedule ID is set.
194+
195+
The body omits scheduleID entirely, which the network rejects in pureChecks, so the
196+
failure arrives as a precheck error rather than a receipt status.
197+
"""
198+
with pytest.raises(PrecheckError, match="failed precheck with status: INVALID_SCHEDULE_ID"):
199+
ScheduleSignTransaction().freeze_with(env.client).sign(env.operator_key).execute(env.client)
200+
201+
190202
def test_integration_schedule_sign_transaction_fails_with_already_executed(env):
191203
"""Test that ScheduleSignTransaction fails when the schedule has already been executed."""
192204
account = env.create_account()

tests/unit/schedule_sign_transaction_test.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,17 @@ def test_build_proto_body_with_schedule_id(schedule_id):
7676
assert proto_body.scheduleID == schedule_id._to_proto()
7777

7878

79-
def test_build_proto_body_without_schedule_id_raises_error():
80-
"""Test building protobuf body without a schedule ID."""
79+
def test_build_proto_body_without_schedule_id_omits_field():
80+
"""Test that building a protobuf body without a schedule ID leaves the field unset.
81+
82+
The network rejects a body with no scheduleID with INVALID_SCHEDULE_ID, so the
83+
omission is left for it to answer instead of failing locally.
84+
"""
8185
schedule_sign_tx = ScheduleSignTransaction()
8286

83-
with pytest.raises(ValueError, match="Missing required ScheduleID"):
84-
schedule_sign_tx._build_proto_body()
87+
proto_body = schedule_sign_tx._build_proto_body()
88+
89+
assert not proto_body.HasField("scheduleID")
8590

8691

8792
def test_build_transaction_body_with_valid_schedule_id(mock_account_ids, schedule_id):

0 commit comments

Comments
 (0)