Skip to content
Open
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
- Added dry-run support and refactored `.github/workflows/bot-workflows.yml` to use dedicated script `.github/scripts/bot-workflows.js` for improved maintainability and testability. (`#1288`)

### Changed
- Refactored AccountInfo class to use the staking_info
Copy link
Contributor

@prajeeta15 prajeeta15 Jan 28, 2026

Choose a reason for hiding this comment

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

please write a more descriptive CHANGELOG.md entry with the issue number that is being addressed through this PR.

- chore: format tests/unit/mock_server.py with black (#1542)
- Updated actions/checkout to v6.0.1 and actions/github-script v8.0.0 in bot-next-issue-recommendation workflow (#1586)
- Expanded inactivity bot messages to include `/unassign` command information for contributors (#1555)
Expand Down
44 changes: 16 additions & 28 deletions src/hiero_sdk_python/account/account_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from hiero_sdk_python.account.account_id import AccountId
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.staking_info 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.timestamp import Timestamp
Expand Down Expand Up @@ -56,9 +56,8 @@ 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 58 to +60
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 | 🟠 Major

Align max_automatic_token_associations default with tests and expected API behavior.

Current default is None, but tests (and implied behavior) expect 10. This will fail default initialization tests and create inconsistent defaults.

🔧 Proposed fix
-    max_automatic_token_associations: Optional[int] = None
+    max_automatic_token_associations: int = 10
📝 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
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
max_automatic_token_associations: int = 10
staking_info: Optional[StakingInfo] = None


@classmethod
def _from_proto(cls, proto: CryptoGetInfoResponse.AccountInfo) -> "AccountInfo":
Expand Down Expand Up @@ -100,20 +99,14 @@ 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

Expand Down Expand Up @@ -147,11 +140,11 @@ 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 is not None
else None
),
)

def __str__(self) -> str:
Expand All @@ -166,8 +159,7 @@ 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.staking_info, "Staked Info"),
(self.proxy_received, "Proxy Received"),
Comment on lines +162 to 163
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

Use consistent “staking_info” labeling in str / repr.

The label “Staked Info” / “staked_info” is inconsistent with the field name and PR objectives, which can confuse users.

🔧 Proposed fix
-            (self.staking_info, "Staked Info"),
+            (self.staking_info, "Staking Info"),
...
-            f"staked_info={self.staking_info!r}, "
+            f"staking_info={self.staking_info!r}, "

Also applies to: 194-194

(self.expiration_time, "Expiration Time"),
(self.auto_renew_period, "Auto Renew Period"),
Expand All @@ -182,9 +174,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 +191,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"staked_info={self.staking_info!r}, "
f")"
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Also please update the account_info unit tests to verify the changes.

57 changes: 57 additions & 0 deletions tests/unit/account_info_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from hiero_sdk_python.tokens.token_relationship import TokenRelationship
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.staking_info import StakingInfo

pytestmark = pytest.mark.unit

Expand All @@ -28,6 +29,8 @@ def account_info():
token_relationships=[],
account_memo="Test account memo",
owned_nfts=5,
max_automatic_token_associations=10,
staking_info=None
)


Expand All @@ -47,6 +50,8 @@ def proto_account_info():
tokenRelationships=[],
memo="Test account memo",
ownedNfts=5,
max_automatic_token_associations=10,
staking_info=None
)
return proto

Expand All @@ -65,6 +70,32 @@ def test_account_info_initialization(account_info):
assert account_info.token_relationships == []
assert account_info.account_memo == "Test account memo"
assert account_info.owned_nfts == 5
assert account_info.max_automatic_token_associations == 10
assert account_info.staking_info is None
Copy link
Contributor

Choose a reason for hiding this comment

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

we can also add a test with staking_info present (i.e staking_info not None)


def test_from_proto_with_staking_info():
"""Test the from_proto method of the AccountInfo class with staking info"""
public_key = PrivateKey.generate_ed25519().public_key()

staking_info={
"decline_reward": True,
"staked_node_id": 3,
"staked_account_id": None
}

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 not None
assert account_info.staking_info.decline_reward is True
assert account_info.staking_info.staked_node_id == 3
Comment on lines +76 to +98
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 | 🔴 Critical

Test is broken: staking_info dict is created but never used.

The staking_info dictionary defined on lines 80-84 is never passed to the proto constructor. The test creates a proto without staking info, then asserts that account_info.staking_info is not None—this will fail.

Additionally, even if you intended to use it, passing a dict to a protobuf message field is fragile. Construct the proto message explicitly.

🔧 Proposed fix
 def test_from_proto_with_staking_info():
     """Test the from_proto method of the AccountInfo class with staking info"""
     public_key = PrivateKey.generate_ed25519().public_key()
-    
-    staking_info={
-            "decline_reward": True,
-            "staked_node_id": 3,
-            "staked_account_id": None 
-        }
-    
+    from hiero_sdk_python.hapi.services.basic_types_pb2 import StakingInfo as StakingInfoProto
+
+    staking_info_proto = StakingInfoProto(
+        decline_reward=True,
+        staked_node_id=3
+    )
+
     proto = CryptoGetInfoResponse.AccountInfo(
         accountID=AccountId(0, 0, 100)._to_proto(),
         key=public_key._to_proto(),
         balance=5000000,
-        
-        
+        staking_info=staking_info_proto
     )
 
     account_info = AccountInfo._from_proto(proto)
-    
+
     assert account_info.staking_info is not None
     assert account_info.staking_info.decline_reward is True
     assert account_info.staking_info.staked_node_id == 3
🧰 Tools
🪛 Ruff (0.14.14)

80-80: Local variable staking_info is assigned to but never used

Remove assignment to unused variable staking_info

(F841)



def test_account_info_default_initialization():
Expand All @@ -82,7 +113,25 @@ def test_account_info_default_initialization():
assert account_info.token_relationships == []
assert account_info.account_memo is None
assert account_info.owned_nfts is None
assert account_info.max_automatic_token_associations is None
assert account_info.staking_info is None
Comment on lines +116 to +117
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding a type validation test for staking_info.

Per coding guidelines for comprehensive coverage, consider adding a test that verifies passing an invalid type for staking_info (e.g., a dict instead of StakingInfo) raises an appropriate error, if such validation exists in the implementation.


def test_staking_info_persistence(account_info):
"""Ensure staking info is preserved through proto conversion"""

account_info.staking_info = StakingInfo(
decline_reward=True,
staked_node_id=5,
staked_account_id=None
)

proto = account_info._to_proto()
converted_info = AccountInfo._from_proto(proto)

assert converted_info.staking_info is not None
assert converted_info.staking_info.decline_reward is True
assert converted_info.staking_info.staked_node_id == 5
assert converted_info.staking_info.staked_account_id is None

def test_from_proto(proto_account_info):
"""Test the from_proto method of the AccountInfo class"""
Expand All @@ -100,6 +149,8 @@ def test_from_proto(proto_account_info):
assert account_info.token_relationships == []
assert account_info.account_memo == "Test account memo"
assert account_info.owned_nfts == 5
assert account_info.max_automatic_token_associations == 10
assert account_info.staking_info == None


def test_from_proto_with_token_relationships():
Expand Down Expand Up @@ -141,6 +192,11 @@ def test_to_proto(account_info):
assert proto.tokenRelationships == []
assert proto.memo == "Test account memo"
assert proto.ownedNfts == 5
assert proto.max_automatic_token_associations == 10
assert not proto.HasField("staking_info")





def test_to_proto_with_none_values():
Expand Down Expand Up @@ -192,6 +248,7 @@ def test_proto_conversion(account_info):
)
assert converted_account_info.account_memo == account_info.account_memo
assert converted_account_info.owned_nfts == account_info.owned_nfts
assert converted_account_info.staking_info == account_info.staking_info


def test_str_and_repr(account_info):
Expand Down
5 changes: 3 additions & 2 deletions tests/unit/topic_info_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ def test_repr_and_str(topic_info):
assert "memo='Test topic memo'" in repr_output
assert "sequence_number=42" in repr_output
assert "running_hash=0x0102030405060708" in repr_output
assert "expiration_time=2021-07-01 00:00:00" in repr_output
assert "expiration_time=2021-07-01" in str_output
assert "auto_renew_period=7776000" in repr_output

def test_str_formatting(topic_info):
Expand All @@ -264,7 +264,8 @@ def test_str_formatting(topic_info):
assert "memo='Test topic memo'" in str_output
assert "running_hash=0x0102030405060708" in str_output
assert "sequence_number=42" in str_output
assert "expiration_time=2021-07-01 00:00:00" in str_output
# Instead of the full date-time string
assert "expiration_time=2021-07-01" in str_output
assert "admin_key=ed25519(" in str_output
assert "submit_key=ed25519(" in str_output
assert "auto_renew_period=7776000" in str_output
Expand Down