Skip to content

Commit 828adc1

Browse files
authored
feat: add staking_info fields to ContractInfo (hiero-ledger#1744)
Signed-off-by: Mounil <mounilkankhara@gmail.com>
1 parent 6ac3e4e commit 828adc1

5 files changed

Lines changed: 204 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
2222
- Fixed duplication in GitHub bot next issue recommendations by parsing actual issue descriptions instead of blind truncation (#1658)
2323

2424
### Src
25+
- Add `staking_info` field to `ContractInfo` class to expose staking metadata using the `StakingInfo` wrapper. (#1365)
2526
- Fix `TopicInfo.__str__()` to format `expiration_time` in UTC so unit tests pass in non-UTC environments. (#1800)
2627
- Resolve CodeQL `reflected-XSS` warning in TCK JSON-RPC endpoint
2728
- Improve `keccak256` docstring formatting for better readability and consistency (#1624)

src/hiero_sdk_python/contract/contract_info.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from hiero_sdk_python.crypto.public_key import PublicKey
1313
from hiero_sdk_python.Duration import Duration
1414
from hiero_sdk_python.hapi.services.contract_get_info_pb2 import ContractGetInfoResponse
15+
from hiero_sdk_python.staking_info import StakingInfo
1516
from hiero_sdk_python.timestamp import Timestamp
1617
from hiero_sdk_python.tokens.token_relationship import TokenRelationship
1718

@@ -38,6 +39,7 @@ class ContractInfo:
3839
max_automatic_token_associations (Optional[int]):
3940
The maximum number of token associations that can be automatically renewed
4041
token_relationships (list[TokenRelationship]): The token relationships of the contract
42+
staking_info (Optional[StakingInfo]): The staking information for this contract
4143
"""
4244

4345
contract_id: Optional[ContractId] = None
@@ -54,6 +56,7 @@ class ContractInfo:
5456
ledger_id: Optional[bytes] = None
5557
max_automatic_token_associations: Optional[int] = None
5658
token_relationships: list[TokenRelationship] = field(default_factory=list)
59+
staking_info: Optional[StakingInfo] = None
5760

5861
@classmethod
5962
def _from_proto(cls, proto: ContractGetInfoResponse.ContractInfo) -> "ContractInfo":
@@ -69,7 +72,7 @@ def _from_proto(cls, proto: ContractGetInfoResponse.ContractInfo) -> "ContractIn
6972
if proto is None:
7073
raise ValueError("Contract info proto is None")
7174

72-
return cls(
75+
contract_info = cls(
7376
contract_id=(
7477
cls._from_proto_field(proto, "contractID", ContractId._from_proto)
7578
),
@@ -99,8 +102,15 @@ def _from_proto(cls, proto: ContractGetInfoResponse.ContractInfo) -> "ContractIn
99102
TokenRelationship._from_proto(relationship)
100103
for relationship in proto.tokenRelationships
101104
],
105+
staking_info=(
106+
StakingInfo._from_proto(proto.staking_info)
107+
if proto.HasField('staking_info')
108+
else None
109+
),
102110
)
103111

112+
return contract_info
113+
104114
def _to_proto(self) -> ContractGetInfoResponse.ContractInfo:
105115
"""
106116
Converts this ContractInfo instance to its protobuf representation.
@@ -133,6 +143,7 @@ def _to_proto(self) -> ContractGetInfoResponse.ContractInfo:
133143
else None
134144
),
135145
max_automatic_token_associations=self.max_automatic_token_associations,
146+
staking_info=self.staking_info._to_proto() if self.staking_info else None,
136147
)
137148

138149
def __repr__(self) -> str:
@@ -184,7 +195,8 @@ def __str__(self) -> str:
184195
f" is_deleted={self.is_deleted},\n"
185196
f" token_relationships={token_relationships_str},\n"
186197
f" ledger_id={ledger_id_display},\n"
187-
f" max_automatic_token_associations={self.max_automatic_token_associations}\n"
198+
f" max_automatic_token_associations={self.max_automatic_token_associations},\n"
199+
f" staking_info={self.staking_info}\n"
188200
")"
189201
)
190202

tests/integration/contract_info_query_e2e_test.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ def test_integration_contract_info_query_can_execute(env):
8282
assert info.auto_renew_account_id == env.operator_id, "Auto renew account ID mismatch"
8383
assert info.auto_renew_period == auto_renew_period, "Auto renew period mismatch"
8484

85+
# Verify staking_info is populated (contracts default to no staking)
86+
assert info.staking_info is not None, "staking_info should not be None"
87+
assert info.staking_info.staked_account_id is None, "staked_account_id should be None by default"
88+
assert info.staking_info.staked_node_id is None, "staked_node_id should be None by default"
89+
assert info.staking_info.decline_reward is False, "decline_reward should be False by default"
90+
8591

8692
@pytest.mark.integration
8793
def test_integration_contract_info_query_get_cost(env):

tests/unit/account_info_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,3 +210,4 @@ def test_str_and_repr(account_info):
210210
assert "account_id=AccountId(shard=0, realm=0, num=100" in info_repr
211211
assert "contract_account_id='0.0.100'" in info_repr
212212
assert "account_memo='Test account memo'" in info_repr
213+

tests/unit/contract_info_test.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
from hiero_sdk_python.contract.contract_info import ContractInfo
1010
from hiero_sdk_python.crypto.private_key import PrivateKey
1111
from hiero_sdk_python.Duration import Duration
12+
from hiero_sdk_python.hapi.services.basic_types_pb2 import StakingInfo as StakingInfoProto
1213
from hiero_sdk_python.hapi.services.contract_get_info_pb2 import ContractGetInfoResponse
14+
from hiero_sdk_python.staking_info import StakingInfo
1315
from hiero_sdk_python.timestamp import Timestamp
1416
from hiero_sdk_python.tokens.token_freeze_status import TokenFreezeStatus
1517
from hiero_sdk_python.tokens.token_id import TokenId
@@ -76,6 +78,10 @@ def contract_info(token_relationship):
7678
ledger_id=b"test_ledger_id",
7779
max_automatic_token_associations=10,
7880
token_relationships=[token_relationship],
81+
staking_info=StakingInfo(
82+
staked_account_id=AccountId(0, 0, 500),
83+
decline_reward=False,
84+
),
7985
)
8086

8187

@@ -99,6 +105,10 @@ def proto_contract_info(token_relationship):
99105
ledger_id=b"test_ledger_id",
100106
max_automatic_token_associations=10,
101107
tokenRelationships=[token_relationship._to_proto()],
108+
staking_info=StakingInfoProto(
109+
staked_account_id=AccountId(0, 0, 500)._to_proto(),
110+
decline_reward=False,
111+
),
102112
)
103113
return proto
104114

@@ -120,6 +130,9 @@ def test_contract_info_initialization(contract_info):
120130
assert contract_info.max_automatic_token_associations == 10
121131
assert len(contract_info.token_relationships) == 1
122132
assert contract_info.token_relationships[0].token_id == TokenId(0, 0, 500)
133+
assert contract_info.staking_info.staked_account_id == AccountId(0, 0, 500)
134+
assert contract_info.staking_info.staked_node_id is None
135+
assert contract_info.staking_info.decline_reward is False
123136

124137

125138
def test_contract_info_default_initialization():
@@ -139,6 +152,7 @@ def test_contract_info_default_initialization():
139152
assert contract_info.ledger_id is None
140153
assert contract_info.max_automatic_token_associations is None
141154
assert not contract_info.token_relationships
155+
assert contract_info.staking_info is None
142156

143157

144158
def test_from_proto(proto_contract_info):
@@ -160,6 +174,10 @@ def test_from_proto(proto_contract_info):
160174
assert contract_info.max_automatic_token_associations == 10
161175
assert len(contract_info.token_relationships) == 1
162176
assert contract_info.token_relationships[0].token_id == TokenId(0, 0, 500)
177+
assert contract_info.staking_info is not None
178+
assert contract_info.staking_info.staked_account_id == AccountId(0, 0, 500)
179+
assert contract_info.staking_info.staked_node_id is None
180+
assert contract_info.staking_info.decline_reward is False
163181

164182

165183
def test_from_proto_with_empty_token_relationships():
@@ -272,6 +290,9 @@ def test_proto_conversion_full_object(contract_info):
272290
== contract_info.max_automatic_token_associations
273291
)
274292
assert len(converted.token_relationships) == len(contract_info.token_relationships)
293+
assert converted.staking_info.staked_account_id == contract_info.staking_info.staked_account_id
294+
assert converted.staking_info.staked_node_id == contract_info.staking_info.staked_node_id
295+
assert converted.staking_info.decline_reward == contract_info.staking_info.decline_reward
275296

276297

277298
def test_proto_conversion_multiple_token_relationships(multiple_token_relationships):
@@ -307,3 +328,164 @@ def test_proto_conversion_minimal_fields():
307328
assert converted.balance == contract_info.balance
308329
assert converted.admin_key is None
309330
assert not converted.token_relationships
331+
332+
333+
def test_from_proto_with_no_staking_info():
334+
"""Test from_proto with no staking info"""
335+
public_key = PrivateKey.generate_ed25519().public_key()
336+
proto = ContractGetInfoResponse.ContractInfo(
337+
contractID=ContractId(0, 0, 200)._to_proto(),
338+
accountID=AccountId(0, 0, 300)._to_proto(),
339+
contractAccountID="0.0.300",
340+
adminKey=public_key._to_proto(),
341+
storage=1024,
342+
balance=5000000,
343+
)
344+
345+
contract_info = ContractInfo._from_proto(proto)
346+
347+
assert contract_info.contract_id == ContractId(0, 0, 200)
348+
assert contract_info.staking_info is None
349+
350+
351+
def test_from_proto_with_staked_node_id():
352+
"""Test from_proto with staked_node_id (staked to node)"""
353+
proto = ContractGetInfoResponse.ContractInfo(
354+
contractID=ContractId(0, 0, 200)._to_proto(),
355+
accountID=AccountId(0, 0, 300)._to_proto(),
356+
storage=1024,
357+
balance=5000000,
358+
staking_info=StakingInfoProto(
359+
staked_node_id=3,
360+
decline_reward=True,
361+
),
362+
)
363+
364+
contract_info = ContractInfo._from_proto(proto)
365+
366+
assert contract_info.staking_info is not None
367+
assert contract_info.staking_info.staked_account_id is None
368+
assert contract_info.staking_info.staked_node_id == 3
369+
assert contract_info.staking_info.decline_reward is True
370+
371+
372+
def test_to_proto_with_staked_account_id():
373+
"""Test to_proto with staked_account_id"""
374+
contract_info = ContractInfo(
375+
contract_id=ContractId(0, 0, 200),
376+
account_id=AccountId(0, 0, 300),
377+
balance=5000000,
378+
staking_info=StakingInfo(
379+
staked_account_id=AccountId(0, 0, 500),
380+
decline_reward=False,
381+
),
382+
)
383+
384+
proto = contract_info._to_proto()
385+
386+
assert proto.HasField('staking_info')
387+
assert proto.staking_info.HasField('staked_account_id')
388+
assert proto.staking_info.staked_account_id == AccountId(0, 0, 500)._to_proto()
389+
assert proto.staking_info.decline_reward is False
390+
391+
392+
def test_to_proto_with_staked_node_id():
393+
"""Test to_proto with staked_node_id"""
394+
contract_info = ContractInfo(
395+
contract_id=ContractId(0, 0, 200),
396+
account_id=AccountId(0, 0, 300),
397+
balance=5000000,
398+
staking_info=StakingInfo(
399+
staked_node_id=5,
400+
decline_reward=True,
401+
),
402+
)
403+
404+
proto = contract_info._to_proto()
405+
406+
assert proto.HasField('staking_info')
407+
assert proto.staking_info.staked_node_id == 5
408+
assert proto.staking_info.decline_reward is True
409+
410+
411+
def test_proto_conversion_staking_node_round_trip():
412+
"""Test proto conversion round trip with staked_node_id"""
413+
contract_info = ContractInfo(
414+
contract_id=ContractId(0, 0, 200),
415+
account_id=AccountId(0, 0, 300),
416+
balance=5000000,
417+
staking_info=StakingInfo(
418+
staked_node_id=7,
419+
decline_reward=False,
420+
),
421+
)
422+
423+
converted = ContractInfo._from_proto(contract_info._to_proto())
424+
425+
assert converted.contract_id == contract_info.contract_id
426+
assert converted.account_id == contract_info.account_id
427+
assert converted.balance == contract_info.balance
428+
assert converted.staking_info.staked_account_id is None
429+
assert converted.staking_info.staked_node_id == 7
430+
assert isinstance(converted.staking_info.staked_node_id, int)
431+
assert converted.staking_info.decline_reward is False
432+
433+
434+
def test_proto_conversion_staking_account_round_trip():
435+
"""Test proto conversion round trip with staked_account_id"""
436+
contract_info = ContractInfo(
437+
contract_id=ContractId(0, 0, 200),
438+
account_id=AccountId(0, 0, 300),
439+
balance=5000000,
440+
staking_info=StakingInfo(
441+
staked_account_id=AccountId(0, 0, 600),
442+
decline_reward=True,
443+
),
444+
)
445+
446+
converted = ContractInfo._from_proto(contract_info._to_proto())
447+
448+
assert converted.contract_id == contract_info.contract_id
449+
assert converted.account_id == contract_info.account_id
450+
assert converted.balance == contract_info.balance
451+
assert converted.staking_info.staked_account_id == AccountId(0, 0, 600)
452+
assert isinstance(converted.staking_info.staked_account_id, AccountId)
453+
assert converted.staking_info.staked_node_id is None
454+
assert converted.staking_info.decline_reward is True
455+
456+
457+
def test_proto_conversion_with_staked_node_zero():
458+
"""Test proto conversion with staked_node_id set to 0 (valid edge case)"""
459+
contract_info = ContractInfo(
460+
contract_id=ContractId(0, 0, 200),
461+
account_id=AccountId(0, 0, 300),
462+
balance=5000000,
463+
staking_info=StakingInfo(
464+
staked_node_id=0,
465+
decline_reward=True,
466+
),
467+
)
468+
469+
converted = ContractInfo._from_proto(contract_info._to_proto())
470+
471+
assert converted.staking_info is not None
472+
assert converted.staking_info.staked_node_id == 0
473+
assert isinstance(converted.staking_info.staked_node_id, int)
474+
assert converted.staking_info.decline_reward is True
475+
476+
477+
def test_proto_conversion_no_staking_info_round_trip():
478+
"""Test proto conversion round trip with no staking info (None should stay None)"""
479+
contract_info = ContractInfo(
480+
contract_id=ContractId(0, 0, 200),
481+
account_id=AccountId(0, 0, 300),
482+
balance=5000000,
483+
staking_info=None,
484+
)
485+
486+
converted = ContractInfo._from_proto(contract_info._to_proto())
487+
488+
assert converted.contract_id == contract_info.contract_id
489+
assert converted.account_id == contract_info.account_id
490+
assert converted.balance == contract_info.balance
491+
assert converted.staking_info is None

0 commit comments

Comments
 (0)