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
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ 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].
- Support for message chunking in `TopicSubmitMessageTransaction`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Also this should not be deleted

- Added validation logic in `.github/workflows/pr-checks.yml` to detect when no new changelog entries are added under [Unreleased]
- feat: Allow `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.
Copy link
Contributor

Choose a reason for hiding this comment

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

line 34 must be move to line after 12


### Changed

Expand All @@ -46,6 +46,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.

## [0.1.9] - 2025-11-26

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ 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 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
28 changes: 17 additions & 11 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 Down Expand Up @@ -59,14 +60,14 @@ 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 as Hbar object).
is_approved (bool, optional): Whether the transfer is approved. Defaults to False.

Returns:
Expand All @@ -75,26 +76,31 @@ def _add_hbar_transfer(
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 not isinstance(amount, (int, Hbar)):
raise TypeError("Amount must be of type int or Hbar.")
if not isinstance(is_approved, bool):
raise TypeError("is_approved must be a boolean.")

tinybar = amount if isinstance(amount, int) else amount.to_tinybars()

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

for transfer in self.hbar_transfers:
if transfer.account_id == account_id:
transfer.amount += amount
transfer.amount += tinybar
return self

self.hbar_transfers.append(HbarTransfer(account_id, amount, is_approved))
self.hbar_transfers.append(HbarTransfer(account_id, tinybar, 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 as Hbar object).

Returns:
TransferTransaction: The current instance of the transaction for chaining.
Expand All @@ -103,14 +109,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 as Hbar object).

Returns:
TransferTransaction: The current instance of the transaction for chaining.
Expand Down
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()
153 changes: 151 additions & 2 deletions tests/unit/test_transfer_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from hiero_sdk_python.hapi.services.schedulable_transaction_body_pb2 import (
SchedulableTransactionBody,
)
from hiero_sdk_python.hbar import Hbar
from hiero_sdk_python.hbar_unit import HbarUnit
from hiero_sdk_python.tokens.nft_id import NftId
from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction

Expand Down Expand Up @@ -284,11 +286,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 Expand Up @@ -510,3 +512,150 @@ def test_approved_token_transfer_validation(mock_account_ids):
# Test zero amount
with pytest.raises(ValueError, match="Amount must be a non-zero integer"):
Copy link
Contributor

Choose a reason for hiding this comment

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

this should be also

 with pytest.raises(ValueError, match="Amount must be a non-zero value"):

transfer_tx.add_approved_token_transfer_with_decimals(token_id_1, account_id_1, 0, 6)


def test_add_hbar_transfer_with_hbar_object(mock_account_ids):
"""Test adding HBAR transfers using Hbar objects."""
account_id_sender, account_id_recipient, _, _, _ = mock_account_ids
transfer_tx = TransferTransaction()

hbar_amount_sender = Hbar(-5, HbarUnit.HBAR)
hbar_amount_recipient = Hbar(5, HbarUnit.HBAR)

transfer_tx.add_hbar_transfer(account_id_sender, hbar_amount_sender)
transfer_tx.add_hbar_transfer(account_id_recipient, hbar_amount_recipient)

sender_transfer = next(
t for t in transfer_tx.hbar_transfers if t.account_id == account_id_sender
)
recipient_transfer = next(
t for t in transfer_tx.hbar_transfers if t.account_id == account_id_recipient
)

assert sender_transfer.amount == hbar_amount_sender.to_tinybars()
assert recipient_transfer.amount == hbar_amount_recipient.to_tinybars()


def test_add_hbar_transfer_with_hbar_tinybars(mock_account_ids):
"""Test adding HBAR transfers using Hbar objects with tinybar unit."""
account_id_sender, account_id_recipient, _, _, _ = mock_account_ids
transfer_tx = TransferTransaction()

tinybar_amount_sender = Hbar(-5_000_000_000, HbarUnit.TINYBAR)
tinybar_amount_recipient = Hbar(5_000_000_000, HbarUnit.TINYBAR)

transfer_tx.add_hbar_transfer(account_id_sender, tinybar_amount_sender)
transfer_tx.add_hbar_transfer(account_id_recipient, tinybar_amount_recipient)

sender_transfer = next(
t for t in transfer_tx.hbar_transfers if t.account_id == account_id_sender
)
recipient_transfer = next(
t for t in transfer_tx.hbar_transfers if t.account_id == account_id_recipient
)

assert sender_transfer.amount == -5_000_000_000
assert recipient_transfer.amount == 5_000_000_000


def test_add_approved_hbar_transfer_with_hbar_object(mock_account_ids):
"""Test adding approved HBAR transfers using Hbar objects."""
account_id_1, account_id_2, _, _, _ = mock_account_ids
transfer_tx = TransferTransaction()

hbar_amount_1 = Hbar(10, HbarUnit.HBAR)
hbar_amount_2 = Hbar(-10, HbarUnit.HBAR)

transfer_tx.add_approved_hbar_transfer(account_id_1, hbar_amount_1)
transfer_tx.add_approved_hbar_transfer(account_id_2, hbar_amount_2)

transfer_1 = next(
t for t in transfer_tx.hbar_transfers if t.account_id == account_id_1
)
transfer_2 = next(
t for t in transfer_tx.hbar_transfers if t.account_id == account_id_2
)

assert transfer_1.amount == hbar_amount_1.to_tinybars()
assert transfer_1.is_approved is True
assert transfer_2.amount == hbar_amount_2.to_tinybars()
assert transfer_2.is_approved is True


def test_hbar_accumulation_with_hbar_objects(mock_account_ids):
"""Test that HBAR transfers accumulate correctly with Hbar objects."""
account_id_1, _, _, _, _ = mock_account_ids
transfer_tx = TransferTransaction()

hbar_amount_1 = Hbar(5, HbarUnit.HBAR)
hbar_amount_2 = Hbar(3, HbarUnit.HBAR)

transfer_tx.add_hbar_transfer(account_id_1, hbar_amount_1)
transfer_tx.add_hbar_transfer(account_id_1, hbar_amount_2)

transfer = next(t for t in transfer_tx.hbar_transfers if t.account_id == account_id_1)

expected_amount = (hbar_amount_1.to_tinybars() + hbar_amount_2.to_tinybars())
assert transfer.amount == expected_amount
assert len(transfer_tx.hbar_transfers) == 1


def test_hbar_and_int_accumulation_mixed(mock_account_ids):
"""Test that HBAR objects and integer amounts accumulate together."""
account_id_1, _, _, _, _ = mock_account_ids
transfer_tx = TransferTransaction()

hbar_amount = Hbar(5, HbarUnit.HBAR)
int_amount = 3_000_000_000

transfer_tx.add_hbar_transfer(account_id_1, hbar_amount)
transfer_tx.add_hbar_transfer(account_id_1, int_amount)

transfer = next(t for t in transfer_tx.hbar_transfers if t.account_id == account_id_1)

expected_amount = hbar_amount.to_tinybars() + int_amount
assert transfer.amount == expected_amount
assert len(transfer_tx.hbar_transfers) == 1


def test_zero_hbar_transfer_validation(mock_account_ids):
"""Test that zero Hbar objects are rejected."""
account_id_1, _, _, _, _ = mock_account_ids
transfer_tx = TransferTransaction()

zero_hbar = Hbar(0, HbarUnit.HBAR)

with pytest.raises(ValueError, match="Amount must be a non-zero value"):
transfer_tx.add_hbar_transfer(account_id_1, zero_hbar)


def test_invalid_hbar_type_validation(mock_account_ids):
"""Test that invalid types are rejected."""
account_id_1, _, _, _, _ = mock_account_ids
transfer_tx = TransferTransaction()

with pytest.raises(TypeError, match="Amount must be of type int or Hbar"):
transfer_tx.add_hbar_transfer(account_id_1, "invalid")

with pytest.raises(TypeError, match="Amount must be of type int or Hbar"):
transfer_tx.add_hbar_transfer(account_id_1, 5.5)


def test_hbar_transfer_conversion_accuracy(mock_account_ids):
"""Test that Hbar objects are accurately converted to tinybars."""
account_id_1, account_id_2, _, _, _ = mock_account_ids
transfer_tx = TransferTransaction()

hbar_value = Hbar(1.5, HbarUnit.HBAR)
expected_tinybars = 150_000_000

transfer_tx.add_hbar_transfer(account_id_1, hbar_value)

transfer = next(t for t in transfer_tx.hbar_transfers if t.account_id == account_id_1)
assert transfer.amount == expected_tinybars

micro_hbar_value = Hbar(1_500_000, HbarUnit.MICROBAR)
transfer_tx.add_hbar_transfer(account_id_2, micro_hbar_value)

transfer_2 = next(t for t in transfer_tx.hbar_transfers if t.account_id == account_id_2)
assert transfer_2.amount == expected_tinybars
Loading