Skip to content

Commit d64f11f

Browse files
authored
feat: Added checksum field to remaining IDs (#422)
Signed-off-by: Manish Dait <[email protected]>
1 parent 8da2726 commit d64f11f

File tree

15 files changed

+739
-170
lines changed

15 files changed

+739
-170
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
1010
- Unified balance and transfer logging format — both now consistently display values in hbars for clarity.
1111

1212
### Added
13-
1413
- Refactored `examples/topic_create.py` into modular functions for better readability and reuse.
1514
- Add Rebasing and Signing section to signing.md with instructions for maintaining commit verification during rebase operations (#556)
1615
- Add `examples/account_id.py` demonstrating AccountId class usage including creating standard AccountIds, parsing from strings, comparing instances, and creating AccountIds with public key aliases
@@ -24,6 +23,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
2423
- docs: Add Google-style docstrings to `AbstractTokenTransferTransaction` class and its methods in `abstract_token_transfer_transaction.py`.
2524
- docs: Add Google-style docstrings to `TokenRelationship` class and its methods in `token_relationship.py`.
2625
- feat: add initial testing guide structure
26+
- Added `checksum` filed for TopicId, FileId, ContractId, ScheduleId class
2727

2828
### Changed
2929

src/hiero_sdk_python/account/account_id.py

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@
22
AccountId class.
33
"""
44

5-
from typing import List
5+
import re
6+
from typing import TYPE_CHECKING
67

78
from hiero_sdk_python.crypto.public_key import PublicKey
89
from hiero_sdk_python.hapi.services import basic_types_pb2
10+
from hiero_sdk_python.utils.entity_id_helper import (
11+
parse_from_string,
12+
validate_checksum,
13+
format_to_string_with_checksum
14+
)
915

16+
if TYPE_CHECKING:
17+
from hiero_sdk_python.client.client import Client
18+
19+
ALIAS_REGEX = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.((?:[0-9a-fA-F][0-9a-fA-F])+)$")
1020

1121
class AccountId:
1222
"""
@@ -37,17 +47,42 @@ def __init__(
3747
self.realm = realm
3848
self.num = num
3949
self.alias_key = alias_key
50+
self.__checksum: str | None = None
4051

4152
@classmethod
4253
def from_string(cls, account_id_str: str) -> "AccountId":
4354
"""
4455
Creates an AccountId instance from a string in the format 'shard.realm.num'.
4556
"""
46-
parts: List[str] = account_id_str.strip().split(".")
47-
if len(parts) != 3:
48-
raise ValueError("Invalid account ID string format. Expected 'shard.realm.num'")
49-
shard, realm, num = map(int, parts)
50-
return cls(shard, realm, num)
57+
if account_id_str is None or not isinstance(account_id_str, str):
58+
raise ValueError(f"Invalid account ID string '{account_id_str}'. Expected format 'shard.realm.num'.")
59+
60+
try:
61+
shard, realm, num, checksum = parse_from_string(account_id_str)
62+
63+
account_id: AccountId = cls(
64+
shard=int(shard),
65+
realm=int(realm),
66+
num=int(num)
67+
)
68+
account_id.__checksum = checksum
69+
70+
return account_id
71+
except Exception as e:
72+
alias_match = ALIAS_REGEX.match(account_id_str)
73+
74+
if alias_match:
75+
shard, realm, alias = alias_match.groups()
76+
return cls(
77+
shard=int(shard),
78+
realm=int(realm),
79+
num=0,
80+
alias_key=PublicKey.from_bytes(bytes.fromhex(alias))
81+
)
82+
83+
raise ValueError(
84+
f"Invalid account ID string '{account_id_str}'. Expected format 'shard.realm.num'."
85+
) from e
5186

5287
@classmethod
5388
def _from_proto(cls, account_id_proto: basic_types_pb2.AccountID) -> "AccountId":
@@ -89,6 +124,24 @@ def _to_proto(self) -> basic_types_pb2.AccountID:
89124

90125
return account_id_proto
91126

127+
@property
128+
def checksum(self) -> str | None:
129+
"""Checksum of the accountId"""
130+
return self.__checksum
131+
132+
def validate_checksum(self, client: "Client") -> None:
133+
"""Validate the checksum for the accountId"""
134+
if self.alias_key is not None:
135+
raise ValueError("Cannot calculate checksum with an account ID that has a aliasKey")
136+
137+
validate_checksum(
138+
self.shard,
139+
self.realm,
140+
self.num,
141+
self.__checksum,
142+
client,
143+
)
144+
92145
def __str__(self) -> str:
93146
"""
94147
Returns the string representation of the AccountId in 'shard.realm.num' format.
@@ -97,6 +150,21 @@ def __str__(self) -> str:
97150
return f"{self.shard}.{self.realm}.{self.alias_key.to_string()}"
98151
return f"{self.shard}.{self.realm}.{self.num}"
99152

153+
def to_string_with_checksum(self, client: "Client") -> str:
154+
"""
155+
Returns the string representation of the AccountId with checksum
156+
in 'shard.realm.num-checksum' format.
157+
"""
158+
if self.alias_key is not None:
159+
raise ValueError("Cannot calculate checksum with an account ID that has a aliasKey")
160+
161+
return format_to_string_with_checksum(
162+
self.shard,
163+
self.realm,
164+
self.num,
165+
client
166+
)
167+
100168
def __repr__(self):
101169
"""
102170
Returns the repr representation of the AccountId.

src/hiero_sdk_python/consensus/topic_id.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@
77
formats within the Hiero SDK.
88
"""
99

10-
from dataclasses import dataclass
10+
from dataclasses import dataclass, field
1111

1212
from hiero_sdk_python.hapi.services import basic_types_pb2
13-
14-
@dataclass
13+
from hiero_sdk_python.client.client import Client
14+
from hiero_sdk_python.utils.entity_id_helper import (
15+
parse_from_string,
16+
validate_checksum,
17+
format_to_string_with_checksum
18+
)
19+
20+
@dataclass(frozen=True)
1521
class TopicId:
1622
"""
1723
Represents the unique identifier of a topic in the Hedera Consensus Service (HCS).
@@ -28,6 +34,7 @@ class TopicId:
2834
shard: int = 0
2935
realm: int = 0
3036
num: int = 0
37+
checksum: str | None = field(default=None, init=False)
3138

3239
@classmethod
3340
def _from_proto(cls, topic_id_proto: basic_types_pb2.TopicID) -> "TopicId":
@@ -82,7 +89,40 @@ def from_string(cls, topic_id_str: str) -> "TopicId":
8289
Raises:
8390
ValueError: If the string format is invalid.
8491
"""
85-
parts = topic_id_str.strip().split(".")
86-
if len(parts) != 3:
87-
raise ValueError("Invalid TopicId format. Expected 'shard.realm.num'")
88-
return cls(shard=int(parts[0]), realm=int(parts[1]), num=int(parts[2]))
92+
try:
93+
shard, realm, num, checksum = parse_from_string(topic_id_str)
94+
95+
topic_id: TopicId = cls(
96+
shard=int(shard),
97+
realm=int(realm),
98+
num=int(num)
99+
)
100+
object.__setattr__(topic_id, "checksum", checksum)
101+
102+
return topic_id
103+
except Exception as e:
104+
raise ValueError(
105+
f"Invalid topic ID string '{topic_id_str}'. Expected format 'shard.realm.num'."
106+
) from e
107+
108+
def validate_checksum(self, client: Client) -> None:
109+
"""Validate the checksum for the topicId"""
110+
validate_checksum(
111+
self.shard,
112+
self.realm,
113+
self.num,
114+
self.checksum,
115+
client,
116+
)
117+
118+
def to_string_with_checksum(self, client: Client) -> str:
119+
"""
120+
Returns the string representation of the TopicId with checksum
121+
in 'shard.realm.num-checksum' format.
122+
"""
123+
return format_to_string_with_checksum(
124+
self.shard,
125+
self.realm,
126+
self.num,
127+
client
128+
)

src/hiero_sdk_python/contract/contract_id.py

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@
22
Contract ID class.
33
"""
44

5-
from dataclasses import dataclass
6-
from typing import Optional
5+
import re
6+
from dataclasses import dataclass, field
7+
from typing import TYPE_CHECKING, Optional
78

89
from hiero_sdk_python.hapi.services import basic_types_pb2
10+
from hiero_sdk_python.utils.entity_id_helper import (
11+
parse_from_string,
12+
validate_checksum,
13+
format_to_string_with_checksum
14+
)
915

16+
if TYPE_CHECKING:
17+
from hiero_sdk_python.client.client import Client
18+
19+
EVM_ADDRESS_REGEX = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.([a-fA-F0-9]{40}$)")
1020

1121
@dataclass(frozen=True)
1222
class ContractId:
@@ -27,6 +37,7 @@ class ContractId:
2737
realm: int = 0
2838
contract: int = 0
2939
evm_address: Optional[bytes] = None
40+
checksum: str | None = field(default=None, init=False)
3041

3142
@classmethod
3243
def _from_proto(cls, contract_id_proto: basic_types_pb2.ContractID) -> "ContractId":
@@ -55,17 +66,47 @@ def from_string(cls, contract_id_str: str) -> "ContractId":
5566
"""
5667
Parses a string in the format 'shard.realm.contract' to create a ContractId instance.
5768
"""
58-
parts = contract_id_str.strip().split(".")
59-
if len(parts) != 3:
69+
if contract_id_str is None or not isinstance(contract_id_str, str):
6070
raise ValueError(
61-
"Invalid ContractId format. Expected 'shard.realm.contract'"
71+
f"Invalid contract ID string '{contract_id_str}'. "
72+
f"Expected format 'shard.realm.contract'."
73+
)
74+
75+
evm_address_match = EVM_ADDRESS_REGEX.match(contract_id_str)
76+
77+
if evm_address_match:
78+
shard, realm, evm_address = evm_address_match.groups()
79+
return cls(
80+
shard=int(shard),
81+
realm=int(realm),
82+
evm_address=bytes.fromhex(evm_address)
6283
)
63-
return cls(shard=int(parts[0]), realm=int(parts[1]), contract=int(parts[2]))
84+
85+
else:
86+
try:
87+
shard, realm, contract, checksum = parse_from_string(contract_id_str)
88+
89+
contract_id: ContractId = cls(
90+
shard=int(shard),
91+
realm=int(realm),
92+
contract=int(contract)
93+
)
94+
object.__setattr__(contract_id, "checksum", checksum)
95+
return contract_id
96+
97+
except Exception as e:
98+
raise ValueError(
99+
f"Invalid contract ID string '{contract_id_str}'. "
100+
f"Expected format 'shard.realm.contract'."
101+
) from e
64102

65103
def __str__(self):
66104
"""
67105
Returns the string representation of the ContractId in the format 'shard.realm.contract'.
68106
"""
107+
if self.evm_address is not None:
108+
return f"{self.shard}.{self.realm}.{self.evm_address.hex()}"
109+
69110
return f"{self.shard}.{self.realm}.{self.contract}"
70111

71112
def to_evm_address(self) -> str:
@@ -84,3 +125,28 @@ def to_evm_address(self) -> str:
84125
evm_bytes = shard_bytes + realm_bytes + contract_bytes
85126

86127
return evm_bytes.hex()
128+
129+
def validate_checksum(self, client: "Client") -> None:
130+
"""Validate the checksum for the contractId"""
131+
validate_checksum(
132+
self.shard,
133+
self.realm,
134+
self.contract,
135+
self.checksum,
136+
client,
137+
)
138+
139+
def to_string_with_checksum(self, client: "Client") -> str:
140+
"""
141+
Returns the string representation of the ContractId with checksum
142+
in 'shard.realm.contract-checksum' format.
143+
"""
144+
if self.evm_address is not None:
145+
raise ValueError("to_string_with_checksum cannot be applied to ContractId with evm_address")
146+
147+
return format_to_string_with_checksum(
148+
self.shard,
149+
self.realm,
150+
self.contract,
151+
client
152+
)

0 commit comments

Comments
 (0)