Skip to content

Commit 5799fe1

Browse files
authored
feat: Add transferCrypto method to TCK (hiero-ledger#2353)
Signed-off-by: aceppaluni <aceppaluni@gmail.com>
1 parent 5df93b7 commit 5799fe1

6 files changed

Lines changed: 204 additions & 17 deletions

File tree

src/hiero_sdk_python/transaction/transfer_transaction.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,6 @@ def _add_hbar_transfer(
7272
amount = amount.to_tinybars()
7373
elif not isinstance(amount, int):
7474
raise TypeError("amount must be an int or Hbar instance.")
75-
if amount == 0:
76-
raise ValueError("Amount must be a non-zero value.")
7775
if not isinstance(is_approved, bool):
7876
raise TypeError("is_approved must be a boolean.")
7977

tck/handlers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
sdk, # setup, reset
1010
token,
1111
topic,
12+
transfer,
1213
)
1314
from .registry import (
1415
get_all_handlers,

tck/handlers/transfer.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from __future__ import annotations
2+
3+
from hiero_sdk_python import (
4+
AccountId,
5+
NftId,
6+
TokenId,
7+
TransferTransaction,
8+
)
9+
from hiero_sdk_python.response_code import ResponseCode
10+
from tck.handlers.registry import rpc_method
11+
from tck.param.transfer import TransferCryptoParams
12+
from tck.response.transfer import TransferCryptoResponse
13+
from tck.util.client_utils import get_client
14+
from tck.util.constants import DEFAULT_GRPC_TIMEOUT
15+
16+
17+
def _build_transfer_transaction(params: TransferCryptoParams) -> TransferTransaction:
18+
tx = TransferTransaction()
19+
20+
tx.set_grpc_deadline(DEFAULT_GRPC_TIMEOUT)
21+
22+
if params.transfers is None:
23+
return tx
24+
25+
for entry in params.transfers:
26+
## HBAR Transfer
27+
if entry.hbar is not None:
28+
hbar = entry.hbar
29+
account = AccountId.from_string(hbar.evmAddress if hbar.evmAddress else hbar.accountId)
30+
amount = int(hbar.amount)
31+
32+
if entry.approved:
33+
tx.add_approved_hbar_transfer(account, amount)
34+
else:
35+
tx.add_hbar_transfer(account, amount)
36+
37+
## Token Transfer
38+
elif entry.token is not None:
39+
token = entry.token
40+
41+
token_id = TokenId.from_string(token.tokenId)
42+
account = AccountId.from_string(token.accountId)
43+
44+
if token.decimals is not None:
45+
decimals = int(token.decimals)
46+
47+
if entry.approved:
48+
tx.add_approved_token_transfer_with_decimals(token_id, account, int(token.amount), decimals)
49+
else:
50+
tx.add_token_transfer_with_decimals(token_id, account, int(token.amount), decimals)
51+
else:
52+
if entry.approved:
53+
tx.add_approved_token_transfer(token_id, account, int(token.amount))
54+
else:
55+
tx.add_token_transfer(token_id, account, int(token.amount))
56+
57+
## NFT Transfer
58+
elif entry.nft is not None:
59+
nft = entry.nft
60+
61+
nft_id = NftId(TokenId.from_string(nft.tokenId), int(nft.serialNumber))
62+
63+
sender = AccountId.from_string(nft.senderAccountId)
64+
receiver = AccountId.from_string(nft.receiverAccountId)
65+
66+
if entry.approved:
67+
tx.add_approved_nft_transfer(nft_id, sender, receiver)
68+
else:
69+
tx.add_nft_transfer(nft_id, sender, receiver)
70+
71+
return tx
72+
73+
74+
@rpc_method("transferCrypto")
75+
def transfer_crypto(params: TransferCryptoParams) -> TransferCryptoResponse:
76+
client = get_client(params.sessionId)
77+
78+
tx = _build_transfer_transaction(params)
79+
80+
if params.commonTransactionParams is not None:
81+
params.commonTransactionParams.apply_common_params(tx, client)
82+
83+
receipt = tx.execute(client, wait_for_receipt=False).get_receipt(
84+
client,
85+
validate_status=True,
86+
)
87+
88+
return TransferCryptoResponse(status=ResponseCode(receipt.status).name)

tck/param/transfer.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
5+
from tck.param.base import BaseTransactionParams
6+
from tck.util.param_utils import (
7+
parse_common_transaction_params,
8+
parse_session_id,
9+
to_bool,
10+
)
11+
12+
13+
@dataclass
14+
class HbarTransferParams:
15+
accountId: str | None = None
16+
evmAddress: str | None = None
17+
amount: str | None = None
18+
19+
@classmethod
20+
def parse_json_params(cls, params: dict[str, object]) -> HbarTransferParams:
21+
return cls(accountId=params.get("accountId"), evmAddress=params.get("evmAddress"), amount=params.get("amount"))
22+
23+
24+
@dataclass
25+
class TokenTransferParams:
26+
tokenId: str | None = None
27+
accountId: str | None = None
28+
amount: str | None = None
29+
decimals: str | None = None
30+
31+
@classmethod
32+
def parse_json_params(cls, params: dict[str, object]) -> TokenTransferParams:
33+
return cls(
34+
tokenId=params.get("tokenId"),
35+
accountId=params.get("accountId"),
36+
amount=params.get("amount"),
37+
decimals=params.get("decimals"),
38+
)
39+
40+
41+
@dataclass
42+
class NftTransferParams:
43+
tokenId: str | None = None
44+
serialNumber: str | None = None
45+
senderAccountId: str | None = None
46+
receiverAccountId: str | None = None
47+
48+
@classmethod
49+
def parse_json_params(cls, params: dict[str, object]) -> NftTransferParams:
50+
return cls(
51+
tokenId=params.get("tokenId"),
52+
serialNumber=params.get("serialNumber"),
53+
senderAccountId=params.get("senderAccountId"),
54+
receiverAccountId=params.get("receiverAccountId"),
55+
)
56+
57+
58+
@dataclass
59+
class TransferParams:
60+
hbar: HbarTransferParams | None = None
61+
token: TokenTransferParams | None = None
62+
nft: NftTransferParams | None = None
63+
approved: bool | None = None
64+
65+
@classmethod
66+
def parse_json_params(cls, params: dict[str, object]) -> TransferParams:
67+
return cls(
68+
hbar=HbarTransferParams.parse_json_params(params["hbar"]) if params.get("hbar") is not None else None,
69+
token=TokenTransferParams.parse_json_params(params["token"]) if params.get("token") is not None else None,
70+
nft=NftTransferParams.parse_json_params(params["nft"]) if params.get("nft") is not None else None,
71+
approved=to_bool(params.get("approved")) if "approved" in params else None,
72+
)
73+
74+
75+
@dataclass
76+
class TransferCryptoParams(BaseTransactionParams):
77+
transfers: list[TransferParams] | None = None
78+
79+
@classmethod
80+
def parse_json_params(cls, params: dict) -> TransferCryptoParams:
81+
transfers_raw = params.get("transfers")
82+
83+
return cls(
84+
sessionId=parse_session_id(params),
85+
commonTransactionParams=parse_common_transaction_params(params),
86+
transfers=[TransferParams.parse_json_params(t) for t in transfers_raw] if transfers_raw else None,
87+
)

tck/response/transfer.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
5+
6+
@dataclass
7+
class TransferCryptoResponse:
8+
status: str | None = None

tests/unit/transfer_transaction_test.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,6 @@ def test_add_invalid_transfer(mock_account_ids):
119119
with pytest.raises(TypeError):
120120
transfer_tx.add_hbar_transfer(12345, -500)
121121

122-
with pytest.raises(ValueError):
123-
transfer_tx.add_hbar_transfer(mock_account_ids[0], 0)
124-
125122
with pytest.raises(TypeError):
126123
transfer_tx.add_token_transfer(12345, mock_account_ids[0], -100)
127124

@@ -271,16 +268,19 @@ def test_edge_case_amounts(mock_account_ids):
271268
assert token1_amounts[account_id_1] == 2
272269

273270

274-
def test_zero_amount_validation(mock_account_ids):
275-
"""Test that zero amounts are properly rejected."""
271+
def test_zero_amount_handling(mock_account_ids):
272+
"""Test handling of zero transfer amounts."""
276273
account_id_1, _, _, token_id_1, _ = mock_account_ids
277274
transfer_tx = TransferTransaction()
278275

279-
# Test zero HBAR amount should raise ValueError with updated message
280-
with pytest.raises(ValueError, match="Amount must be a non-zero value"):
281-
transfer_tx.add_hbar_transfer(account_id_1, 0)
276+
# Zero HBAR transfers are allowed
277+
transfer_tx.add_hbar_transfer(account_id_1, 0)
278+
279+
assert len(transfer_tx.hbar_transfers) == 1
280+
assert transfer_tx.hbar_transfers[0].account_id == account_id_1
281+
assert transfer_tx.hbar_transfers[0].amount == 0
282282

283-
# Test zero token amount should raise ValueError
283+
# Token transfers still reject zero amounts (if unchanged)
284284
with pytest.raises(ValueError, match="Amount must be a non-zero integer"):
285285
transfer_tx.add_token_transfer(token_id_1, account_id_1, 0)
286286

@@ -555,16 +555,21 @@ def test_hbar_accumulation_with_mixed_int_and_hbar(mock_account_ids):
555555
assert transfer.amount == 100 + 100_000_000 - 50
556556

557557

558-
def test_zero_hbar_value_validation(mock_account_ids):
559-
"""Test that zero Hbar amounts are properly rejected."""
558+
def test_zero_hbar_value_handling(mock_account_ids):
559+
"""Test that zero Hbar amounts are accepted."""
560560
account_id_1, _, _, _, _ = mock_account_ids
561+
561562
transfer_tx = TransferTransaction()
563+
transfer_tx.add_hbar_transfer(account_id_1, Hbar(0))
562564

563-
with pytest.raises(ValueError, match="Amount must be a non-zero value"):
564-
transfer_tx.add_hbar_transfer(account_id_1, Hbar(0))
565+
assert len(transfer_tx.hbar_transfers) == 1
566+
assert transfer_tx.hbar_transfers[0].amount == 0
567+
568+
transfer_tx = TransferTransaction()
569+
transfer_tx.add_hbar_transfer(account_id_1, 0)
565570

566-
with pytest.raises(ValueError, match="Amount must be a non-zero value"):
567-
transfer_tx.add_hbar_transfer(account_id_1, 0)
571+
assert len(transfer_tx.hbar_transfers) == 1
572+
assert transfer_tx.hbar_transfers[0].amount == 0
568573

569574

570575
def test_add_hbar_transfer_with_various_hbar_units(mock_account_ids):

0 commit comments

Comments
 (0)