Skip to content

Commit 5f61d7f

Browse files
committed
feat(tck): implement updateContract JSON-RPC method
Signed-off-by: achintya2k5 <achintyasin@gmail.com>
1 parent 19e7f37 commit 5f61d7f

5 files changed

Lines changed: 185 additions & 22 deletions

File tree

src/hiero_sdk_python/contract/contract_update_transaction.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from hiero_sdk_python.crypto.key import Key
1515
from hiero_sdk_python.Duration import Duration
1616
from hiero_sdk_python.executable import _Method
17-
from hiero_sdk_python.hapi.services import contract_update_pb2, transaction_pb2
17+
from hiero_sdk_python.hapi.services import basic_types_pb2, contract_update_pb2, transaction_pb2
1818
from hiero_sdk_python.hapi.services.schedulable_transaction_body_pb2 import (
1919
SchedulableTransactionBody,
2020
)
@@ -190,6 +190,8 @@ def set_auto_renew_account_id(self, auto_renew_account_id: AccountId | None) ->
190190
"""
191191
Sets the new account ID that will be charged for the contract's auto-renewal.
192192
193+
Passing an AccountId of `0.0.0` clears the auto-renew account configuration.
194+
193195
Args:
194196
auto_renew_account_id (AccountId | None): The new account ID that will be
195197
charged for the contract's auto-renewal.
@@ -213,6 +215,7 @@ def set_staked_node_id(self, staked_node_id: int | None) -> ContractUpdateTransa
213215
"""
214216
self._require_not_frozen()
215217
self.staked_node_id = staked_node_id
218+
self.staked_account_id = None
216219
return self
217220

218221
def set_decline_reward(self, decline_reward: bool | None) -> ContractUpdateTransaction:
@@ -233,6 +236,10 @@ def set_staked_account_id(self, staked_account_id: AccountId | None) -> Contract
233236
"""
234237
Sets the new account ID to which the contract stakes.
235238
239+
This field is mutually exclusive with staked_node_id. Setting this will
240+
clear any previously set staked_node_id. Passing an AccountId of
241+
`0.0.0` removes staking and sends the sentinel AccountId (0.0.0) to the network.
242+
236243
Args:
237244
staked_account_id (AccountId | None): The new account ID to which the contract
238245
stakes.
@@ -242,27 +249,18 @@ def set_staked_account_id(self, staked_account_id: AccountId | None) -> Contract
242249
"""
243250
self._require_not_frozen()
244251
self.staked_account_id = staked_account_id
252+
self.staked_node_id = None
245253
return self
246254

247255
def _convert_to_proto(self, obj: Any | None) -> Any:
248-
"""Convert object to proto if it exists, otherwise return None."""
249-
return obj._to_proto() if obj else None
256+
"""Convert object to proto if it is not None, otherwise return None."""
257+
return obj._to_proto() if obj is not None else None
250258

251259
def _build_proto_body(self):
252260
"""
253261
Returns the protobuf body for the contract update transaction.
254-
255-
Returns:
256-
ContractUpdateTransactionBody: The protobuf body for this transaction.
257-
258-
Raises:
259-
ValueError: If contract_id is not set.
260262
"""
261-
if self.contract_id is None:
262-
raise ValueError("Missing required ContractID")
263-
264-
return contract_update_pb2.ContractUpdateTransactionBody(
265-
contractID=self.contract_id._to_proto(),
263+
body = contract_update_pb2.ContractUpdateTransactionBody(
266264
expirationTime=(self.expiration_time._to_protobuf() if self.expiration_time else None),
267265
adminKey=self.admin_key.to_proto_key() if self.admin_key else None,
268266
autoRenewPeriod=self._convert_to_proto(self.auto_renew_period),
@@ -273,11 +271,23 @@ def _build_proto_body(self):
273271
if self.max_automatic_token_associations is not None
274272
else None
275273
),
276-
staked_account_id=self._convert_to_proto(self.staked_account_id),
277-
auto_renew_account_id=self._convert_to_proto(self.auto_renew_account_id),
278274
decline_reward=(BoolValue(value=self.decline_reward) if self.decline_reward is not None else None),
279275
)
280276

277+
if self.contract_id is not None:
278+
body.contractID.CopyFrom(self.contract_id._to_proto())
279+
280+
if self.auto_renew_account_id is not None:
281+
if self.auto_renew_account_id == AccountId.from_string("0.0.0"):
282+
body.auto_renew_account_id.CopyFrom(basic_types_pb2.AccountID())
283+
else:
284+
body.auto_renew_account_id.CopyFrom(self.auto_renew_account_id._to_proto())
285+
286+
if self.staked_account_id is not None:
287+
body.staked_account_id.CopyFrom(self.staked_account_id._to_proto())
288+
289+
return body
290+
281291
def build_transaction_body(self) -> transaction_pb2.TransactionBody:
282292
"""
283293
Builds and returns the protobuf transaction body for contract update.

tck/handlers/contract.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22

33
from hiero_sdk_python.account.account_id import AccountId
44
from hiero_sdk_python.contract.contract_create_transaction import ContractCreateTransaction
5+
from hiero_sdk_python.contract.contract_id import ContractId
6+
from hiero_sdk_python.contract.contract_update_transaction import ContractUpdateTransaction
57
from hiero_sdk_python.Duration import Duration
68
from hiero_sdk_python.file.file_id import FileId
79
from hiero_sdk_python.response_code import ResponseCode
10+
from hiero_sdk_python.timestamp import Timestamp
11+
from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt
812
from tck.errors import JsonRpcError
913
from tck.handlers.registry import rpc_method
10-
from tck.param.contract import CreateContractParams
11-
from tck.response.contract import CreateContractResponse
14+
from tck.param.contract import CreateContractParams, UpdateContractParams
15+
from tck.response.contract import CreateContractResponse, UpdateContractResponse
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
@@ -89,6 +93,48 @@ def _build_create_contract_transaction(params: CreateContractParams) -> Contract
8993
return transaction
9094

9195

96+
def _build_update_contract_transaction(params: UpdateContractParams) -> ContractUpdateTransaction:
97+
"""
98+
Maps updateContract JSON-RPC params onto a ContractUpdateTransaction.
99+
100+
Only supplied params are applied, SDK defaults remain intact.
101+
"""
102+
103+
transaction = ContractUpdateTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT)
104+
105+
if params.contractId is not None:
106+
transaction.set_contract_id(ContractId.from_string(params.contractId))
107+
108+
if params.adminKey is not None:
109+
transaction.set_admin_key(get_key_from_string(params.adminKey))
110+
111+
if params.autoRenewPeriod is not None:
112+
transaction.set_auto_renew_period(Duration(_require_int64(params.autoRenewPeriod, "autoRenewPeriod")))
113+
114+
if params.expirationTime is not None:
115+
transaction.set_expiration_time(Timestamp(seconds=to_int(params.expirationTime), nanos=0))
116+
117+
if params.memo is not None:
118+
transaction.set_contract_memo(params.memo)
119+
120+
if params.autoRenewAccountId is not None:
121+
transaction.set_auto_renew_account_id(AccountId.from_string(params.autoRenewAccountId))
122+
123+
if params.maxAutomaticTokenAssociations is not None:
124+
transaction.set_max_automatic_token_associations(params.maxAutomaticTokenAssociations)
125+
126+
if params.stakedAccountId is not None:
127+
transaction.set_staked_account_id(AccountId.from_string(params.stakedAccountId))
128+
129+
if params.stakedNodeId is not None:
130+
transaction.set_staked_node_id(_require_int64(params.stakedNodeId, "stakedNodeId"))
131+
132+
if params.declineStakingReward is not None:
133+
transaction.set_decline_reward(params.declineStakingReward)
134+
135+
return transaction
136+
137+
92138
@rpc_method("createContract")
93139
def create_contract(params: CreateContractParams) -> CreateContractResponse:
94140
"""Create a smart contract."""
@@ -106,3 +152,19 @@ def create_contract(params: CreateContractParams) -> CreateContractResponse:
106152
contract_id = str(receipt.contract_id)
107153

108154
return CreateContractResponse(contract_id, ResponseCode(receipt.status).name)
155+
156+
157+
@rpc_method("updateContract")
158+
def update_contract(params: UpdateContractParams) -> UpdateContractResponse:
159+
"""Update a smart contract."""
160+
client = get_client(params.sessionId)
161+
162+
transaction = _build_update_contract_transaction(params)
163+
164+
if params.commonTransactionParams is not None:
165+
params.commonTransactionParams.apply_common_params(transaction, client)
166+
167+
response = transaction.execute(client, wait_for_receipt=False)
168+
receipt: TransactionReceipt = response.get_receipt(client, validate_status=True)
169+
170+
return UpdateContractResponse(status=ResponseCode(receipt.status).name)

tck/param/contract.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,36 @@ def parse_json_params(cls, params: dict) -> CreateContractParams:
4545
sessionId=parse_session_id(params),
4646
commonTransactionParams=parse_common_transaction_params(params),
4747
)
48+
49+
50+
@dataclass
51+
class UpdateContractParams(BaseTransactionParams):
52+
"""Parameters for updating a smart contract. Extends BaseTransactionParams to include CommonTransactionParams."""
53+
54+
contractId: str | None = None
55+
adminKey: str | None = None
56+
autoRenewPeriod: str | None = None
57+
expirationTime: str | None = None
58+
memo: str | None = None
59+
autoRenewAccountId: str | None = None
60+
maxAutomaticTokenAssociations: int | None = None
61+
stakedAccountId: str | None = None
62+
stakedNodeId: str | None = None
63+
declineStakingReward: bool | None = None
64+
65+
@classmethod
66+
def parse_json_params(cls, params: dict) -> UpdateContractParams:
67+
return cls(
68+
contractId=params.get("contractId"),
69+
adminKey=params.get("adminKey"),
70+
autoRenewPeriod=params.get("autoRenewPeriod"),
71+
expirationTime=params.get("expirationTime"),
72+
memo=params.get("memo"),
73+
autoRenewAccountId=params.get("autoRenewAccountId"),
74+
maxAutomaticTokenAssociations=params.get("maxAutomaticTokenAssociations"),
75+
stakedAccountId=params.get("stakedAccountId"),
76+
stakedNodeId=params.get("stakedNodeId"),
77+
declineStakingReward=params.get("declineStakingReward"),
78+
sessionId=parse_session_id(params),
79+
commonTransactionParams=parse_common_transaction_params(params),
80+
)

tck/response/contract.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22

33
from dataclasses import dataclass
44

5+
from tck.response.base import StatusOnlyResponse
6+
57

68
@dataclass
79
class CreateContractResponse:
810
"""Response payload for createContract."""
911

1012
contractId: str | None = None
1113
status: str | None = None
14+
15+
16+
@dataclass
17+
class UpdateContractResponse(StatusOnlyResponse):
18+
"""Response payload for updateContract."""

tests/unit/contract_update_transaction_test.py

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,34 @@ def test_set_decline_reward():
162162
assert result is tx # Method chaining
163163

164164

165+
def test_set_auto_renew_account_id_to_zero():
166+
"""Test setting auto renew account ID to 0.0.0 triggers clear flag."""
167+
tx = ContractUpdateTransaction()
168+
tx.set_auto_renew_account_id(AccountId(0, 0, 0))
169+
170+
assert tx.auto_renew_account_id == AccountId(0, 0, 0)
171+
172+
173+
def test_set_staked_account_id():
174+
"""Test setting a valid staked account ID."""
175+
tx = ContractUpdateTransaction()
176+
staked_account_id = AccountId(0, 0, 999)
177+
result = tx.set_staked_account_id(staked_account_id)
178+
179+
assert tx.staked_account_id == staked_account_id
180+
assert tx.staked_node_id is None
181+
assert result is tx # Method chaining
182+
183+
184+
def test_set_staked_account_id_to_zero():
185+
"""Test setting staked account ID to 0.0.0 clears staking."""
186+
tx = ContractUpdateTransaction()
187+
tx.set_staked_account_id(AccountId(0, 0, 0))
188+
189+
assert tx.staked_account_id == AccountId(0, 0, 0)
190+
assert tx.staked_node_id is None
191+
192+
165193
########### Method Chaining Tests ###########
166194

167195

@@ -227,13 +255,18 @@ def test_build_proto_body_with_generic_admin_key(contract_id, admin_key):
227255
assert proto_body.adminKey == admin_key.to_proto_key()
228256

229257

230-
def test_build_transaction_body_missing_contract_id():
231-
"""Test building transaction body without contract ID raises ValueError."""
258+
def test_build_transaction_body_missing_contract_id(mock_account_ids, transaction_id):
259+
"""Test building transaction body without contract ID omits the field."""
260+
_, _, node_account_id, _, _ = mock_account_ids
261+
232262
tx = ContractUpdateTransaction()
233263
tx.set_contract_memo("Test memo")
264+
tx.transaction_id = transaction_id
265+
tx.set_node_account_ids([node_account_id])
234266

235-
with pytest.raises(ValueError, match="Missing required ContractID"):
236-
tx.build_transaction_body()
267+
transaction_body = tx.build_transaction_body()
268+
269+
assert not transaction_body.contractUpdateInstance.HasField("contractID")
237270

238271

239272
def test_build_transaction_body_with_all_parameters(update_params, mock_account_ids, transaction_id):
@@ -299,6 +332,24 @@ def test_build_scheduled_body_with_all_parameters(update_params, mock_account_id
299332
assert schedulable_body.contractUpdateInstance.contractID.realmNum == update_params["contract_id"].realm
300333

301334

335+
def test_build_proto_body_with_cleared_fields(contract_id):
336+
"""Test building a contract update body with cleared auto-renew and staked accounts."""
337+
tx = ContractUpdateTransaction()
338+
tx.set_contract_id(contract_id)
339+
tx.set_auto_renew_account_id(AccountId(0, 0, 0))
340+
tx.set_staked_account_id(AccountId(0, 0, 0))
341+
342+
proto_body = tx._build_proto_body()
343+
344+
# Check auto-renew account handles the explicit empty message
345+
assert proto_body.HasField("auto_renew_account_id")
346+
assert proto_body.auto_renew_account_id.accountNum == 0
347+
348+
# Check staked account ID natively serializes the 0.0.0 sentinel
349+
assert proto_body.HasField("staked_account_id")
350+
assert proto_body.staked_account_id.accountNum == 0
351+
352+
302353
########### Transaction Execution Tests ###########
303354

304355

0 commit comments

Comments
 (0)