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 63c41f026..ce1f0bb2a 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 @@ -296,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 @@ -316,6 +320,28 @@ 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 +406,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.""" 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):