Skip to content

Commit 1b15756

Browse files
authored
feat(tck): add token grant/revoke KYC handlers (hiero-ledger#2433)
Signed-off-by: iron-prog <dt915725@gmail.com>
1 parent df3bd1e commit 1b15756

7 files changed

Lines changed: 138 additions & 40 deletions

File tree

src/hiero_sdk_python/tokens/token_grant_kyc_transaction.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,17 +76,14 @@ def _build_proto_body(self) -> token_grant_kyc_pb2.TokenGrantKycTransactionBody:
7676
7777
Returns:
7878
TokenGrantKycTransactionBody: The protobuf body for this transaction.
79-
80-
Raises:
81-
ValueError: If the token ID or account ID is not set.
8279
"""
83-
if self.token_id is None:
84-
raise ValueError("Missing token ID")
85-
86-
if self.account_id is None:
87-
raise ValueError("Missing account ID")
80+
kwargs = {}
81+
if self.token_id is not None:
82+
kwargs["token"] = self.token_id._to_proto()
83+
if self.account_id is not None:
84+
kwargs["account"] = self.account_id._to_proto()
8885

89-
return TokenGrantKycTransactionBody(token=self.token_id._to_proto(), account=self.account_id._to_proto())
86+
return TokenGrantKycTransactionBody(**kwargs)
9087

9188
def build_transaction_body(self) -> transaction_pb2.TransactionBody:
9289
"""

src/hiero_sdk_python/tokens/token_revoke_kyc_transaction.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,14 @@ def _build_proto_body(self) -> token_revoke_kyc_pb2.TokenRevokeKycTransactionBod
7777
7878
Returns:
7979
TokenRevokeKycTransactionBody: The protobuf body for this transaction.
80-
81-
Raises:
82-
ValueError: If the token ID or account ID is not set.
8380
"""
84-
if self.token_id is None:
85-
raise ValueError("Missing token ID")
86-
87-
if self.account_id is None:
88-
raise ValueError("Missing account ID")
81+
kwargs = {}
82+
if self.token_id is not None:
83+
kwargs["token"] = self.token_id._to_proto()
84+
if self.account_id is not None:
85+
kwargs["account"] = self.account_id._to_proto()
8986

90-
return TokenRevokeKycTransactionBody(token=self.token_id._to_proto(), account=self.account_id._to_proto())
87+
return TokenRevokeKycTransactionBody(**kwargs)
9188

9289
def build_transaction_body(self) -> transaction_pb2.AtomicBatchTransactionBody:
9390
"""

tck/handlers/token.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@
2323
from hiero_sdk_python.tokens.token_delete_transaction import TokenDeleteTransaction
2424
from hiero_sdk_python.tokens.token_freeze_status import TokenFreezeStatus
2525
from hiero_sdk_python.tokens.token_freeze_transaction import TokenFreezeTransaction
26+
from hiero_sdk_python.tokens.token_grant_kyc_transaction import TokenGrantKycTransaction
2627
from hiero_sdk_python.tokens.token_id import TokenId
2728
from hiero_sdk_python.tokens.token_info import TokenInfo
2829
from hiero_sdk_python.tokens.token_kyc_status import TokenKycStatus
2930
from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction
3031
from hiero_sdk_python.tokens.token_pause_status import TokenPauseStatus
3132
from hiero_sdk_python.tokens.token_pause_transaction import TokenPauseTransaction
33+
from hiero_sdk_python.tokens.token_revoke_kyc_transaction import TokenRevokeKycTransaction
3234
from hiero_sdk_python.tokens.token_type import TokenType
3335
from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt
3436
from tck.handlers.registry import rpc_method
@@ -41,8 +43,10 @@
4143
DeleteTokenParams,
4244
FreezeTokenParams,
4345
GetTokenInfoParams,
46+
GrantTokenKycParams,
4447
MintTokenParams,
4548
PauseTokenParams,
49+
RevokeTokenKycParams,
4650
)
4751
from tck.response.token import (
4852
AirdropTokenResponse,
@@ -53,8 +57,10 @@
5357
DeleteTokenResponse,
5458
FreezeTokenResponse,
5559
GetTokenInfoResponse,
60+
GrantTokenKycResponse,
5661
MintTokenResponse,
5762
PauseTokenResponse,
63+
RevokeTokenKycResponse,
5864
)
5965
from tck.util.client_utils import get_client
6066
from tck.util.constants import DEFAULT_GRPC_TIMEOUT
@@ -296,13 +302,11 @@ def _build_delete_token_transaction(params: DeleteTokenParams) -> TokenDeleteTra
296302
def _build_freeze_token_transaction(params: FreezeTokenParams) -> TokenFreezeTransaction:
297303
"""Build a TokenFreezeTransaction from TCK params."""
298304
transaction = TokenFreezeTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT)
299-
300305
if params.tokenId is not None:
301306
transaction.set_token_id(TokenId.from_string(params.tokenId))
302307

303308
if params.accountId is not None:
304309
transaction.set_account_id(AccountId.from_string(params.accountId))
305-
306310
return transaction
307311

308312

@@ -316,6 +320,28 @@ def _build_pause_token_transaction(params: PauseTokenParams) -> TokenPauseTransa
316320
return transaction
317321

318322

323+
def _build_grant_token_kyc_transaction(params: GrantTokenKycParams) -> TokenGrantKycTransaction:
324+
"""Build a TokenGrantKycTransaction from TCK params."""
325+
transaction = TokenGrantKycTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT)
326+
if params.tokenId is not None:
327+
transaction.set_token_id(TokenId.from_string(params.tokenId))
328+
329+
if params.accountId is not None:
330+
transaction.set_account_id(AccountId.from_string(params.accountId))
331+
return transaction
332+
333+
334+
def _build_revoke_token_kyc_transaction(params: RevokeTokenKycParams) -> TokenRevokeKycTransaction:
335+
"""Build a TokenRevokeKycTransaction from TCK params."""
336+
transaction = TokenRevokeKycTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT)
337+
if params.tokenId is not None:
338+
transaction.set_token_id(TokenId.from_string(params.tokenId))
339+
340+
if params.accountId is not None:
341+
transaction.set_account_id(AccountId.from_string(params.accountId))
342+
return transaction
343+
344+
319345
@rpc_method("associateToken")
320346
def associate_token(params: AssociateTokenParams) -> AssociateTokenResponse:
321347
"""Associate tokens with an account using TCK associateToken parameters."""
@@ -380,6 +406,38 @@ def pause_token(params: PauseTokenParams) -> PauseTokenResponse:
380406
return PauseTokenResponse(status=ResponseCode(receipt.status).name)
381407

382408

409+
@rpc_method("grantTokenKyc")
410+
def grant_token_kyc(params: GrantTokenKycParams) -> GrantTokenKycResponse:
411+
"""Grant KYC to an account for a token using TCK grantTokenKyc parameters."""
412+
client = get_client(params.sessionId)
413+
414+
transaction = _build_grant_token_kyc_transaction(params)
415+
416+
if params.commonTransactionParams is not None:
417+
params.commonTransactionParams.apply_common_params(transaction, client)
418+
419+
response = transaction.execute(client, wait_for_receipt=False)
420+
receipt: TransactionReceipt = response.get_receipt(client, validate_status=True)
421+
422+
return GrantTokenKycResponse(status=ResponseCode(receipt.status).name)
423+
424+
425+
@rpc_method("revokeTokenKyc")
426+
def revoke_token_kyc(params: RevokeTokenKycParams) -> RevokeTokenKycResponse:
427+
"""Revoke KYC from an account for a token using TCK revokeTokenKyc parameters."""
428+
client = get_client(params.sessionId)
429+
430+
transaction = _build_revoke_token_kyc_transaction(params)
431+
432+
if params.commonTransactionParams is not None:
433+
params.commonTransactionParams.apply_common_params(transaction, client)
434+
435+
response = transaction.execute(client, wait_for_receipt=False)
436+
receipt: TransactionReceipt = response.get_receipt(client, validate_status=True)
437+
438+
return RevokeTokenKycResponse(status=ResponseCode(receipt.status).name)
439+
440+
383441
def _build_airdrop_token_transaction(params: AirdropTokenParams) -> TokenAirdropTransaction:
384442
"""Build a TokenAirdropTransaction from TCK params."""
385443
tx = TokenAirdropTransaction()

tck/param/token.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,42 @@ def parse_json_params(cls, params: dict) -> FreezeTokenParams:
166166
)
167167

168168

169+
@dataclass
170+
class GrantTokenKycParams(BaseTransactionParams):
171+
"""Request parameters for the grantTokenKyc endpoint."""
172+
173+
tokenId: str | None = None
174+
accountId: str | None = None
175+
176+
@classmethod
177+
def parse_json_params(cls, params: dict) -> GrantTokenKycParams:
178+
"""Parse JSON-RPC params into a GrantTokenKycParams instance."""
179+
return cls(
180+
tokenId=params.get("tokenId"),
181+
accountId=params.get("accountId"),
182+
sessionId=parse_session_id(params),
183+
commonTransactionParams=parse_common_transaction_params(params),
184+
)
185+
186+
187+
@dataclass
188+
class RevokeTokenKycParams(BaseTransactionParams):
189+
"""Request parameters for the revokeTokenKyc endpoint."""
190+
191+
tokenId: str | None = None
192+
accountId: str | None = None
193+
194+
@classmethod
195+
def parse_json_params(cls, params: dict) -> RevokeTokenKycParams:
196+
"""Parse JSON-RPC params into a RevokeTokenKycParams instance."""
197+
return cls(
198+
tokenId=params.get("tokenId"),
199+
accountId=params.get("accountId"),
200+
sessionId=parse_session_id(params),
201+
commonTransactionParams=parse_common_transaction_params(params),
202+
)
203+
204+
169205
@dataclass
170206
class PauseTokenParams(BaseTransactionParams):
171207
"""Request parameters for the pauseToken endpoint."""

tck/response/token.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ class FreezeTokenResponse(StatusOnlyResponse):
3939
"""Response payload for freezeToken."""
4040

4141

42+
@dataclass
43+
class GrantTokenKycResponse(StatusOnlyResponse):
44+
"""Response payload for grantTokenKyc."""
45+
46+
47+
@dataclass
48+
class RevokeTokenKycResponse(StatusOnlyResponse):
49+
"""Response payload for revokeTokenKyc."""
50+
51+
4252
@dataclass
4353
class PauseTokenResponse(StatusOnlyResponse):
4454
"""Response payload for pauseToken."""

tests/unit/token_grant_kyc_transaction_test.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,21 @@ def test_build_transaction_body(mock_account_ids):
3535
assert transaction_body.tokenGrantKyc.account == account_id._to_proto()
3636

3737

38-
def test_build_transaction_body_validation(mock_account_ids):
39-
"""Test validation when building transaction body."""
38+
def test_build_transaction_body_allows_missing_ids(mock_account_ids):
39+
"""Do not reject missing IDs locally; defer validation to the network per TCK."""
4040
account_id, _, _, token_id, _ = mock_account_ids
4141

42-
# Test missing token ID
42+
# Missing token ID: build must succeed, with the token field left unset.
4343
grant_kyc_tx = TokenGrantKycTransaction(account_id=account_id)
44+
body = grant_kyc_tx._build_proto_body()
45+
assert not body.HasField("token")
46+
assert body.account == account_id._to_proto()
4447

45-
with pytest.raises(ValueError, match="Missing token ID"):
46-
grant_kyc_tx.build_transaction_body()
47-
48-
# Test missing account ID
48+
# Missing account ID: build must succeed, with the account field left unset.
4949
grant_kyc_tx = TokenGrantKycTransaction(token_id=token_id)
50-
51-
with pytest.raises(ValueError, match="Missing account ID"):
52-
grant_kyc_tx.build_transaction_body()
50+
body = grant_kyc_tx._build_proto_body()
51+
assert body.token == token_id._to_proto()
52+
assert not body.HasField("account")
5353

5454

5555
def test_constructor_with_parameters(mock_account_ids):

tests/unit/token_revoke_kyc_transaction_test.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,21 @@ def test_build_transaction_body(mock_account_ids):
3535
assert transaction_body.tokenRevokeKyc.account == account_id._to_proto()
3636

3737

38-
def test_build_transaction_body_validation(mock_account_ids):
39-
"""Test validation when building transaction body."""
38+
def test_build_transaction_body_allows_missing_ids(mock_account_ids):
39+
"""Allow missing IDs so validation is performed by the network, per the TCK spec."""
4040
account_id, _, _, token_id, _ = mock_account_ids
4141

42-
# Test missing token ID
42+
# Missing token ID: build must succeed, with the token field left unset.
4343
revoke_kyc_tx = TokenRevokeKycTransaction(account_id=account_id)
44+
body = revoke_kyc_tx._build_proto_body()
45+
assert not body.HasField("token")
46+
assert body.account == account_id._to_proto()
4447

45-
with pytest.raises(ValueError, match="Missing token ID"):
46-
revoke_kyc_tx.build_transaction_body()
47-
48-
# Test missing account ID
48+
# Missing account ID: build must succeed, with the account field left unset.
4949
revoke_kyc_tx = TokenRevokeKycTransaction(token_id=token_id)
50-
51-
with pytest.raises(ValueError, match="Missing account ID"):
52-
revoke_kyc_tx.build_transaction_body()
50+
body = revoke_kyc_tx._build_proto_body()
51+
assert body.token == token_id._to_proto()
52+
assert not body.HasField("account")
5353

5454

5555
def test_constructor_with_parameters(mock_account_ids):

0 commit comments

Comments
 (0)