Skip to content
Merged
7 changes: 2 additions & 5 deletions src/hiero_sdk_python/query/account_info_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,15 @@ def _make_request(self):
Query: The protobuf query message.

Raises:
ValueError: If the account ID is not set.
Exception: If any other error occurs during request construction.
"""
try:
if not self.account_id:
raise ValueError("Account ID must be set before making the request.")

query_header = self._make_request_header()

crypto_info_query = crypto_get_info_pb2.CryptoGetInfoQuery()
crypto_info_query.header.CopyFrom(query_header)
crypto_info_query.accountID.CopyFrom(self.account_id._to_proto())
if self.account_id is not None:
crypto_info_query.accountID.CopyFrom(self.account_id._to_proto())
Comment thread
manishdait marked this conversation as resolved.

query = query_pb2.Query()
query.cryptoGetInfo.CopyFrom(crypto_info_query)
Expand Down
111 changes: 109 additions & 2 deletions tck/handlers/account.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
"""TCK RPC handlers for account-related endpoints."""

from __future__ import annotations

from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction
from hiero_sdk_python.account.account_id import AccountId
from hiero_sdk_python.account.account_info import AccountInfo
from hiero_sdk_python.hbar import Hbar
from hiero_sdk_python.query.account_info_query import AccountInfoQuery
from hiero_sdk_python.response_code import ResponseCode
from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt
from tck.handlers.registry import rpc_method
from tck.param.account import CreateAccountParams
from tck.response.account import CreateAccountResponse
from tck.param.account import CreateAccountParams, GetAccountInfoParams
from tck.response.account import (
CreateAccountResponse,
GetAccountInfoResponse,
StakingInfoResponse,
TokenRelationshipResponse,
)
from tck.util.client_utils import get_client
from tck.util.constants import DEFAULT_GRPC_TIMEOUT
from tck.util.key_utils import get_key_from_string
Expand Down Expand Up @@ -51,6 +60,7 @@ def _build_create_account_transaction(params: CreateAccountParams) -> AccountCre

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

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

return CreateAccountResponse(account_id, ResponseCode(receipt.status).name)


def _enum_name(value) -> str | None:
if value is None:
return None
return getattr(value, "name", str(value))


def _serialize_key(key) -> str | None:
if key is None:
return None

to_string_der = getattr(key, "to_string_der", None)
if callable(to_string_der):
return to_string_der()

return key.to_bytes().hex()


def _to_staking_info_response(info: AccountInfo) -> StakingInfoResponse | None:
if info.staking_info is None:
return None

staking_info = info.staking_info
return StakingInfoResponse(
declineStakingReward=staking_info.decline_reward,
stakePeriodStart=str(staking_info.stake_period_start) if staking_info.stake_period_start is not None else None,
pendingReward=str(staking_info.pending_reward.to_tinybars())
if staking_info.pending_reward is not None
else "0",
stakedToMe=str(staking_info.staked_to_me.to_tinybars()) if staking_info.staked_to_me is not None else "0",
stakedAccountId=str(staking_info.staked_account_id) if staking_info.staked_account_id is not None else None,
stakedNodeId=str(staking_info.staked_node_id) if staking_info.staked_node_id is not None else None,
)


def _to_token_relationships_response(info: AccountInfo) -> dict[str, TokenRelationshipResponse]:
token_relationships_response: dict[str, TokenRelationshipResponse] = {}

for relationship in info.token_relationships:
token_id = str(relationship.token_id) if relationship.token_id is not None else None
if token_id is None:
continue

token_relationships_response[token_id] = TokenRelationshipResponse(
tokenId=token_id,
symbol=relationship.symbol,
balance=str(relationship.balance) if relationship.balance is not None else "0",
kycStatus=_enum_name(relationship.kyc_status),
freezeStatus=_enum_name(relationship.freeze_status),
decimals=str(relationship.decimals) if relationship.decimals is not None else None,
automaticAssociation=relationship.automatic_association,
)

return token_relationships_response


def _build_account_info_response(info: AccountInfo) -> GetAccountInfoResponse:
auto_renew_period_seconds = str(info.auto_renew_period.seconds) if info.auto_renew_period is not None else "0"

return GetAccountInfoResponse(
accountId=str(info.account_id) if info.account_id is not None else None,
contractAccountId=info.contract_account_id or "",
isDeleted=bool(info.is_deleted),
proxyAccountId="",
proxyReceived=str(info.proxy_received.to_tinybars()) if info.proxy_received is not None else "0",
key=_serialize_key(info.key),
balance=str(info.balance.to_tinybars()) if info.balance is not None else "0",
sendRecordThreshold="0",
receiveRecordThreshold="0",
isReceiverSignatureRequired=bool(info.receiver_signature_required),
expirationTime=str(info.expiration_time) if info.expiration_time is not None else None,
autoRenewPeriod=auto_renew_period_seconds,
tokenRelationships=_to_token_relationships_response(info),
accountMemo=info.account_memo or "",
ownedNfts=str(info.owned_nfts) if info.owned_nfts is not None else "0",
maxAutomaticTokenAssociations=str(info.max_automatic_token_associations)
if info.max_automatic_token_associations is not None
else "0",
aliasKey="",
ledgerId="",
ethereumNonce="0",
stakingInfo=_to_staking_info_response(info),
)


@rpc_method("getAccountInfo")
def get_account_info(params: GetAccountInfoParams) -> GetAccountInfoResponse:
"""Query account info and map SDK fields to the TCK response contract."""
client = get_client(params.sessionId)

Comment thread
manishdait marked this conversation as resolved.
query = AccountInfoQuery().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT)
if params.accountId is not None:
query.set_account_id(AccountId.from_string(params.accountId))

info = query.execute(client)
Comment thread
manishdait marked this conversation as resolved.
return _build_account_info_response(info)
19 changes: 18 additions & 1 deletion tck/param/account.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""TCK request parameter models for account endpoints."""

from __future__ import annotations

from dataclasses import dataclass

from tck.param.base import BaseTransactionParams
from tck.param.base import BaseParams, BaseTransactionParams
from tck.util.param_utils import (
parse_common_transaction_params,
parse_session_id,
Expand All @@ -13,6 +15,8 @@

@dataclass
class CreateAccountParams(BaseTransactionParams):
"""Request parameters for the createAccount endpoint."""

key: str | None = None
initialBalance: int | None = None
receiverSignatureRequired: bool | None = None
Expand All @@ -26,6 +30,7 @@ class CreateAccountParams(BaseTransactionParams):

@classmethod
def parse_json_params(cls, params: dict) -> CreateAccountParams:
"""Parse JSON-RPC params into a CreateAccountParams instance."""
return cls(
key=params.get("key"),
initialBalance=to_int(params.get("initialBalance")),
Expand All @@ -40,3 +45,15 @@ def parse_json_params(cls, params: dict) -> CreateAccountParams:
sessionId=parse_session_id(params),
commonTransactionParams=parse_common_transaction_params(params),
)


@dataclass
class GetAccountInfoParams(BaseParams):
"""Request parameters for the getAccountInfo endpoint."""

accountId: str | None = None

@classmethod
def parse_json_params(cls, params: dict) -> GetAccountInfoParams:
"""Parse JSON-RPC params into a GetAccountInfoParams instance."""
return cls(accountId=params.get("accountId"), sessionId=parse_session_id(params))
61 changes: 60 additions & 1 deletion tck/response/account.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,68 @@
"""TCK response models for account endpoints."""

from __future__ import annotations

from dataclasses import dataclass
from dataclasses import dataclass, field


@dataclass
class CreateAccountResponse:
"""Response payload for createAccount."""

accountId: str | None = None
status: str | None = None


@dataclass
class StakingInfoResponse:
"""Nested staking fields in the getAccountInfo response."""

declineStakingReward: bool | None = None
stakePeriodStart: str | None = None
pendingReward: str | None = None
stakedToMe: str | None = None
stakedAccountId: str | None = None
stakedNodeId: str | None = None


@dataclass
class TokenRelationshipResponse:
"""Nested token relationship details for getAccountInfo."""

tokenId: str | None = None
symbol: str | None = None
balance: str | None = None
kycStatus: str | None = None
freezeStatus: str | None = None
decimals: str | None = None
automaticAssociation: bool | None = None


@dataclass
class GetAccountInfoResponse:
"""Response payload for getAccountInfo."""

accountId: str | None = None
contractAccountId: str | None = None
isDeleted: bool | None = None
proxyAccountId: str | None = None
proxyReceived: str | None = None
key: str | None = None
balance: str | None = None
sendRecordThreshold: str | None = None
receiveRecordThreshold: str | None = None
isReceiverSignatureRequired: bool | None = None
expirationTime: str | None = None
autoRenewPeriod: str | None = None
liveHashes: list[dict] = field(default_factory=list)
tokenRelationships: dict[str, TokenRelationshipResponse] = field(default_factory=dict)
accountMemo: str | None = None
ownedNfts: str | None = None
maxAutomaticTokenAssociations: str | None = None
aliasKey: str | None = None
ledgerId: str | None = None
ethereumNonce: str | None = None
hbarAllowances: list[dict] = field(default_factory=list)
tokenAllowances: list[dict] = field(default_factory=list)
nftAllowances: list[dict] = field(default_factory=list)
stakingInfo: StakingInfoResponse | None = None
25 changes: 21 additions & 4 deletions tests/unit/account_info_query_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,29 @@ def test_constructor():
assert query.account_id == account_id


def test_execute_fails_with_missing_account_id(mock_client):
Comment thread
manishdait marked this conversation as resolved.
"""Test request creation with missing Account ID."""
def test_execute_fails_with_missing_account_id():
"""Test request execution with missing Account ID throws PrecheckError."""
from hiero_sdk_python.exceptions import PrecheckError

query = AccountInfoQuery()

with pytest.raises(ValueError, match=r"Account ID must be set before making the request\."):
query.execute(mock_client)
response_sequences = [
[
response_pb2.Response(
cryptoGetInfo=crypto_get_info_pb2.CryptoGetInfoResponse(
header=response_header_pb2.ResponseHeader(
nodeTransactionPrecheckCode=ResponseCode.INVALID_ACCOUNT_ID,
responseType=ResponseType.ANSWER_ONLY,
cost=0,
)
)
)
]
]
with mock_hedera_servers(response_sequences) as client:
with pytest.raises(PrecheckError) as exc_info:
query.execute(client)
assert exc_info.value.status == ResponseCode.INVALID_ACCOUNT_ID


def test_get_method():
Expand Down
Loading