Skip to content

Commit d02fb59

Browse files
authored
feat(tck): Added getAccountBalance method to tck (hiero-ledger#2336)
Signed-off-by: Manish Dait <daitmanish88@gmail.com>
1 parent 1b77834 commit d02fb59

8 files changed

Lines changed: 139 additions & 26 deletions

File tree

src/hiero_sdk_python/account/account_balance.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,23 @@ class AccountBalance:
1616
Attributes:
1717
hbars (Hbar): The balance in hbars.
1818
token_balances (dict): A dictionary mapping TokenId to token balances.
19+
token_decimals (dict, optional): A dictionary mapping TokenId to token deimals.
1920
"""
2021

21-
def __init__(self, hbars: Hbar, token_balances: dict[TokenId, int] = None) -> None:
22+
def __init__(
23+
self, hbars: Hbar, token_balances: dict[TokenId, int] = None, token_decimals: dict[TokenId, int] = None
24+
) -> None:
2225
"""
2326
Initializes the AccountBalance with the given hbar balance and token balances.
2427
2528
Args:
2629
hbars (Hbar): The balance in hbars.
2730
token_balances (dict, optional): A dictionary mapping TokenId to token balances.
31+
token_decimals (dict, optional): A dictionary mapping TokenId to token deimals.
2832
"""
2933
self.hbars = hbars
3034
self.token_balances = token_balances or {}
35+
self.token_decimals = token_decimals or {}
3136

3237
@classmethod
3338
def _from_proto(cls, proto: CryptoGetAccountBalanceResponse) -> AccountBalance:
@@ -43,13 +48,18 @@ def _from_proto(cls, proto: CryptoGetAccountBalanceResponse) -> AccountBalance:
4348
hbars: Hbar = Hbar.from_tinybars(tinybars=proto.balance)
4449

4550
token_balances: dict[TokenId, int] = {}
51+
token_decimals: dict[TokenId, int] = {}
52+
4653
if proto.tokenBalances:
4754
for token_balance in proto.tokenBalances:
4855
token_id: TokenId = TokenId._from_proto(token_balance.tokenId)
4956
balance: int = token_balance.balance
57+
decimal: int = token_balance.decimals
58+
5059
token_balances[token_id] = balance
60+
token_decimals[token_id] = decimal
5161

52-
return cls(hbars=hbars, token_balances=token_balances)
62+
return cls(hbars=hbars, token_balances=token_balances, token_decimals=token_decimals)
5363

5464
def __str__(self) -> str:
5565
"""
@@ -59,10 +69,17 @@ def __str__(self) -> str:
5969
str: A string showing HBAR balance and token balances.
6070
"""
6171
lines = [f"HBAR Balance: {self.hbars} hbars"]
72+
6273
if self.token_balances:
6374
lines.append("Token Balances:")
6475
for token_id, balance in self.token_balances.items():
6576
lines.append(f" - Token ID {token_id}: {balance} units")
77+
78+
if self.token_decimals:
79+
lines.append("Token Decimals:")
80+
for token_id, decimal in self.token_decimals.items():
81+
lines.append(f" - Token ID {token_id}: {decimal} decimals")
82+
6683
return "\n".join(lines)
6784

6885
def __repr__(self) -> str:
@@ -77,4 +94,9 @@ def __repr__(self) -> str:
7794
if self.token_balances
7895
else "{}"
7996
)
80-
return f"AccountBalance(hbars={self.hbars!r}, token_balances={token_balances_repr})"
97+
token_decimals_repr = (
98+
f"{{{', '.join(f'{token_id!r}: {decimals}' for token_id, decimals in self.token_decimals.items())}}}"
99+
if self.token_decimals
100+
else "{}"
101+
)
102+
return f"AccountBalance(hbars={self.hbars!r}, token_balances={token_balances_repr}) token_decimals={token_decimals_repr})"

src/hiero_sdk_python/query/account_balance_query.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,19 +90,17 @@ def _make_request(self) -> query_pb2.Query:
9090
Exception: If any other error occurs during request construction.
9191
"""
9292
try:
93-
if not self.account_id and not self.contract_id:
94-
raise ValueError("Either account_id or contract_id must be set before making the request.")
95-
9693
if self.account_id and self.contract_id:
9794
raise ValueError("Specify either account_id or contract_id, not both.")
9895

9996
query_header = self._make_request_header()
10097
crypto_get_balance = crypto_get_account_balance_pb2.CryptoGetAccountBalanceQuery()
10198
crypto_get_balance.header.CopyFrom(query_header)
10299

103-
if self.account_id:
100+
if self.account_id is not None:
104101
crypto_get_balance.accountID.CopyFrom(self.account_id._to_proto())
105-
else:
102+
103+
if self.contract_id is not None:
106104
crypto_get_balance.contractID.CopyFrom(self.contract_id._to_proto())
107105

108106
query = query_pb2.Query()

tck/handlers/account.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,32 @@
22

33
from __future__ import annotations
44

5+
from hiero_sdk_python.account.account_balance import AccountBalance
56
from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction
67
from hiero_sdk_python.account.account_delete_transaction import AccountDeleteTransaction
78
from hiero_sdk_python.account.account_id import AccountId
89
from hiero_sdk_python.account.account_info import AccountInfo
910
from hiero_sdk_python.account.account_update_transaction import AccountUpdateTransaction
11+
from hiero_sdk_python.contract.contract_id import ContractId
1012
from hiero_sdk_python.Duration import Duration
1113
from hiero_sdk_python.hbar import Hbar
14+
from hiero_sdk_python.query.account_balance_query import CryptoGetAccountBalanceQuery
1215
from hiero_sdk_python.query.account_info_query import AccountInfoQuery
1316
from hiero_sdk_python.response_code import ResponseCode
1417
from hiero_sdk_python.timestamp import Timestamp
1518
from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt
1619
from tck.handlers.registry import rpc_method
17-
from tck.param.account import CreateAccountParams, DeleteAccountParams, GetAccountInfoParams, UpdateAccountParams
20+
from tck.param.account import (
21+
CreateAccountParams,
22+
DeleteAccountParams,
23+
GetAccountBalanceParams,
24+
GetAccountInfoParams,
25+
UpdateAccountParams,
26+
)
1827
from tck.response.account import (
1928
CreateAccountResponse,
2029
DeleteAccountResponse,
30+
GetAccountBalanceResponse,
2131
GetAccountInfoResponse,
2232
StakingInfoResponse,
2333
TokenRelationshipResponse,
@@ -254,3 +264,30 @@ def delete_account(params: DeleteAccountParams) -> DeleteAccountResponse:
254264
receipt: TransactionReceipt = response.get_receipt(client, validate_status=True)
255265

256266
return DeleteAccountResponse(status=ResponseCode(receipt.status).name)
267+
268+
269+
@rpc_method("getAccountBalance")
270+
def get_account_balance(params: GetAccountBalanceParams) -> GetAccountBalanceResponse:
271+
"""Get account balance for an account."""
272+
client = get_client(params.sessionId)
273+
274+
query = CryptoGetAccountBalanceQuery().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT)
275+
276+
if params.accountId is not None:
277+
query.set_account_id(AccountId.from_string(params.accountId))
278+
if params.contractId is not None:
279+
query.set_contract_id(ContractId.from_string(params.contractId))
280+
281+
account_balance = query.execute(client)
282+
return map_account_balance_response(account_balance)
283+
284+
285+
def map_account_balance_response(account_balance: AccountBalance) -> GetAccountBalanceResponse:
286+
"""Map AccountBalance class to the GetAccountBalanceResponse."""
287+
token_balances = {str(token_id): int(balance) for token_id, balance in account_balance.token_balances.items()}
288+
289+
token_decimals = {str(token_id): int(decimals) for token_id, decimals in account_balance.token_decimals.items()}
290+
291+
return GetAccountBalanceResponse(
292+
hbars=str(account_balance.hbars.to_tinybars()), tokenBalances=token_balances, tokenDecimals=token_decimals
293+
)

tck/param/account.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,18 @@ class GetAccountInfoParams(BaseParams):
113113
def parse_json_params(cls, params: dict) -> GetAccountInfoParams:
114114
"""Parse JSON-RPC params into a GetAccountInfoParams instance."""
115115
return cls(accountId=params.get("accountId"), sessionId=parse_session_id(params))
116+
117+
118+
@dataclass
119+
class GetAccountBalanceParams(BaseParams):
120+
"""Request parameters for the getAccountBalance endpoint."""
121+
122+
accountId: str | None = None
123+
contractId: str | None = None
124+
125+
@classmethod
126+
def parse_json_params(cls, params):
127+
"""Parse JSON-RPC params into a GetAccountBalanceParams instance."""
128+
return cls(
129+
accountId=params.get("accountId"), contractId=params.get("contractId"), sessionId=parse_session_id(params)
130+
)

tck/response/account.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,12 @@ class GetAccountInfoResponse:
8080
tokenAllowances: list[dict] = field(default_factory=list)
8181
nftAllowances: list[dict] = field(default_factory=list)
8282
stakingInfo: StakingInfoResponse | None = None
83+
84+
85+
@dataclass
86+
class GetAccountBalanceResponse:
87+
"""Response payload for getAccountBalance."""
88+
89+
hbars: str | None = None
90+
tokenBalances: dict[str, int] = field(default_factory=dict)
91+
tokenDecimals: dict[str, int] = field(default_factory=dict)

tests/integration/account_balance_query_e2e_test.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from hiero_sdk_python.contract.contract_create_transaction import ContractCreateTransaction
77
from hiero_sdk_python.contract.contract_delete_transaction import ContractDeleteTransaction
88
from hiero_sdk_python.contract.contract_id import ContractId
9+
from hiero_sdk_python.exceptions import PrecheckError
910
from hiero_sdk_python.query.account_balance_query import CryptoGetAccountBalanceQuery
1011
from hiero_sdk_python.response_code import ResponseCode
1112
from tests.integration.utils import IntegrationTestEnv
@@ -89,11 +90,10 @@ def test_integration_contract_balance_query_can_execute():
8990
def test_integration_balance_query_raises_when_neither_source_set():
9091
env = IntegrationTestEnv()
9192
try:
92-
with pytest.raises(
93-
ValueError,
94-
match=r"Either account_id or contract_id must be set before making the request\.",
95-
):
93+
with pytest.raises(PrecheckError) as err:
9694
CryptoGetAccountBalanceQuery().execute(env.client)
95+
96+
assert err.value.status == ResponseCode.INVALID_ACCOUNT_ID
9797
finally:
9898
env.close()
9999

tests/unit/account_balance_query_test.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,15 +132,13 @@ def test_last_wins_when_both_account_id_and_contract_id_are_set(
132132
assert query.account_id is None
133133

134134

135-
def test_make_request_raises_when_neither_account_id_nor_contract_id_is_set():
136-
"""_make_request should raise if neither account_id nor contract_id is set."""
135+
def test_make_request_when_neither_account_id_nor_contract_id_is_set():
136+
"""_make_request should create request without ids when neither account_id nor contract_id is set."""
137137
query = CryptoGetAccountBalanceQuery()
138+
proto = query._make_request()
138139

139-
with pytest.raises(
140-
ValueError,
141-
match=r"Either account_id or contract_id must be set before making the request\.",
142-
):
143-
query._make_request()
140+
assert not proto.cryptogetAccountBalance.HasField("accountID")
141+
assert not proto.cryptogetAccountBalance.HasField("contractID")
144142

145143

146144
def test_make_request_populates_contract_id_only():

tests/unit/account_balance_test.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import pytest
66

77
from hiero_sdk_python.account.account_balance import AccountBalance
8+
from hiero_sdk_python.account.account_id import AccountId
9+
from hiero_sdk_python.hapi.services.basic_types_pb2 import TokenBalance
10+
from hiero_sdk_python.hapi.services.crypto_get_account_balance_pb2 import CryptoGetAccountBalanceResponse
811
from hiero_sdk_python.hbar import Hbar
912
from hiero_sdk_python.tokens.token_id import TokenId
1013

@@ -26,13 +29,14 @@ def test_account_balance_str_with_hbars_only():
2629
assert "Token Balances:" not in result
2730

2831

29-
def test_account_balance_str_with_token_balances():
30-
"""Test __str__ method with hbars and token balances."""
32+
def test_account_balance_str_with_token_balances_and_decimal():
33+
"""Test __str__ method with hbars and token balances and decimals."""
3134
hbars = Hbar(10)
3235
token_id_1 = TokenId(0, 0, 100)
3336
token_id_2 = TokenId(0, 0, 200)
3437
token_balances = {token_id_1: 1000, token_id_2: 500}
35-
account_balance = AccountBalance(hbars=hbars, token_balances=token_balances)
38+
token_decimals = {token_id_1: 1, token_id_2: 2}
39+
account_balance = AccountBalance(hbars=hbars, token_balances=token_balances, token_decimals=token_decimals)
3640

3741
result = str(account_balance)
3842

@@ -42,10 +46,13 @@ def test_account_balance_str_with_token_balances():
4246
assert "Token Balances:" in result
4347
assert " - Token ID 0.0.100: 1000 units" in result
4448
assert " - Token ID 0.0.200: 500 units" in result
49+
assert "Token Decimals:" in result
50+
assert " - Token ID 0.0.100: 1 decimals" in result
51+
assert " - Token ID 0.0.200: 2 decimals" in result
4552

4653

47-
def test_account_balance_str_with_empty_token_balances():
48-
"""Test __str__ method with empty token balances dict."""
54+
def test_account_balance_str_with_empty_token_balances_and_decimals():
55+
"""Test __str__ method with empty token balances and decimals dict."""
4956
hbars = Hbar(5.5)
5057
account_balance = AccountBalance(hbars=hbars, token_balances={})
5158

@@ -56,6 +63,7 @@ def test_account_balance_str_with_empty_token_balances():
5663
assert " hbars" in result
5764
# Should not include token balances section when empty
5865
assert "Token Balances:" not in result
66+
assert "Token Decimals:" not in result
5967

6068

6169
def test_account_balance_repr_with_hbars_only():
@@ -77,7 +85,8 @@ def test_account_balance_repr_with_token_balances():
7785
token_id_1 = TokenId(0, 0, 100)
7886
token_id_2 = TokenId(0, 0, 200)
7987
token_balances = {token_id_1: 1000, token_id_2: 500}
80-
account_balance = AccountBalance(hbars=hbars, token_balances=token_balances)
88+
token_decimals = {token_id_1: 1, token_id_2: 2}
89+
account_balance = AccountBalance(hbars=hbars, token_balances=token_balances, token_decimals=token_decimals)
8190

8291
result = repr(account_balance)
8392

@@ -87,3 +96,28 @@ def test_account_balance_repr_with_token_balances():
8796
assert "0.0.100" in result or "TokenId" in result
8897
assert "1000" in result
8998
assert "500" in result
99+
assert "token_decimals=" in result
100+
assert "1" in result
101+
assert "2" in result
102+
103+
104+
def test_create_account_balance_from_proto():
105+
token_blances_proto = [
106+
TokenBalance(tokenId=TokenId(0, 0, 100)._to_proto(), balance=100, decimals=1),
107+
TokenBalance(tokenId=TokenId(0, 0, 102)._to_proto(), balance=0, decimals=0),
108+
]
109+
110+
proto = CryptoGetAccountBalanceResponse(
111+
accountID=AccountId(0, 0, 1)._to_proto(), balance=10, tokenBalances=token_blances_proto
112+
)
113+
114+
account_balance = AccountBalance._from_proto(proto=proto)
115+
116+
assert account_balance is not None
117+
assert account_balance.hbars.to_tinybars() == 10
118+
119+
assert len(account_balance.token_balances) == 2
120+
assert account_balance.token_balances == {TokenId(0, 0, 100): 100, TokenId(0, 0, 102): 0}
121+
122+
assert len(account_balance.token_decimals) == 2
123+
assert account_balance.token_decimals == {TokenId(0, 0, 100): 1, TokenId(0, 0, 102): 0}

0 commit comments

Comments
 (0)