Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
- Unified the inactivity-unassign bot into a single script with `DRY_RUN` support, and fixed handling of cross-repo PR references for stale detection.
- Added unit tests for `SubscriptionHandle` class covering cancellation state, thread management, and join operations.
- 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)
-
- Modularized `transfer_transaction_fungible` example by introducing `account_balance_query()` & `transfer_transaction()`.Renamed `transfer_tokens()` → `main()`
- 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.
- Added `__str__()` to CustomFixedFee and updated examples and tests accordingly.
Expand All @@ -31,8 +30,9 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
- Support selecting specific node account ID(s) for queries and transactions and added `Network._get_node()` with updated execution flow (#362)
- 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)
- Add PR inactivity reminder bot for stale pull requests `.github/workflows/pr-inactivity-reminder-bot.yml`
- Add comprehensive training documentation for _Executable class `docs/sdk_developers/training/executable.md`
- Add comprehensive training documentation for \_Executable class `docs/sdk_developers/training/executable.md`
- Added empty `docs/maintainers/good_first_issues.md` file for maintainers to write Good First Issue guidelines (#1034)
- Added `add_hbar_transfer`, `add_approved_hbar_transfer`, and internal `_add_hbar_transfer` to accept `Hbar` objects in addition to raw tinybar integers, with internal normalization to tinybars. Added tests validating the new behavior.

### Changed

Expand Down Expand Up @@ -64,7 +64,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
- Added `.github/workflows/merge-conflict-bot.yml` to automatically detect and notify users of merge conflicts in Pull Requests.
- Added `.github/workflows/bot-office-hours.yml` to automate the Weekly Office Hour Reminder.
- feat: Implement account creation with EVM-style alias transaction example.
- Added validation logic in `.github/workflows/pr-checks.yml` to detect when no new chnagelog entries are added under [Unreleased].
- Added validation logic in `.github/workflows/pr-checks.yml` to detect when no new changelog entries are added under [Unreleased].
- Support for message chunking in `TopicSubmitMessageTransaction`.

### Changed
Expand All @@ -79,6 +79,10 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.

- fixed workflow: changelog check with improved sensitivity to deletions, additions, new releases

### Breaking Changes

- Changed error message in `TransferTransaction._add_hbar_transfer()` and `AbstractTokenTransferTransaction._add_token_transfer()` when amount is zero from "Amount must be a non-zero integer" to "Amount must be a non-zero value." for clarity and consistency.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This must be move at line 53


## [0.1.9] - 2025-11-26

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,9 @@ def _add_token_transfer(
if not isinstance(account_id, AccountId):
raise TypeError("account_id must be an AccountId instance.")
if not isinstance(amount, int) or amount == 0:
raise ValueError("Amount must be a non-zero integer.")
raise ValueError("Amount must be a integer.")
Comment on lines 151 to +152
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd keep the previous version, is a bit cleaner and also here you have two if with quite te same condition:

  1. if not isinstance(amount, int) or amount == 0 (look after the or)
  2. if amount == 0

if amount == 0:
raise ValueError("amount must be a non-zero value.")
if expected_decimals is not None and not isinstance(expected_decimals, int):
raise TypeError("expected_decimals must be an integer.")
if not isinstance(is_approved, bool):
Expand Down
51 changes: 35 additions & 16 deletions src/hiero_sdk_python/transaction/transfer_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
Defines TransferTransaction for transferring HBAR or tokens between accounts.
"""

from typing import Dict, List, Optional, Tuple
from typing import Dict, List, Optional, Tuple, Union

from hiero_sdk_python.account.account_id import AccountId
from hiero_sdk_python.channels import _Channel
from hiero_sdk_python.executable import _Method
from hiero_sdk_python.hbar import Hbar
from hiero_sdk_python.hapi.services import basic_types_pb2, crypto_transfer_pb2, transaction_pb2
from hiero_sdk_python.hapi.services.schedulable_transaction_body_pb2 import (
SchedulableTransactionBody,
Expand All @@ -29,7 +30,8 @@ def __init__(
self,
hbar_transfers: Optional[Dict[AccountId, int]] = None,
token_transfers: Optional[Dict[TokenId, Dict[AccountId, int]]] = None,
nft_transfers: Optional[Dict[TokenId, List[Tuple[AccountId, AccountId, int, bool]]]] = None,
nft_transfers: Optional[Dict[TokenId,
List[Tuple[AccountId, AccountId, int, bool]]]] = None,
) -> None:
"""
Initializes a new TransferTransaction instance.
Expand Down Expand Up @@ -59,24 +61,35 @@ def _init_hbar_transfers(self, hbar_transfers: Dict[AccountId, int]) -> None:
self.add_hbar_transfer(account_id, amount)

def _add_hbar_transfer(
self, account_id: AccountId, amount: int, is_approved: bool = False
self, account_id: AccountId, amount: Union[int, Hbar], is_approved: bool = False
) -> "TransferTransaction":
"""
Internal method to add a HBAR transfer to the transaction.

Args:
account_id (AccountId): The account ID of the sender or receiver.
amount (int): The amount of the HBAR to transfer.
amount (Union[int, Hbar]): The amount of the HBAR to transfer (in tinybars if int, or Hbar object).
is_approved (bool, optional): Whether the transfer is approved. Defaults to False.

Returns:
TransferTransaction: The current instance of the transaction for chaining.
"""
self._require_not_frozen()

if not isinstance(account_id, AccountId):
raise TypeError("account_id must be an AccountId instance.")
if not isinstance(amount, int) or amount == 0:
raise ValueError("Amount must be a non-zero integer.")

if amount is None:
raise TypeError("amount cannot be None.")

if isinstance(amount, Hbar):
amount = amount.to_tinybars()
elif not isinstance(amount, int):
raise TypeError("amount must be an integer or Hbar object.")

if amount == 0:
raise ValueError("Amount must be a non-zero value.")

if not isinstance(is_approved, bool):
raise TypeError("is_approved must be a boolean.")

Expand All @@ -85,16 +98,17 @@ def _add_hbar_transfer(
transfer.amount += amount
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what could happen if the amount is equal to 0 here?

return self

self.hbar_transfers.append(HbarTransfer(account_id, amount, is_approved))
self.hbar_transfers.append(
HbarTransfer(account_id, amount, is_approved))
return self

def add_hbar_transfer(self, account_id: AccountId, amount: int) -> "TransferTransaction":
def add_hbar_transfer(self, account_id: AccountId, amount: Union[int, Hbar]) -> "TransferTransaction":
"""
Adds a HBAR transfer to the transaction.

Args:
account_id (AccountId): The account ID of the sender or receiver.
amount (int): The amount of the HBAR to transfer.
amount (Union[int, Hbar]): The amount of the HBAR to transfer (in tinybars if int, or Hbar object).

Returns:
TransferTransaction: The current instance of the transaction for chaining.
Expand All @@ -103,14 +117,14 @@ def add_hbar_transfer(self, account_id: AccountId, amount: int) -> "TransferTran
return self

def add_approved_hbar_transfer(
self, account_id: AccountId, amount: int
self, account_id: AccountId, amount: Union[int, Hbar]
) -> "TransferTransaction":
"""
Adds a HBAR transfer with approval to the transaction.

Args:
account_id (AccountId): The account ID of the sender or receiver.
amount (int): The amount of the HBAR to transfer.
amount (Union[int, Hbar]): The amount of the HBAR to transfer (in tinybars if int, or Hbar object).

Returns:
TransferTransaction: The current instance of the transaction for chaining.
Expand Down Expand Up @@ -190,7 +204,8 @@ def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map):

if crypto_transfer.HasField("transfers"):
for account_amount in crypto_transfer.transfers.accountAmounts:
account_id = AccountId._from_proto(account_amount.accountID)
account_id = AccountId._from_proto(
account_amount.accountID)
amount = account_amount.amount
is_approved = account_amount.is_approval
transaction.hbar_transfers.append(
Expand All @@ -210,17 +225,21 @@ def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map):
expected_decimals = token_transfer_list.expected_decimals.value

transaction.token_transfers[token_id].append(
TokenTransfer(token_id, account_id, amount, expected_decimals, is_approved)
TokenTransfer(token_id, account_id, amount,
expected_decimals, is_approved)
)

for nft_transfer in token_transfer_list.nftTransfers:
sender_id = AccountId._from_proto(nft_transfer.senderAccountID)
receiver_id = AccountId._from_proto(nft_transfer.receiverAccountID)
sender_id = AccountId._from_proto(
nft_transfer.senderAccountID)
receiver_id = AccountId._from_proto(
nft_transfer.receiverAccountID)
serial_number = nft_transfer.serialNumber
is_approved = nft_transfer.is_approval

transaction.nft_transfers[token_id].append(
TokenNftTransfer(token_id, sender_id, receiver_id, serial_number, is_approved)
TokenNftTransfer(
token_id, sender_id, receiver_id, serial_number, is_approved)
)

return transaction
45 changes: 45 additions & 0 deletions tests/integration/transfer_transaction_e2e_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from hiero_sdk_python.crypto.private_key import PrivateKey
from hiero_sdk_python.exceptions import PrecheckError
from hiero_sdk_python.hbar import Hbar
from hiero_sdk_python.hbar_unit import HbarUnit
from hiero_sdk_python.query.account_balance_query import CryptoGetAccountBalanceQuery
from hiero_sdk_python.response_code import ResponseCode
from hiero_sdk_python.tokens.nft_id import NftId
Expand Down Expand Up @@ -448,3 +449,47 @@ def test_integration_transfer_transaction_approved_token_transfer():

finally:
env.close()


@pytest.mark.integration
def test_integration_transfer_transaction_with_hbar_units():
env = IntegrationTestEnv()

try:
new_account_private_key = PrivateKey.generate()
new_account_public_key = new_account_private_key.public_key()

initial_balance = Hbar(1)

account_transaction = AccountCreateTransaction(
key=new_account_public_key, initial_balance=initial_balance, memo="Recipient Account"
)

receipt = account_transaction.execute(env.client)

assert (
receipt.status == ResponseCode.SUCCESS
), f"Account creation failed with status: {ResponseCode(receipt.status).name}"

account_id = receipt.account_id
assert account_id is not None

transfer_transaction = TransferTransaction()
transfer_transaction.add_hbar_transfer(env.operator_id, Hbar(-1.5, HbarUnit.HBAR))
transfer_transaction.add_hbar_transfer(account_id, Hbar(1.5, HbarUnit.HBAR))

receipt = transfer_transaction.execute(env.client)

assert (
receipt.status == ResponseCode.SUCCESS
), f"Transfer failed with status: {ResponseCode(receipt.status).name}"

query_transaction = CryptoGetAccountBalanceQuery(account_id)
balance = query_transaction.execute(env.client)

expected_balance_tinybars = Hbar(1).to_tinybars() + Hbar(1.5, HbarUnit.HBAR).to_tinybars()
assert (
balance and balance.hbars.to_tinybars() == expected_balance_tinybars
), f"Expected balance: {expected_balance_tinybars}, actual balance: {balance.hbars.to_tinybars()}"
finally:
env.close()
4 changes: 2 additions & 2 deletions tests/unit/test_transfer_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,11 +284,11 @@ def test_zero_amount_validation(mock_account_ids):
transfer_tx = TransferTransaction()

# Test zero HBAR amount should raise ValueError
with pytest.raises(ValueError, match="Amount must be a non-zero integer"):
with pytest.raises(ValueError, match="Amount must be a non-zero value"):
transfer_tx.add_hbar_transfer(account_id_1, 0)

# Test zero token amount should raise ValueError
with pytest.raises(ValueError, match="Amount must be a non-zero integer"):
with pytest.raises(ValueError, match="Amount must be a non-zero value"):
transfer_tx.add_token_transfer(token_id_1, account_id_1, 0)


Expand Down