Skip to content

Commit 4c3df39

Browse files
authored
feat: Add CancelAirdrop to TCK (#2555)
Signed-off-by: aceppaluni <aceppaluni@gmail.com>
1 parent 47fe801 commit 4c3df39

5 files changed

Lines changed: 125 additions & 14 deletions

File tree

src/hiero_sdk_python/tokens/token_airdrop_transaction_cancel.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,11 @@ def _build_proto_body(self):
6868
6969
Returns:
7070
TokenCancelAirdropTransactionBody: The protobuf body for this transaction.
71-
72-
Raises:
73-
ValueError: If pending airdrops list is invalid.
7471
"""
7572
pending_airdrops_proto: list[basic_types_pb2.PendingAirdropId] = [
7673
pending_airdrop._to_proto() for pending_airdrop in self.pending_airdrops
7774
]
7875

79-
if len(pending_airdrops_proto) < 1 or len(pending_airdrops_proto) > 10:
80-
raise ValueError("Pending airdrops list must contain mininum 1 and maximum 10 pendingAirdrop.")
81-
8276
return token_cancel_airdrop_pb2.TokenCancelAirdropTransactionBody(pending_airdrops=pending_airdrops_proto)
8377

8478
def build_transaction_body(self):

tck/handlers/token.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from hiero_sdk_python.tokens.token_airdrop_claim import TokenClaimAirdropTransaction
2020
from hiero_sdk_python.tokens.token_airdrop_pending_id import PendingAirdropId
2121
from hiero_sdk_python.tokens.token_airdrop_transaction import TokenAirdropTransaction
22+
from hiero_sdk_python.tokens.token_airdrop_transaction_cancel import TokenCancelAirdropTransaction
2223
from hiero_sdk_python.tokens.token_associate_transaction import TokenAssociateTransaction
2324
from hiero_sdk_python.tokens.token_burn_transaction import TokenBurnTransaction
2425
from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction
@@ -47,6 +48,7 @@
4748
AirdropTokenParams,
4849
AssociateTokenParams,
4950
BurnTokenParams,
51+
CancelAirdropParams,
5052
ClaimTokenParams,
5153
CreateTokenParams,
5254
DeleteTokenParams,
@@ -67,6 +69,7 @@
6769
AirdropTokenResponse,
6870
AssociateTokenResponse,
6971
BurnTokenResponse,
72+
CancelAirdropResponse,
7073
ClaimTokenResponse,
7174
CreateTokenResponse,
7275
CustomFeeResponse,
@@ -257,6 +260,54 @@ def create_token(params: CreateTokenParams) -> CreateTokenResponse:
257260
)
258261

259262

263+
def _build_cancel_airdrop_transaction(params: CancelAirdropParams) -> TokenCancelAirdropTransaction:
264+
"""Build a TokenCancelAirdropTransaction from TCK params."""
265+
transaction = TokenCancelAirdropTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT)
266+
267+
if params.pendingAirdrops:
268+
for pending_airdrop in params.pendingAirdrops:
269+
sender_id = AccountId.from_string(pending_airdrop.senderAccountId)
270+
receiver_id = AccountId.from_string(pending_airdrop.receiverAccountId)
271+
token_id = TokenId.from_string(pending_airdrop.tokenId)
272+
273+
if pending_airdrop.serialNumbers:
274+
for serial_number in pending_airdrop.serialNumbers:
275+
nft_id = NftId(token_id, int(serial_number))
276+
transaction.add_pending_airdrop(
277+
PendingAirdropId(
278+
sender_id,
279+
receiver_id,
280+
nft_id=nft_id,
281+
)
282+
)
283+
else:
284+
transaction.add_pending_airdrop(
285+
PendingAirdropId(
286+
sender_id,
287+
receiver_id,
288+
token_id=token_id,
289+
)
290+
)
291+
292+
return transaction
293+
294+
295+
@rpc_method("cancelAirdrop")
296+
def cancel_airdrop(params: CancelAirdropParams) -> CancelAirdropResponse:
297+
"""Cancel a token airdrop using TCK cancelAirdrop parameters."""
298+
client = get_client(params.sessionId)
299+
300+
transaction = _build_cancel_airdrop_transaction(params)
301+
302+
if params.commonTransactionParams is not None:
303+
params.commonTransactionParams.apply_common_params(transaction, client)
304+
305+
response = transaction.execute(client, wait_for_receipt=False)
306+
receipt: TransactionReceipt = response.get_receipt(client, validate_status=True)
307+
308+
return CancelAirdropResponse(status=ResponseCode(receipt.status).name)
309+
310+
260311
def _build_mint_token_transaction(params: MintTokenParams) -> TokenMintTransaction:
261312
"""Build a TokenMintTransaction from TCK params."""
262313
transaction = TokenMintTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT)

tck/param/token.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,56 @@ def parse_json_params(cls, params: dict) -> ClaimTokenParams:
311311
)
312312

313313

314+
@dataclass
315+
class PendingAirdropParams:
316+
"""Parameters identifying a pending airdrop."""
317+
318+
senderAccountId: str | None = None
319+
receiverAccountId: str | None = None
320+
tokenId: str | None = None
321+
serialNumbers: list[str] | None = None
322+
323+
@classmethod
324+
def parse_json_params(cls, params: dict) -> PendingAirdropParams:
325+
"""Parse JSON parameters into a PendingAirdropParams instance."""
326+
serial_numbers = params.get("serialNumbers")
327+
328+
if serial_numbers is not None and (
329+
not isinstance(serial_numbers, list)
330+
or not all(isinstance(serial_number, str) for serial_number in serial_numbers)
331+
):
332+
raise ValueError("serialNumbers must be a list of strings")
333+
334+
return cls(
335+
senderAccountId=params.get("senderAccountId"),
336+
receiverAccountId=params.get("receiverAccountId"),
337+
tokenId=params.get("tokenId"),
338+
serialNumbers=serial_numbers,
339+
)
340+
341+
342+
@dataclass
343+
class CancelAirdropParams(BaseTransactionParams):
344+
"""Request parameters for the cancelAirdrop endpoint."""
345+
346+
pendingAirdrops: list[PendingAirdropParams] | None = None
347+
348+
@classmethod
349+
def parse_json_params(cls, params: dict) -> CancelAirdropParams:
350+
"""Parse JSON-RPC params into a CancelAirdropParams instance."""
351+
pending_airdrops_raw = params.get("pendingAirdrops")
352+
353+
return cls(
354+
pendingAirdrops=(
355+
[PendingAirdropParams.parse_json_params(p) for p in pending_airdrops_raw]
356+
if pending_airdrops_raw is not None
357+
else None
358+
),
359+
sessionId=parse_session_id(params),
360+
commonTransactionParams=parse_common_transaction_params(params),
361+
)
362+
363+
314364
@dataclass
315365
class GetTokenInfoParams(BaseParams):
316366
"""Request parameters for the getTokenInfo endpoint."""

tck/response/token.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ class ClaimTokenResponse(StatusOnlyResponse):
7979
"""Response payload for claimToken."""
8080

8181

82+
@dataclass
83+
class CancelAirdropResponse(StatusOnlyResponse):
84+
"""Response payload for cancelAirdrop."""
85+
86+
8287
@dataclass
8388
class CustomFeeResponse:
8489
"""Nested custom fee details for getTokenInfo."""

tests/unit/token_airdrop_transaction_cancel_test.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,20 +92,31 @@ def test_build_transaction_body(mock_account_ids):
9292

9393
def test_transaction_for_invalid_params(mock_account_ids):
9494
"""Test building the token cancel airdrop transaction body with invalid params."""
95-
sender_id, receiver_id, _, token_id, _ = mock_account_ids
96-
sample_pending_airdrop = PendingAirdropId(sender_id=sender_id, receiver_id=receiver_id, token_id=token_id)
95+
sender_id, receiver_id, node_account_id, token_id, _ = mock_account_ids
96+
sample_pending_airdrop = PendingAirdropId(
97+
sender_id=sender_id,
98+
receiver_id=receiver_id,
99+
token_id=token_id,
100+
)
97101

98-
# With empty pending airdrops list
99102
cancel_airdrop_tx_1 = TokenCancelAirdropTransaction()
100-
with pytest.raises(ValueError, match="Pending airdrops list must contain mininum 1 and maximum 10 pendingAirdrop."):
101-
cancel_airdrop_tx_1.build_transaction_body()
103+
cancel_airdrop_tx_1.transaction_id = generate_transaction_id(sender_id)
104+
cancel_airdrop_tx_1.set_node_account_ids([node_account_id])
105+
106+
transaction_body = cancel_airdrop_tx_1.build_transaction_body()
107+
108+
assert len(transaction_body.tokenCancelAirdrop.pending_airdrops) == 0
102109

103-
# With pending airdrops list containing more than 10 ids
104110
cancel_airdrop_tx_2 = TokenCancelAirdropTransaction()
105111
for _ in range(11):
106112
cancel_airdrop_tx_2.add_pending_airdrop(sample_pending_airdrop)
107-
with pytest.raises(ValueError, match="Pending airdrops list must contain mininum 1 and maximum 10 pendingAirdrop."):
108-
cancel_airdrop_tx_2.build_transaction_body()
113+
114+
cancel_airdrop_tx_2.transaction_id = generate_transaction_id(sender_id)
115+
cancel_airdrop_tx_2.set_node_account_ids([node_account_id])
116+
117+
transaction_body = cancel_airdrop_tx_2.build_transaction_body()
118+
119+
assert len(transaction_body.tokenCancelAirdrop.pending_airdrops) == 11
109120

110121

111122
def test_set_pending_airdrops(mock_account_ids):

0 commit comments

Comments
 (0)