Skip to content

Commit 44f5945

Browse files
authored
feat: hbar support (#1199)
Signed-off-by: prajeeta pal <[email protected]> Signed-off-by: prajeeta <[email protected]>
1 parent 630267a commit 44f5945

File tree

4 files changed

+554
-51
lines changed

4 files changed

+554
-51
lines changed

CHANGELOG.md

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
66

77
## [Unreleased]
88

9-
10-
119
### Added
10+
11+
- Added Hbar object support for TransferTransaction HBAR transfers:
12+
- Methods now accept `Union[int, Hbar]` for amount parameters with immediate normalization to tinybars
13+
- Includes comprehensive unit tests covering various Hbar units (HBAR, MICROBAR, NANOBAR, TINYBAR) and accumulation behavior with mixed `int` and `Hbar` inputs
1214
- Added a module-level docstring to the HBAR allowance approval example to clarify
1315
delegated spending behavior and key concepts. [#1202](https://github.com/hiero-ledger/hiero-sdk-python/issues/1202)
1416
- Added a GitHub Actions workflow to validate broken Markdown links in pull requests.
@@ -28,7 +30,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
2830
- Added unit tests for `SubscriptionHandle` class covering cancellation state, thread management, and join operations.
2931
- Refactored `account_create_transaction_create_with_alias.py` example by splitting monolithic function into modular functions: `generate_main_and_alias_keys()`, `create_account_with_ecdsa_alias()`, `fetch_account_info()`, `print_account_summary()` (#1016)
3032
- Added `.github/workflows/bot-pr-auto-draft-on-changes.yml` to automatically convert PRs to draft and notify authors when reviewers request changes.
31-
-
33+
-
3234
- Modularized `transfer_transaction_fungible` example by introducing `account_balance_query()` & `transfer_transaction()`.Renamed `transfer_tokens()``main()`
3335
- Phase 2 of the inactivity-unassign bot: Automatically detects stale open pull requests (no commit activity for 21+ days), comments with a helpful InactivityBot message, closes the stale PR, and unassigns the contributor from the linked issue.
3436
- Added `__str__()` to CustomFixedFee and updated examples and tests accordingly.
@@ -48,7 +50,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
4850
- Support selecting specific node account ID(s) for queries and transactions and added `Network._get_node()` with updated execution flow (#362)
4951
- Add TLS support with two-stage control (`set_transport_security()` and `set_verify_certificates()`) for encrypted connections to Hedera networks. TLS is enabled by default for hosted networks (mainnet, testnet, previewnet) and disabled for local networks (solo, localhost) (#855)
5052
- Add PR inactivity reminder bot for stale pull requests `.github/workflows/pr-inactivity-reminder-bot.yml`
51-
- Add comprehensive training documentation for _Executable class `docs/sdk_developers/training/executable.md`
53+
- Add comprehensive training documentation for \_Executable class `docs/sdk_developers/training/executable.md`
5254
- Added empty `docs/maintainers/good_first_issues.md` file for maintainers to write Good First Issue guidelines (#1034)
5355
- Added new `.github/ISSUE_TEMPLATE/04_good_first_issue_candidate.yml` file (1068)(https://github.com/hiero-ledger/hiero-sdk-python/issues/1068)
5456
- Enhanced `.github/ISSUE_TEMPLATE/01_good_first_issue.yml` with welcoming message and acceptance criteria sections to guide contributors in creating quality GFIs (#1052)
@@ -57,12 +59,12 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
5759
- Add support for include_children in TransactionGetReceiptQuery (#1100)(https://github.com/hiero-ledger/hiero-sdk-python/issues/1100)
5860
- Add new `.github/ISSUE_TEMPLATE/05_intermediate_issue.yml` file (1072)(https://github.com/hiero-ledger/hiero-sdk-python/issues/1072)
5961
- Add a workflow to notify the team when issues are labeled as “good first issues” or identified as candidates for that label: `bot-gfi-notify-team.yml`(#1115)
60-
- Added __str__ and __repr__ to AccountBalance
62+
- Added **str** and **repr** to AccountBalance
6163
- Added GitHub workflow that makes sure newly added test files follow pytest test files naming conventions (#1054)
6264
- Added advanced issue template for contributors `.github/ISSUE_TEMPLATE/06_advanced_issue.yml`.
6365
- Add new tests to `tests/unit/topic_info_query_test.py` (#1124)
6466
- Added `coding_token_transactions.md` for a high level overview training on how token transactions are created in the python sdk.
65-
- Added prompt for codeRabbit on how to review /examples ([#1180](https://github.com/hiero-ledger/hiero-sdk-python/issues/1180))
67+
- Added prompt for codeRabbit on how to review /examples ([#1180](https://github.com/hiero-ledger/hiero-sdk-python/issues/1180))
6668
- Add Linked Issue Enforcer to automatically close PRs without linked issues `.github/workflows/bot-linked-issue-enforcer.yml`.
6769
- Added support for include duplicates in get transaction receipt query (#1166)
6870
- Added `.github/workflows/cron-check-broken-links.yml` workflow to perform scheduled monthly Markdown link validation across the entire repository with automatic issue creation for broken links ([#1210](https://github.com/hiero-ledger/hiero-sdk-python/issues/1210))
@@ -92,21 +94,20 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
9294
- Cleaned up `token_airdrop_claim_auto` example for pylint compliance (no functional changes). (#1079)
9395
- Formatted `examples/query` using black (#1082)(https://github.com/hiero-ledger/hiero-sdk-python/issues/1082)
9496
- Update team notification script and workflow for P0 issues 'p0_issues_notify_team.js'
95-
- Rename test files across the repository to ensure they consistently end with _test.py (#1055)
97+
- Rename test files across the repository to ensure they consistently end with \_test.py (#1055)
9698
- Cleaned up `token_airdrop_claim_signature_required` example for pylint compliance (no functional changes). (#1080)
97-
- Rename the file 'test_token_fee_schedule_update_transaction_e2e.py' to make it ends with _test.py as all other test files.(#1117)
99+
- Rename the file 'test_token_fee_schedule_update_transaction_e2e.py' to make it ends with \_test.py as all other test files.(#1117)
98100
- Format token examples with Black for consistent code style and improved readability (#1119)
99101
- Transformed `examples/tokens/custom_fee_fixed.py` to be an end-to-end example, that interacts with the Hedera network, rather than a static object demo.
100-
- Format token examples with Black for consistent code style and improved readability (#1119)
101-
- Replaced `ResponseCode.get_name(receipt.status)` with the `ResponseCode(receipt.status).name` across examples and integration tests for consistency. (#1136)
102+
- Format token examples with Black for consistent code style and improved readability (#1119)
103+
- Replaced `ResponseCode.get_name(receipt.status)` with the `ResponseCode(receipt.status).name` across examples and integration tests for consistency. (#1136)
102104
- Moved helpful references to Additional Context section and added clickable links.
103105
- Transformed `examples\tokens\custom_royalty_fee.py` to be an end-to-end example, that interacts with the Hedera network, rather than a static object demo.
104106
- Refactored `examples/tokens/custom_royalty_fee.py` by splitting monolithic function custom_royalty_fee_example() into modular functions create_royalty_fee_object(), create_token_with_fee(), verify_token_fee(), and main() to improve readability, cleaned up setup_client() (#1169)
105107
- Added comprehensive unit tests for Timestamp class (#1158)
106108
- Enhance unit and integration test review instructions for clarity and coverage `.coderabbit.yaml`.
107109
- Issue reminder bot now explicitly mentions assignees (e.g., `@user`) in comments. ([#1232](https://github.com/hiero-ledger/hiero-sdk-python/issues/1232))
108110

109-
110111
### Fixed
111112

112113
- Fix token association verification in `token_airdrop_transaction.py` to correctly check if tokens are associated by using `token_id in token_balances` instead of incorrectly displaying zero balances which was misleading (#[815])
@@ -120,7 +121,6 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
120121
- Fixed `cron-check-broken-links.yml` string parsing issue in context input `dry_run` (#1235)
121122
- Flaky tests by disabling TLS in mock Hedera nodes in `mock_server.py`
122123

123-
124124
### Breaking Change
125125

126126
- Remove deprecated 'in_tinybars' parameter and update related tests `/src/hiero_sdk_python/hbar.py`, `/tests/unit/hbar_test.py` and `/src/hiero_sdk_python/tokens/custom_fixed_fee.py`.
@@ -686,4 +686,3 @@ contract_call_local_pb2.ContractLoginfo -> contract_types_pb2.ContractLoginfo
686686
### Removed
687687

688688
- N/A
689-

src/hiero_sdk_python/transaction/transfer_transaction.py

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
Defines TransferTransaction for transferring HBAR or tokens between accounts.
33
"""
44

5-
from typing import Dict, List, Optional, Tuple
5+
from typing import Dict, List, Optional, Tuple, Union
66

77
from hiero_sdk_python.account.account_id import AccountId
88
from hiero_sdk_python.channels import _Channel
99
from hiero_sdk_python.executable import _Method
10+
from hiero_sdk_python.hbar import Hbar
1011
from hiero_sdk_python.hapi.services import basic_types_pb2, crypto_transfer_pb2, transaction_pb2
1112
from hiero_sdk_python.hapi.services.schedulable_transaction_body_pb2 import (
1213
SchedulableTransactionBody,
@@ -29,7 +30,8 @@ def __init__(
2930
self,
3031
hbar_transfers: Optional[Dict[AccountId, int]] = None,
3132
token_transfers: Optional[Dict[TokenId, Dict[AccountId, int]]] = None,
32-
nft_transfers: Optional[Dict[TokenId, List[Tuple[AccountId, AccountId, int, bool]]]] = None,
33+
nft_transfers: Optional[Dict[TokenId,
34+
List[Tuple[AccountId, AccountId, int, bool]]]] = None,
3335
) -> None:
3436
"""
3537
Initializes a new TransferTransaction instance.
@@ -59,14 +61,14 @@ def _init_hbar_transfers(self, hbar_transfers: Dict[AccountId, int]) -> None:
5961
self.add_hbar_transfer(account_id, amount)
6062

6163
def _add_hbar_transfer(
62-
self, account_id: AccountId, amount: int, is_approved: bool = False
64+
self, account_id: AccountId, amount: Union[int, Hbar], is_approved: bool = False
6365
) -> "TransferTransaction":
6466
"""
6567
Internal method to add a HBAR transfer to the transaction.
6668
6769
Args:
6870
account_id (AccountId): The account ID of the sender or receiver.
69-
amount (int): The amount of the HBAR to transfer.
71+
amount (Union[int, Hbar]): The amount of the HBAR to transfer.
7072
is_approved (bool, optional): Whether the transfer is approved. Defaults to False.
7173
7274
Returns:
@@ -75,8 +77,12 @@ def _add_hbar_transfer(
7577
self._require_not_frozen()
7678
if not isinstance(account_id, AccountId):
7779
raise TypeError("account_id must be an AccountId instance.")
78-
if not isinstance(amount, int) or amount == 0:
79-
raise ValueError("Amount must be a non-zero integer.")
80+
if isinstance(amount, Hbar):
81+
amount = amount.to_tinybars()
82+
elif not isinstance(amount, int):
83+
raise TypeError("amount must be an int or Hbar instance.")
84+
if amount == 0:
85+
raise ValueError("Amount must be a non-zero value.")
8086
if not isinstance(is_approved, bool):
8187
raise TypeError("is_approved must be a boolean.")
8288

@@ -85,16 +91,17 @@ def _add_hbar_transfer(
8591
transfer.amount += amount
8692
return self
8793

88-
self.hbar_transfers.append(HbarTransfer(account_id, amount, is_approved))
94+
self.hbar_transfers.append(
95+
HbarTransfer(account_id, amount, is_approved))
8996
return self
9097

91-
def add_hbar_transfer(self, account_id: AccountId, amount: int) -> "TransferTransaction":
98+
def add_hbar_transfer(self, account_id: AccountId, amount: Union[int, Hbar]) -> "TransferTransaction":
9299
"""
93100
Adds a HBAR transfer to the transaction.
94101
95102
Args:
96103
account_id (AccountId): The account ID of the sender or receiver.
97-
amount (int): The amount of the HBAR to transfer.
104+
amount (Union[int, Hbar]): The amount of the HBAR to transfer.
98105
99106
Returns:
100107
TransferTransaction: The current instance of the transaction for chaining.
@@ -103,14 +110,14 @@ def add_hbar_transfer(self, account_id: AccountId, amount: int) -> "TransferTran
103110
return self
104111

105112
def add_approved_hbar_transfer(
106-
self, account_id: AccountId, amount: int
113+
self, account_id: AccountId, amount: Union[int, Hbar]
107114
) -> "TransferTransaction":
108115
"""
109116
Adds a HBAR transfer with approval to the transaction.
110117
111118
Args:
112119
account_id (AccountId): The account ID of the sender or receiver.
113-
amount (int): The amount of the HBAR to transfer.
120+
amount (Union[int, Hbar]): The amount of the HBAR to transfer.
114121
115122
Returns:
116123
TransferTransaction: The current instance of the transaction for chaining.
@@ -190,7 +197,8 @@ def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map):
190197

191198
if crypto_transfer.HasField("transfers"):
192199
for account_amount in crypto_transfer.transfers.accountAmounts:
193-
account_id = AccountId._from_proto(account_amount.accountID)
200+
account_id = AccountId._from_proto(
201+
account_amount.accountID)
194202
amount = account_amount.amount
195203
is_approved = account_amount.is_approval
196204
transaction.hbar_transfers.append(
@@ -210,17 +218,21 @@ def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map):
210218
expected_decimals = token_transfer_list.expected_decimals.value
211219

212220
transaction.token_transfers[token_id].append(
213-
TokenTransfer(token_id, account_id, amount, expected_decimals, is_approved)
221+
TokenTransfer(token_id, account_id, amount,
222+
expected_decimals, is_approved)
214223
)
215224

216225
for nft_transfer in token_transfer_list.nftTransfers:
217-
sender_id = AccountId._from_proto(nft_transfer.senderAccountID)
218-
receiver_id = AccountId._from_proto(nft_transfer.receiverAccountID)
226+
sender_id = AccountId._from_proto(
227+
nft_transfer.senderAccountID)
228+
receiver_id = AccountId._from_proto(
229+
nft_transfer.receiverAccountID)
219230
serial_number = nft_transfer.serialNumber
220231
is_approved = nft_transfer.is_approval
221232

222233
transaction.nft_transfers[token_id].append(
223-
TokenNftTransfer(token_id, sender_id, receiver_id, serial_number, is_approved)
234+
TokenNftTransfer(
235+
token_id, sender_id, receiver_id, serial_number, is_approved)
224236
)
225237

226238
return transaction

tests/integration/transfer_transaction_e2e_test.py

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,11 @@ def test_integration_token_transfer_transaction_can_transfer_nft():
183183
query_transaction = CryptoGetAccountBalanceQuery(account_id)
184184
balance = query_transaction.execute(env.client)
185185

186-
# We check if the nft has transfered to the new account
187-
# For now, token_balances is a map so we check it this way
188-
assert balance and balance.token_balances == {token_id: serial_number}
186+
assert balance is not None, "Balance query returned None"
187+
assert balance.token_balances == {token_id: serial_number}, (
188+
f"Expected token_balances {{{token_id}: {serial_number}}}, "
189+
f"got {balance.token_balances}"
190+
)
189191
finally:
190192
env.close()
191193

@@ -448,3 +450,90 @@ def test_integration_transfer_transaction_approved_token_transfer():
448450

449451
finally:
450452
env.close()
453+
454+
455+
@pytest.mark.integration
456+
def test_integration_transfer_transaction_approved_nft_transfer():
457+
"""Test NFT transfer with approval flag set to True."""
458+
env = IntegrationTestEnv()
459+
460+
try:
461+
new_account_private_key = PrivateKey.generate()
462+
new_account_public_key = new_account_private_key.public_key()
463+
464+
initial_balance = Hbar(10)
465+
466+
account_transaction = AccountCreateTransaction(
467+
key=new_account_public_key, initial_balance=initial_balance, memo="Recipient Account"
468+
)
469+
470+
receipt = account_transaction.execute(env.client)
471+
472+
assert (
473+
receipt.status == ResponseCode.SUCCESS
474+
), f"Account creation failed with status: {ResponseCode(receipt.status).name}"
475+
476+
account_id = receipt.account_id
477+
assert account_id is not None
478+
479+
token_id = create_nft_token(env)
480+
assert token_id is not None
481+
482+
mint_transaction = TokenMintTransaction(token_id=token_id, metadata=[b"test"])
483+
484+
receipt = mint_transaction.execute(env.client)
485+
486+
assert (
487+
receipt.status == ResponseCode.SUCCESS
488+
), f"NFT mint failed with status: {ResponseCode(receipt.status).name}"
489+
490+
serial_number = receipt.serial_numbers[0]
491+
492+
nft_id = NftId(token_id, serial_number)
493+
494+
associate_transaction = TokenAssociateTransaction(
495+
account_id=account_id, token_ids=[token_id]
496+
)
497+
498+
associate_transaction.freeze_with(env.client)
499+
associate_transaction.sign(new_account_private_key)
500+
receipt = associate_transaction.execute(env.client)
501+
502+
assert (
503+
receipt.status == ResponseCode.SUCCESS
504+
), f"NFT association failed with status: {ResponseCode(receipt.status).name}"
505+
506+
allowance_receipt = (
507+
AccountAllowanceApproveTransaction()
508+
.approve_token_nft_allowance(nft_id, env.operator_id, account_id)
509+
.execute(env.client)
510+
)
511+
512+
assert (
513+
allowance_receipt.status == ResponseCode.SUCCESS
514+
), f"Allowance approval failed with status: {ResponseCode(allowance_receipt.status).name}"
515+
516+
env.client.set_operator(account_id, new_account_private_key)
517+
518+
transfer_transaction = TransferTransaction()
519+
transfer_transaction.set_transaction_id(TransactionId.generate(account_id))
520+
transfer_transaction.add_approved_nft_transfer(nft_id, env.operator_id, account_id)
521+
transfer_transaction.freeze_with(env.client)
522+
transfer_transaction.sign(new_account_private_key)
523+
524+
receipt = transfer_transaction.execute(env.client)
525+
526+
assert (
527+
receipt.status == ResponseCode.SUCCESS
528+
), f"NFT transfer failed with status: {ResponseCode(receipt.status).name}"
529+
530+
query_transaction = CryptoGetAccountBalanceQuery(account_id)
531+
balance = query_transaction.execute(env.client)
532+
533+
assert balance is not None, "Balance query returned None"
534+
assert balance.token_balances == {token_id: serial_number}, (
535+
f"Expected token_balances {{{token_id}: {serial_number}}}, "
536+
f"got {balance.token_balances}"
537+
)
538+
finally:
539+
env.close()

0 commit comments

Comments
 (0)