Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.

### Examples

### Added
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this under Src section

- Add staking_info fields (`staked_account_id`, `staked_node_id`, `decline_staking_reward`) to ContractInfo class to expose staking metadata from protobuf. (#1365)
Comment on lines +14 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing blank line below heading (MD022).

The ### Added heading on line 14 needs a blank line below it before the list item, per Markdown lint rules (MD022).

Proposed fix
 ### Added
+
 - Add staking_info fields (`staked_account_id`, `staked_node_id`, `decline_staking_reward`) to ContractInfo class to expose staking metadata from protobuf. (`#1365`)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### Added
- Add staking_info fields (`staked_account_id`, `staked_node_id`, `decline_staking_reward`) to ContractInfo class to expose staking metadata from protobuf. (#1365)
### Added
- Add staking_info fields (`staked_account_id`, `staked_node_id`, `decline_staking_reward`) to ContractInfo class to expose staking metadata from protobuf. (`#1365`)
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 14-14: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


### Docs
- Improved Google-style docstring for `compress_point_unchecked` in `crypto_utils.py`. (#1625)
- chore: update office hours and community calls to use direct links (`#1804`)
Expand Down
43 changes: 11 additions & 32 deletions src/hiero_sdk_python/account/account_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
from hiero_sdk_python.account.account_id import AccountId
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe PR #1595 is already addressing this issue. We can remove this change from the current PR to avoid duplication and keep the scope focused.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, sorry, didn't see that there is another PR for the account_info, I will edit my comment ;D

from hiero_sdk_python.crypto.public_key import PublicKey
from hiero_sdk_python.Duration import Duration
from hiero_sdk_python.hapi.services.basic_types_pb2 import StakingInfo
from hiero_sdk_python.hapi.services.crypto_get_info_pb2 import CryptoGetInfoResponse
from hiero_sdk_python.hbar import Hbar
from hiero_sdk_python.staking_info import StakingInfo
from hiero_sdk_python.timestamp import Timestamp
from hiero_sdk_python.tokens.token_relationship import TokenRelationship

Expand All @@ -38,9 +38,7 @@ class AccountInfo:
associated with this account.
account_memo (Optional[str]): The memo associated with this account.
owned_nfts (Optional[int]): The number of NFTs owned by this account.
staked_account_id (Optional[AccountId]): The account to which this account is staked.
staked_node_id (Optional[int]): The node to which this account is staked.
decline_staking_reward (bool): Whether this account declines receiving staking rewards.
staking_info (Optional[StakingInfo]): The staking information for this account.
"""

account_id: Optional[AccountId] = None
Expand All @@ -56,9 +54,7 @@ class AccountInfo:
account_memo: Optional[str] = None
owned_nfts: Optional[int] = None
max_automatic_token_associations: Optional[int] = None
staked_account_id: Optional[AccountId] = None
staked_node_id: Optional[int] = None
decline_staking_reward: Optional[bool] = None
staking_info: Optional[StakingInfo] = None
Comment on lines -59 to +57
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the removal of staked_account_id and the other fields introduces breaking change. If someone (like in the integration tests) access the staked_account_id directly it will break for them. @exploreriii, what do you think?


@classmethod
def _from_proto(cls, proto: CryptoGetInfoResponse.AccountInfo) -> "AccountInfo":
Expand Down Expand Up @@ -100,21 +96,13 @@ def _from_proto(cls, proto: CryptoGetInfoResponse.AccountInfo) -> "AccountInfo":
account_memo=proto.memo,
owned_nfts=proto.ownedNfts,
max_automatic_token_associations=proto.max_automatic_token_associations,
staking_info=(
StakingInfo._from_proto(proto.staking_info)
if proto.HasField('staking_info')
else None
),
)

staking_info = proto.staking_info if proto.HasField('staking_info') else None

if staking_info:
account_info.staked_account_id = (
AccountId._from_proto(staking_info.staked_account_id)
if staking_info.HasField('staked_account_id') else None
)
account_info.staked_node_id = (
staking_info.staked_node_id
if staking_info.HasField('staked_node_id') else None
)
account_info.decline_staking_reward = staking_info.decline_reward

return account_info

def _to_proto(self) -> CryptoGetInfoResponse.AccountInfo:
Expand Down Expand Up @@ -147,11 +135,7 @@ def _to_proto(self) -> CryptoGetInfoResponse.AccountInfo:
memo=self.account_memo,
ownedNfts=self.owned_nfts,
max_automatic_token_associations=self.max_automatic_token_associations,
staking_info=StakingInfo(
staked_account_id=self.staked_account_id._to_proto() if self.staked_account_id else None,
staked_node_id=self.staked_node_id if self.staked_node_id else None,
decline_reward=self.decline_staking_reward
),
staking_info=self.staking_info._to_proto() if self.staking_info else None,
)

def __str__(self) -> str:
Expand All @@ -166,11 +150,10 @@ def __str__(self) -> str:
(self.account_memo, "Memo"),
(self.owned_nfts, "Owned NFTs"),
(self.max_automatic_token_associations, "Max Automatic Token Associations"),
(self.staked_account_id, "Staked Account ID"),
(self.staked_node_id, "Staked Node ID"),
(self.proxy_received, "Proxy Received"),
(self.expiration_time, "Expiration Time"),
(self.auto_renew_period, "Auto Renew Period"),
(self.staking_info, "Staking Info"),
]

# Use a list comprehension to process simple fields (reduces complexity score)
Expand All @@ -182,9 +165,6 @@ def __str__(self) -> str:

if self.receiver_signature_required is not None:
lines.append(f"Receiver Signature Required: {self.receiver_signature_required}")

if self.decline_staking_reward is not None:
lines.append(f"Decline Staking Reward: {self.decline_staking_reward}")

if self.token_relationships:
lines.append(f"Token Relationships: {len(self.token_relationships)}")
Expand All @@ -202,7 +182,6 @@ def __repr__(self) -> str:
f"receiver_signature_required={self.receiver_signature_required!r}, "
f"owned_nfts={self.owned_nfts!r}, "
f"account_memo={self.account_memo!r}, "
f"staked_node_id={self.staked_node_id!r}, "
f"staked_account_id={self.staked_account_id!r}"
f"staking_info={self.staking_info!r}"
f")"
)
16 changes: 14 additions & 2 deletions src/hiero_sdk_python/contract/contract_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from hiero_sdk_python.crypto.public_key import PublicKey
from hiero_sdk_python.Duration import Duration
from hiero_sdk_python.hapi.services.contract_get_info_pb2 import ContractGetInfoResponse
from hiero_sdk_python.staking_info import StakingInfo
from hiero_sdk_python.timestamp import Timestamp
from hiero_sdk_python.tokens.token_relationship import TokenRelationship

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

contract_id: Optional[ContractId] = None
Expand All @@ -54,6 +56,7 @@ class ContractInfo:
ledger_id: Optional[bytes] = None
max_automatic_token_associations: Optional[int] = None
token_relationships: list[TokenRelationship] = field(default_factory=list)
staking_info: Optional[StakingInfo] = None

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

return cls(
contract_info = cls(
contract_id=(
cls._from_proto_field(proto, "contractID", ContractId._from_proto)
),
Expand Down Expand Up @@ -99,8 +102,15 @@ def _from_proto(cls, proto: ContractGetInfoResponse.ContractInfo) -> "ContractIn
TokenRelationship._from_proto(relationship)
for relationship in proto.tokenRelationships
],
staking_info=(
StakingInfo._from_proto(proto.staking_info)
if proto.HasField('staking_info')
else None
),
)

return contract_info

def _to_proto(self) -> ContractGetInfoResponse.ContractInfo:
"""
Converts this ContractInfo instance to its protobuf representation.
Expand Down Expand Up @@ -133,6 +143,7 @@ def _to_proto(self) -> ContractGetInfoResponse.ContractInfo:
else None
),
max_automatic_token_associations=self.max_automatic_token_associations,
staking_info=self.staking_info._to_proto() if self.staking_info else None,
)

def __repr__(self) -> str:
Expand Down Expand Up @@ -184,7 +195,8 @@ def __str__(self) -> str:
f" is_deleted={self.is_deleted},\n"
f" token_relationships={token_relationships_str},\n"
f" ledger_id={ledger_id_display},\n"
f" max_automatic_token_associations={self.max_automatic_token_associations}\n"
f" max_automatic_token_associations={self.max_automatic_token_associations},\n"
f" staking_info={self.staking_info}\n"
")"
)

Expand Down
154 changes: 154 additions & 0 deletions tests/unit/account_info_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from hiero_sdk_python.tokens.token_relationship import TokenRelationship
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test can also be remove due to same

from hiero_sdk_python.tokens.token_id import TokenId
from hiero_sdk_python.hapi.services.crypto_get_info_pb2 import CryptoGetInfoResponse
from hiero_sdk_python.hapi.services.basic_types_pb2 import StakingInfo as StakingInfoProto
from hiero_sdk_python.staking_info import StakingInfo

pytestmark = pytest.mark.unit

Expand Down Expand Up @@ -210,3 +212,155 @@ def test_str_and_repr(account_info):
assert "account_id=AccountId(shard=0, realm=0, num=100" in info_repr
assert "contract_account_id='0.0.100'" in info_repr
assert "account_memo='Test account memo'" in info_repr


def test_from_proto_with_staking_info():
"""Test from_proto with staking account info"""
public_key = PrivateKey.generate_ed25519().public_key()
proto = CryptoGetInfoResponse.AccountInfo(
accountID=AccountId(0, 0, 100)._to_proto(),
key=public_key._to_proto(),
balance=5000000,
staking_info=StakingInfoProto(
staked_account_id=AccountId(0, 0, 500)._to_proto(),
decline_reward=True,
),
)

account_info = AccountInfo._from_proto(proto)

assert account_info.staking_info is not None
assert account_info.staking_info.staked_account_id == AccountId(0, 0, 500)
assert account_info.staking_info.decline_reward is True


def test_from_proto_with_staked_node_id():
"""Test from_proto with staked_node_id"""
public_key = PrivateKey.generate_ed25519().public_key()
proto = CryptoGetInfoResponse.AccountInfo(
accountID=AccountId(0, 0, 100)._to_proto(),
key=public_key._to_proto(),
balance=5000000,
staking_info=StakingInfoProto(
staked_node_id=3,
decline_reward=False,
),
)

account_info = AccountInfo._from_proto(proto)

assert account_info.staking_info is not None
assert account_info.staking_info.staked_node_id == 3
assert account_info.staking_info.decline_reward is False


def test_from_proto_with_no_staking_info():
"""Test from_proto without staking info"""
public_key = PrivateKey.generate_ed25519().public_key()
proto = CryptoGetInfoResponse.AccountInfo(
accountID=AccountId(0, 0, 100)._to_proto(),
key=public_key._to_proto(),
balance=5000000,
)

account_info = AccountInfo._from_proto(proto)

assert account_info.staking_info is None


def test_to_proto_with_staking_info():
"""Test to_proto with staking info"""
account_info = AccountInfo(
account_id=AccountId(0, 0, 100),
balance=Hbar.from_tinybars(5000000),
staking_info=StakingInfo(
staked_account_id=AccountId(0, 0, 500),
decline_reward=True,
),
)

proto = account_info._to_proto()

assert proto.HasField('staking_info')
assert proto.staking_info.HasField('staked_account_id')
assert proto.staking_info.staked_account_id == AccountId(0, 0, 500)._to_proto()
assert proto.staking_info.decline_reward is True


def test_to_proto_with_staked_node_id():
"""Test to_proto with staked_node_id"""
account_info = AccountInfo(
account_id=AccountId(0, 0, 100),
balance=Hbar.from_tinybars(5000000),
staking_info=StakingInfo(
staked_node_id=5,
decline_reward=False,
),
)

proto = account_info._to_proto()

assert proto.HasField('staking_info')
assert proto.staking_info.staked_node_id == 5
assert proto.staking_info.decline_reward is False


def test_proto_conversion_staking_node_round_trip():
"""Test proto conversion round trip with staked_node_id"""
account_info = AccountInfo(
account_id=AccountId(0, 0, 100),
key=PrivateKey.generate_ed25519().public_key(),
balance=Hbar.from_tinybars(5000000),
staking_info=StakingInfo(
staked_node_id=7,
decline_reward=False,
),
)

converted = AccountInfo._from_proto(account_info._to_proto())

assert converted.account_id == account_info.account_id
assert converted.balance.to_tinybars() == account_info.balance.to_tinybars()
assert converted.staking_info.staked_account_id is None
assert converted.staking_info.staked_node_id == 7
assert converted.staking_info.decline_reward is False


def test_proto_conversion_staking_account_round_trip():
"""Test proto conversion round trip with staked_account_id"""
account_info = AccountInfo(
account_id=AccountId(0, 0, 100),
key=PrivateKey.generate_ed25519().public_key(),
balance=Hbar.from_tinybars(5000000),
staking_info=StakingInfo(
staked_account_id=AccountId(0, 0, 600),
decline_reward=True,
),
)

converted = AccountInfo._from_proto(account_info._to_proto())

assert converted.account_id == account_info.account_id
assert converted.balance.to_tinybars() == account_info.balance.to_tinybars()
assert converted.staking_info.staked_account_id == AccountId(0, 0, 600)
assert converted.staking_info.staked_node_id is None
assert converted.staking_info.decline_reward is True


def test_proto_conversion_with_staked_node_zero():
"""Test proto conversion with staked_node_id set to 0"""
account_info = AccountInfo(
account_id=AccountId(0, 0, 100),
key=PrivateKey.generate_ed25519().public_key(),
balance=Hbar.from_tinybars(5000000),
staking_info=StakingInfo(
staked_node_id=0,
decline_reward=True,
),
)

converted = AccountInfo._from_proto(account_info._to_proto())

assert converted.staking_info is not None
assert converted.staking_info.staked_node_id == 0
assert converted.staking_info.decline_reward is True
Loading
Loading