Skip to content

Commit 81ee6f6

Browse files
committed
feat: implemented getAccountInfo Tck endpt
Signed-off-by: Adityarya11 <arya050411@gmail.com>
1 parent 2e13131 commit 81ee6f6

3 files changed

Lines changed: 284 additions & 48 deletions

File tree

tck/handlers/account.py

Lines changed: 102 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction
22
from hiero_sdk_python.account.account_id import AccountId
3+
from hiero_sdk_python.account.account_info import AccountInfo
34
from hiero_sdk_python.query.account_info_query import AccountInfoQuery
45
from hiero_sdk_python.hbar import Hbar
56
from hiero_sdk_python.response_code import ResponseCode
67
from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt
8+
from tck.errors import JsonRpcError
79
from tck.handlers.registry import rpc_method
810
from tck.param.account import CreateAccountParams, GetAccountInfoParams
911
from tck.response.account import (
@@ -72,64 +74,119 @@ def create_account(params: CreateAccountParams) -> CreateAccountResponse:
7274
return CreateAccountResponse(account_id, ResponseCode(receipt.status).name)
7375

7476

75-
def _build_account_info_response(info) -> GetAccountInfoResponse:
76-
staking_info_response = None
77-
if info.staking_info:
78-
staking_info_response = StakingInfoResponse(
79-
declineStakingReward=info.staking_info.decline_staking_reward,
80-
stakePeriodStart=str(info.staking_info.stake_period_start)
81-
if info.staking_info.stake_period_start
82-
else None,
83-
pendingReward=str(info.staking_info.pending_reward.to_tinybars())
84-
if info.staking_info.pending_reward
85-
else None,
86-
stakedToMe=str(info.staking_info.staked_to_me.to_tinybars()) if info.staking_info.staked_to_me else None,
87-
stakedAccountId=str(info.staking_info.staked_account_id) if info.staking_info.staked_account_id else None,
88-
stakedNodeId=str(info.staking_info.staked_node_id) if info.staking_info.staked_node_id else None,
77+
def _enum_name(value) -> str | None:
78+
if value is None:
79+
return None
80+
return getattr(value, "name", str(value))
81+
82+
83+
def _serialize_key(key) -> str | None:
84+
if key is None:
85+
return None
86+
87+
to_string_der = getattr(key, "to_string_der", None)
88+
if callable(to_string_der):
89+
return to_string_der()
90+
91+
return key.to_bytes().hex()
92+
93+
94+
def _to_staking_info_response(info: AccountInfo) -> StakingInfoResponse:
95+
if info.staking_info is None:
96+
return StakingInfoResponse(
97+
declineStakingReward=False,
98+
pendingReward="0",
99+
stakedToMe="0",
100+
)
101+
102+
staking_info = info.staking_info
103+
return StakingInfoResponse(
104+
declineStakingReward=staking_info.decline_reward,
105+
stakePeriodStart=str(staking_info.stake_period_start)
106+
if staking_info.stake_period_start is not None
107+
else None,
108+
pendingReward=str(staking_info.pending_reward.to_tinybars())
109+
if staking_info.pending_reward is not None
110+
else "0",
111+
stakedToMe=str(staking_info.staked_to_me.to_tinybars())
112+
if staking_info.staked_to_me is not None
113+
else "0",
114+
stakedAccountId=str(staking_info.staked_account_id)
115+
if staking_info.staked_account_id is not None
116+
else None,
117+
stakedNodeId=str(staking_info.staked_node_id)
118+
if staking_info.staked_node_id is not None
119+
else None,
120+
)
121+
122+
123+
def _to_token_relationships_response(info: AccountInfo) -> dict[str, TokenRelationshipResponse]:
124+
token_relationships_response: dict[str, TokenRelationshipResponse] = {}
125+
126+
for relationship in info.token_relationships:
127+
token_id = str(relationship.token_id) if relationship.token_id is not None else None
128+
if token_id is None:
129+
continue
130+
131+
token_relationships_response[token_id] = TokenRelationshipResponse(
132+
tokenId=token_id,
133+
symbol=relationship.symbol,
134+
balance=str(relationship.balance) if relationship.balance is not None else "0",
135+
kycStatus=_enum_name(relationship.kyc_status),
136+
freezeStatus=_enum_name(relationship.freeze_status),
137+
decimals=str(relationship.decimals) if relationship.decimals is not None else None,
138+
automaticAssociation=relationship.automatic_association,
89139
)
90140

91-
token_relationships_response = []
92-
if info.token_relationships:
93-
for rel in info.token_relationships:
94-
token_relationships_response.append(
95-
TokenRelationshipResponse(
96-
tokenId=str(rel.token_id) if rel.token_id else None,
97-
symbol=rel.symbol,
98-
balance=str(rel.balance) if rel.balance is not None else None,
99-
kycStatus=str(rel.kyc_status) if rel.kyc_status is not None else None,
100-
freezeStatus=str(rel.freeze_status) if rel.freeze_status is not None else None,
101-
decimals=str(rel.decimals) if rel.decimals is not None else None,
102-
automaticAssociation=rel.automatic_association,
103-
)
104-
)
141+
return token_relationships_response
142+
105143

144+
def _build_account_info_response(info: AccountInfo) -> GetAccountInfoResponse:
106145
return GetAccountInfoResponse(
107-
accountId=str(info.account_id) if info.account_id else None,
108-
contractAccountId=info.contract_account_id,
109-
isDeleted=info.is_deleted,
110-
proxyReceived=str(info.proxy_received.to_tinybars()) if info.proxy_received else None,
111-
key=info.key.to_bytes().hex() if info.key else None,
112-
balance=str(info.balance.to_tinybars()) if info.balance else None,
113-
isReceiverSignatureRequired=info.receiver_signature_required,
114-
expirationTime=str(info.expiration_time) if info.expiration_time else None,
115-
autoRenewPeriod=str(info.auto_renew_period.seconds) if info.auto_renew_period else None,
116-
tokenRelationships=token_relationships_response if token_relationships_response else None,
117-
accountMemo=info.account_memo,
118-
ownedNfts=str(info.owned_nfts) if info.owned_nfts is not None else None,
146+
accountId=str(info.account_id) if info.account_id is not None else None,
147+
contractAccountId=info.contract_account_id or "",
148+
isDeleted=bool(info.is_deleted),
149+
proxyAccountId="",
150+
proxyReceived=str(info.proxy_received.to_tinybars())
151+
if info.proxy_received is not None
152+
else "0",
153+
key=_serialize_key(info.key),
154+
balance=str(info.balance.to_tinybars()) if info.balance is not None else "0",
155+
sendRecordThreshold="0",
156+
receiveRecordThreshold="0",
157+
isReceiverSignatureRequired=bool(info.receiver_signature_required),
158+
expirationTime=str(info.expiration_time) if info.expiration_time is not None else None,
159+
autoRenewPeriod=str(info.auto_renew_period.seconds)
160+
if info.auto_renew_period is not None
161+
else "0",
162+
tokenRelationships=_to_token_relationships_response(info),
163+
accountMemo=info.account_memo or "",
164+
ownedNfts=str(info.owned_nfts) if info.owned_nfts is not None else "0",
119165
maxAutomaticTokenAssociations=str(info.max_automatic_token_associations)
120166
if info.max_automatic_token_associations is not None
121-
else None,
122-
stakingInfo=staking_info_response,
167+
else "0",
168+
aliasKey="",
169+
ledgerId="",
170+
ethereumNonce="0",
171+
stakingInfo=_to_staking_info_response(info),
123172
)
124173

125174

126175
@rpc_method("getAccountInfo")
127176
def get_account_info(params: GetAccountInfoParams) -> GetAccountInfoResponse:
128177
client = get_client(params.sessionId)
129178

130-
query = AccountInfoQuery()
131-
if params.accountId:
132-
query.set_account_id(AccountId.from_string(params.accountId))
179+
if not params.accountId:
180+
raise JsonRpcError.hiero_error({"status": ResponseCode.INVALID_ACCOUNT_ID.name})
181+
182+
try:
183+
account_id = AccountId.from_string(params.accountId)
184+
except (TypeError, ValueError) as error:
185+
raise JsonRpcError.hiero_error(
186+
{"status": ResponseCode.INVALID_ACCOUNT_ID.name}
187+
) from error
188+
189+
query = AccountInfoQuery().set_account_id(account_id)
133190

134191
info = query.execute(client)
135192
return _build_account_info_response(info)

tck/response/account.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
from dataclasses import dataclass
2-
from typing import List, Optional
1+
from dataclasses import dataclass, field
32

43

54
@dataclass
@@ -34,14 +33,24 @@ class GetAccountInfoResponse:
3433
accountId: str | None = None
3534
contractAccountId: str | None = None
3635
isDeleted: bool | None = None
36+
proxyAccountId: str | None = None
3737
proxyReceived: str | None = None
3838
key: str | None = None
3939
balance: str | None = None
40+
sendRecordThreshold: str | None = None
41+
receiveRecordThreshold: str | None = None
4042
isReceiverSignatureRequired: bool | None = None
4143
expirationTime: str | None = None
4244
autoRenewPeriod: str | None = None
43-
tokenRelationships: List[TokenRelationshipResponse] | None = None
45+
liveHashes: list[dict] = field(default_factory=list)
46+
tokenRelationships: dict[str, TokenRelationshipResponse] = field(default_factory=dict)
4447
accountMemo: str | None = None
4548
ownedNfts: str | None = None
4649
maxAutomaticTokenAssociations: str | None = None
50+
aliasKey: str | None = None
51+
ledgerId: str | None = None
52+
ethereumNonce: str | None = None
53+
hbarAllowances: list[dict] = field(default_factory=list)
54+
tokenAllowances: list[dict] = field(default_factory=list)
55+
nftAllowances: list[dict] = field(default_factory=list)
4756
stakingInfo: StakingInfoResponse | None = None

tests/tck/test_account_info.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import importlib
2+
from unittest.mock import MagicMock, patch
3+
4+
import pytest
5+
6+
from hiero_sdk_python.Duration import Duration
7+
from hiero_sdk_python.account.account_id import AccountId
8+
from hiero_sdk_python.account.account_info import AccountInfo
9+
from hiero_sdk_python.crypto.private_key import PrivateKey
10+
from hiero_sdk_python.exceptions import PrecheckError
11+
from hiero_sdk_python.hbar import Hbar
12+
from hiero_sdk_python.response_code import ResponseCode
13+
from hiero_sdk_python.staking_info import StakingInfo
14+
from hiero_sdk_python.timestamp import Timestamp
15+
from hiero_sdk_python.tokens.token_freeze_status import TokenFreezeStatus
16+
from hiero_sdk_python.tokens.token_id import TokenId
17+
from hiero_sdk_python.tokens.token_kyc_status import TokenKycStatus
18+
from hiero_sdk_python.tokens.token_relationship import TokenRelationship
19+
from tck.errors import HIERO_ERROR, JsonRpcError
20+
from tck.handlers import registry
21+
from tck.handlers.registry import dispatch
22+
from tck.param.account import GetAccountInfoParams
23+
from tck.util import client_utils
24+
25+
pytestmark = pytest.mark.unit
26+
27+
28+
@pytest.fixture(autouse=True)
29+
def clear_registry_and_clients():
30+
registry._HANDLERS.clear()
31+
client_utils._CLIENTS.clear()
32+
33+
import tck.handlers.account as account_handlers
34+
35+
importlib.reload(account_handlers)
36+
yield
37+
38+
registry._HANDLERS.clear()
39+
client_utils._CLIENTS.clear()
40+
41+
42+
@pytest.fixture
43+
def params_dict():
44+
return {"accountId": "0.0.123", "sessionId": "sess1"}
45+
46+
47+
def test_parse_json_params_success(params_dict):
48+
params = GetAccountInfoParams.parse_json_params(params_dict)
49+
50+
assert params.accountId == "0.0.123"
51+
assert params.sessionId == "sess1"
52+
53+
54+
def test_parse_json_params_missing_account_id_defaults_to_none():
55+
params = GetAccountInfoParams.parse_json_params({"sessionId": "sess1"})
56+
57+
assert params.accountId is None
58+
assert params.sessionId == "sess1"
59+
60+
61+
@patch("tck.handlers.account.get_client")
62+
@patch("hiero_sdk_python.query.account_info_query.AccountInfoQuery.execute")
63+
def test_get_account_info_success_mapping(mock_execute, mock_get_client, params_dict):
64+
mock_get_client.return_value = MagicMock()
65+
66+
key = PrivateKey.generate_ed25519().public_key()
67+
expected_key = key.to_string_der()
68+
69+
mock_execute.return_value = AccountInfo(
70+
account_id=AccountId.from_string("0.0.123"),
71+
contract_account_id="000000000000000000000000000000000000007b",
72+
is_deleted=False,
73+
proxy_received=Hbar.from_tinybars(10),
74+
key=key,
75+
balance=Hbar.from_tinybars(1000),
76+
receiver_signature_required=True,
77+
expiration_time=Timestamp(1700000000, 123),
78+
auto_renew_period=Duration(7776000),
79+
token_relationships=[
80+
TokenRelationship(
81+
token_id=TokenId(0, 0, 456),
82+
symbol="TOK",
83+
balance=12,
84+
kyc_status=TokenKycStatus.GRANTED,
85+
freeze_status=TokenFreezeStatus.UNFROZEN,
86+
decimals=2,
87+
automatic_association=True,
88+
)
89+
],
90+
account_memo="memo",
91+
owned_nfts=2,
92+
max_automatic_token_associations=11,
93+
staking_info=StakingInfo(
94+
decline_reward=False,
95+
stake_period_start=Timestamp(1700000001, 0),
96+
pending_reward=Hbar.from_tinybars(3),
97+
staked_to_me=Hbar.from_tinybars(4),
98+
staked_account_id=AccountId.from_string("0.0.7"),
99+
),
100+
)
101+
102+
response = dispatch("getAccountInfo", params_dict)
103+
104+
assert response["accountId"] == "0.0.123"
105+
assert response["contractAccountId"] == "000000000000000000000000000000000000007b"
106+
assert response["isDeleted"] is False
107+
assert response["proxyReceived"] == "10"
108+
assert response["key"] == expected_key
109+
assert response["balance"] == "1000"
110+
assert response["sendRecordThreshold"] == "0"
111+
assert response["receiveRecordThreshold"] == "0"
112+
assert response["isReceiverSignatureRequired"] is True
113+
assert response["expirationTime"] == "1700000000.000000123"
114+
assert response["autoRenewPeriod"] == "7776000"
115+
assert response["tokenRelationships"]["0.0.456"] == {
116+
"tokenId": "0.0.456",
117+
"symbol": "TOK",
118+
"balance": "12",
119+
"kycStatus": "GRANTED",
120+
"freezeStatus": "UNFROZEN",
121+
"decimals": "2",
122+
"automaticAssociation": True,
123+
}
124+
assert response["accountMemo"] == "memo"
125+
assert response["ownedNfts"] == "2"
126+
assert response["maxAutomaticTokenAssociations"] == "11"
127+
assert response["aliasKey"] == ""
128+
assert response["ledgerId"] == ""
129+
assert response["ethereumNonce"] == "0"
130+
assert response["liveHashes"] == []
131+
assert response["hbarAllowances"] == []
132+
assert response["tokenAllowances"] == []
133+
assert response["nftAllowances"] == []
134+
assert response["stakingInfo"] == {
135+
"declineStakingReward": False,
136+
"stakePeriodStart": "1700000001.000000000",
137+
"pendingReward": "3",
138+
"stakedToMe": "4",
139+
"stakedAccountId": "0.0.7",
140+
"stakedNodeId": None,
141+
}
142+
143+
144+
def test_get_account_info_missing_account_id_maps_to_hiero_error():
145+
with pytest.raises(JsonRpcError) as exception:
146+
dispatch("getAccountInfo", {"sessionId": "sess1"})
147+
148+
assert exception.value.code == HIERO_ERROR
149+
assert exception.value.data == {"status": ResponseCode.INVALID_ACCOUNT_ID.name}
150+
151+
152+
def test_get_account_info_invalid_account_id_maps_to_hiero_error():
153+
with pytest.raises(JsonRpcError) as exception:
154+
dispatch("getAccountInfo", {"sessionId": "sess1", "accountId": "invalid-id"})
155+
156+
assert exception.value.code == HIERO_ERROR
157+
assert exception.value.data == {"status": ResponseCode.INVALID_ACCOUNT_ID.name}
158+
159+
160+
@patch("tck.handlers.account.get_client")
161+
@patch("hiero_sdk_python.query.account_info_query.AccountInfoQuery.execute")
162+
def test_get_account_info_precheck_error_maps_to_hiero_error(mock_execute, mock_get_client):
163+
mock_get_client.return_value = MagicMock()
164+
mock_execute.side_effect = PrecheckError(status=ResponseCode.ACCOUNT_DELETED)
165+
166+
with pytest.raises(JsonRpcError) as exception:
167+
dispatch("getAccountInfo", {"sessionId": "sess1", "accountId": "0.0.123"})
168+
169+
assert exception.value.code == HIERO_ERROR
170+
assert exception.value.data == {"status": ResponseCode.ACCOUNT_DELETED.name}

0 commit comments

Comments
 (0)