Skip to content

Commit 7fb2654

Browse files
feat: add contract_id support for CryptoGetAccountBalanceQuery (hiero-ledger#1389)
Signed-off-by: Antonio Ceppellini <antonio.ceppellini@gmail.com> Signed-off-by: AntonioCeppellini <128388022+AntonioCeppellini@users.noreply.github.com>
1 parent 5ce953f commit 7fb2654

5 files changed

Lines changed: 413 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
9090
- Added workflow to prevent assigning intermediate issues to contributors without prior Good First Issue completion (#1143).
9191
- Added `Client.from_env()` and network-specific factory methods (e.g., `Client.for_testnet()`) to simplify client initialization and reduce boilerplate. [[#1251](https://github.com/hiero-ledger/hiero-sdk-python/issues/1251)]
9292
- Improved unit test coverage for `TransactionId` class, covering parsing logic, hashing, and scheduled transactions.
93+
- Add contract_id support for CryptoGetAccountBalanceQuery([#1293](https://github.com/hiero-ledger/hiero-sdk-python/issues/1293))
9394
- Chained Good First Issue assignment with mentor assignment to bypass GitHub's anti-recursion protection - mentor assignment now occurs immediately after successful user assignment in the same workflow execution. (#1369)
9495
- Add GitHub Actions script and workflow for automatic spam list updates.
9596
- Added technical docstrings and hardening (set -euo pipefail) to the pr-check-test-files.sh script (#1336)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""
2+
Contract Balance Query Example
3+
4+
This script demonstrates how to:
5+
1. Set up a client connection to the Hedera network
6+
2. Create a file containing contract bytecode
7+
3. Create a contract
8+
4. Query the contract balance using CryptoGetAccountBalanceQuery.set_contract_id()
9+
10+
Run with:
11+
uv run -m examples.contract.contract_balance_query
12+
python -m examples.contract.contract_balance_query
13+
"""
14+
15+
import os
16+
import sys
17+
from dotenv import load_dotenv
18+
19+
from hiero_sdk_python import (
20+
Client,
21+
ContractCreateTransaction,
22+
CryptoGetAccountBalanceQuery,
23+
Hbar,
24+
)
25+
26+
from hiero_sdk_python.contract.contract_id import ContractId
27+
28+
from .contracts import SIMPLE_CONTRACT_BYTECODE
29+
from hiero_sdk_python.response_code import ResponseCode
30+
31+
load_dotenv()
32+
33+
34+
def setup_client() -> Client:
35+
print("Initializing client from environment variables...")
36+
try:
37+
client = Client.from_env()
38+
print(f"✅ Success! Connected as operator: {client.operator_account_id}")
39+
except Exception as e:
40+
print(f"❌ Failed: {e}")
41+
sys.exit(1)
42+
43+
return client
44+
45+
46+
def create_contract(client: Client, initial_balance_tinybars: int) -> ContractId:
47+
"""Create a contract using the bytecode file and return its ContractId."""
48+
bytecode = bytes.fromhex(SIMPLE_CONTRACT_BYTECODE)
49+
50+
receipt = (
51+
ContractCreateTransaction()
52+
.set_bytecode(bytecode)
53+
.set_gas(2_000_000)
54+
.set_initial_balance(initial_balance_tinybars)
55+
.set_contract_memo("Contract for balance query example")
56+
.execute(client)
57+
)
58+
59+
status_code = ResponseCode(receipt.status)
60+
status_name = status_code.name
61+
62+
if status_name == ResponseCode.SUCCESS.name:
63+
print("✅ Transaction succeeded!")
64+
elif status_code.is_unknown:
65+
print(f"❓ Unknown transaction status: {status_name}")
66+
sys.exit(1)
67+
else:
68+
print("❌ Transaction failed!")
69+
sys.exit(1)
70+
71+
return receipt.contract_id
72+
73+
74+
def get_contract_balance(client: Client, contract_id: ContractId):
75+
"""Query contract balance using CryptoGetAccountBalanceQuery.set_contract_id()."""
76+
print(f"Querying balance for contract {contract_id} ...")
77+
balance = CryptoGetAccountBalanceQuery().set_contract_id(contract_id).execute(client)
78+
79+
print("✅ Balance retrieved successfully!")
80+
print(f" Contract: {contract_id}")
81+
print(f" Hbars: {balance.hbars}")
82+
return balance
83+
84+
85+
def main():
86+
try:
87+
client = setup_client()
88+
89+
initial_balance_tinybars = Hbar(1)
90+
contract_id = create_contract(client, initial_balance_tinybars.to_tinybars())
91+
92+
print(f"✅ Contract created with ID: {contract_id}")
93+
get_contract_balance(client, contract_id)
94+
95+
except Exception as e:
96+
print(f"❌Error: {e}", file=sys.stderr)
97+
sys.exit(1)
98+
99+
100+
if __name__ == "__main__":
101+
main()

src/hiero_sdk_python/query/account_balance_query.py

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from hiero_sdk_python.account.account_balance import AccountBalance
77
from hiero_sdk_python.executable import _Method
88
from hiero_sdk_python.channels import _Channel
9+
from hiero_sdk_python.contract.contract_id import ContractId
10+
911

1012
class CryptoGetAccountBalanceQuery(Query):
1113
"""
@@ -15,29 +17,61 @@ class CryptoGetAccountBalanceQuery(Query):
1517
including hbars and tokens.
1618
"""
1719

18-
def __init__(self, account_id: Optional[AccountId] = None) -> None:
20+
def __init__(
21+
self,
22+
account_id: Optional[AccountId] = None,
23+
contract_id: Optional[ContractId] = None,
24+
) -> None:
1925
"""
2026
Initializes a new instance of the CryptoGetAccountBalanceQuery class.
2127
2228
Args:
2329
account_id (AccountId, optional): The ID of the account to retrieve the balance for.
30+
contract_id (ContractId, optional): The ID of the contract to retrieve the balance for.
2431
"""
2532
super().__init__()
26-
self.account_id: Optional[AccountId] = account_id
33+
self.account_id: Optional[AccountId] = None
34+
self.contract_id: Optional[ContractId] = None
35+
36+
if account_id is not None:
37+
self.set_account_id(account_id)
38+
if contract_id is not None:
39+
self.set_contract_id(contract_id)
2740

2841
def set_account_id(self, account_id: AccountId) -> "CryptoGetAccountBalanceQuery":
2942
"""
3043
Sets the account ID for which to retrieve the balance.
44+
Resets to None the contract ID.
3145
3246
Args:
3347
account_id (AccountId): The ID of the account.
3448
3549
Returns:
3650
CryptoGetAccountBalanceQuery: The current instance for method chaining.
3751
"""
52+
if not isinstance(account_id, AccountId):
53+
raise TypeError("account_id must be an AccountId.")
54+
self.contract_id = None
3855
self.account_id = account_id
3956
return self
4057

58+
def set_contract_id(self, contract_id: ContractId) -> "CryptoGetAccountBalanceQuery":
59+
"""
60+
Sets the contract ID for which to retrieve the balance.
61+
Resets to None the account ID.
62+
63+
Args:
64+
contract_id (ContractId): The ID of the contract.
65+
66+
Returns:
67+
CryptoGetAccountBalanceQuery: The current instance for method chaining.
68+
"""
69+
if not isinstance(contract_id, ContractId):
70+
raise TypeError("contract_id must be a ContractId.")
71+
self.account_id = None
72+
self.contract_id = contract_id
73+
return self
74+
4175
def _make_request(self) -> query_pb2.Query:
4276
"""
4377
Constructs the protobuf request for the account balance query.
@@ -46,22 +80,34 @@ def _make_request(self) -> query_pb2.Query:
4680
query_pb2.Query: The protobuf Query object containing the account balance query.
4781
4882
Raises:
49-
ValueError: If the account ID is not set.
83+
ValueError: If both the account ID and contract ID are not set.
84+
ValueError: If both the account ID and contract ID are set.
5085
AttributeError: If the Query protobuf structure is invalid.
5186
Exception: If any other error occurs during request construction.
5287
"""
5388
try:
54-
if not self.account_id:
55-
raise ValueError("Account ID must be set before making the request.")
89+
if not self.account_id and not self.contract_id:
90+
raise ValueError("Either account_id or contract_id must be set before making the request.")
91+
92+
if self.account_id and self.contract_id:
93+
raise ValueError("Specify either account_id or contract_id, not both.")
5694

5795
query_header = self._make_request_header()
58-
crypto_get_balance = crypto_get_account_balance_pb2.CryptoGetAccountBalanceQuery()
96+
crypto_get_balance = (
97+
crypto_get_account_balance_pb2.CryptoGetAccountBalanceQuery()
98+
)
5999
crypto_get_balance.header.CopyFrom(query_header)
60-
crypto_get_balance.accountID.CopyFrom(self.account_id._to_proto())
100+
101+
if self.account_id:
102+
crypto_get_balance.accountID.CopyFrom(self.account_id._to_proto())
103+
else:
104+
crypto_get_balance.contractID.CopyFrom(self.contract_id._to_proto())
61105

62106
query = query_pb2.Query()
63-
if not hasattr(query, 'cryptogetAccountBalance'):
64-
raise AttributeError("Query object has no attribute 'cryptogetAccountBalance'")
107+
if not hasattr(query, "cryptogetAccountBalance"):
108+
raise AttributeError(
109+
"Query object has no attribute 'cryptogetAccountBalance'"
110+
)
65111
query.cryptogetAccountBalance.CopyFrom(crypto_get_balance)
66112

67113
return query
@@ -73,7 +119,7 @@ def _make_request(self) -> query_pb2.Query:
73119
def _get_method(self, channel: _Channel) -> _Method:
74120
"""
75121
Returns the appropriate gRPC method for the account balance query.
76-
122+
77123
Implements the abstract method from Query to provide the specific
78124
gRPC method for getting account balances.
79125
@@ -84,16 +130,15 @@ def _get_method(self, channel: _Channel) -> _Method:
84130
_Method: The method wrapper containing the query function
85131
"""
86132
return _Method(
87-
transaction_func=None,
88-
query_func=channel.crypto.cryptoGetBalance
133+
transaction_func=None, query_func=channel.crypto.cryptoGetBalance
89134
)
90135

91136
def execute(self, client) -> AccountBalance:
92137
"""
93138
Executes the account balance query.
94-
139+
95140
This function delegates the core logic to `_execute()`, and may propagate exceptions raised by it.
96-
141+
97142
Sends the query to the Hedera network and processes the response
98143
to return an AccountBalance object.
99144
@@ -113,26 +158,28 @@ def execute(self, client) -> AccountBalance:
113158

114159
return AccountBalance._from_proto(response.cryptogetAccountBalance)
115160

116-
def _get_query_response(self, response: Any) -> crypto_get_account_balance_pb2.CryptoGetAccountBalanceResponse:
161+
def _get_query_response(
162+
self, response: Any
163+
) -> crypto_get_account_balance_pb2.CryptoGetAccountBalanceResponse:
117164
"""
118165
Extracts the account balance response from the full response.
119-
166+
120167
Implements the abstract method from Query to extract the
121168
specific account balance response object.
122-
169+
123170
Args:
124171
response: The full response from the network
125-
172+
126173
Returns:
127174
The crypto get account balance response object
128175
"""
129176
return response.cryptogetAccountBalance
130-
177+
131178
def _is_payment_required(self):
132179
"""
133180
Account balance query does not require payment.
134-
181+
135182
Returns:
136183
bool: False
137184
"""
138-
return False
185+
return False

0 commit comments

Comments
 (0)