diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d86155d1..e67685b21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -157,6 +157,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Updated `pyproject.toml` to enforce stricter Ruff linting rules, including Google-style docstrings (`D`), import sorting (`I`), and modern Python syntax (`UP`). - Modified and renamed hasIntermediateOrAdvancedLabel() to check if issue label is beginner or higher (#1385) - Updated `.github/scripts/bot-office-hours.sh` to detect and skip PRs created by bot accounts when posting office hours reminders. (#1384) +- Refactored `examples/account/account_create_transaction_create_with_alias.py` and `examples/account/account_create_transaction_evm_alias.py` to use the native `AccountInfo.__str__` method for printing account details, replacing manual JSON serialization. ([#1263](https://github.com/hiero-ledger/hiero-sdk-python/issues/1263)) ### Fixed - Good First Issue bot no longer posts `/assign` reminders for repository collaborators. (#1367) @@ -179,6 +180,10 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Fixed bot-pr-missing-linked-issue to skip commenting on pull requests created by automated bots. (#1382) - Updated `.github/scripts/bot-community-calls.sh` to skip posting reminders on issues created by bot accounts. (#1383) +### Removed + +- Deleted `examples/utils.py` as its helper functions are no longer needed. ([#1263](https://github.com/hiero-ledger/hiero-sdk-python/issues/1263)) + ### Breaking Change - Remove deprecated 'in_tinybars' parameter and update related tests `/src/hiero_sdk_python/hbar.py`, `/tests/unit/hbar_test.py` and `/src/hiero_sdk_python/tokens/custom_fixed_fee.py`. diff --git a/examples/account/account_create_transaction_create_with_alias.py b/examples/account/account_create_transaction_create_with_alias.py index de867a029..11b0f03c4 100644 --- a/examples/account/account_create_transaction_create_with_alias.py +++ b/examples/account/account_create_transaction_create_with_alias.py @@ -1,5 +1,4 @@ -""" -Example: Create an account using a separate ECDSA key for the EVM alias. +"""Example: Create an account using a separate ECDSA key for the EVM alias. This demonstrates: - Using a "main" key for the account @@ -7,47 +6,36 @@ - 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`) + uv run examples/account/account_create_transaction_create_with_alias.py + python examples/account/account_create_transaction_create_with_alias.py """ -import os import sys -import json -from dotenv import load_dotenv -from examples.utils import info_to_dict +from dotenv import load_dotenv from hiero_sdk_python import ( - Client, - PrivateKey, AccountCreateTransaction, + AccountId, AccountInfo, AccountInfoQuery, - Network, - AccountId, + Client, Hbar, + PrivateKey, ) 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) +def setup_client() -> Client: + """Initialize client from environment variables.""" 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) + client = Client.from_env() 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.") + print("Error: Please check OPERATOR_ID, OPERATOR_KEY, and NETWORK in your .env file.") sys.exit(1) @@ -56,6 +44,7 @@ def generate_main_and_alias_keys() -> tuple[PrivateKey, PrivateKey]: Returns: tuple: (main_private_key, alias_private_key) + """ print("\nSTEP 1: Generating main account key and separate ECDSA alias key...") @@ -84,6 +73,10 @@ def create_account_with_ecdsa_alias( ) -> AccountId: """Create an account with a separate ECDSA key as the EVM alias. + This uses `set_key_with_alias` to map the main key to the alias key. + The transaction requires signatures from both the alias key (to authorize + the use of the alias) and the operator (to pay fees). + Args: client: The Hedera client. main_private_key: The main account private key. @@ -91,6 +84,7 @@ def create_account_with_ecdsa_alias( Returns: AccountId: The newly created account ID. + """ print("\nSTEP 2: Creating the account with the EVM alias from the ECDSA key...") @@ -103,17 +97,16 @@ def create_account_with_ecdsa_alias( ).set_key_with_alias(main_private_key, alias_public_key) # Freeze and sign: - # - operator key signs as payer (via client) + # - operator key signs as payer (handled by client.execute) # - 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 + # Safe retrieval of account ID + 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." - ) + raise RuntimeError("AccountID not found in receipt. Account may not have been created.") print(f"✅ Account created with ID: {new_account_id}\n") return new_account_id @@ -128,10 +121,10 @@ def fetch_account_info(client: Client, account_id: AccountId) -> AccountInfo: Returns: AccountInfo: The account info object. + """ print("\nSTEP 3: Fetching account information...") - account_info = AccountInfoQuery().set_account_id(account_id).execute(client) - return account_info + return AccountInfoQuery().set_account_id(account_id).execute(client) def print_account_summary(account_info: AccountInfo) -> None: @@ -139,30 +132,27 @@ def print_account_summary(account_info: AccountInfo) -> None: Args: account_info: The account info object to display. + """ - out = info_to_dict(account_info) - print("Account Info:") - print(json.dumps(out, indent=2) + "\n") + print("--- Account Info ---") + print(account_info) + print("--------------------\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}" - ) + print(f"✅ Contract Account ID (EVM alias on-chain): {account_info.contract_account_id}") else: print("❌ Error: Contract Account ID (alias) does not exist.") def main(): - """Main entry point.""" + """Execute the example workflow.""" try: client = setup_client() main_private_key, alias_private_key = generate_main_and_alias_keys() - account_id = create_account_with_ecdsa_alias( - client, main_private_key, alias_private_key - ) + account_id = create_account_with_ecdsa_alias(client, main_private_key, alias_private_key) account_info = fetch_account_info(client, account_id) print_account_summary(account_info) + except Exception as error: print(f"❌ Error: {error}") sys.exit(1) diff --git a/examples/account/account_create_transaction_evm_alias.py b/examples/account/account_create_transaction_evm_alias.py index 6f289b5fe..fc5caa23e 100644 --- a/examples/account/account_create_transaction_evm_alias.py +++ b/examples/account/account_create_transaction_evm_alias.py @@ -1,141 +1,129 @@ -# 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). +"""Example: Create an account using an EVM-style alias (evm_address). + +This example demonstrates: +1. Generating an ECDSA key pair (required for EVM compatibility). +2. Deriving the EVM address from the public key. +3. Creating a new Hedera account with that EVM address as its alias. +4. Retrieving the account info to verify the alias. + +Usage: + uv run examples/account/account_create_transaction_evm_alias.py + python examples/account/account_create_transaction_evm_alias.py """ -import os import sys -import json -from dotenv import load_dotenv -from examples.utils import info_to_dict +from dotenv import load_dotenv from hiero_sdk_python import ( - Client, - PrivateKey, AccountCreateTransaction, - AccountInfoQuery, - Network, AccountId, + AccountInfo, + AccountInfoQuery, + Client, + EvmAddress, Hbar, - ResponseCode, + PrivateKey, + PublicKey, ) load_dotenv() -network_name = os.getenv("NETWORK", "testnet").lower() - -def setup_client(): - network = Network(network_name) - print(f"Connecting to Hedera {network_name} network!") - client = Client(network) - # Get the operator account from the .env file +def setup_client() -> Client: + """Initialize client from environment variables.""" try: - operator_id = AccountId.from_string(os.getenv("OPERATOR_ID", "")) - operator_key = PrivateKey.from_string(os.getenv("OPERATOR_KEY", "")) - # Set the operator (payer) account for the client - client.set_operator(operator_id, operator_key) + client = Client.from_env() 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.") + print("Error: Please check OPERATOR_ID, OPERATOR_KEY, and NETWORK in your .env file.") sys.exit(1) -def generate_alias_key(): - """ - Generate a new ECDSA key pair and derive its EVM-style alias address. - This function creates a new ECDSA private/public key pair and derives the - corresponding EVM address (alias) from the public key. If the EVM address - cannot be generated, the process is terminated. +def generate_alias_key() -> tuple[PrivateKey, PublicKey, EvmAddress]: + """Generate a new ECDSA key pair and derive its EVM-style alias address. + + EVM aliases on Hedera must be derived from an ECDSA (secp256k1) key pair. + The EVM address is the last 20 bytes of the keccak256 hash of the public key. + Returns: tuple: A 3-tuple of: - private_key: The newly generated ECDSA private key. - public_key: The public key corresponding to the private key. - - evm_address (str): The derived EVM address to be used as an alias. + - evm_address (EvmAddress): The derived EVM address to be used as an alias. """ print("\nSTEP 1: Generating a new ECDSA key pair for the account alias...") + + # ECDSA is required for EVM compatibility private_key = PrivateKey.generate("ecdsa") public_key = private_key.public_key() - evm_address = public_key.to_evm_address() - if evm_address is None: - print("❌ Error: Failed to generate EVM address from public key.") + + # Compute the EVM address from the public key + try: + evm_address = public_key.to_evm_address() + except ValueError as e: + print(f"❌ Error: {e}") sys.exit(1) + print(f"✅ Generated new ECDSA key pair. EVM Address (alias): {evm_address}") return private_key, public_key, evm_address -def create_account_with_alias(client, private_key, public_key, evm_address): - """ - Create a new Hedera account using the provided EVM-style alias. +def create_account_with_alias( + client: Client, private_key: PrivateKey, public_key: PublicKey, evm_address: EvmAddress +) -> AccountId: + """Create a new Hedera account using the provided EVM-style alias. + + Important: When creating an account with an alias, the transaction must be + signed by the private key corresponding to that alias. This proves ownership + of the alias (the EVM address) being claimed. + Args: - client: An initialized `Client` instance with an operator set, used to - freeze, sign, and execute the account creation transaction. + client: An initialized `Client` instance with an operator set. private_key: The newly generated `PrivateKey` corresponding to the alias public key, used to sign the transaction. public_key: The public key associated with `private_key`, which is set as the new account's key. evm_address: The EVM-style alias (derived from `public_key`) to assign to the new account. + Returns: The `AccountId` of the newly created account. + """ print("\nSTEP 2: Creating the account with the EVM address alias...") - transaction = ( - AccountCreateTransaction() - .set_key(public_key) - .set_initial_balance(Hbar(5)) - .set_alias(evm_address) - ) - - # Sign the transaction with both the new key and the operator key - transaction = ( - transaction.freeze_with(client) - .sign(private_key) - .sign(client.operator_private_key) - ) + + transaction = AccountCreateTransaction().set_key(public_key).set_initial_balance(Hbar(5)).set_alias(evm_address) + + # Sign with new key (required for alias) + # The client.execute() call below will attach the operator signature automatically. + transaction = transaction.freeze_with(client).sign(private_key) # Execute the transaction response = transaction.execute(client) - # Validate the receipt status before accessing account_id - if response.status != ResponseCode.SUCCESS: - print( - f"❌ Account creation failed with status: " - f"{ResponseCode(response.status).name}" - ) - sys.exit(1) - + # Safe retrieval of account ID 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") return new_account_id -def fetch_account_info(client, account_id): - """ - Retrieve detailed information for a given account from the Hedera network. - Args: - client: An initialized `Client` instance configured with an operator. - account_id: The identifier of the account whose information is being queried. - Returns: - The account information object returned by `AccountInfoQuery.execute(client)`. - """ +def fetch_account_info(client: Client, account_id: AccountId) -> AccountInfo: + """Retrieve detailed information for a given account from the Hedera network.""" return AccountInfoQuery().set_account_id(account_id).execute(client) -def print_account_summary(account_info): - """ - Print a human-readable summary of Hedera account information. - Args: - account_info: The account information object returned by `AccountInfoQuery`, - containing fields such as the account ID and optional contract account - ID (EVM alias) to be displayed. - """ - out = info_to_dict(account_info) +def print_account_summary(account_info: AccountInfo) -> None: + """Print a human-readable summary of Hedera account information.""" print("🧾 Account Info:") - print(json.dumps(out, indent=2) + "\n") + print(account_info) + print("") + if account_info.contract_account_id is not None: print(f"✅ Contract Account ID (alias): {account_info.contract_account_id}") else: @@ -143,23 +131,12 @@ def print_account_summary(account_info): def main(): - """ - Orchestrate the example workflow for creating an account using an EVM-style alias. - This function: - 1. Sets up the Hedera client using operator credentials from the environment. - 2. Generates a new ECDSA key pair and derives its EVM address for use as an alias. - 3. Creates a new account with the generated alias and an initial HBAR balance. - 4. Fetches the newly created account's information from the network. - 5. Prints a human-readable summary of the account details, including the contract account ID alias. - Any unexpected errors are caught and reported before the process exits with a non-zero status. - """ + """Orchestrate the example workflow.""" client = setup_client() try: private_key, public_key, evm_address = generate_alias_key() - new_account_id = create_account_with_alias( - client, private_key, public_key, evm_address - ) + new_account_id = create_account_with_alias(client, private_key, public_key, evm_address) account_info = fetch_account_info(client, new_account_id) diff --git a/examples/account/account_create_transaction_with_fallback_alias.py b/examples/account/account_create_transaction_with_fallback_alias.py index b8bf7b88b..49661e09a 100644 --- a/examples/account/account_create_transaction_with_fallback_alias.py +++ b/examples/account/account_create_transaction_with_fallback_alias.py @@ -1,51 +1,39 @@ -""" -Example: Create an account where the EVM alias is derived from the main ECDSA key. +"""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`) + uv run examples/account/account_create_transaction_with_fallback_alias.py + python examples/account/account_create_transaction_with_fallback_alias.py """ -import os import sys -import json -from dotenv import load_dotenv -from examples.utils import info_to_dict +from dotenv import load_dotenv from hiero_sdk_python import ( - Client, - PrivateKey, AccountCreateTransaction, - AccountInfoQuery, - Network, AccountId, + AccountInfo, + AccountInfoQuery, + Client, Hbar, + PrivateKey, ) 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) - +def setup_client() -> Client: + """Initialize client from environment variables.""" 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) + client = Client.from_env() 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.") + print("Error: Please check OPERATOR_ID, OPERATOR_KEY, and NETWORK in your .env file.") sys.exit(1) @@ -54,22 +42,25 @@ def generate_fallback_key() -> PrivateKey: 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.") + # Validate that the key is ECDSA-compatible by deriving the EVM address. + # The actual alias will be derived by set_key_with_alias() in the next step. + try: + evm_address = account_public_key.to_evm_address() + except ValueError as e: + print(f"❌ Error: {e}") sys.exit(1) print(f"✅ Account ECDSA public key: {account_public_key}") - print(f"✅ Derived EVM address: {evm_address}") + print(f"✅ Derived EVM address: {evm_address}") + return account_private_key -def create_account_with_fallback_alias( - client: Client, account_private_key: PrivateKey -) -> AccountId: +def create_account_with_fallback_alias(client: Client, account_private_key: PrivateKey) -> AccountId: """Create an account whose alias is derived from the provided ECDSA key.""" 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", @@ -81,30 +72,31 @@ def create_account_with_fallback_alias( 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." - ) + raise RuntimeError("AccountID not found in receipt. Account may not have been created.") print(f"✅ Account created with ID: {new_account_id}\n") + return new_account_id -def fetch_account_info(client: Client, account_id: AccountId): +def fetch_account_info(client: Client, account_id: AccountId) -> AccountInfo: """Fetch account info for the given account ID.""" print("\nSTEP 3: Fetching account information...") + return AccountInfoQuery().set_account_id(account_id).execute(client) -def print_account_summary(account_info) -> None: +def print_account_summary(account_info: AccountInfo) -> None: """Print an account summary (including EVM alias).""" print("\nSTEP 4: Printing account EVM alias and summary...") - 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}" - ) + print("🧾 Account Info:") + print(account_info) + print("") + + if account_info.contract_account_id is not None: + print(f"✅ contract_account_id (EVM alias on-chain): {account_info.contract_account_id}") + else: + print("❌ Error: Contract Account ID (alias) does not exist.") def main(): @@ -115,6 +107,7 @@ def main(): new_account_id = create_account_with_fallback_alias(client, account_private_key) account_info = fetch_account_info(client, new_account_id) print_account_summary(account_info) + except Exception as error: print(f"❌ Error: {error}") sys.exit(1) diff --git a/examples/utils.py b/examples/utils.py deleted file mode 100644 index 0669a1279..000000000 --- a/examples/utils.py +++ /dev/null @@ -1,23 +0,0 @@ -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