|
13 | 13 | AccountAllowanceDeleteTransaction, |
14 | 14 | ) |
15 | 15 | from hiero_sdk_python.hbar import Hbar |
| 16 | +from hiero_sdk_python.query.token_nft_info_query import TokenNftInfoQuery |
16 | 17 | from hiero_sdk_python.response_code import ResponseCode |
17 | 18 | from hiero_sdk_python.tokens.nft_id import NftId |
18 | | -from hiero_sdk_python.tokens.token_associate_transaction import TokenAssociateTransaction |
| 19 | +from hiero_sdk_python.tokens.token_associate_transaction import ( |
| 20 | + TokenAssociateTransaction, |
| 21 | +) |
19 | 22 | from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction |
20 | 23 | from hiero_sdk_python.transaction.transaction_id import TransactionId |
21 | 24 | from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction |
@@ -61,7 +64,9 @@ def _mint_nft(env, token_id, metadata): |
61 | 64 |
|
62 | 65 |
|
63 | 66 | @pytest.mark.integration |
64 | | -def test_integration_cannot_transfer_on_behalf_of_spender_without_allowance_approval(env): |
| 67 | +def test_integration_cannot_transfer_on_behalf_of_spender_without_allowance_approval( |
| 68 | + env, |
| 69 | +): |
65 | 70 | """Test that a spender cannot transfer NFTs on behalf of account without allowance approval.""" |
66 | 71 | spender_account, receiver_account = _create_spender_and_receiver_accounts(env) |
67 | 72 |
|
@@ -264,7 +269,9 @@ def test_integration_fungible_token_allowance(env): |
264 | 269 |
|
265 | 270 |
|
266 | 271 | @pytest.mark.integration |
267 | | -def test_integration_cant_transfer_on_behalf_of_spender_after_removing_the_allowance_approval(env): |
| 272 | +def test_integration_cant_transfer_on_behalf_of_spender_after_removing_the_allowance_approval( |
| 273 | + env, |
| 274 | +): |
268 | 275 | """Test that a spender cannot transfer NFTs after the allowance approval is removed.""" |
269 | 276 | spender_account, receiver_account = _create_spender_and_receiver_accounts(env) |
270 | 277 |
|
@@ -486,3 +493,64 @@ def test_integration_cannot_send_deleted_token_nft_serials(env): |
486 | 493 | f"Transfer should have failed with SPENDER_DOES_NOT_HAVE_ALLOWANCE" |
487 | 494 | f"status but got: {ResponseCode(transfer_receipt.status).name}" |
488 | 495 | ) |
| 496 | + |
| 497 | + |
| 498 | +@pytest.mark.integration |
| 499 | +def test_integration_can_approve_serial_and_delete_all_serials_in_one_transaction(env): |
| 500 | + """Test that approve-serial + delete-all-serials in one transaction preserves the per-serial allowance.""" |
| 501 | + spender_account, receiver_account = _create_spender_and_receiver_accounts(env) |
| 502 | + |
| 503 | + token_id = create_nft_token(env) |
| 504 | + assert token_id is not None |
| 505 | + |
| 506 | + _associate_token_with_account(env, receiver_account, token_id) |
| 507 | + |
| 508 | + nft_ids = _mint_nft(env, token_id, [b"\x01", b"\x02"]) |
| 509 | + nft1 = nft_ids[0] |
| 510 | + nft2 = nft_ids[1] |
| 511 | + |
| 512 | + # Approve nft1 specifically and delete-all-serials for the same (token, spender) |
| 513 | + # pair in the same transaction. The per-serial approval is preserved because the |
| 514 | + # two operations produce independent NftAllowance entries. |
| 515 | + receipt = ( |
| 516 | + AccountAllowanceApproveTransaction() |
| 517 | + .approve_token_nft_allowance(nft1, env.operator_id, spender_account.id) |
| 518 | + .delete_token_nft_allowance_all_serials(token_id, env.operator_id, spender_account.id) |
| 519 | + .execute(env.client) |
| 520 | + ) |
| 521 | + assert receipt.status == ResponseCode.SUCCESS, ( |
| 522 | + f"Allowance approval failed with status: {ResponseCode(receipt.status).name}" |
| 523 | + ) |
| 524 | + |
| 525 | + # Transfer nft1 (should succeed - per-serial allowance preserved) |
| 526 | + transfer_receipt = ( |
| 527 | + TransferTransaction() |
| 528 | + .set_transaction_id(TransactionId.generate(spender_account.id)) |
| 529 | + .add_approved_nft_transfer(nft1, env.operator_id, receiver_account.id) |
| 530 | + .freeze_with(env.client) |
| 531 | + .sign(spender_account.key) |
| 532 | + .execute(env.client) |
| 533 | + ) |
| 534 | + assert transfer_receipt.status == ResponseCode.SUCCESS, ( |
| 535 | + f"Transfer failed with status: {ResponseCode(transfer_receipt.status).name}" |
| 536 | + ) |
| 537 | + |
| 538 | + # Confirm nft1 ownership has actually moved to the receiver |
| 539 | + nft1_info = TokenNftInfoQuery(nft1).execute(env.client) |
| 540 | + assert nft1_info.account_id == receiver_account.id, ( |
| 541 | + f"Expected nft1 owner to be receiver {receiver_account.id} after transfer, got {nft1_info.account_id}" |
| 542 | + ) |
| 543 | + |
| 544 | + # Transfer nft2 (should fail - delete-all-serials revoked any blanket allowance) |
| 545 | + transfer_receipt2 = ( |
| 546 | + TransferTransaction() |
| 547 | + .set_transaction_id(TransactionId.generate(spender_account.id)) |
| 548 | + .add_approved_nft_transfer(nft2, env.operator_id, receiver_account.id) |
| 549 | + .freeze_with(env.client) |
| 550 | + .sign(spender_account.key) |
| 551 | + .execute(env.client) |
| 552 | + ) |
| 553 | + assert transfer_receipt2.status == ResponseCode.SPENDER_DOES_NOT_HAVE_ALLOWANCE, ( |
| 554 | + f"Transfer should have failed with SPENDER_DOES_NOT_HAVE_ALLOWANCE" |
| 555 | + f"status but got: {ResponseCode(transfer_receipt2.status).name}" |
| 556 | + ) |
0 commit comments