Skip to content

Commit c4d535f

Browse files
Adityarya11NssGourav
authored andcommitted
feat: implemented getAccountInfoQuery TCK endpoint (hiero-ledger#2081)
Signed-off-by: Adityarya11 <arya050411@gmail.com>
1 parent f3078af commit c4d535f

5 files changed

Lines changed: 210 additions & 13 deletions

File tree

src/hiero_sdk_python/query/account_info_query.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,15 @@ def _make_request(self):
5252
Query: The protobuf query message.
5353
5454
Raises:
55-
ValueError: If the account ID is not set.
5655
Exception: If any other error occurs during request construction.
5756
"""
5857
try:
59-
if not self.account_id:
60-
raise ValueError("Account ID must be set before making the request.")
61-
6258
query_header = self._make_request_header()
6359

6460
crypto_info_query = crypto_get_info_pb2.CryptoGetInfoQuery()
6561
crypto_info_query.header.CopyFrom(query_header)
66-
crypto_info_query.accountID.CopyFrom(self.account_id._to_proto())
62+
if self.account_id is not None:
63+
crypto_info_query.accountID.CopyFrom(self.account_id._to_proto())
6764

6865
query = query_pb2.Query()
6966
query.cryptoGetInfo.CopyFrom(crypto_info_query)

tck/handlers/account.py

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
1+
"""TCK RPC handlers for account-related endpoints."""
2+
13
from __future__ import annotations
24

35
from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction
46
from hiero_sdk_python.account.account_id import AccountId
7+
from hiero_sdk_python.account.account_info import AccountInfo
58
from hiero_sdk_python.hbar import Hbar
9+
from hiero_sdk_python.query.account_info_query import AccountInfoQuery
610
from hiero_sdk_python.response_code import ResponseCode
711
from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt
812
from tck.handlers.registry import rpc_method
9-
from tck.param.account import CreateAccountParams
10-
from tck.response.account import CreateAccountResponse
13+
from tck.param.account import CreateAccountParams, GetAccountInfoParams
14+
from tck.response.account import (
15+
CreateAccountResponse,
16+
GetAccountInfoResponse,
17+
StakingInfoResponse,
18+
TokenRelationshipResponse,
19+
)
1120
from tck.util.client_utils import get_client
1221
from tck.util.constants import DEFAULT_GRPC_TIMEOUT
1322
from tck.util.key_utils import get_key_from_string
@@ -51,6 +60,7 @@ def _build_create_account_transaction(params: CreateAccountParams) -> AccountCre
5160

5261
@rpc_method("createAccount")
5362
def create_account(params: CreateAccountParams) -> CreateAccountResponse:
63+
"""Create a new account using TCK createAccount parameters."""
5464
client = get_client(params.sessionId)
5565

5666
transaction = _build_create_account_transaction(params)
@@ -66,3 +76,100 @@ def create_account(params: CreateAccountParams) -> CreateAccountResponse:
6676
account_id = str(receipt.account_id)
6777

6878
return CreateAccountResponse(account_id, ResponseCode(receipt.status).name)
79+
80+
81+
def _enum_name(value) -> str | None:
82+
if value is None:
83+
return None
84+
return getattr(value, "name", str(value))
85+
86+
87+
def _serialize_key(key) -> str | None:
88+
if key is None:
89+
return None
90+
91+
to_string_der = getattr(key, "to_string_der", None)
92+
if callable(to_string_der):
93+
return to_string_der()
94+
95+
return key.to_bytes().hex()
96+
97+
98+
def _to_staking_info_response(info: AccountInfo) -> StakingInfoResponse | None:
99+
if info.staking_info is None:
100+
return None
101+
102+
staking_info = info.staking_info
103+
return StakingInfoResponse(
104+
declineStakingReward=staking_info.decline_reward,
105+
stakePeriodStart=str(staking_info.stake_period_start) if staking_info.stake_period_start is not None else None,
106+
pendingReward=str(staking_info.pending_reward.to_tinybars())
107+
if staking_info.pending_reward is not None
108+
else "0",
109+
stakedToMe=str(staking_info.staked_to_me.to_tinybars()) if staking_info.staked_to_me is not None else "0",
110+
stakedAccountId=str(staking_info.staked_account_id) if staking_info.staked_account_id is not None else None,
111+
stakedNodeId=str(staking_info.staked_node_id) if staking_info.staked_node_id is not None else None,
112+
)
113+
114+
115+
def _to_token_relationships_response(info: AccountInfo) -> dict[str, TokenRelationshipResponse]:
116+
token_relationships_response: dict[str, TokenRelationshipResponse] = {}
117+
118+
for relationship in info.token_relationships:
119+
token_id = str(relationship.token_id) if relationship.token_id is not None else None
120+
if token_id is None:
121+
continue
122+
123+
token_relationships_response[token_id] = TokenRelationshipResponse(
124+
tokenId=token_id,
125+
symbol=relationship.symbol,
126+
balance=str(relationship.balance) if relationship.balance is not None else "0",
127+
kycStatus=_enum_name(relationship.kyc_status),
128+
freezeStatus=_enum_name(relationship.freeze_status),
129+
decimals=str(relationship.decimals) if relationship.decimals is not None else None,
130+
automaticAssociation=relationship.automatic_association,
131+
)
132+
133+
return token_relationships_response
134+
135+
136+
def _build_account_info_response(info: AccountInfo) -> GetAccountInfoResponse:
137+
auto_renew_period_seconds = str(info.auto_renew_period.seconds) if info.auto_renew_period is not None else "0"
138+
139+
return GetAccountInfoResponse(
140+
accountId=str(info.account_id) if info.account_id is not None else None,
141+
contractAccountId=info.contract_account_id or "",
142+
isDeleted=bool(info.is_deleted),
143+
proxyAccountId="",
144+
proxyReceived=str(info.proxy_received.to_tinybars()) if info.proxy_received is not None else "0",
145+
key=_serialize_key(info.key),
146+
balance=str(info.balance.to_tinybars()) if info.balance is not None else "0",
147+
sendRecordThreshold="0",
148+
receiveRecordThreshold="0",
149+
isReceiverSignatureRequired=bool(info.receiver_signature_required),
150+
expirationTime=str(info.expiration_time) if info.expiration_time is not None else None,
151+
autoRenewPeriod=auto_renew_period_seconds,
152+
tokenRelationships=_to_token_relationships_response(info),
153+
accountMemo=info.account_memo or "",
154+
ownedNfts=str(info.owned_nfts) if info.owned_nfts is not None else "0",
155+
maxAutomaticTokenAssociations=str(info.max_automatic_token_associations)
156+
if info.max_automatic_token_associations is not None
157+
else "0",
158+
aliasKey="",
159+
ledgerId="",
160+
ethereumNonce="0",
161+
stakingInfo=_to_staking_info_response(info),
162+
)
163+
164+
165+
@rpc_method("getAccountInfo")
166+
def get_account_info(params: GetAccountInfoParams) -> GetAccountInfoResponse:
167+
"""Query account info and map SDK fields to the TCK response contract."""
168+
client = get_client(params.sessionId)
169+
170+
query = AccountInfoQuery().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT)
171+
if params.accountId is not None:
172+
query.set_account_id(AccountId.from_string(params.accountId))
173+
174+
info = query.execute(client)
175+
return _build_account_info_response(info)

tck/param/account.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
"""TCK request parameter models for account endpoints."""
2+
13
from __future__ import annotations
24

35
from dataclasses import dataclass
46

5-
from tck.param.base import BaseTransactionParams
7+
from tck.param.base import BaseParams, BaseTransactionParams
68
from tck.util.param_utils import (
79
parse_common_transaction_params,
810
parse_session_id,
@@ -13,6 +15,8 @@
1315

1416
@dataclass
1517
class CreateAccountParams(BaseTransactionParams):
18+
"""Request parameters for the createAccount endpoint."""
19+
1620
key: str | None = None
1721
initialBalance: int | None = None
1822
receiverSignatureRequired: bool | None = None
@@ -26,6 +30,7 @@ class CreateAccountParams(BaseTransactionParams):
2630

2731
@classmethod
2832
def parse_json_params(cls, params: dict) -> CreateAccountParams:
33+
"""Parse JSON-RPC params into a CreateAccountParams instance."""
2934
return cls(
3035
key=params.get("key"),
3136
initialBalance=to_int(params.get("initialBalance")),
@@ -40,3 +45,15 @@ def parse_json_params(cls, params: dict) -> CreateAccountParams:
4045
sessionId=parse_session_id(params),
4146
commonTransactionParams=parse_common_transaction_params(params),
4247
)
48+
49+
50+
@dataclass
51+
class GetAccountInfoParams(BaseParams):
52+
"""Request parameters for the getAccountInfo endpoint."""
53+
54+
accountId: str | None = None
55+
56+
@classmethod
57+
def parse_json_params(cls, params: dict) -> GetAccountInfoParams:
58+
"""Parse JSON-RPC params into a GetAccountInfoParams instance."""
59+
return cls(accountId=params.get("accountId"), sessionId=parse_session_id(params))

tck/response/account.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,68 @@
1+
"""TCK response models for account endpoints."""
2+
13
from __future__ import annotations
24

3-
from dataclasses import dataclass
5+
from dataclasses import dataclass, field
46

57

68
@dataclass
79
class CreateAccountResponse:
10+
"""Response payload for createAccount."""
11+
812
accountId: str | None = None
913
status: str | None = None
14+
15+
16+
@dataclass
17+
class StakingInfoResponse:
18+
"""Nested staking fields in the getAccountInfo response."""
19+
20+
declineStakingReward: bool | None = None
21+
stakePeriodStart: str | None = None
22+
pendingReward: str | None = None
23+
stakedToMe: str | None = None
24+
stakedAccountId: str | None = None
25+
stakedNodeId: str | None = None
26+
27+
28+
@dataclass
29+
class TokenRelationshipResponse:
30+
"""Nested token relationship details for getAccountInfo."""
31+
32+
tokenId: str | None = None
33+
symbol: str | None = None
34+
balance: str | None = None
35+
kycStatus: str | None = None
36+
freezeStatus: str | None = None
37+
decimals: str | None = None
38+
automaticAssociation: bool | None = None
39+
40+
41+
@dataclass
42+
class GetAccountInfoResponse:
43+
"""Response payload for getAccountInfo."""
44+
45+
accountId: str | None = None
46+
contractAccountId: str | None = None
47+
isDeleted: bool | None = None
48+
proxyAccountId: str | None = None
49+
proxyReceived: str | None = None
50+
key: str | None = None
51+
balance: str | None = None
52+
sendRecordThreshold: str | None = None
53+
receiveRecordThreshold: str | None = None
54+
isReceiverSignatureRequired: bool | None = None
55+
expirationTime: str | None = None
56+
autoRenewPeriod: str | None = None
57+
liveHashes: list[dict] = field(default_factory=list)
58+
tokenRelationships: dict[str, TokenRelationshipResponse] = field(default_factory=dict)
59+
accountMemo: str | None = None
60+
ownedNfts: str | None = None
61+
maxAutomaticTokenAssociations: str | None = None
62+
aliasKey: str | None = None
63+
ledgerId: str | None = None
64+
ethereumNonce: str | None = None
65+
hbarAllowances: list[dict] = field(default_factory=list)
66+
tokenAllowances: list[dict] = field(default_factory=list)
67+
nftAllowances: list[dict] = field(default_factory=list)
68+
stakingInfo: StakingInfoResponse | None = None

tests/unit/account_info_query_test.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,29 @@ def test_constructor():
3434
assert query.account_id == account_id
3535

3636

37-
def test_execute_fails_with_missing_account_id(mock_client):
38-
"""Test request creation with missing Account ID."""
37+
def test_execute_fails_with_missing_account_id():
38+
"""Test request execution with missing Account ID throws PrecheckError."""
39+
from hiero_sdk_python.exceptions import PrecheckError
40+
3941
query = AccountInfoQuery()
4042

41-
with pytest.raises(ValueError, match=r"Account ID must be set before making the request\."):
42-
query.execute(mock_client)
43+
response_sequences = [
44+
[
45+
response_pb2.Response(
46+
cryptoGetInfo=crypto_get_info_pb2.CryptoGetInfoResponse(
47+
header=response_header_pb2.ResponseHeader(
48+
nodeTransactionPrecheckCode=ResponseCode.INVALID_ACCOUNT_ID,
49+
responseType=ResponseType.ANSWER_ONLY,
50+
cost=0,
51+
)
52+
)
53+
)
54+
]
55+
]
56+
with mock_hedera_servers(response_sequences) as client:
57+
with pytest.raises(PrecheckError) as exc_info:
58+
query.execute(client)
59+
assert exc_info.value.status == ResponseCode.INVALID_ACCOUNT_ID
4360

4461

4562
def test_get_method():

0 commit comments

Comments
 (0)