diff --git a/CHANGELOG.md b/CHANGELOG.md index e00cee42f..199d72917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Added `docs/sdk_developers/training/transaction_lifecycle.md` to explain the typical lifecycle of executing a transaction using the Hedera Python SDK. - Add inactivity bot workflow to unassign stale issue assignees (#952) - Made custom fraction fee end to end +- feat: AccountCreateTransaction now supports both PrivateKey and PublicKey [#939](https://github.com/hiero-ledger/hiero-sdk-python/issues/939) - Added Acceptance Criteria section to Good First Issue template for better contributor guidance (#997) - Added __str__() to CustomRoyaltyFee and updated examples and tests accordingly (#986) diff --git a/examples/account/account_create_transaction_create_with_alias.py b/examples/account/account_create_transaction_create_with_alias.py new file mode 100644 index 000000000..9d8e6cade --- /dev/null +++ b/examples/account/account_create_transaction_create_with_alias.py @@ -0,0 +1,132 @@ +""" +Example: Create an account using a separate ECDSA key for the EVM alias. + +This demonstrates: +- Using a "main" key for the account +- Using a separate ECDSA public key as the EVM alias +- The need to sign the transaction with the alias private key + +Usage: +- uv run -m examples.account.account_create_transaction_create_with_alias +- python -m examples.account.account_create_transaction_create_with_alias +(we use -m because we use the util `info_to_dict`) +""" + +import os +import sys +import json +from dotenv import load_dotenv + +from examples.utils import info_to_dict + +from hiero_sdk_python import ( + Client, + PrivateKey, + AccountCreateTransaction, + AccountInfoQuery, + Network, + AccountId, + Hbar, +) + +load_dotenv() +network_name = os.getenv("NETWORK", "testnet").lower() + + +def setup_client(): + """Setup Client.""" + network = Network(network_name) + print(f"Connecting to Hedera {network_name} network!") + client = Client(network) + + try: + operator_id = AccountId.from_string(os.getenv("OPERATOR_ID", "")) + operator_key = PrivateKey.from_string(os.getenv("OPERATOR_KEY", "")) + client.set_operator(operator_id, operator_key) + print(f"Client set up with operator id {client.operator_account_id}") + return client + except Exception: + print("Error: Please check OPERATOR_ID and OPERATOR_KEY in your .env file.") + sys.exit(1) + +def create_account_with_separate_ecdsa_alias(client: Client) -> None: + """Create an account whose alias comes from a separate ECDSA key.""" + try: + print("\nSTEP 1: Generating main account key and separate ECDSA alias key...") + + # Main account key (can be any key type, here ed25519) + main_private_key = PrivateKey.generate() + main_public_key = main_private_key.public_key() + + # Separate ECDSA key used only for the EVM alias + alias_private_key = PrivateKey.generate("ecdsa") + alias_public_key = alias_private_key.public_key() + alias_evm_address = alias_public_key.to_evm_address() + + if alias_evm_address is None: + print("❌ Error: Failed to generate EVM address from alias ECDSA key.") + sys.exit(1) + + print(f"✅ Main account public key: {main_public_key}") + print(f"✅ Alias ECDSA public key: {alias_public_key}") + print(f"✅ Alias EVM address: {alias_evm_address}") + + print("\nSTEP 2: Creating the account with the EVM alias from the ECDSA key...") + + # Use the helper that accepts both the main key and the ECDSA alias key + transaction = ( + AccountCreateTransaction( + initial_balance=Hbar(5), + memo="Account with separate ECDSA alias", + ) + .set_key_with_alias(main_private_key, alias_public_key) + ) + + # Freeze and sign: + # - operator key signs as payer (via client) + # - alias private key MUST sign to authorize the alias + transaction = ( + transaction.freeze_with(client) + .sign(alias_private_key) + ) + + response = transaction.execute(client) + new_account_id = response.account_id + + if new_account_id is None: + raise RuntimeError( + "AccountID not found in receipt. Account may not have been created." + ) + + print(f"✅ Account created with ID: {new_account_id}\n") + + account_info = ( + AccountInfoQuery() + .set_account_id(new_account_id) + .execute(client) + ) + + out = info_to_dict(account_info) + print("Account Info:") + print(json.dumps(out, indent=2) + "\n") + + if account_info.contract_account_id is not None: + print( + f"✅ Contract Account ID (EVM alias on-chain): " + f"{account_info.contract_account_id}" + ) + else: + print("❌ Error: Contract Account ID (alias) does not exist.") + + except Exception as error: + print(f"❌ Error: {error}") + sys.exit(1) + +def main(): + """Main entry point.""" + client = setup_client() + create_account_with_separate_ecdsa_alias(client) + + +if __name__ == "__main__": + main() diff --git a/examples/account/account_create_transaction_evm_alias.py b/examples/account/account_create_transaction_evm_alias.py index 2b8a4eab6..b5bf4d1f8 100644 --- a/examples/account/account_create_transaction_evm_alias.py +++ b/examples/account/account_create_transaction_evm_alias.py @@ -1,5 +1,5 @@ -# uv run examples/account/account_create_transaction_evm_alias.py -# python examples/account/account_create_transaction_evm_alias.py +# uv run -m examples.account.account_create_transaction_evm_alias +# python -m examples.account.account_create_transaction_evm_alias """ Example: Create an account using an EVM-style alias (evm_address). """ @@ -9,6 +9,8 @@ import json from dotenv import load_dotenv +from examples.utils import info_to_dict + from hiero_sdk_python import ( Client, PrivateKey, @@ -40,22 +42,6 @@ def setup_client(): print("Error: Please check OPERATOR_ID and OPERATOR_KEY in your .env file.") sys.exit(1) -def info_to_dict(info): - """Convert AccountInfo to dictionary for easy printing.""" - out = {} - for name in dir(info): - if name.startswith("_"): - continue - try: - val = getattr(info, name) - except Exception as error: - out[name] = f"Error retrieving value: {error}" - continue - if callable(val): - continue - out[name] = str(val) - return out - def create_account_with_alias(client): """Create an account with an alias transaction.""" try: diff --git a/examples/account/account_create_transaction_with_fallback_alias.py b/examples/account/account_create_transaction_with_fallback_alias.py new file mode 100644 index 000000000..38cb74c01 --- /dev/null +++ b/examples/account/account_create_transaction_with_fallback_alias.py @@ -0,0 +1,117 @@ +""" +Example: Create an account where the EVM alias is derived from the main ECDSA key. + +This demonstrates: +- Passing only an ECDSA PrivateKey to `set_key_with_alias` +- The alias being derived from the main key's EVM address (fallback behaviour) + +Usage: +- uv run -m examples.account.account_create_transaction_with_fallback_alias +- python -m examples.account.account_create_transaction_with_fallback_alias +(we use -m because we use the util `info_to_dict`) +""" + +import os +import sys +import json +from dotenv import load_dotenv + +from examples.utils import info_to_dict + +from hiero_sdk_python import ( + Client, + PrivateKey, + AccountCreateTransaction, + AccountInfoQuery, + Network, + AccountId, + Hbar, +) + +load_dotenv() +network_name = os.getenv("NETWORK", "testnet").lower() + + +def setup_client(): + """Setup Client.""" + network = Network(network_name) + print(f"Connecting to Hedera {network_name} network!") + client = Client(network) + + try: + operator_id = AccountId.from_string(os.getenv("OPERATOR_ID", "")) + operator_key = PrivateKey.from_string(os.getenv("OPERATOR_KEY", "")) + client.set_operator(operator_id, operator_key) + print(f"Client set up with operator id {client.operator_account_id}") + return client + except Exception: + print("Error: Please check OPERATOR_ID and OPERATOR_KEY in your .env file.") + sys.exit(1) + +def create_account_with_fallback_alias(client: Client) -> None: + """Create an account whose alias is derived from the main ECDSA key.""" + try: + print("\nSTEP 1: Generating a single ECDSA key pair for the account...") + account_private_key = PrivateKey.generate("ecdsa") + account_public_key = account_private_key.public_key() + evm_address = account_public_key.to_evm_address() + + if evm_address is None: + print("❌ Error: Failed to generate EVM address from ECDSA public key.") + sys.exit(1) + + print(f"✅ Account ECDSA public key: {account_public_key}") + print(f"✅ Derived EVM address: {evm_address}") + + print("\nSTEP 2: Creating the account using the fallback alias behaviour...") + transaction = ( + AccountCreateTransaction( + initial_balance=Hbar(5), + memo="Account with alias derived from main ECDSA key", + ) + # Fallback path: only one ECDSA key is provided + .set_key_with_alias(account_private_key) + ) + + # Freeze & sign with the account key as well + transaction = ( + transaction.freeze_with(client) + .sign(account_private_key) + ) + + response = transaction.execute(client) + new_account_id = response.account_id + + if new_account_id is None: + raise RuntimeError( + "AccountID not found in receipt. Account may not have been created." + ) + + print(f"✅ Account created with ID: {new_account_id}\n") + + account_info = ( + AccountInfoQuery() + .set_account_id(new_account_id) + .execute(client) + ) + + out = info_to_dict(account_info) + print("Account Info:") + print(json.dumps(out, indent=2) + "\n") + + print( + "✅ contract_account_id (EVM alias on-chain): " + f"{account_info.contract_account_id}" + ) + + except Exception as error: + print(f"❌ Error: {error}") + sys.exit(1) + +def main(): + """Main entry point.""" + client = setup_client() + create_account_with_fallback_alias(client) + +if __name__ == "__main__": + main() diff --git a/examples/account/account_create_transaction_without_alias.py b/examples/account/account_create_transaction_without_alias.py new file mode 100644 index 000000000..99f1f44f2 --- /dev/null +++ b/examples/account/account_create_transaction_without_alias.py @@ -0,0 +1,110 @@ +""" +Example: Create an account without using any alias. + +This demonstrates: +- Using `set_key_without_alias` so that no EVM alias is set +- The resulting `contract_account_id` being the zero-padded value + +Usage: +- uv run -m examples.account.account_create_transaction_without_alias +- python -m examples.account.account_create_transaction_without_alias +(we use -m because we use the util `info_to_dict`) +""" + +import os +import sys +import json +from dotenv import load_dotenv + +from examples.utils import info_to_dict + +from hiero_sdk_python import ( + Client, + PrivateKey, + AccountCreateTransaction, + AccountInfoQuery, + Network, + AccountId, + Hbar, +) + +load_dotenv() +network_name = os.getenv("NETWORK", "testnet").lower() + + +def setup_client(): + """Setup Client.""" + network = Network(network_name) + print(f"Connecting to Hedera {network_name} network!") + client = Client(network) + + try: + operator_id = AccountId.from_string(os.getenv("OPERATOR_ID", "")) + operator_key = PrivateKey.from_string(os.getenv("OPERATOR_KEY", "")) + client.set_operator(operator_id, operator_key) + print(f"Client set up with operator id {client.operator_account_id}") + return client + except Exception: + print("Error: Please check OPERATOR_ID and OPERATOR_KEY in your .env file.") + sys.exit(1) + +def create_account_without_alias(client: Client) -> None: + """Create an account explicitly without an alias.""" + try: + print("\nSTEP 1: Generating a key pair for the account (no alias)...") + account_private_key = PrivateKey.generate() + account_public_key = account_private_key.public_key() + + print(f"✅ Account public key (no alias): {account_public_key}") + + print("\nSTEP 2: Creating the account without setting any alias...") + + transaction = ( + AccountCreateTransaction( + initial_balance=Hbar(5), + memo="Account created without alias", + ) + .set_key_without_alias(account_private_key) + ) + + transaction = ( + transaction.freeze_with(client) + .sign(account_private_key) + ) + + response = transaction.execute(client) + new_account_id = response.account_id + + if new_account_id is None: + raise RuntimeError( + "AccountID not found in receipt. Account may not have been created." + ) + + print(f"✅ Account created with ID: {new_account_id}\n") + + account_info = ( + AccountInfoQuery() + .set_account_id(new_account_id) + .execute(client) + ) + + out = info_to_dict(account_info) + print("Account Info:") + print(json.dumps(out, indent=2) + "\n") + + print( + "✅ contract_account_id (no alias, zero-padded): " + f"{account_info.contract_account_id}" + ) + + except Exception as error: + print(f"❌ Error: {error}") + sys.exit(1) + +def main(): + """Main entry point.""" + client = setup_client() + create_account_without_alias(client) + +if __name__ == "__main__": + main() diff --git a/examples/transaction/transaction_to_bytes.py b/examples/transaction/transaction_to_bytes.py index 47e25218f..4831196a2 100644 --- a/examples/transaction/transaction_to_bytes.py +++ b/examples/transaction/transaction_to_bytes.py @@ -1,15 +1,11 @@ """ -Example demonstrating transaction byte serialization and deserialization. +Refactored example demonstrating transaction byte serialization and deserialization. This example shows how to: -- Freeze a transaction -- Serialize to bytes (for storage, transmission, or external signing) +- Create and freeze a transaction +- Serialize to bytes (for storage, transmission, or signing) - Deserialize from bytes -- Sign after deserialization - -Run with: - uv run examples/transaction/transaction_to_bytes.py - python examples/transaction/transaction_to_bytes.py +- Sign a deserialized transaction """ import os @@ -26,77 +22,89 @@ ) load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +NETWORK = os.getenv("NETWORK", "testnet").lower() +OPERATOR_ID = os.getenv("OPERATOR_ID", "") +OPERATOR_KEY = os.getenv("OPERATOR_KEY", "") -def setup_client(): - """Initialize and set up the client with operator account""" - network = Network(network_name) - print(f"Connecting to Hedera {network_name} network!") - client = Client(network) +def setup_client() -> Client: + """Initialize the client using operator credentials from .env.""" try: - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + network = Network(NETWORK) + client = Client(network) + + operator_id = AccountId.from_string(OPERATOR_ID) + operator_key = PrivateKey.from_string(OPERATOR_KEY) + client.set_operator(operator_id, operator_key) - print(f"Client set up with operator id {client.operator_account_id}") - return client, operator_id, operator_key - - except (TypeError, ValueError): - print("❌ Error: Creating client, Please check your .env file") - sys.exit(1) + print(f"Connected to Hedera {NETWORK} as {operator_id}") + return client -def transaction_bytes_example(): - """ - Demonstrates transaction serialization and deserialization workflow. - """ - client, operator_id, operator_key = setup_client() + except Exception: + print("❌ Error: Could not initialize client. Check your .env file.") + sys.exit(1) - receiver_id = AccountId.from_string("0.0.3") # Node account - # Step 1: Create and freeze transaction - print("\nSTEP 1: Creating and freezing transaction...") - transaction = ( +def create_and_freeze_transaction(client: Client, sender: AccountId, receiver: AccountId): + """Create and freeze a simple HBAR transfer transaction.""" + tx = ( TransferTransaction() - .add_hbar_transfer(operator_id, -100_000_000) # -1 HBAR - .add_hbar_transfer(receiver_id, 100_000_000) # +1 HBAR + .add_hbar_transfer(sender, -100_000_000) # -1 HBAR + .add_hbar_transfer(receiver, 100_000_000) # +1 HBAR .set_transaction_memo("Transaction bytes example") ) - transaction.freeze_with(client) - print(f"✅ Transaction frozen with ID: {transaction.transaction_id}") - - # Step 2: Serialize to bytes - print("\nSTEP 2: Serializing transaction to bytes...") - transaction_bytes = transaction.to_bytes() - print(f"✅ Transaction serialized: {len(transaction_bytes)} bytes") - print(f" First 40 bytes (hex): {transaction_bytes[:40].hex()}") - - # Step 3: Deserialize from bytes - print("\nSTEP 3: Deserializing transaction from bytes...") - restored_transaction = Transaction.from_bytes(transaction_bytes) - print(f"✅ Transaction restored from bytes") - print(f" Transaction ID: {restored_transaction.transaction_id}") - print(f" Node ID: {restored_transaction.node_account_id}") - print(f" Memo: {restored_transaction.memo}") - - # Step 4: Sign the restored transaction - print("\nSTEP 4: Signing the restored transaction...") - restored_transaction.sign(operator_key) - print(f"✅ Transaction signed") - - # Step 5: Verify round-trip produces identical bytes - print("\nSTEP 5: Verifying serialization...") - original_signed = transaction.sign(operator_key).to_bytes() - final_bytes = restored_transaction.to_bytes() - print(f"✅ Round-trip successful") - - print("\n✅ Example completed successfully!") - print("\nUse cases for transaction bytes:") - print(" • Store transactions in a database") - print(" • Send transactions to external signing services (HSM, hardware wallet)") - print(" • Transmit transactions over a network") - print(" • Create offline signing workflows") + + tx.freeze_with(client) + return tx + + +def serialize_transaction(transaction: Transaction) -> bytes: + """Serialize transaction to bytes.""" + return transaction.to_bytes() + + +def deserialize_transaction(bytes_data: bytes) -> Transaction: + """Restore a transaction from its byte representation.""" + return Transaction.from_bytes(bytes_data) + + +def main(): + client = setup_client() + operator_id = client.operator_account_id + operator_key = client.operator_private_key + + receiver_id = AccountId.from_string("0.0.3") + + print("\nSTEP 1 — Creating and freezing transaction...") + tx = create_and_freeze_transaction(client, operator_id, receiver_id) + print(f"Transaction ID: {tx.transaction_id}") + + print("\nSTEP 2 — Serializing transaction...") + tx_bytes = serialize_transaction(tx) + print(f"Serialized size: {len(tx_bytes)} bytes") + print(f"Preview (hex): {tx_bytes[:40].hex()}") + + print("\nSTEP 3 — Deserializing transaction...") + restored_tx = deserialize_transaction(tx_bytes) + print(f"Restored ID: {restored_tx.transaction_id}") + print(f"Memo: {restored_tx.memo}") + + print("\nSTEP 4 — Signing restored transaction...") + restored_tx.sign(operator_key) + print("Signed successfully.") + + print("\nSTEP 5 — Verifying round-trip...") + original_signed_bytes = tx.sign(operator_key).to_bytes() + restored_signed_bytes = restored_tx.to_bytes() + + if original_signed_bytes == restored_signed_bytes: + print("✅ Round-trip serialization successful.") + else: + print("❌ Round-trip mismatch!") + + print("\nExample completed.") if __name__ == "__main__": - transaction_bytes_example() + main() diff --git a/examples/utils.py b/examples/utils.py new file mode 100644 index 000000000..4b35f83d7 --- /dev/null +++ b/examples/utils.py @@ -0,0 +1,23 @@ +def info_to_dict(info): + """ + Convert an AccountInfo object into a dictionary of serializable strings. + Useful for pretty-printing information in examples. + """ + out = {} + + for name in dir(info): + if name.startswith("_"): + continue + + try: + val = getattr(info, name) + except Exception as error: + out[name] = f"Error retrieving value: {error}" + continue + + if callable(val): + continue + + out[name] = str(val) + + return out \ No newline at end of file diff --git a/src/hiero_sdk_python/account/account_create_transaction.py b/src/hiero_sdk_python/account/account_create_transaction.py index 6dc1fb1b5..43c33f4f9 100644 --- a/src/hiero_sdk_python/account/account_create_transaction.py +++ b/src/hiero_sdk_python/account/account_create_transaction.py @@ -9,14 +9,16 @@ from hiero_sdk_python.channels import _Channel from hiero_sdk_python.crypto.evm_address import EvmAddress from hiero_sdk_python.crypto.public_key import PublicKey +from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.Duration import Duration from hiero_sdk_python.executable import _Method -from hiero_sdk_python.hapi.services import crypto_create_pb2, duration_pb2, transaction_pb2 +from hiero_sdk_python.hapi.services import crypto_create_pb2, duration_pb2, transaction_pb2, basic_types_pb2 from hiero_sdk_python.hapi.services.schedulable_transaction_body_pb2 import ( SchedulableTransactionBody, ) from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.transaction.transaction import Transaction +from hiero_sdk_python.utils.key_utils import Key, key_to_proto AUTO_RENEW_PERIOD = Duration(7890000) # around 90 days in seconds DEFAULT_TRANSACTION_FEE = Hbar(3).to_tinybars() # 3 Hbars @@ -29,7 +31,7 @@ class AccountCreateTransaction(Transaction): def __init__( self, - key: Optional[PublicKey] = None, + key: Optional[Key] = None, initial_balance: Union[Hbar, int] = 0, receiver_signature_required: Optional[bool] = None, auto_renew_period: Optional[Duration] = AUTO_RENEW_PERIOD, @@ -59,7 +61,7 @@ def __init__( staking reward (default is False). """ super().__init__() - self.key: Optional[PublicKey] = key + self.key: Optional[Key] = key self.initial_balance: Union[Hbar, int] = initial_balance self.receiver_signature_required: Optional[bool] = receiver_signature_required self.auto_renew_period: Optional[Duration] = auto_renew_period @@ -71,12 +73,12 @@ def __init__( self.staked_node_id: Optional[int] = staked_node_id self.decline_staking_reward = decline_staking_reward - def set_key(self, key: PublicKey) -> "AccountCreateTransaction": + def set_key(self, key: Key) -> "AccountCreateTransaction": """ - Sets the public key for the new account. + Sets the key for the new account (accepts both PrivateKey or PublicKey). Args: - key (PublicKey): The public key to assign to the account. + key (Key): The key to assign to the account. Returns: AccountCreateTransaction: The current transaction instance for method chaining. @@ -89,33 +91,34 @@ def set_key(self, key: PublicKey) -> "AccountCreateTransaction": self.key = key return self - def set_key_without_alias(self, key: PublicKey) -> "AccountCreateTransaction": + def set_key_without_alias(self, key: Key) -> "AccountCreateTransaction": """ - Sets the public key for the new account without alias. + Sets the key for the new account without alias (accepts both PrivateKey or PublicKey). Args: - key (PublicKey): The public key to assign to the account. + key (Key): The key to assign to the account. Returns: AccountCreateTransaction: The current transaction instance for method chaining. """ self._require_not_frozen() self.key = key + self.alias = None return self def set_key_with_alias( self, - key: PublicKey, - ecdsa_key: Optional[PublicKey]=None + key: Key, + ecdsa_key: Optional[Key]=None ) -> "AccountCreateTransaction": """ - Sets the public key for the new account and assigns an alias derived from an ECDSA key. + Sets the key for the new account and assigns an alias derived from an ECDSA key. If `ecdsa_key` is provided, its corresponding EVM address will be used as the account alias. Otherwise, the alias will be derived from the provided `key`. Args: - key (PublicKey): The public key to assign to the account. + key (Key): The key to assign to the account (PrivateKey or PublicKey). ecdsa_key (Optional[PublicKey]): An optional ECDSA public key used to derive the account alias. @@ -124,7 +127,11 @@ def set_key_with_alias( """ self._require_not_frozen() self.key = key - self.alias = ecdsa_key.to_evm_address() if ecdsa_key is not None else key.to_evm_address() + evm_source_key: Key = ecdsa_key if ecdsa_key is not None else key + if isinstance(evm_source_key, PrivateKey): + evm_source_key = evm_source_key.public_key() + + self.alias = evm_source_key.to_evm_address() return self def set_initial_balance(self, balance: Union[Hbar, int]) -> "AccountCreateTransaction": @@ -318,8 +325,10 @@ def _build_proto_body(self) -> crypto_create_pb2.CryptoCreateTransactionBody: else: raise TypeError("initial_balance must be Hbar or int (tinybars).") + proto_key = key_to_proto(self.key) + proto_body = crypto_create_pb2.CryptoCreateTransactionBody( - key=self.key._to_proto(), + key=proto_key, initialBalance=initial_balance_tinybars, receiverSigRequired=self.receiver_signature_required, autoRenewPeriod=duration_pb2.Duration(seconds=self.auto_renew_period.seconds), diff --git a/tests/integration/account_create_transaction_e2e_test.py b/tests/integration/account_create_transaction_e2e_test.py index 24e1b4e3d..865337c66 100644 --- a/tests/integration/account_create_transaction_e2e_test.py +++ b/tests/integration/account_create_transaction_e2e_test.py @@ -267,3 +267,103 @@ def test_create_account_with_decline_reward(env): assert info.account_id == account_id assert info.staked_account_id == env.operator_id assert info.decline_staking_reward is True + +def test_integration_account_create_transaction_can_execute_with_private_key(env): + """Test AccountCreateTransaction can be executed when key is a PrivateKey.""" + new_account_private_key = PrivateKey.generate() + initial_balance = Hbar(2) + + assert initial_balance.to_tinybars() == 200000000 + + tx = AccountCreateTransaction( + key=new_account_private_key, + initial_balance=initial_balance, + memo="Recipient Account With PrivateKey", + ) + + tx.freeze_with(env.client) + receipt = tx.execute(env.client) + + assert receipt.status == ResponseCode.SUCCESS + assert receipt.account_id is not None, ( + "AccountID not found in receipt. Account may not have been created." + ) + +def test_proto_includes_alias_from_ecdsa_key(env): + """Proto alias should come from the separate ECDSA key when provided.""" + # Main account key (can be any key type) + account_private_key = PrivateKey.generate() + account_public_key = account_private_key.public_key() + + # Separate ECDSA key used only for alias + alias_private_key = PrivateKey.generate_ecdsa() + alias_public_key = alias_private_key.public_key() + expected_evm_address = alias_public_key.to_evm_address() + + tx = ( + AccountCreateTransaction( + key=account_public_key, + initial_balance=Hbar(2), + memo="Account with alias from ECDSA key", + ) + .set_key_with_alias(account_public_key, alias_public_key) + .freeze_with(env.client) + .sign(alias_private_key) + ) + + receipt = tx.execute(env.client) + tx_body = tx.build_transaction_body() + + assert receipt.account_id is not None, "AccountID not found in receipt. Account may not have been created." + # Alias in the proto must come from the ECDSA alias key + assert tx_body.cryptoCreateAccount.alias == expected_evm_address.address_bytes + # Key in the proto must still be the main account key + assert tx_body.cryptoCreateAccount.key == account_public_key._to_proto() + +def test_proto_includes_alias_from_main_key(env): + """Proto alias should be derived from the main ECDSA key when no separate alias key is provided.""" + account_private_key = PrivateKey.generate_ecdsa() + account_public_key = account_private_key.public_key() + expected_evm_address = account_public_key.to_evm_address() + + tx = ( + AccountCreateTransaction( + initial_balance=Hbar(2), + memo="Account with alias from main key", + ) + .set_key_with_alias(account_private_key) # no ecdsa_key param + ) + + tx.freeze_with(env.client) + receipt = tx.execute(env.client) + tx_body = tx.build_transaction_body() + + assert receipt.account_id is not None, "AccountID not found in receipt. Account may not have been created." + # Alias must be derived from the main ECDSA key + assert tx_body.cryptoCreateAccount.alias == expected_evm_address.address_bytes + # Key in proto is still the public key of the account + assert tx_body.cryptoCreateAccount.key == account_public_key._to_proto() + +def test_proto_excludes_alias_when_not_set(env): + """Proto should not include alias when we use the 'without alias' path.""" + account_private_key = PrivateKey.generate() + account_public_key = account_private_key.public_key() + + tx = ( + AccountCreateTransaction( + key=account_public_key, + initial_balance=Hbar(2), + memo="Account without alias", + ) + .set_key_without_alias(account_public_key) + ) + + tx.freeze_with(env.client) + receipt = tx.execute(env.client) + tx_body = tx.build_transaction_body() + + assert receipt.account_id is not None, "AccountID not found in receipt. Account may not have been created." + # No alias should be set in the proto + assert not tx_body.cryptoCreateAccount.alias + # Key must still be present + assert tx_body.cryptoCreateAccount.key == account_public_key._to_proto() diff --git a/tests/unit/test_account_create_transaction.py b/tests/unit/test_account_create_transaction.py index 44dac0952..224e20181 100644 --- a/tests/unit/test_account_create_transaction.py +++ b/tests/unit/test_account_create_transaction.py @@ -395,3 +395,131 @@ def test_create_account_transaction_with_set_alias_from_invalid_type(): with pytest.raises(TypeError): tx.set_alias(1234) + +# This test uses fixture mock_account_ids as parameter +def test_account_create_transaction_build_with_private_key(mock_account_ids): + """AccountCreateTransaction should accept also PrivateKey and serialize the PublicKey in the proto.""" + operator_id, node_account_id = mock_account_ids + + private_key = PrivateKey.generate() + public_key = private_key.public_key() + + tx = ( + AccountCreateTransaction() + .set_key_without_alias(private_key) + .set_initial_balance(100000000) + .set_account_memo("Account with private key") + ) + + tx.transaction_id = generate_transaction_id(operator_id) + tx.node_account_id = node_account_id + + body = tx.build_transaction_body() + + # In the proto we should have the public key not the private one + assert body.cryptoCreateAccount.key.ed25519 == public_key.to_bytes_raw() + assert body.cryptoCreateAccount.initialBalance == 100000000 + assert body.cryptoCreateAccount.memo == "Account with private key" + +# This test uses fixture mock_account_ids as parameter +def test_create_account_transaction_set_key_with_alias_private_keys(mock_account_ids): + """set_key_with_alias should work also with PrivateKey ECDSA (account key + alias key).""" + operator_id, node_id = mock_account_ids + + # Account key(ECDSA) + account_private_key = PrivateKey.generate_ecdsa() + account_public_key = account_private_key.public_key() + + # Alias key (ECDSA) + alias_private_key = PrivateKey.generate_ecdsa() + alias_public_key = alias_private_key.public_key() + expected_evm_address = alias_public_key.to_evm_address() + + tx = ( + AccountCreateTransaction() + .set_key_with_alias(account_private_key, alias_private_key) + ) + + assert tx.key == account_private_key + assert tx.alias == expected_evm_address + + tx.operator_account_id = operator_id + tx.node_account_id = node_id + tx_body = tx.build_transaction_body() + + # In the proto we should have account public key + assert tx_body.cryptoCreateAccount.key == account_public_key._to_proto() + # Alias should be the address bytes from the key for the alias + assert tx_body.cryptoCreateAccount.alias == expected_evm_address.address_bytes + +# This test uses fixture mock_account_ids as parameter +def test_create_account_transaction_set_key_with_alias_private_key_without_ecdsa_key(mock_account_ids): + """set_key_with_alias should work also without ecdsa_key.""" + operator_id, node_id = mock_account_ids + + # Account key(ECDSA) + account_private_key = PrivateKey.generate_ecdsa() + account_public_key = account_private_key.public_key() + + expected_evm_address = account_public_key.to_evm_address() + + tx = ( + AccountCreateTransaction() + .set_key_with_alias(account_private_key) + ) + + assert tx.key == account_private_key + assert tx.alias == expected_evm_address + + tx.operator_account_id = operator_id + tx.node_account_id = node_id + tx_body = tx.build_transaction_body() + + # In the proto we should have account public key + assert tx_body.cryptoCreateAccount.key == account_public_key._to_proto() + # Alias should be the address bytes from the key for the alias + assert tx_body.cryptoCreateAccount.alias == expected_evm_address.address_bytes + +def test_set_key_without_alias_then_with_alias_overrides_alias(): + """set_key_with_alias should ovveride set_key_without_alias""" + # Account key(ECDSA) + account_private_key = PrivateKey.generate_ecdsa() + account_public_key = account_private_key.public_key() + + # Alias key (ECDSA) + alias_private_key = PrivateKey.generate_ecdsa() + alias_public_key = alias_private_key.public_key() + expected_evm_address = alias_public_key.to_evm_address() + + tx = ( + AccountCreateTransaction() + .set_key_without_alias(account_private_key) + ) + + assert tx.alias is None + + tx.set_key_with_alias(account_private_key, alias_private_key) + + assert tx.alias == expected_evm_address + +def test_set_key_with_alias_then_without_alias_overrides_alias(): + """set_key_without_alias should ovveride set_key_with_alias""" + # Account key(ECDSA) + account_private_key = PrivateKey.generate_ecdsa() + account_public_key = account_private_key.public_key() + + # Alias key (ECDSA) + alias_private_key = PrivateKey.generate_ecdsa() + alias_public_key = alias_private_key.public_key() + expected_evm_address = alias_public_key.to_evm_address() + + tx = ( + AccountCreateTransaction() + .set_key_with_alias(account_private_key, alias_private_key) + ) + + assert tx.alias == expected_evm_address + + tx.set_key_without_alias(account_private_key) + + assert tx.alias is None \ No newline at end of file