Skip to content

Commit 4844f31

Browse files
authored
feat: Added missing fields for AccountCreateTransaction (#779)
Signed-off-by: Manish Dait <daitmanish88@gmail.com>
1 parent 4f874cf commit 4844f31

File tree

10 files changed

+794
-28
lines changed

10 files changed

+794
-28
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
1717
- Add `examples/account_info.py` to demonstrate `AccountInfo` opeartions
1818
- Added `HbarUnit` class and Extend `Hbar` class to handle floating-point numbers
1919
- feat: Allow `PrivateKey` to be used for keys in `TopicCreateTransaction` for consistency.
20-
20+
- EvmAddress class
21+
- `alias`, `staked_account_id`, `staked_node_id` and `decline_staking_reward` fields to AccountCreateTransaction
22+
- `staked_account_id`, `staked_node_id` and `decline_staking_reward` fields to AccountInfo
2123

2224
### Changed
2325
- Refactored token-related example scripts (`token_delete.py`, `token_dissociate.py`, etc.) for improved readability and modularity. [#370]

src/hiero_sdk_python/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# Crypto
1616
from .crypto.private_key import PrivateKey
1717
from .crypto.public_key import PublicKey
18+
from .crypto.evm_address import EvmAddress
1819

1920
# Tokens
2021
from .tokens.token_create_transaction import TokenCreateTransaction
@@ -164,6 +165,7 @@
164165
# Crypto
165166
"PrivateKey",
166167
"PublicKey",
168+
"EvmAddress",
167169

168170
# Tokens
169171
"TokenCreateTransaction",

src/hiero_sdk_python/account/account_create_transaction.py

Lines changed: 170 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
"""
44

55
from typing import Optional, Union
6+
import warnings
67

8+
from hiero_sdk_python.account.account_id import AccountId
79
from hiero_sdk_python.channels import _Channel
10+
from hiero_sdk_python.crypto.evm_address import EvmAddress
811
from hiero_sdk_python.crypto.public_key import PublicKey
912
from hiero_sdk_python.Duration import Duration
1013
from hiero_sdk_python.executable import _Method
@@ -31,7 +34,11 @@ def __init__(
3134
receiver_signature_required: Optional[bool] = None,
3235
auto_renew_period: Optional[Duration] = AUTO_RENEW_PERIOD,
3336
memo: Optional[str] = None,
34-
max_automatic_token_associations: Optional[int] = 0
37+
max_automatic_token_associations: Optional[int] = 0,
38+
alias: Optional[EvmAddress] = None,
39+
staked_account_id: Optional[AccountId] = None,
40+
staked_node_id: Optional[int] = None,
41+
decline_staking_reward: Optional[bool] = False
3542
) -> None:
3643
"""
3744
Initializes a new AccountCreateTransaction instance with default values
@@ -43,6 +50,13 @@ def __init__(
4350
receiver_signature_required (Optional[bool]): Whether receiver signature is required.
4451
auto_renew_period (Duration): Auto-renew period in seconds (default is ~90 days).
4552
memo (Optional[str]): Memo for the account.
53+
max_automatic_token_associations (Optional[int]): The maximum number of tokens that
54+
can be auto-associated.
55+
alias (Optional[EvmAddress]): The 20-byte EVM address to be used as the account's alias.
56+
staked_account_id (Optional[AccountId]): The account to which this account will stake.
57+
staked_node_id (Optional[int]): ID of the node this account is staked to.
58+
decline_staking_reward (Optional[bool]): If true, the account declines receiving a
59+
staking reward (default is False).
4660
"""
4761
super().__init__()
4862
self.key: Optional[PublicKey] = key
@@ -52,6 +66,10 @@ def __init__(
5266
self.account_memo: Optional[str] = memo
5367
self.max_automatic_token_associations: Optional[int] = max_automatic_token_associations
5468
self._default_transaction_fee = DEFAULT_TRANSACTION_FEE
69+
self.alias: Optional[EvmAddress] = alias
70+
self.staked_account_id: Optional[AccountId] = staked_account_id
71+
self.staked_node_id: Optional[int] = staked_node_id
72+
self.decline_staking_reward = decline_staking_reward
5573

5674
def set_key(self, key: PublicKey) -> "AccountCreateTransaction":
5775
"""
@@ -63,8 +81,50 @@ def set_key(self, key: PublicKey) -> "AccountCreateTransaction":
6381
Returns:
6482
AccountCreateTransaction: The current transaction instance for method chaining.
6583
"""
84+
warnings.warn(
85+
"The 'set_key' method is deprecated, Use `set_key_without_alias` instead.",
86+
DeprecationWarning,
87+
)
88+
self._require_not_frozen()
89+
self.key = key
90+
return self
91+
92+
def set_key_without_alias(self, key: PublicKey) -> "AccountCreateTransaction":
93+
"""
94+
Sets the public key for the new account without alias.
95+
96+
Args:
97+
key (PublicKey): The public key to assign to the account.
98+
99+
Returns:
100+
AccountCreateTransaction: The current transaction instance for method chaining.
101+
"""
102+
self._require_not_frozen()
103+
self.key = key
104+
return self
105+
106+
def set_key_with_alias(
107+
self,
108+
key: PublicKey,
109+
ecdsa_key: Optional[PublicKey]=None
110+
) -> "AccountCreateTransaction":
111+
"""
112+
Sets the public key for the new account and assigns an alias derived from an ECDSA key.
113+
114+
If `ecdsa_key` is provided, its corresponding EVM address will be used as the account alias.
115+
Otherwise, the alias will be derived from the provided `key`.
116+
117+
Args:
118+
key (PublicKey): The public key to assign to the account.
119+
ecdsa_key (Optional[PublicKey]): An optional ECDSA public key used
120+
to derive the account alias.
121+
122+
Returns:
123+
AccountCreateTransaction: The current transaction instance to allow method chaining.
124+
"""
66125
self._require_not_frozen()
67126
self.key = key
127+
self.alias = ecdsa_key.to_evm_address() if ecdsa_key is not None else key.to_evm_address()
68128
return self
69129

70130
def set_initial_balance(self, balance: Union[Hbar, int]) -> "AccountCreateTransaction":
@@ -133,15 +193,111 @@ def set_account_memo(self, memo: str) -> "AccountCreateTransaction":
133193
return self
134194

135195
def set_max_automatic_token_associations(self, max_assoc: int) -> "AccountCreateTransaction":
136-
"""Sets the maximum number of automatic token associations for the account."""
196+
"""
197+
Sets the maximum number of automatic token associations for the account.
198+
199+
Args:
200+
max_assoc (int): The maximum number of automatic
201+
token associations to allow (default 0).
202+
203+
Returns:
204+
AccountCreateTransaction: The current transaction instance for method chaining.
205+
"""
137206
self._require_not_frozen()
138207
# FIX
139208
if max_assoc < -1:
140209
raise ValueError("max_automatic_token_associations must be -1 (unlimited) or a non-negative integer.")
141210
self.max_automatic_token_associations = max_assoc
142211
return self
143212

144-
def _build_proto_body(self):
213+
def set_alias(self, alias_evm_address: Union[EvmAddress, str]) -> "AccountCreateTransaction":
214+
"""
215+
Sets the EVM Address alias for the account.
216+
217+
Args:
218+
alias_evm_address (Union[EvmAddress, str]): The 20-byte EVM address to
219+
be used as the account's alias.
220+
221+
Returns:
222+
AccountCreateTransaction: The current transaction instance for method chaining.
223+
"""
224+
self._require_not_frozen()
225+
if isinstance(alias_evm_address, str):
226+
if len(alias_evm_address.removeprefix("0x")) == 40:
227+
self.alias = EvmAddress.from_string(alias_evm_address)
228+
else:
229+
raise ValueError("alias_evm_address must be a valid 20-byte EVM address")
230+
231+
elif isinstance(alias_evm_address, EvmAddress):
232+
self.alias = alias_evm_address
233+
234+
else:
235+
raise TypeError("alias_evm_address must be of type str or EvmAddress")
236+
237+
return self
238+
239+
def set_staked_account_id(
240+
self,
241+
account_id: Union[AccountId, str]
242+
) -> "AccountCreateTransaction":
243+
"""
244+
Sets the staked account id for the account.
245+
246+
Args:
247+
account_id (Union[AccountId, str]): The account to which this account will stake.
248+
249+
Returns:
250+
AccountCreateTransaction: The current transaction instance for method chaining.
251+
"""
252+
self._require_not_frozen()
253+
if isinstance(account_id, str):
254+
self.staked_account_id = AccountId.from_string(account_id)
255+
elif isinstance(account_id, AccountId):
256+
self.staked_account_id = account_id
257+
else:
258+
raise TypeError("account_id must be of type str or AccountId")
259+
260+
return self
261+
262+
def set_staked_node_id(self, node_id: int) -> "AccountCreateTransaction":
263+
"""
264+
Sets the staked node id for the account.
265+
266+
Args:
267+
node_id (int): The node to which this account will stake.
268+
269+
Returns:
270+
AccountCreateTransaction: The current transaction instance for method chaining.
271+
"""
272+
self._require_not_frozen()
273+
if not isinstance(node_id, int):
274+
raise TypeError("node_id must be of type int")
275+
276+
self.staked_node_id = node_id
277+
return self
278+
279+
def set_decline_staking_reward(
280+
self,
281+
decline_staking_reward: bool
282+
) -> "AccountCreateTransaction":
283+
"""
284+
Sets the decline staking reward for the account.
285+
286+
Args:
287+
decline_staking_reward (bool): If true, the account declines
288+
receiving a staking reward (default is False)
289+
290+
Returns:
291+
AccountCreateTransaction: The current transaction instance for method chaining.
292+
"""
293+
self._require_not_frozen()
294+
if not isinstance(decline_staking_reward, bool):
295+
raise TypeError("decline_staking_reward must be of type bool")
296+
297+
self.decline_staking_reward = decline_staking_reward
298+
return self
299+
300+
def _build_proto_body(self) -> crypto_create_pb2.CryptoCreateTransactionBody:
145301
"""
146302
Returns the protobuf body for the account create transaction.
147303
@@ -162,15 +318,24 @@ def _build_proto_body(self):
162318
else:
163319
raise TypeError("initial_balance must be Hbar or int (tinybars).")
164320

165-
return crypto_create_pb2.CryptoCreateTransactionBody(
321+
proto_body = crypto_create_pb2.CryptoCreateTransactionBody(
166322
key=self.key._to_proto(),
167323
initialBalance=initial_balance_tinybars,
168324
receiverSigRequired=self.receiver_signature_required,
169325
autoRenewPeriod=duration_pb2.Duration(seconds=self.auto_renew_period.seconds),
170326
memo=self.account_memo,
171-
max_automatic_token_associations=self.max_automatic_token_associations
327+
max_automatic_token_associations=self.max_automatic_token_associations,
328+
alias=self.alias.address_bytes if self.alias else None,
329+
decline_reward=self.decline_staking_reward
172330
)
173331

332+
if self.staked_account_id:
333+
proto_body.staked_account_id.CopyFrom(self.staked_account_id._to_proto())
334+
elif self.staked_node_id:
335+
proto_body.staked_node_id = self.staked_node_id
336+
337+
return proto_body
338+
174339
def build_transaction_body(self) -> transaction_pb2.TransactionBody:
175340
"""
176341
Builds and returns the protobuf transaction body for account creation.

src/hiero_sdk_python/account/account_info.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from hiero_sdk_python.account.account_id import AccountId
1010
from hiero_sdk_python.crypto.public_key import PublicKey
1111
from hiero_sdk_python.Duration import Duration
12+
from hiero_sdk_python.hapi.services.basic_types_pb2 import StakingInfo
1213
from hiero_sdk_python.hapi.services.crypto_get_info_pb2 import CryptoGetInfoResponse
1314
from hiero_sdk_python.hbar import Hbar
1415
from hiero_sdk_python.timestamp import Timestamp
@@ -37,6 +38,9 @@ class AccountInfo:
3738
associated with this account.
3839
account_memo (Optional[str]): The memo associated with this account.
3940
owned_nfts (Optional[int]): The number of NFTs owned by this account.
41+
staked_account_id (Optional[AccountId]): The account to which this account is staked.
42+
staked_node_id (Optional[int]): The node to which this account is staked.
43+
decline_staking_reward (bool): Whether this account declines receiving staking rewards.
4044
"""
4145

4246
account_id: Optional[AccountId] = None
@@ -52,6 +56,9 @@ class AccountInfo:
5256
account_memo: Optional[str] = None
5357
owned_nfts: Optional[int] = None
5458
max_automatic_token_associations: Optional[int] = None
59+
staked_account_id: Optional[AccountId] = None
60+
staked_node_id: Optional[int] = None
61+
decline_staking_reward: Optional[bool] = None
5562

5663
@classmethod
5764
def _from_proto(cls, proto: CryptoGetInfoResponse.AccountInfo) -> "AccountInfo":
@@ -72,7 +79,7 @@ def _from_proto(cls, proto: CryptoGetInfoResponse.AccountInfo) -> "AccountInfo":
7279
if proto is None:
7380
raise ValueError("Account info proto is None")
7481

75-
return cls(
82+
account_info: "AccountInfo" = cls(
7683
account_id=AccountId._from_proto(proto.accountID) if proto.accountID else None,
7784
contract_account_id=proto.contractAccountID,
7885
is_deleted=proto.deleted,
@@ -95,6 +102,21 @@ def _from_proto(cls, proto: CryptoGetInfoResponse.AccountInfo) -> "AccountInfo":
95102
max_automatic_token_associations=proto.max_automatic_token_associations,
96103
)
97104

105+
staking_info = proto.staking_info if proto.HasField('staking_info') else None
106+
107+
if staking_info:
108+
account_info.staked_account_id = (
109+
AccountId._from_proto(staking_info.staked_account_id)
110+
if staking_info.HasField('staked_account_id') else None
111+
)
112+
account_info.staked_node_id = (
113+
staking_info.staked_node_id
114+
if staking_info.HasField('staked_node_id') else None
115+
)
116+
account_info.decline_staking_reward = staking_info.decline_reward
117+
118+
return account_info
119+
98120
def _to_proto(self) -> CryptoGetInfoResponse.AccountInfo:
99121
"""Converts this AccountInfo object to its protobuf representation.
100122
Serializes this `AccountInfo` instance into a
@@ -125,4 +147,9 @@ def _to_proto(self) -> CryptoGetInfoResponse.AccountInfo:
125147
memo=self.account_memo,
126148
ownedNfts=self.owned_nfts,
127149
max_automatic_token_associations=self.max_automatic_token_associations,
150+
staking_info=StakingInfo(
151+
staked_account_id=self.staked_account_id._to_proto() if self.staked_account_id else None,
152+
staked_node_id=self.staked_node_id if self.staked_node_id else None,
153+
decline_reward=self.decline_staking_reward
154+
),
128155
)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
class EvmAddress:
2+
"""
3+
Represents a 20-byte EVM address derived from the rightmost 20 bytes of
4+
32 byte Keccak-256 hash of an ECDSA public key.
5+
"""
6+
def __init__(self, address_bytes: bytes) -> None:
7+
"""
8+
Initialize an EvmAddress instance from bytes.
9+
10+
Args:
11+
address_bytes (bytes): A 20-byte sequence representing the EVM address.
12+
"""
13+
if len(address_bytes) != 20:
14+
raise ValueError("EvmAddress must be exactly 20 bytes long.")
15+
16+
self.address_bytes: bytes = address_bytes
17+
18+
@classmethod
19+
def from_string(cls, evm_address: str) -> "EvmAddress":
20+
"""
21+
Create an EvmAddress from a hex string (with or without '0x' prefix).
22+
"""
23+
if not isinstance(evm_address, str):
24+
raise TypeError("evm_address must be a of type string.")
25+
26+
address = evm_address[2:] if evm_address.startswith('0x') else evm_address
27+
28+
if len(address) == 40:
29+
return cls(address_bytes=bytes.fromhex(address))
30+
31+
raise ValueError("Invalid hex string for evm_address.")
32+
33+
@classmethod
34+
def from_bytes(cls, address_bytes: "bytes") -> "EvmAddress":
35+
"""Create an EvmAddress from raw bytes."""
36+
return cls(address_bytes)
37+
38+
def to_string(self) -> str:
39+
"""Return the EVM address as a hex string"""
40+
return bytes.hex(self.address_bytes)
41+
42+
def __str__(self) -> str:
43+
return self.to_string()
44+
45+
def __repr__(self) -> str:
46+
return f"<EvmAddress hex={self.to_string()}>"
47+
48+
def __eq__(self, obj: object) -> bool:
49+
if not isinstance(obj, EvmAddress):
50+
return False
51+
52+
return self.address_bytes == obj.address_bytes
53+
54+
def __hash__(self) -> int:
55+
return hash(self.address_bytes)

0 commit comments

Comments
 (0)