Skip to content

Commit ed76db3

Browse files
committed
feat(token): Add getTokenInfo request and response models, including custom fee serialization
Signed-off-by: Ntege Daniel <danientege785@gmail.com>
1 parent 07be7f6 commit ed76db3

3 files changed

Lines changed: 226 additions & 2 deletions

File tree

tck/handlers/token.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from hiero_sdk_python.account.account_id import AccountId
66
from hiero_sdk_python.Duration import Duration
7+
from hiero_sdk_python.hbar import Hbar
8+
from hiero_sdk_python.query.token_info_query import TokenInfoQuery
79
from hiero_sdk_python.response_code import ResponseCode
810
from hiero_sdk_python.timestamp import Timestamp
911
from hiero_sdk_python.tokens.custom_fee import CustomFee
@@ -19,9 +21,13 @@
1921
from hiero_sdk_python.tokens.token_associate_transaction import TokenAssociateTransaction
2022
from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction
2123
from hiero_sdk_python.tokens.token_delete_transaction import TokenDeleteTransaction
24+
from hiero_sdk_python.tokens.token_freeze_status import TokenFreezeStatus
2225
from hiero_sdk_python.tokens.token_freeze_transaction import TokenFreezeTransaction
2326
from hiero_sdk_python.tokens.token_id import TokenId
27+
from hiero_sdk_python.tokens.token_info import TokenInfo
28+
from hiero_sdk_python.tokens.token_kyc_status import TokenKycStatus
2429
from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction
30+
from hiero_sdk_python.tokens.token_pause_status import TokenPauseStatus
2531
from hiero_sdk_python.tokens.token_pause_transaction import TokenPauseTransaction
2632
from hiero_sdk_python.tokens.token_type import TokenType
2733
from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt
@@ -34,6 +40,7 @@
3440
CreateTokenParams,
3541
DeleteTokenParams,
3642
FreezeTokenParams,
43+
GetTokenInfoParams,
3744
MintTokenParams,
3845
PauseTokenParams,
3946
)
@@ -42,8 +49,10 @@
4249
AssociateTokenResponse,
4350
ClaimTokenResponse,
4451
CreateTokenResponse,
52+
CustomFeeResponse,
4553
DeleteTokenResponse,
4654
FreezeTokenResponse,
55+
GetTokenInfoResponse,
4756
MintTokenResponse,
4857
PauseTokenResponse,
4958
)
@@ -478,3 +487,154 @@ def claim_token(params: ClaimTokenParams) -> ClaimTokenResponse:
478487
receipt: TransactionReceipt = response.get_receipt(client, validate_status=True)
479488

480489
return ClaimTokenResponse(status=ResponseCode(receipt.status).name)
490+
491+
492+
def _serialize_key(key) -> str | None:
493+
"""Serialize a key to its DER-encoded hex string representation."""
494+
if key is None:
495+
return None
496+
497+
to_string_der = getattr(key, "to_string_der", None)
498+
if callable(to_string_der):
499+
return to_string_der()
500+
501+
return key.to_bytes().hex()
502+
503+
504+
def _serialize_custom_fee(fee: CustomFee) -> CustomFeeResponse:
505+
"""Serialize a CustomFee to a CustomFeeResponse."""
506+
response = CustomFeeResponse()
507+
508+
if fee.fee_collector_account_id is not None:
509+
response.feeCollectorAccountId = str(fee.fee_collector_account_id)
510+
response.feeCollectorsExempt = fee.all_collectors_are_exempt
511+
512+
if isinstance(fee, CustomFixedFee):
513+
fixed_fee_dict: dict = {"amount": str(fee.amount)}
514+
if fee.denominating_token_id is not None:
515+
fixed_fee_dict["denominatingTokenId"] = str(fee.denominating_token_id)
516+
response.fixedFee = fixed_fee_dict
517+
elif isinstance(fee, CustomFractionalFee):
518+
response.fractionalFee = {
519+
"numerator": str(fee.numerator),
520+
"denominator": str(fee.denominator),
521+
"minimumAmount": str(fee.min_amount),
522+
"maximumAmount": str(fee.max_amount),
523+
"assessmentMethod": "inclusive" if fee.assessment_method == FeeAssessmentMethod.INCLUSIVE else "exclusive",
524+
}
525+
elif isinstance(fee, CustomRoyaltyFee):
526+
royalty_fee_dict: dict = {
527+
"numerator": str(fee.numerator),
528+
"denominator": str(fee.denominator),
529+
}
530+
if fee.fallback_fee is not None:
531+
fallback: dict = {"amount": str(fee.fallback_fee.amount)}
532+
if fee.fallback_fee.denominating_token_id is not None:
533+
fallback["denominatingTokenId"] = str(fee.fallback_fee.denominating_token_id)
534+
royalty_fee_dict["fallbackFee"] = fallback
535+
response.royaltyFee = royalty_fee_dict
536+
537+
return response
538+
539+
540+
def _map_pause_status(pause_status: TokenPauseStatus) -> str:
541+
"""Map TokenPauseStatus enum to TCK string representation."""
542+
mapping = {
543+
TokenPauseStatus.PAUSED: "PAUSED",
544+
TokenPauseStatus.UNPAUSED: "UNPAUSED",
545+
TokenPauseStatus.PAUSE_NOT_APPLICABLE: "NOT_APPLICABLE",
546+
}
547+
return mapping.get(pause_status, "NOT_APPLICABLE")
548+
549+
550+
def _map_token_type(token_type: TokenType | None) -> str | None:
551+
"""Map TokenType enum to TCK string representation."""
552+
if token_type is None:
553+
return None
554+
mapping = {
555+
TokenType.FUNGIBLE_COMMON: "FUNGIBLE_COMMON",
556+
TokenType.NON_FUNGIBLE_UNIQUE: "NON_FUNGIBLE_UNIQUE",
557+
}
558+
return mapping.get(token_type)
559+
560+
561+
def _map_supply_type(supply_type: SupplyType | None) -> str | None:
562+
"""Map SupplyType enum to TCK string representation."""
563+
if supply_type is None:
564+
return None
565+
mapping = {
566+
SupplyType.INFINITE: "INFINITE",
567+
SupplyType.FINITE: "FINITE",
568+
}
569+
return mapping.get(supply_type)
570+
571+
572+
def _build_token_info_response(info: TokenInfo) -> GetTokenInfoResponse:
573+
"""Build a GetTokenInfoResponse from a TokenInfo object."""
574+
# Map default freeze status to boolean
575+
default_freeze: bool | None = None
576+
if info.default_freeze_status == TokenFreezeStatus.FROZEN:
577+
default_freeze = True
578+
elif info.default_freeze_status == TokenFreezeStatus.UNFROZEN:
579+
default_freeze = False
580+
581+
# Map default KYC status to boolean
582+
default_kyc: bool | None = None
583+
if info.default_kyc_status == TokenKycStatus.GRANTED:
584+
default_kyc = True
585+
elif info.default_kyc_status == TokenKycStatus.REVOKED:
586+
default_kyc = False
587+
588+
# Serialize custom fees
589+
custom_fees = [_serialize_custom_fee(fee) for fee in info.custom_fees] if info.custom_fees else []
590+
591+
return GetTokenInfoResponse(
592+
tokenId=str(info.token_id) if info.token_id else None,
593+
name=info.name or "",
594+
symbol=info.symbol or "",
595+
decimals=info.decimals,
596+
totalSupply=str(info.total_supply) if info.total_supply is not None else "0",
597+
treasuryAccountId=str(info.treasury) if info.treasury else None,
598+
adminKey=_serialize_key(info.admin_key),
599+
kycKey=_serialize_key(info.kyc_key),
600+
freezeKey=_serialize_key(info.freeze_key),
601+
pauseKey=_serialize_key(info.pause_key),
602+
wipeKey=_serialize_key(info.wipe_key),
603+
supplyKey=_serialize_key(info.supply_key),
604+
feeScheduleKey=_serialize_key(info.fee_schedule_key),
605+
metadataKey=_serialize_key(info.metadata_key),
606+
defaultFreezeStatus=default_freeze,
607+
defaultKycStatus=default_kyc,
608+
pauseStatus=_map_pause_status(info.pause_status),
609+
isDeleted=info.is_deleted,
610+
autoRenewAccountId=str(info.auto_renew_account) if info.auto_renew_account else None,
611+
autoRenewPeriod=str(info.auto_renew_period.seconds) if info.auto_renew_period else None,
612+
expirationTime=str(info.expiry.seconds) if info.expiry else None,
613+
tokenMemo=info.memo if info.memo is not None else "",
614+
customFees=custom_fees,
615+
tokenType=_map_token_type(info.token_type),
616+
supplyType=_map_supply_type(info.supply_type),
617+
maxSupply=str(info.max_supply) if info.max_supply is not None else "0",
618+
metadata=info.metadata.hex() if info.metadata else "",
619+
ledgerId=info.ledger_id.hex() if info.ledger_id else "",
620+
)
621+
622+
623+
@rpc_method("getTokenInfo")
624+
def get_token_info(params: GetTokenInfoParams) -> GetTokenInfoResponse:
625+
"""Query token info using TCK getTokenInfo parameters."""
626+
client = get_client(params.sessionId)
627+
628+
query = TokenInfoQuery().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT)
629+
630+
if params.tokenId is not None:
631+
query.set_token_id(TokenId.from_string(params.tokenId))
632+
633+
if params.queryPayment is not None:
634+
query.set_query_payment(Hbar.from_tinybars(int(params.queryPayment)))
635+
636+
if params.maxQueryPayment is not None:
637+
query.set_max_query_payment(Hbar.from_tinybars(int(params.maxQueryPayment)))
638+
639+
info = query.execute(client)
640+
return _build_token_info_response(info)

tck/param/token.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from dataclasses import dataclass
66

7-
from tck.param.base import BaseTransactionParams
7+
from tck.param.base import BaseParams, BaseTransactionParams
88
from tck.param.custom_fee import CustomFeeParams
99
from tck.util.param_utils import (
1010
parse_common_transaction_params,
@@ -232,3 +232,22 @@ def parse_json_params(cls, params: dict) -> ClaimTokenParams:
232232
sessionId=parse_session_id(params),
233233
commonTransactionParams=parse_common_transaction_params(params),
234234
)
235+
236+
237+
@dataclass
238+
class GetTokenInfoParams(BaseParams):
239+
"""Request parameters for the getTokenInfo endpoint."""
240+
241+
tokenId: str | None = None
242+
queryPayment: str | None = None
243+
maxQueryPayment: str | None = None
244+
245+
@classmethod
246+
def parse_json_params(cls, params: dict) -> GetTokenInfoParams:
247+
"""Parse JSON-RPC params into a GetTokenInfoParams instance."""
248+
return cls(
249+
tokenId=params.get("tokenId"),
250+
queryPayment=params.get("queryPayment"),
251+
maxQueryPayment=params.get("maxQueryPayment"),
252+
sessionId=parse_session_id(params),
253+
)

tck/response/token.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
from dataclasses import dataclass
5+
from dataclasses import dataclass, field
66

77
from tck.response.base import StatusOnlyResponse
88

@@ -52,3 +52,48 @@ class AirdropTokenResponse(StatusOnlyResponse):
5252
@dataclass
5353
class ClaimTokenResponse(StatusOnlyResponse):
5454
"""Response payload for claimToken."""
55+
56+
57+
@dataclass
58+
class CustomFeeResponse:
59+
"""Nested custom fee details for getTokenInfo."""
60+
61+
fixedFee: dict | None = None
62+
fractionalFee: dict | None = None
63+
royaltyFee: dict | None = None
64+
feeCollectorAccountId: str | None = None
65+
feeCollectorsExempt: bool | None = None
66+
67+
68+
@dataclass
69+
class GetTokenInfoResponse:
70+
"""Response payload for getTokenInfo."""
71+
72+
tokenId: str | None = None
73+
name: str | None = None
74+
symbol: str | None = None
75+
decimals: int | None = None
76+
totalSupply: str | None = None
77+
treasuryAccountId: str | None = None
78+
adminKey: str | None = None
79+
kycKey: str | None = None
80+
freezeKey: str | None = None
81+
pauseKey: str | None = None
82+
wipeKey: str | None = None
83+
supplyKey: str | None = None
84+
feeScheduleKey: str | None = None
85+
metadataKey: str | None = None
86+
defaultFreezeStatus: bool | None = None
87+
defaultKycStatus: bool | None = None
88+
pauseStatus: str | None = None
89+
isDeleted: bool | None = None
90+
autoRenewAccountId: str | None = None
91+
autoRenewPeriod: str | None = None
92+
expirationTime: str | None = None
93+
tokenMemo: str | None = None
94+
customFees: list[CustomFeeResponse] = field(default_factory=list)
95+
tokenType: str | None = None
96+
supplyType: str | None = None
97+
maxSupply: str | None = None
98+
metadata: str | None = None
99+
ledgerId: str | None = None

0 commit comments

Comments
 (0)