From 56aeb666004f5eff30a83b7e6c1713d3dd7f5f4d Mon Sep 17 00:00:00 2001 From: iron-prog Date: Sun, 12 Jul 2026 09:16:45 +0530 Subject: [PATCH 1/4] feat(tck): add token grant/revoke KYC handlers Signed-off-by: iron-prog --- tck/handlers/token.py | 64 +++++++++++++++++++++++++++++++++++++++++++ tck/param/token.py | 36 ++++++++++++++++++++++++ tck/response/token.py | 10 +++++++ 3 files changed, 110 insertions(+) diff --git a/tck/handlers/token.py b/tck/handlers/token.py index 63c41f026..c12214dce 100644 --- a/tck/handlers/token.py +++ b/tck/handlers/token.py @@ -23,12 +23,14 @@ from hiero_sdk_python.tokens.token_delete_transaction import TokenDeleteTransaction from hiero_sdk_python.tokens.token_freeze_status import TokenFreezeStatus from hiero_sdk_python.tokens.token_freeze_transaction import TokenFreezeTransaction +from hiero_sdk_python.tokens.token_grant_kyc_transaction import TokenGrantKycTransaction from hiero_sdk_python.tokens.token_id import TokenId from hiero_sdk_python.tokens.token_info import TokenInfo from hiero_sdk_python.tokens.token_kyc_status import TokenKycStatus from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction from hiero_sdk_python.tokens.token_pause_status import TokenPauseStatus from hiero_sdk_python.tokens.token_pause_transaction import TokenPauseTransaction +from hiero_sdk_python.tokens.token_revoke_kyc_transaction import TokenRevokeKycTransaction from hiero_sdk_python.tokens.token_type import TokenType from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt from tck.handlers.registry import rpc_method @@ -41,8 +43,10 @@ DeleteTokenParams, FreezeTokenParams, GetTokenInfoParams, + GrantTokenKycParams, MintTokenParams, PauseTokenParams, + RevokeTokenKycParams, ) from tck.response.token import ( AirdropTokenResponse, @@ -53,8 +57,10 @@ DeleteTokenResponse, FreezeTokenResponse, GetTokenInfoResponse, + GrantTokenKycResponse, MintTokenResponse, PauseTokenResponse, + RevokeTokenKycResponse, ) from tck.util.client_utils import get_client from tck.util.constants import DEFAULT_GRPC_TIMEOUT @@ -316,6 +322,32 @@ def _build_pause_token_transaction(params: PauseTokenParams) -> TokenPauseTransa return transaction +def _build_grant_token_kyc_transaction(params: GrantTokenKycParams) -> TokenGrantKycTransaction: + """Build a TokenGrantKycTransaction from TCK params.""" + transaction = TokenGrantKycTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT) + + if params.tokenId is not None: + transaction.set_token_id(TokenId.from_string(params.tokenId)) + + if params.accountId is not None: + transaction.set_account_id(AccountId.from_string(params.accountId)) + + return transaction + + +def _build_revoke_token_kyc_transaction(params: RevokeTokenKycParams) -> TokenRevokeKycTransaction: + """Build a TokenRevokeKycTransaction from TCK params.""" + transaction = TokenRevokeKycTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT) + + if params.tokenId is not None: + transaction.set_token_id(TokenId.from_string(params.tokenId)) + + if params.accountId is not None: + transaction.set_account_id(AccountId.from_string(params.accountId)) + + return transaction + + @rpc_method("associateToken") def associate_token(params: AssociateTokenParams) -> AssociateTokenResponse: """Associate tokens with an account using TCK associateToken parameters.""" @@ -380,6 +412,38 @@ def pause_token(params: PauseTokenParams) -> PauseTokenResponse: return PauseTokenResponse(status=ResponseCode(receipt.status).name) +@rpc_method("grantTokenKyc") +def grant_token_kyc(params: GrantTokenKycParams) -> GrantTokenKycResponse: + """Grant KYC to an account for a token using TCK grantTokenKyc parameters.""" + client = get_client(params.sessionId) + + transaction = _build_grant_token_kyc_transaction(params) + + if params.commonTransactionParams is not None: + params.commonTransactionParams.apply_common_params(transaction, client) + + response = transaction.execute(client, wait_for_receipt=False) + receipt: TransactionReceipt = response.get_receipt(client, validate_status=True) + + return GrantTokenKycResponse(status=ResponseCode(receipt.status).name) + + +@rpc_method("revokeTokenKyc") +def revoke_token_kyc(params: RevokeTokenKycParams) -> RevokeTokenKycResponse: + """Revoke KYC from an account for a token using TCK revokeTokenKyc parameters.""" + client = get_client(params.sessionId) + + transaction = _build_revoke_token_kyc_transaction(params) + + if params.commonTransactionParams is not None: + params.commonTransactionParams.apply_common_params(transaction, client) + + response = transaction.execute(client, wait_for_receipt=False) + receipt: TransactionReceipt = response.get_receipt(client, validate_status=True) + + return RevokeTokenKycResponse(status=ResponseCode(receipt.status).name) + + def _build_airdrop_token_transaction(params: AirdropTokenParams) -> TokenAirdropTransaction: """Build a TokenAirdropTransaction from TCK params.""" tx = TokenAirdropTransaction() diff --git a/tck/param/token.py b/tck/param/token.py index 0659ffeec..d5294edf0 100644 --- a/tck/param/token.py +++ b/tck/param/token.py @@ -166,6 +166,42 @@ def parse_json_params(cls, params: dict) -> FreezeTokenParams: ) +@dataclass +class GrantTokenKycParams(BaseTransactionParams): + """Request parameters for the grantTokenKyc endpoint.""" + + tokenId: str | None = None + accountId: str | None = None + + @classmethod + def parse_json_params(cls, params: dict) -> GrantTokenKycParams: + """Parse JSON-RPC params into a GrantTokenKycParams instance.""" + return cls( + tokenId=params.get("tokenId"), + accountId=params.get("accountId"), + sessionId=parse_session_id(params), + commonTransactionParams=parse_common_transaction_params(params), + ) + + +@dataclass +class RevokeTokenKycParams(BaseTransactionParams): + """Request parameters for the revokeTokenKyc endpoint.""" + + tokenId: str | None = None + accountId: str | None = None + + @classmethod + def parse_json_params(cls, params: dict) -> RevokeTokenKycParams: + """Parse JSON-RPC params into a RevokeTokenKycParams instance.""" + return cls( + tokenId=params.get("tokenId"), + accountId=params.get("accountId"), + sessionId=parse_session_id(params), + commonTransactionParams=parse_common_transaction_params(params), + ) + + @dataclass class PauseTokenParams(BaseTransactionParams): """Request parameters for the pauseToken endpoint.""" diff --git a/tck/response/token.py b/tck/response/token.py index 7810ba83d..25a845fa6 100644 --- a/tck/response/token.py +++ b/tck/response/token.py @@ -39,6 +39,16 @@ class FreezeTokenResponse(StatusOnlyResponse): """Response payload for freezeToken.""" +@dataclass +class GrantTokenKycResponse(StatusOnlyResponse): + """Response payload for grantTokenKyc.""" + + +@dataclass +class RevokeTokenKycResponse(StatusOnlyResponse): + """Response payload for revokeTokenKyc.""" + + @dataclass class PauseTokenResponse(StatusOnlyResponse): """Response payload for pauseToken.""" From abe4664fd6005e3a0b03da8772a6d1f71946a7ea Mon Sep 17 00:00:00 2001 From: iron-prog Date: Sun, 12 Jul 2026 13:49:29 +0530 Subject: [PATCH 2/4] test(tck): add comprehensive tests covering params parsing, transaction-building helpers, full end-to-end dispatch against a mocked network, and robustness checks for unknown sessionId / wrong-type params (confirmed against freezeToken as a control - pre-existing TCK-layer behavior) Signed-off-by: iron-prog --- tests/tck/token_kyc_handlers_test.py | 205 +++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 tests/tck/token_kyc_handlers_test.py diff --git a/tests/tck/token_kyc_handlers_test.py b/tests/tck/token_kyc_handlers_test.py new file mode 100644 index 000000000..81210ed66 --- /dev/null +++ b/tests/tck/token_kyc_handlers_test.py @@ -0,0 +1,205 @@ +from __future__ import annotations + +import importlib + +import pytest + +from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.hapi.services import response_header_pb2, response_pb2, transaction_get_receipt_pb2 +from hiero_sdk_python.hapi.services.transaction_receipt_pb2 import TransactionReceipt as TransactionReceiptProto +from hiero_sdk_python.hapi.services.transaction_response_pb2 import TransactionResponse as TransactionResponseProto +from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.tokens.token_id import TokenId +from tck.errors import JsonRpcError +from tck.handlers.registry import get_handler +from tck.handlers.token import ( + _build_grant_token_kyc_transaction, + _build_revoke_token_kyc_transaction, +) +from tck.param.token import FreezeTokenParams, GrantTokenKycParams, RevokeTokenKycParams +from tck.util.client_utils import remove_client, store_client +from tests.unit.mock_server import mock_hedera_servers + + +pytestmark = pytest.mark.unit + + +ACCOUNT_ID = AccountId(0, 0, 5555) +TOKEN_ID = TokenId(0, 0, 9999) + + +@pytest.fixture(autouse=True) +def _ensure_handlers_registered(): + from tck.handlers import token as token_handlers + + importlib.reload(token_handlers) + yield + + +def _success_response_sequence(): + """Build a single-transaction response sequence that reports SUCCESS.""" + ok_response = TransactionResponseProto() + ok_response.nodeTransactionPrecheckCode = ResponseCode.OK + + receipt_proto = TransactionReceiptProto(status=ResponseCode.SUCCESS) + receipt_query_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader(nodeTransactionPrecheckCode=ResponseCode.OK), + receipt=receipt_proto, + ) + ) + return [[ok_response, receipt_query_response]] + + +class TestGrantTokenKycParamsParsing: + def test_parses_full_valid_payload(self): + params = GrantTokenKycParams.parse_json_params( + {"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": "session-1"} + ) + assert params.tokenId == "0.0.9999" + assert params.accountId == "0.0.5555" + assert params.sessionId == "session-1" + + def test_missing_session_id_raises(self): + with pytest.raises(ValueError, match="sessionId"): + GrantTokenKycParams.parse_json_params({"tokenId": "0.0.9999", "accountId": "0.0.5555"}) + + def test_empty_session_id_raises(self): + with pytest.raises(ValueError, match="sessionId"): + GrantTokenKycParams.parse_json_params({"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": ""}) + + def test_missing_token_and_account_id_parse_as_none(self): + """tokenId/accountId are optional at parse time; the TCK spec uses + their absence to test the endpoint's own validation errors.""" + params = GrantTokenKycParams.parse_json_params({"sessionId": "session-1"}) + assert params.tokenId is None + assert params.accountId is None + + +class TestRevokeTokenKycParamsParsing: + def test_parses_full_valid_payload(self): + params = RevokeTokenKycParams.parse_json_params( + {"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": "session-1"} + ) + assert params.tokenId == "0.0.9999" + assert params.accountId == "0.0.5555" + assert params.sessionId == "session-1" + + def test_missing_session_id_raises(self): + with pytest.raises(ValueError, match="sessionId"): + RevokeTokenKycParams.parse_json_params({"tokenId": "0.0.9999", "accountId": "0.0.5555"}) + + +class TestBuildGrantTokenKycTransaction: + def test_sets_both_fields_when_present(self): + params = GrantTokenKycParams.parse_json_params( + {"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": "s"} + ) + tx = _build_grant_token_kyc_transaction(params) + assert tx.token_id == TOKEN_ID + assert tx.account_id == ACCOUNT_ID + + def test_leaves_account_id_none_when_absent(self): + params = GrantTokenKycParams.parse_json_params({"tokenId": "0.0.9999", "sessionId": "s"}) + tx = _build_grant_token_kyc_transaction(params) + assert tx.token_id == TOKEN_ID + assert tx.account_id is None + + def test_missing_account_id_fails_at_proto_build_not_silently(self): + """Partial params build an executable-looking transaction object, + but the SDK's own validation must still catch the missing field + before anything reaches the network.""" + params = GrantTokenKycParams.parse_json_params({"tokenId": "0.0.9999", "sessionId": "s"}) + tx = _build_grant_token_kyc_transaction(params) + with pytest.raises(ValueError, match="Missing account ID"): + tx._build_proto_body() + + +class TestBuildRevokeTokenKycTransaction: + def test_sets_both_fields_when_present(self): + params = RevokeTokenKycParams.parse_json_params( + {"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": "s"} + ) + tx = _build_revoke_token_kyc_transaction(params) + assert tx.token_id == TOKEN_ID + assert tx.account_id == ACCOUNT_ID + + def test_missing_token_id_fails_at_proto_build_not_silently(self): + params = RevokeTokenKycParams.parse_json_params({"accountId": "0.0.5555", "sessionId": "s"}) + tx = _build_revoke_token_kyc_transaction(params) + with pytest.raises(ValueError, match="Missing token ID"): + tx._build_proto_body() + + +class TestEndToEndDispatch: + def test_grant_token_kyc_success(self): + session_id = "e2e-grant-session" + with mock_hedera_servers(_success_response_sequence()) as client: + store_client(session_id, client) + try: + handler = get_handler("grantTokenKyc") + params = GrantTokenKycParams.parse_json_params( + {"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": session_id} + ) + response = handler(params) + assert response.status == "SUCCESS" + finally: + remove_client(session_id) + + def test_revoke_token_kyc_success(self): + session_id = "e2e-revoke-session" + with mock_hedera_servers(_success_response_sequence()) as client: + store_client(session_id, client) + try: + handler = get_handler("revokeTokenKyc") + params = RevokeTokenKycParams.parse_json_params( + {"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": session_id} + ) + response = handler(params) + assert response.status == "SUCCESS" + finally: + remove_client(session_id) + + +class TestErrorHandlingRobustness: + def test_unknown_session_id_does_not_leak_raw_exception(self): + handler = get_handler("revokeTokenKyc") + params = RevokeTokenKycParams.parse_json_params( + {"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": "never-registered"} + ) + with pytest.raises(JsonRpcError) as exc_info: + handler(params) + assert exc_info.value.code == JsonRpcError.internal_error().code + assert exc_info.value.message == "Internal error" + assert exc_info.value.data is None + + def test_unknown_session_id_same_behavior_as_freeze_token_control(self): + handler = get_handler("freezeToken") + params = FreezeTokenParams.parse_json_params( + {"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": "never-registered"} + ) + with pytest.raises(JsonRpcError) as exc_info: + handler(params) + assert exc_info.value.code == JsonRpcError.internal_error().code + assert exc_info.value.message == "Internal error" + + def test_wrong_type_token_id_does_not_leak_raw_exception(self): + handler = get_handler("revokeTokenKyc") + params = RevokeTokenKycParams.parse_json_params( + {"tokenId": 9999, "accountId": "0.0.5555", "sessionId": "irrelevant"} + ) + with pytest.raises(JsonRpcError) as exc_info: + handler(params) + assert exc_info.value.code == JsonRpcError.internal_error().code + assert exc_info.value.message == "Internal error" + assert exc_info.value.data is None + + def test_wrong_type_account_id_does_not_leak_raw_exception(self): + handler = get_handler("grantTokenKyc") + params = GrantTokenKycParams.parse_json_params( + {"tokenId": "0.0.9999", "accountId": ["not", "a", "string"], "sessionId": "irrelevant"} + ) + with pytest.raises(JsonRpcError) as exc_info: + handler(params) + assert exc_info.value.code == JsonRpcError.internal_error().code + assert exc_info.value.message == "Internal error" From 21bded80cbe366abbd144ffe7c306f15da29e1ef Mon Sep 17 00:00:00 2001 From: iron-prog Date: Sun, 12 Jul 2026 14:21:54 +0530 Subject: [PATCH 3/4] test(tck): add symmetric KYC handler coverage Signed-off-by: iron-prog --- tests/tck/token_kyc_handlers_test.py | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/tck/token_kyc_handlers_test.py b/tests/tck/token_kyc_handlers_test.py index 81210ed66..c054b606c 100644 --- a/tests/tck/token_kyc_handlers_test.py +++ b/tests/tck/token_kyc_handlers_test.py @@ -89,6 +89,17 @@ def test_missing_session_id_raises(self): with pytest.raises(ValueError, match="sessionId"): RevokeTokenKycParams.parse_json_params({"tokenId": "0.0.9999", "accountId": "0.0.5555"}) + def test_empty_session_id_raises(self): + with pytest.raises(ValueError, match="sessionId"): + RevokeTokenKycParams.parse_json_params({"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": ""}) + + def test_missing_token_and_account_id_parse_as_none(self): + """tokenId/accountId are optional at parse time; the TCK spec uses + their absence to test the endpoint's own validation errors.""" + params = RevokeTokenKycParams.parse_json_params({"sessionId": "session-1"}) + assert params.tokenId is None + assert params.accountId is None + class TestBuildGrantTokenKycTransaction: def test_sets_both_fields_when_present(self): @@ -114,6 +125,18 @@ def test_missing_account_id_fails_at_proto_build_not_silently(self): with pytest.raises(ValueError, match="Missing account ID"): tx._build_proto_body() + def test_leaves_token_id_none_when_absent(self): + params = GrantTokenKycParams.parse_json_params({"accountId": "0.0.5555", "sessionId": "s"}) + tx = _build_grant_token_kyc_transaction(params) + assert tx.token_id is None + assert tx.account_id == ACCOUNT_ID + + def test_missing_token_id_fails_at_proto_build_not_silently(self): + params = GrantTokenKycParams.parse_json_params({"accountId": "0.0.5555", "sessionId": "s"}) + tx = _build_grant_token_kyc_transaction(params) + with pytest.raises(ValueError, match="Missing token ID"): + tx._build_proto_body() + class TestBuildRevokeTokenKycTransaction: def test_sets_both_fields_when_present(self): @@ -130,6 +153,18 @@ def test_missing_token_id_fails_at_proto_build_not_silently(self): with pytest.raises(ValueError, match="Missing token ID"): tx._build_proto_body() + def test_leaves_account_id_none_when_absent(self): + params = RevokeTokenKycParams.parse_json_params({"tokenId": "0.0.9999", "sessionId": "s"}) + tx = _build_revoke_token_kyc_transaction(params) + assert tx.token_id == TOKEN_ID + assert tx.account_id is None + + def test_missing_account_id_fails_at_proto_build_not_silently(self): + params = RevokeTokenKycParams.parse_json_params({"tokenId": "0.0.9999", "sessionId": "s"}) + tx = _build_revoke_token_kyc_transaction(params) + with pytest.raises(ValueError, match="Missing account ID"): + tx._build_proto_body() + class TestEndToEndDispatch: def test_grant_token_kyc_success(self): From 205a8e81edfd35a176bcdecf0634e6d1ba14239a Mon Sep 17 00:00:00 2001 From: iron-prog Date: Tue, 14 Jul 2026 10:08:43 +0530 Subject: [PATCH 4/4] fix(tokens): align TokenGrantKyc/RevokeKyc validation with TCK spec Signed-off-by: iron-prog --- .../tokens/token_grant_kyc_transaction.py | 15 +- .../tokens/token_revoke_kyc_transaction.py | 15 +- tck/handlers/token.py | 6 - tests/tck/token_kyc_handlers_test.py | 240 ------------------ .../unit/token_grant_kyc_transaction_test.py | 20 +- .../unit/token_revoke_kyc_transaction_test.py | 20 +- 6 files changed, 32 insertions(+), 284 deletions(-) delete mode 100644 tests/tck/token_kyc_handlers_test.py diff --git a/src/hiero_sdk_python/tokens/token_grant_kyc_transaction.py b/src/hiero_sdk_python/tokens/token_grant_kyc_transaction.py index 4e71bfa87..f64cbf2c2 100644 --- a/src/hiero_sdk_python/tokens/token_grant_kyc_transaction.py +++ b/src/hiero_sdk_python/tokens/token_grant_kyc_transaction.py @@ -76,17 +76,14 @@ def _build_proto_body(self) -> token_grant_kyc_pb2.TokenGrantKycTransactionBody: Returns: TokenGrantKycTransactionBody: The protobuf body for this transaction. - - Raises: - ValueError: If the token ID or account ID is not set. """ - if self.token_id is None: - raise ValueError("Missing token ID") - - if self.account_id is None: - raise ValueError("Missing account ID") + kwargs = {} + if self.token_id is not None: + kwargs["token"] = self.token_id._to_proto() + if self.account_id is not None: + kwargs["account"] = self.account_id._to_proto() - return TokenGrantKycTransactionBody(token=self.token_id._to_proto(), account=self.account_id._to_proto()) + return TokenGrantKycTransactionBody(**kwargs) def build_transaction_body(self) -> transaction_pb2.TransactionBody: """ diff --git a/src/hiero_sdk_python/tokens/token_revoke_kyc_transaction.py b/src/hiero_sdk_python/tokens/token_revoke_kyc_transaction.py index 13b4f0e3c..7802862ef 100644 --- a/src/hiero_sdk_python/tokens/token_revoke_kyc_transaction.py +++ b/src/hiero_sdk_python/tokens/token_revoke_kyc_transaction.py @@ -77,17 +77,14 @@ def _build_proto_body(self) -> token_revoke_kyc_pb2.TokenRevokeKycTransactionBod Returns: TokenRevokeKycTransactionBody: The protobuf body for this transaction. - - Raises: - ValueError: If the token ID or account ID is not set. """ - if self.token_id is None: - raise ValueError("Missing token ID") - - if self.account_id is None: - raise ValueError("Missing account ID") + kwargs = {} + if self.token_id is not None: + kwargs["token"] = self.token_id._to_proto() + if self.account_id is not None: + kwargs["account"] = self.account_id._to_proto() - return TokenRevokeKycTransactionBody(token=self.token_id._to_proto(), account=self.account_id._to_proto()) + return TokenRevokeKycTransactionBody(**kwargs) def build_transaction_body(self) -> transaction_pb2.AtomicBatchTransactionBody: """ diff --git a/tck/handlers/token.py b/tck/handlers/token.py index c12214dce..ce1f0bb2a 100644 --- a/tck/handlers/token.py +++ b/tck/handlers/token.py @@ -302,13 +302,11 @@ def _build_delete_token_transaction(params: DeleteTokenParams) -> TokenDeleteTra def _build_freeze_token_transaction(params: FreezeTokenParams) -> TokenFreezeTransaction: """Build a TokenFreezeTransaction from TCK params.""" transaction = TokenFreezeTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT) - if params.tokenId is not None: transaction.set_token_id(TokenId.from_string(params.tokenId)) if params.accountId is not None: transaction.set_account_id(AccountId.from_string(params.accountId)) - return transaction @@ -325,26 +323,22 @@ def _build_pause_token_transaction(params: PauseTokenParams) -> TokenPauseTransa def _build_grant_token_kyc_transaction(params: GrantTokenKycParams) -> TokenGrantKycTransaction: """Build a TokenGrantKycTransaction from TCK params.""" transaction = TokenGrantKycTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT) - if params.tokenId is not None: transaction.set_token_id(TokenId.from_string(params.tokenId)) if params.accountId is not None: transaction.set_account_id(AccountId.from_string(params.accountId)) - return transaction def _build_revoke_token_kyc_transaction(params: RevokeTokenKycParams) -> TokenRevokeKycTransaction: """Build a TokenRevokeKycTransaction from TCK params.""" transaction = TokenRevokeKycTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT) - if params.tokenId is not None: transaction.set_token_id(TokenId.from_string(params.tokenId)) if params.accountId is not None: transaction.set_account_id(AccountId.from_string(params.accountId)) - return transaction diff --git a/tests/tck/token_kyc_handlers_test.py b/tests/tck/token_kyc_handlers_test.py deleted file mode 100644 index c054b606c..000000000 --- a/tests/tck/token_kyc_handlers_test.py +++ /dev/null @@ -1,240 +0,0 @@ -from __future__ import annotations - -import importlib - -import pytest - -from hiero_sdk_python.account.account_id import AccountId -from hiero_sdk_python.hapi.services import response_header_pb2, response_pb2, transaction_get_receipt_pb2 -from hiero_sdk_python.hapi.services.transaction_receipt_pb2 import TransactionReceipt as TransactionReceiptProto -from hiero_sdk_python.hapi.services.transaction_response_pb2 import TransactionResponse as TransactionResponseProto -from hiero_sdk_python.response_code import ResponseCode -from hiero_sdk_python.tokens.token_id import TokenId -from tck.errors import JsonRpcError -from tck.handlers.registry import get_handler -from tck.handlers.token import ( - _build_grant_token_kyc_transaction, - _build_revoke_token_kyc_transaction, -) -from tck.param.token import FreezeTokenParams, GrantTokenKycParams, RevokeTokenKycParams -from tck.util.client_utils import remove_client, store_client -from tests.unit.mock_server import mock_hedera_servers - - -pytestmark = pytest.mark.unit - - -ACCOUNT_ID = AccountId(0, 0, 5555) -TOKEN_ID = TokenId(0, 0, 9999) - - -@pytest.fixture(autouse=True) -def _ensure_handlers_registered(): - from tck.handlers import token as token_handlers - - importlib.reload(token_handlers) - yield - - -def _success_response_sequence(): - """Build a single-transaction response sequence that reports SUCCESS.""" - ok_response = TransactionResponseProto() - ok_response.nodeTransactionPrecheckCode = ResponseCode.OK - - receipt_proto = TransactionReceiptProto(status=ResponseCode.SUCCESS) - receipt_query_response = response_pb2.Response( - transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( - header=response_header_pb2.ResponseHeader(nodeTransactionPrecheckCode=ResponseCode.OK), - receipt=receipt_proto, - ) - ) - return [[ok_response, receipt_query_response]] - - -class TestGrantTokenKycParamsParsing: - def test_parses_full_valid_payload(self): - params = GrantTokenKycParams.parse_json_params( - {"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": "session-1"} - ) - assert params.tokenId == "0.0.9999" - assert params.accountId == "0.0.5555" - assert params.sessionId == "session-1" - - def test_missing_session_id_raises(self): - with pytest.raises(ValueError, match="sessionId"): - GrantTokenKycParams.parse_json_params({"tokenId": "0.0.9999", "accountId": "0.0.5555"}) - - def test_empty_session_id_raises(self): - with pytest.raises(ValueError, match="sessionId"): - GrantTokenKycParams.parse_json_params({"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": ""}) - - def test_missing_token_and_account_id_parse_as_none(self): - """tokenId/accountId are optional at parse time; the TCK spec uses - their absence to test the endpoint's own validation errors.""" - params = GrantTokenKycParams.parse_json_params({"sessionId": "session-1"}) - assert params.tokenId is None - assert params.accountId is None - - -class TestRevokeTokenKycParamsParsing: - def test_parses_full_valid_payload(self): - params = RevokeTokenKycParams.parse_json_params( - {"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": "session-1"} - ) - assert params.tokenId == "0.0.9999" - assert params.accountId == "0.0.5555" - assert params.sessionId == "session-1" - - def test_missing_session_id_raises(self): - with pytest.raises(ValueError, match="sessionId"): - RevokeTokenKycParams.parse_json_params({"tokenId": "0.0.9999", "accountId": "0.0.5555"}) - - def test_empty_session_id_raises(self): - with pytest.raises(ValueError, match="sessionId"): - RevokeTokenKycParams.parse_json_params({"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": ""}) - - def test_missing_token_and_account_id_parse_as_none(self): - """tokenId/accountId are optional at parse time; the TCK spec uses - their absence to test the endpoint's own validation errors.""" - params = RevokeTokenKycParams.parse_json_params({"sessionId": "session-1"}) - assert params.tokenId is None - assert params.accountId is None - - -class TestBuildGrantTokenKycTransaction: - def test_sets_both_fields_when_present(self): - params = GrantTokenKycParams.parse_json_params( - {"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": "s"} - ) - tx = _build_grant_token_kyc_transaction(params) - assert tx.token_id == TOKEN_ID - assert tx.account_id == ACCOUNT_ID - - def test_leaves_account_id_none_when_absent(self): - params = GrantTokenKycParams.parse_json_params({"tokenId": "0.0.9999", "sessionId": "s"}) - tx = _build_grant_token_kyc_transaction(params) - assert tx.token_id == TOKEN_ID - assert tx.account_id is None - - def test_missing_account_id_fails_at_proto_build_not_silently(self): - """Partial params build an executable-looking transaction object, - but the SDK's own validation must still catch the missing field - before anything reaches the network.""" - params = GrantTokenKycParams.parse_json_params({"tokenId": "0.0.9999", "sessionId": "s"}) - tx = _build_grant_token_kyc_transaction(params) - with pytest.raises(ValueError, match="Missing account ID"): - tx._build_proto_body() - - def test_leaves_token_id_none_when_absent(self): - params = GrantTokenKycParams.parse_json_params({"accountId": "0.0.5555", "sessionId": "s"}) - tx = _build_grant_token_kyc_transaction(params) - assert tx.token_id is None - assert tx.account_id == ACCOUNT_ID - - def test_missing_token_id_fails_at_proto_build_not_silently(self): - params = GrantTokenKycParams.parse_json_params({"accountId": "0.0.5555", "sessionId": "s"}) - tx = _build_grant_token_kyc_transaction(params) - with pytest.raises(ValueError, match="Missing token ID"): - tx._build_proto_body() - - -class TestBuildRevokeTokenKycTransaction: - def test_sets_both_fields_when_present(self): - params = RevokeTokenKycParams.parse_json_params( - {"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": "s"} - ) - tx = _build_revoke_token_kyc_transaction(params) - assert tx.token_id == TOKEN_ID - assert tx.account_id == ACCOUNT_ID - - def test_missing_token_id_fails_at_proto_build_not_silently(self): - params = RevokeTokenKycParams.parse_json_params({"accountId": "0.0.5555", "sessionId": "s"}) - tx = _build_revoke_token_kyc_transaction(params) - with pytest.raises(ValueError, match="Missing token ID"): - tx._build_proto_body() - - def test_leaves_account_id_none_when_absent(self): - params = RevokeTokenKycParams.parse_json_params({"tokenId": "0.0.9999", "sessionId": "s"}) - tx = _build_revoke_token_kyc_transaction(params) - assert tx.token_id == TOKEN_ID - assert tx.account_id is None - - def test_missing_account_id_fails_at_proto_build_not_silently(self): - params = RevokeTokenKycParams.parse_json_params({"tokenId": "0.0.9999", "sessionId": "s"}) - tx = _build_revoke_token_kyc_transaction(params) - with pytest.raises(ValueError, match="Missing account ID"): - tx._build_proto_body() - - -class TestEndToEndDispatch: - def test_grant_token_kyc_success(self): - session_id = "e2e-grant-session" - with mock_hedera_servers(_success_response_sequence()) as client: - store_client(session_id, client) - try: - handler = get_handler("grantTokenKyc") - params = GrantTokenKycParams.parse_json_params( - {"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": session_id} - ) - response = handler(params) - assert response.status == "SUCCESS" - finally: - remove_client(session_id) - - def test_revoke_token_kyc_success(self): - session_id = "e2e-revoke-session" - with mock_hedera_servers(_success_response_sequence()) as client: - store_client(session_id, client) - try: - handler = get_handler("revokeTokenKyc") - params = RevokeTokenKycParams.parse_json_params( - {"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": session_id} - ) - response = handler(params) - assert response.status == "SUCCESS" - finally: - remove_client(session_id) - - -class TestErrorHandlingRobustness: - def test_unknown_session_id_does_not_leak_raw_exception(self): - handler = get_handler("revokeTokenKyc") - params = RevokeTokenKycParams.parse_json_params( - {"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": "never-registered"} - ) - with pytest.raises(JsonRpcError) as exc_info: - handler(params) - assert exc_info.value.code == JsonRpcError.internal_error().code - assert exc_info.value.message == "Internal error" - assert exc_info.value.data is None - - def test_unknown_session_id_same_behavior_as_freeze_token_control(self): - handler = get_handler("freezeToken") - params = FreezeTokenParams.parse_json_params( - {"tokenId": "0.0.9999", "accountId": "0.0.5555", "sessionId": "never-registered"} - ) - with pytest.raises(JsonRpcError) as exc_info: - handler(params) - assert exc_info.value.code == JsonRpcError.internal_error().code - assert exc_info.value.message == "Internal error" - - def test_wrong_type_token_id_does_not_leak_raw_exception(self): - handler = get_handler("revokeTokenKyc") - params = RevokeTokenKycParams.parse_json_params( - {"tokenId": 9999, "accountId": "0.0.5555", "sessionId": "irrelevant"} - ) - with pytest.raises(JsonRpcError) as exc_info: - handler(params) - assert exc_info.value.code == JsonRpcError.internal_error().code - assert exc_info.value.message == "Internal error" - assert exc_info.value.data is None - - def test_wrong_type_account_id_does_not_leak_raw_exception(self): - handler = get_handler("grantTokenKyc") - params = GrantTokenKycParams.parse_json_params( - {"tokenId": "0.0.9999", "accountId": ["not", "a", "string"], "sessionId": "irrelevant"} - ) - with pytest.raises(JsonRpcError) as exc_info: - handler(params) - assert exc_info.value.code == JsonRpcError.internal_error().code - assert exc_info.value.message == "Internal error" diff --git a/tests/unit/token_grant_kyc_transaction_test.py b/tests/unit/token_grant_kyc_transaction_test.py index 46d5cd67e..ab0873174 100644 --- a/tests/unit/token_grant_kyc_transaction_test.py +++ b/tests/unit/token_grant_kyc_transaction_test.py @@ -35,21 +35,21 @@ def test_build_transaction_body(mock_account_ids): assert transaction_body.tokenGrantKyc.account == account_id._to_proto() -def test_build_transaction_body_validation(mock_account_ids): - """Test validation when building transaction body.""" +def test_build_transaction_body_allows_missing_ids(mock_account_ids): + """Do not reject missing IDs locally; defer validation to the network per TCK.""" account_id, _, _, token_id, _ = mock_account_ids - # Test missing token ID + # Missing token ID: build must succeed, with the token field left unset. grant_kyc_tx = TokenGrantKycTransaction(account_id=account_id) + body = grant_kyc_tx._build_proto_body() + assert not body.HasField("token") + assert body.account == account_id._to_proto() - with pytest.raises(ValueError, match="Missing token ID"): - grant_kyc_tx.build_transaction_body() - - # Test missing account ID + # Missing account ID: build must succeed, with the account field left unset. grant_kyc_tx = TokenGrantKycTransaction(token_id=token_id) - - with pytest.raises(ValueError, match="Missing account ID"): - grant_kyc_tx.build_transaction_body() + body = grant_kyc_tx._build_proto_body() + assert body.token == token_id._to_proto() + assert not body.HasField("account") def test_constructor_with_parameters(mock_account_ids): diff --git a/tests/unit/token_revoke_kyc_transaction_test.py b/tests/unit/token_revoke_kyc_transaction_test.py index b82b768a2..7f4492b45 100644 --- a/tests/unit/token_revoke_kyc_transaction_test.py +++ b/tests/unit/token_revoke_kyc_transaction_test.py @@ -35,21 +35,21 @@ def test_build_transaction_body(mock_account_ids): assert transaction_body.tokenRevokeKyc.account == account_id._to_proto() -def test_build_transaction_body_validation(mock_account_ids): - """Test validation when building transaction body.""" +def test_build_transaction_body_allows_missing_ids(mock_account_ids): + """Allow missing IDs so validation is performed by the network, per the TCK spec.""" account_id, _, _, token_id, _ = mock_account_ids - # Test missing token ID + # Missing token ID: build must succeed, with the token field left unset. revoke_kyc_tx = TokenRevokeKycTransaction(account_id=account_id) + body = revoke_kyc_tx._build_proto_body() + assert not body.HasField("token") + assert body.account == account_id._to_proto() - with pytest.raises(ValueError, match="Missing token ID"): - revoke_kyc_tx.build_transaction_body() - - # Test missing account ID + # Missing account ID: build must succeed, with the account field left unset. revoke_kyc_tx = TokenRevokeKycTransaction(token_id=token_id) - - with pytest.raises(ValueError, match="Missing account ID"): - revoke_kyc_tx.build_transaction_body() + body = revoke_kyc_tx._build_proto_body() + assert body.token == token_id._to_proto() + assert not body.HasField("account") def test_constructor_with_parameters(mock_account_ids):