diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..eb17bfddd --- /dev/null +++ b/.coveragerc @@ -0,0 +1,8 @@ +[run] +source = src + +omit = + */hiero_sdk_python/hapi/* + */__pycache__/* + */cache/* + */.cache/* diff --git a/.github/ISSUE_TEMPLATE/01_good_first_issue.yml b/.github/ISSUE_TEMPLATE/01_good_first_issue.yml index 2cebe793c..320c370f7 100644 --- a/.github/ISSUE_TEMPLATE/01_good_first_issue.yml +++ b/.github/ISSUE_TEMPLATE/01_good_first_issue.yml @@ -4,6 +4,15 @@ title: "[Good First Issue]: " labels: ["Good First Issue"] assignees: [] body: + - type: markdown + attributes: + value: | + --- + ## **Thanks for contributing!** 😊 + + We truly appreciate your time and effort. If this is your first open-source contribution, welcome! + This template is designed to help you create a Good First Issue (GFI) : a small, well-scoped task that helps new contributors learn the codebase and workflow. + --- - type: textarea id: intro attributes: @@ -16,6 +25,39 @@ body: validations: required: false + - type: markdown + attributes: + value: | + > [!IMPORTANT] + > ### πŸ“‹ Good First Issue (GFI) Guidelines + > + > **What we generally consider good first issues:** + > + > - **Narrow changes or additions to `src` functionality** that use generic Python skills which can be tested by adding to an existing test. For example: + > - `__str__` functions + > - `__repr__` functions + > - Typing fixes, like return type hints or basic type conflicts + > - **Refactors of existing examples:** + > - Separating existing examples into separate functions + > - Or, conversely, taking a split example into a monolithic function + > - **Improvements to documentation** in examples and source code: + > - Docstrings: module docstrings, function docstrings + > - Inline comments + > - Addition or changes to print statements to improve clarity + > - **Functional improvements to examples:** + > - Additional steps that would help to illustrate functionality + > - **Specific additions to existing unit or integration tests** + > + > **What we generally do NOT consider good first issues:** + > + > - Creation of new examples + > - Creation of new unit and integration tests + > - Changes to DLT functionality, like `to_proto` and `from_proto` + > - Anything requiring knowledge of multiple areas of the codebase + > + > πŸ“– *For a more detailed explanation, refer to: + > [`docs/maintainers/good_first_issues_guidelines.md`](docs/maintainers/good_first_issues_guidelines.md).* + - type: textarea id: issue attributes: diff --git a/.github/ISSUE_TEMPLATE/04_good_first_issue_candidate.yml b/.github/ISSUE_TEMPLATE/04_good_first_issue_candidate.yml new file mode 100644 index 000000000..7d42838ee --- /dev/null +++ b/.github/ISSUE_TEMPLATE/04_good_first_issue_candidate.yml @@ -0,0 +1,282 @@ +name: Good First Issue Candidate Template +description: Propose a potential Good First Issue (candidate) +title: "[Good First Issue]:" +labels: ["Good First Issue Candidate"] +assignees: [] +body: + - type: textarea + id: intro-gfi-candidate + attributes: + label: ⚠️ Good First Issue β€” Candidate + value: | + > This issue is not yet a confirmed Good First Issue. + > It is being evaluated for suitability and may require + > clarification or refinement before it is ready to be picked up. + > + > Please wait for maintainer confirmation before starting work. + > + > Maintainers and reviewers can read more about Good First Issues: + > docs/maintainers/good_first_issue_guidelines.md + + validations: + required: false + + - type: textarea + id: intro + attributes: + label: πŸ†•πŸ₯ First Timers Only + description: Who is this issue for? + value: | + This issue is reserved for people who have never contributed or have made minimal contributions to [Hiero Python SDK](https://hiero.org). + We know that creating a pull request (PR) is a major barrier for new contributors. + The goal of this issue and all other issues in [**find a good first issue**](https://github.com/issues?q=is%3Aopen+is%3Aissue+org%3Ahiero-ledger+archived%3Afalse+label%3A%22good+first+issue%22+) is to help you make your first contribution to the Hiero Python SDK. + validations: + required: false + + - type: markdown + attributes: + value: | + > [!IMPORTANT] + > ### πŸ“‹ Good First Issue (GFI) Guidelines + > + > **What we generally consider good first issues:** + > + > - **Narrow changes or additions to `src` functionality** that use generic Python skills which can be tested by adding to an existing test. For example: + > - `__str__` functions + > - `__repr__` functions + > - Typing fixes, like return type hints or basic type conflicts + > - **Refactors of existing examples:** + > - Separating existing examples into separate functions + > - Or, conversely, taking a split example into a monolithic function + > - **Improvements to documentation** in examples and source code: + > - Docstrings: module docstrings, function docstrings + > - Inline comments + > - Addition or changes to print statements to improve clarity + > - **Functional improvements to examples:** + > - Additional steps that would help to illustrate functionality + > - **Specific additions to existing unit or integration tests** + > + > **What we generally do NOT consider good first issues:** + > + > - Creation of new examples + > - Creation of new unit and integration tests + > - Changes to DLT functionality, like `to_proto` and `from_proto` + > - Anything requiring knowledge of multiple areas of the codebase + > + > πŸ“– *For a more detailed explanation, refer to: + > [`docs/maintainers/good_first_issue_candidate_guidelines.md`](docs/maintainers/good_first_issue_candidate_guidelines.md).* + + - type: textarea + id: issue + attributes: + label: πŸ‘Ύ Description of the issue + description: | + DESCRIBE THE ISSUE IN A WAY THAT IS UNDERSTANDABLE TO NEW CONTRIBUTORS. + YOU MUST NOT ASSUME THAT SUCH CONTRIBUTORS HAVE ANY KNOWLEDGE ABOUT THE CODEBASE OR HIERO. + IT IS HELPFUL TO ADD LINKS TO THE RELEVANT DOCUMENTATION AND/OR CODE SECTIONS. + BELOW IS AN EXAMPLE. + value: | + Edit here. Example provided below. + + validations: + required: true + + - type: markdown + attributes: + value: | + + ## πŸ‘Ύ Description of the issue - Example + + The example for Token Associate Transaction located at examples/tokens/token_associate_transaction.py can be improved. It correctly illustrates how to associate a token, however, it does so all from one function main() + + As everything is grouped together in main(), it is difficult for a user to understand all the individual steps required to associate a token. + + For example: + ```python + + def run_demo(): + """Monolithic token association demo.""" + print(f"πŸš€ Connecting to Hedera {network_name} network!") + client = Client(Network(network_name)) + 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 ready (operator {operator_id})") + + test_key = PrivateKey.generate_ed25519() + receipt = ( + AccountCreateTransaction() + .set_key(test_key.public_key()) + .set_initial_balance(Hbar(1)) + .set_account_memo("Test account for token association demo") + .freeze_with(client) + .sign(operator_key) + .execute(client) + ) + if receipt.status != ResponseCode.SUCCESS: + raise Exception(receipt.status) + account_id = receipt.account_id + print(f"βœ… Created test account {account_id}") + + # Create tokens + tokens = [] + for i in range(3): + try: + receipt = ( + TokenCreateTransaction() + .set_token_name(f"DemoToken{i}") + .set_token_symbol(f"DTK{i}") + .set_decimals(2) + .set_initial_supply(100_000) + .set_treasury_account_id(operator_id) + .freeze_with(client) + .sign(operator_key) + .execute(client) + ) + if receipt.status != ResponseCode.SUCCESS: + raise Exception(receipt.status) + token_id = receipt.token_id + tokens.append(token_id) + print(f"βœ… Created token {token_id}") + except Exception as e: + print(f"❌ Token creation failed: {e}") + sys.exit(1) + + # Associate first token + try: + TokenAssociateTransaction().set_account_id(account_id).add_token_id(tokens[0]).freeze_with(client).sign(test_key).execute(client) + print(f"βœ… Token {tokens[0]} associated with account {account_id}") + except Exception as e: + print(f"❌ Token association failed: {e}") + sys.exit(1) + ``` + + - type: textarea + id: solution + attributes: + label: πŸ’‘ Proposed Solution + description: | + AT THIS SECTION YOU NEED TO DESCRIBE THE STEPS NEEDED TO SOLVE THE ISSUE. + PLEASE BREAK DOWN THE STEPS AS MUCH AS POSSIBLE AND MAKE SURE THAT THEY + ARE EASY TO FOLLOW. IF POSSIBLE, ADD LINKS TO THE RELEVANT + DOCUMENTATION AND/OR CODE SECTIONS. + value: | + Edit here. Example provided below. + + validations: + required: true + + - type: markdown + attributes: + value: | + + ## πŸ’‘ Solution - Example + + For the TokenAssociateTransaction example, the solution is to split the monolithic main() function for illustrating TokenAssociateTransaction into separate smaller functions which are called from main(). + Such as: + - Setting up the client + - Creating an account + - Creating a token + - Associating the account to the token + + - type: textarea + id: implementation + attributes: + label: πŸ‘©β€πŸ’» Implementation Steps + description: | + AT THIS SECTION YOU NEED TO DESCRIBE THE TECHNICAL STEPS NEEDED TO SOLVE THE ISSUE. + PLEASE BREAK DOWN THE STEPS AS MUCH AS POSSIBLE AND MAKE SURE THAT THEY ARE EASY TO FOLLOW. + IF POSSIBLE, ADD LINKS TO THE RELEVANT DOCUMENTATION AND/OR CODE. + value: | + Edit here. Example provided below. + + validations: + required: true + + - type: markdown + attributes: + value: | + + ### πŸ‘©β€πŸ’» Implementation - Example + + To break down the monolithic main function, you need to: + - [ ] Extract the Key Steps (set up a client, create a test account, create a token, associate the token) + - [ ] Copy and paste the functionality for each key step into its own function + - [ ] Pass to each function the variables you need to run it + - [ ] Call each function in main() + - [ ] Ensure you return the values you'll need to pass on to the next step in main + - [ ] Ensure the example still runs and has the same output! + + For example: + ```python + + def setup_client(): + """Initialize and set up the client with operator account.""" + + def create_test_account(client, operator_key): + """Create a new test account for demonstration.""" + + def create_fungible_token(client, operator_id, operator_key): + """Create a fungible token for association with test account.""" + + def associate_token_with_account(client, token_id, account_id, account_key): + """Associate the token with the test account.""" + + def main(): + client, operator_id, operator_key = setup_client() + account_id, account_private_key = create_test_account(client, operator_key) + token_id = create_fungible_token(client, operator_id, operator_key) + associate_token_with_account(client, token_id, account_id, account_private_key) + ``` + + - type: textarea + id: acceptance-criteria + attributes: + label: βœ… Acceptance Criteria + description: | + EDIT OR EXPAND THE CHECKLIST ON WHAT IS REQUIRED TO BE ABLE TO MERGE A PULL REQUEST FOR THIS ISSUE + value: | + To be able to merge a pull request for this issue, we need: + - [ ] **Changelog Entry:** Correct changelog entry (please link to the documentation - [see guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/changelog_entry.md)) + - [ ] **Signed commits:** commits must be DCO and GPG key signed ([see guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/signing.md)) + - [ ] **All Tests Pass:** our workflow checks like unit and integration tests must pass + - [ ] **Issue is Solved:** The implementation fully addresses the issue requirements as described above + - [ ] **No Further Changes are Made:** Code review feedback has been addressed and no further changes are requested + validations: + required: true + + - type: textarea + id: contribution_steps + attributes: + label: πŸ“‹ Step-by-Step Contribution Guide + description: Provide a contribution workflow suitable for new contributors + value: | + If you have never contributed to an open source project at GitHub, the following step-by-step guide will introduce you to the workflow. + + - [ ] **Claim this issue:** Comment below that you are interested in working on the issue. Without assignment, your pull requests might be closed and the issue given to another developer. + - [ ] **Wait for assignment:** A community member with the given rights will add you as an assignee of the issue + - [ ] **Fork, Branch and Work on the issue:** Create a copy of the repository, create a branch for the issue and solve the problem. For instructions, please read our [Contributing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/CONTRIBUTING.md) file. Further help can be found at [Set-up Training](https://github.com/hiero-ledger/hiero-sdk-python/tree/main/docs/sdk_developers/training/setup) and [Workflow Training](https://github.com/hiero-ledger/hiero-sdk-python/tree/main/docs/sdk_developers/training/workflow). + - [ ] **DCO and GPG key sign each commit :** each commit must be -s and -S signed. An explanation on how to do this is at [Signing Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/signing.md) + - [ ] **Add a Changelog Entry :** your pull request will require a changelog. Read [Changelog Entry Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/changelog_entry.md) to learn how. + - [ ] **Push and Create a Pull Request :** Once your issue is resolved, and your commits are signed, and you have a changelog entry, push your changes and create a pull request. Detailed instructions can be found at [Submit PR Training](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/11_submit_pull_request.md), part of [Workflow Training](https://github.com/hiero-ledger/hiero-sdk-python/tree/main/docs/sdk_developers/training/workflow). + - [ ] **You did it πŸŽ‰:** A maintainer or committer will review your pull request and provide feedback. If approved, we will merge the fix in the main branch. Thanks for being part of the Hiero community as an open-source contributor ❀️ + + ***IMPORTANT*** Your pull request CANNOT BE MERGED until you add a changelog entry AND sign your commits each with `git commit -S -s -m "chore: your commit message"` with a GPG key setup. + validations: + required: true + + - type: textarea + id: information + attributes: + label: πŸ€” Additional Information + description: Provide any extra resources or context for contributors to solve this good first issue + value: | + For more help, we have extensive documentation attributes: + - [SDK Developer Docs](https://github.com/hiero-ledger/hiero-sdk-python/tree/main/docs/sdk_developers) + - [SDK Developer Training](https://github.com/hiero-ledger/hiero-sdk-python/tree/main/docs/sdk_developers/training) + + Additionally, we invite you to join our community on our [Discord](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md) server. + + We also invite you to attend each Wednesday, 2pm UTC our [Python SDK Office Hour and Community Calls](https://zoom-lfx.platform.linuxfoundation.org/meetings/hiero?view=week). The Python SDK Office hour is for hands-on-help and the Community Call for general community discussion. + + You can also ask for help in a comment below! diff --git a/.github/ISSUE_TEMPLATE/05_intermediate_issue.yml b/.github/ISSUE_TEMPLATE/05_intermediate_issue.yml new file mode 100644 index 000000000..f6c096df5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/05_intermediate_issue.yml @@ -0,0 +1,211 @@ +name: Intermediate Issue Template +description: Create a well-documented issue for contributors with some familiarity with the codebase +title: "[Intermediate]: " +labels: ["intermediate"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + --- + ## **Thanks for contributing!** 😊 + + We truly appreciate your time and effort. + This template is designed to help you create an Intermediate issue. + + The goal is to create an issue for users that have: + - basic familiarity with the Hiero Python SDK codebase + - experience following our contribution workflow + - confidence navigating existing source code and examples + --- + + - type: textarea + id: intro + attributes: + label: 🧩 Intermediate Contributors + description: Who is this issue for? + value: | + This issue is intended for contributors who already have some familiarity with the + [Hiero Python SDK](https://hiero.org) codebase and contribution workflow. + + You should feel comfortable: + - navigating existing source code and examples + - understanding SDK concepts without step-by-step guidance + - following the standard PR workflow without additional onboarding + + If this is your very first contribution to the project, we recommend starting with a few + **Good First Issues** before working on this one. + validations: + required: false + + - type: markdown + attributes: + value: | + > [!IMPORTANT] + > ### 🧭 What we consider an *Intermediate Issue* + > + > This issue generally: + > + > - Requires **some knowledge of the existing codebase**, but not deep architectural knowledge + > - Is **narrowly scoped** and well-contained + > - Is **low to medium risk**, not touching highly sensitive or critical DLT logic + > - May involve **refactors or small feature additions** + > - Provides **enough documentation or examples** to reason about a solution + > - Often has **similar patterns elsewhere in the codebase** + > + > **What this issue is NOT:** + > - A beginner-friendly onboarding task + > - A large architectural redesign + > - A change requiring extensive cross-module refactors + > - A breaking API change + + - type: textarea + id: problem + attributes: + label: 🐞 Problem Description + description: | + Describe the problem clearly and precisely. + + You may assume the reader: + - understands the SDK structure + - can navigate `src/`, `examples/`, and `tests/` + - is comfortable reading existing implementations + + Still, explain: + - what is wrong or missing + - where it lives in the codebase + - why it matters + value: | + Describe the problem here. + validations: + required: true + + - type: markdown + attributes: + value: | + + ## 🐞 Problem – Example + + The `TransactionGetReceiptQuery` currently exposes the `get_children()` method, + but the behavior is inconsistent with how child receipts are returned by the Mirror Node. + + In particular: + - The SDK always returns only the parent receipt + - There is no way to opt-in to retrieving child receipts + - Similar query objects already support optional flags to extend the response + + This limitation makes it difficult to inspect scheduled or child transactions + without issuing additional manual queries. + + Relevant files: + - `src/hiero_sdk_python/query/transaction_get_receipt_query.py` + - `examples/query/transaction_get_receipt_query.py` + + - type: textarea + id: solution + attributes: + label: πŸ’‘ Expected Solution + description: | + Describe the intended outcome. + + This does NOT need to be a full implementation plan, + but should explain: + - what should change + - what should NOT change + - any constraints or boundaries + value: | + Describe the expected solution here. + validations: + required: true + + - type: markdown + attributes: + value: | + + ## πŸ’‘ Expected Solution – Example + + Introduce an optional configuration flag on `TransactionGetReceiptQuery` + that allows callers to explicitly request child receipts. + + The change should: + - Be opt-in (default behavior must remain unchanged) + - Reuse existing response parsing logic where possible + - Avoid introducing breaking API changes + + Example usage: + + ```python + receipt = ( + TransactionGetReceiptQuery() + .set_transaction_id(tx_id) + .set_include_children(True) + .execute(client) + ) + ``` + + - type: textarea + id: implementation + attributes: + label: 🧠 Implementation Notes + description: | + Provide technical guidance to help implementation. + + Examples: + - files or modules likely involved + - patterns already used elsewhere + - things to be careful about + - known edge cases + value: | + Add implementation notes here. + validations: + required: false + + - type: markdown + attributes: + value: | + + ## 🧠 Implementation Notes – Example + + Likely steps: + + - Add an optional boolean field (e.g. `_include_children`) to + `TransactionGetReceiptQuery` + - Ensure the flag is passed to the mirror node request + - Update response parsing to include child receipts when present + - Extend the existing example in + `examples/query/transaction_get_receipt_query.py` + to demonstrate the new behavior + + Similar patterns can be found in other query classes that support + optional response extensions. + + - type: textarea + id: acceptance-criteria + attributes: + label: βœ… Acceptance Criteria + description: Define what "done" means for this issue + value: | + To merge this issue, the pull request must: + + - [ ] Fully address the problem described above + - [ ] Follow existing project conventions and patterns + - [ ] Include tests or example updates where appropriate + - [ ] Pass all CI checks + - [ ] Include a valid changelog entry + - [ ] Use DCO and GPG-signed commits + validations: + required: true + + - type: textarea + id: additional-info + attributes: + label: πŸ“š Additional Context or Resources + description: | + Add any links, references, or extra notes that may help. + value: | + - [SDK Developer Docs](https://github.com/hiero-ledger/hiero-sdk-python/tree/main/docs/sdk_developers) + - [SDK Developer Training](https://github.com/hiero-ledger/hiero-sdk-python/tree/main/docs/sdk_developers/training) + + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/06_advanced_issue.yml b/.github/ISSUE_TEMPLATE/06_advanced_issue.yml new file mode 100644 index 000000000..3a76942b9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/06_advanced_issue.yml @@ -0,0 +1,226 @@ +name: Advanced Issue Template +description: Create a high-impact issue for experienced contributors with deep familiarity with the codebase +title: "[Advanced]: " +labels: ["advanced"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + --- + ## **Thanks for contributing!** πŸš€ + + We truly appreciate your interest in tackling an **Advanced issue**. + + This template is designed for work that requires **deep familiarity with the Hiero Python SDK** + and confidence making changes that may span multiple modules or affect core behavior. + + The goal is to create issues for contributors who: + - have strong familiarity with the Hiero Python SDK internals + - are comfortable reasoning about trade-offs and design decisions + - can work independently with minimal guidance + --- + + - type: textarea + id: intro + attributes: + label: 🧠 Advanced Contributors + description: Who is this issue intended for? + value: | + This issue is intended for contributors who are already very familiar with the + [Hiero Python SDK](https://hiero.org) codebase and its architectural patterns. + + You should feel comfortable: + - navigating multiple modules across `src/` + - understanding and modifying core SDK abstractions + - reasoning about API design and backwards compatibility + - updating or extending tests, examples, and documentation as needed + - making changes that may affect public-facing behavior + + New developers should start with + **Good First Issues** or **Intermediate Issues** first. + validations: + required: false + + - type: markdown + attributes: + value: | + > [!WARNING] + > ### 🧭 What we consider an *Advanced Issue* + > + > This issue typically: + > + > - Requires **deep understanding of existing SDK design and behavior** + > - May touch **core abstractions**, shared utilities, or cross-cutting concerns + > - May involve **non-trivial refactors**, design changes, or behavior extensions + > - Has **medium to high risk** if implemented incorrectly + > - Requires careful consideration of **backwards compatibility** + > - May require updating **tests, examples, and documentation together** + > + > **What this issue is NOT:** + > - A simple bug fix + > - A narrowly scoped refactor + > - A task solvable by following existing patterns alone + > + > πŸ“– Helpful references: + > - `docs/sdk_developers/training` + > - `docs/sdk_developers/project_structure.md` + > - `docs/sdk_developers/rebasing.md` + + - type: textarea + id: problem + attributes: + label: 🐞 Problem Description + description: | + Describe the problem in depth. + + You may assume the reader: + - understands the overall SDK architecture + - can navigate and reason about multiple modules + - is comfortable reading and modifying core logic + + Clearly explain: + - what the current behavior is + - why it is insufficient or incorrect + - which components or layers are involved + - any relevant historical or design context + value: | + Describe the problem here. + validations: + required: true + + - type: markdown + attributes: + value: | + + ## 🐞 Problem – Example + + The current transaction execution pipeline tightly couples + receipt retrieval, record retrieval, and retry logic into a single + execution flow. + + This coupling makes it difficult to: + - customize retry behavior + - extend execution semantics for scheduled or mirror-node-backed workflows + - test individual stages of transaction execution in isolation + + Several downstream SDK features would benefit from a clearer separation + of concerns in this area. + + Relevant areas: + - `src/hiero_sdk_python/transaction/` + - `src/hiero_sdk_python/execution/` + - `src/hiero_sdk_python/client/` + + - type: textarea + id: solution + attributes: + label: πŸ’‘ Proposed / Expected Solution + description: | + Describe the intended direction or design. + + This should include: + - the high-level approach + - any new abstractions or changes to existing ones + - constraints (e.g. backwards compatibility, performance, API stability) + - known alternatives and why they were rejected (if applicable) + + A full design document is not required, but reasoning and intent should be clear. + value: | + Describe the proposed solution here. + validations: + required: true + + - type: markdown + attributes: + value: | + + ## πŸ’‘ Proposed Solution – Example + + Introduce a dedicated execution pipeline abstraction that separates: + - transaction submission + - receipt polling + - record retrieval + - retry and timeout logic + + The new design should: + - preserve existing public APIs + - allow advanced users to override or extend execution behavior + - make individual stages independently testable + + Existing transaction execution should be reimplemented + using the new pipeline internally. + + - type: textarea + id: implementation + attributes: + label: 🧠 Implementation & Design Notes + description: | + Provide detailed technical guidance. + + This section is especially important for Advanced issues. + + Consider including: + - specific modules or classes involved + - suggested refactoring strategy + - migration or deprecation concerns + - testing strategy + - performance or security considerations + value: | + Add detailed implementation notes here. + validations: + required: false + + - type: markdown + attributes: + value: | + + ## 🧠 Implementation Notes – Example + + Suggested approach: + + - Introduce a new `ExecutionPipeline` abstraction under + `src/hiero_sdk_python/execution/` + - Refactor existing transaction execution logic to delegate + to this pipeline + - Ensure existing public APIs remain unchanged + - Add focused unit tests for each pipeline stage + - Update at least one example to demonstrate extensibility + + Care should be taken to avoid breaking timeout semantics + relied upon by existing users. + + - type: textarea + id: acceptance-criteria + attributes: + label: βœ… Acceptance Criteria + description: Define what "done" means for this issue + value: | + To merge this issue, the pull request must: + + - [ ] Fully address the problem and design goals described above + - [ ] Maintain backwards compatibility unless explicitly approved otherwise + - [ ] Follow existing architectural and coding conventions + - [ ] Include comprehensive tests covering new and existing behavior + - [ ] Update relevant examples and documentation + - [ ] Pass all CI checks + - [ ] Include a valid changelog entry + - [ ] Use DCO and GPG-signed commits + validations: + required: true + + - type: textarea + id: additional-info + attributes: + label: πŸ“š Additional Context, Links, or Prior Art + description: | + Add any references that may help: + - design docs + - prior discussions + - related issues or PRs + - external references + value: | + Optional. + validations: + required: false diff --git a/.github/scripts/check_test_files.sh b/.github/scripts/check_test_files.sh new file mode 100755 index 000000000..b0131b6c7 --- /dev/null +++ b/.github/scripts/check_test_files.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +RED="\033[31m" +YELLOW="\033[33m" +RESET="\033[0m" + +# Base directories where test files should reside +TEST_DIRS=("tests/unit" "tests/integration") +EXCEPTION_NAMES=("conftest.py" "init.py" "__init__.py" "mock_server.py" "utils.py") +DIFF_FILES=$(git diff --name-status origin/main) +ERRORS=() + +function is_in_test_dir() { + local file="$1" + for dir in "${TEST_DIRS[@]}"; do + case "$file" in + "$dir"*) + return 0 + ;; + esac + done + return 1 +} + +function check_test_file_name() { + local filename="$1" + if is_in_test_dir "$filename"; then + if [[ $(basename "$filename") != *.py ]]; then + return 0 + fi + for exception in "${EXCEPTION_NAMES[@]}"; do + if [[ $(basename "$filename") == "$exception" ]]; then + return 0 + fi + done + if [[ $(basename "$filename") != *_test.py ]]; then + ERRORS+=("${RED}ERROR${RESET}: Test file '$filename' doesn't end with '_test.py'. ${YELLOW}It has to follow the pytest naming convention.") + return 1 + fi + fi + return 0 +} + +while IFS=$'\t' read -r status file1 file2; do + case "$status" in + A) check_test_file_name "$file1" ;; + R*) check_test_file_name "$file2" ;; + C*) check_test_file_name "$file2" ;; + esac +done <<< "$DIFF_FILES" + +if (( ${#ERRORS[@]} > 0 )); then + for err in "${ERRORS[@]}"; do + echo -e "$err" + done + exit 1 +fi diff --git a/.github/scripts/gfi_notify_team.js b/.github/scripts/gfi_notify_team.js new file mode 100644 index 000000000..80c9e4c3c --- /dev/null +++ b/.github/scripts/gfi_notify_team.js @@ -0,0 +1,73 @@ +// Script to notify the team when a GFI issue is labeled. + +const marker = ''; +const TEAM_ALIAS = '@hiero-ledger/hiero-sdk-good-first-issue-support'; + +async function notifyTeam(github, owner, repo, issue, message, marker) { + const comment = `${marker} :wave: Hello Team :wave: +${TEAM_ALIAS} + +${message} + +Repository: ${owner}/${repo} : Issue: #${issue.number} - ${issue.title || '(no title)'} + +Best Regards, +Python SDK team`; + + try { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issue.number, + body: comment, + }); + console.log(`Notified team about GFI issue #${issue.number}`); + return true; + } catch (commentErr) { + console.log(`Failed to notify team about GFI issue #${issue.number}:`, commentErr.message || commentErr); + return false; + } +} + +module.exports = async ({ github, context }) => { + try { + const { owner, repo } = context.repo; + const { issue, label } = context.payload; + + if (!issue?.number) return console.log('No issue in payload'); + + const labelName = label?.name; + if (!labelName) return; + + let message = ''; + if (labelName === 'Good First Issue') { + message = 'There is a new GFI in the Python SDK which is ready to be assigned'; + } else if (labelName === 'Good First Issue Candidate') { + message = 'An issue in the Python SDK requires immediate attention to verify if it is a GFI and label it appropriately'; + } else { + return; + } + + // Check for existing comment + const comments = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number: issue.number, per_page: 100 + }); + if (comments.some(c => c.body?.includes(marker))) { + return console.log(`Notification already exists for #${issue.number}`); + } + + // Post notification + const success = await notifyTeam(github, owner, repo, issue, message, marker); + + if (success) { + console.log('=== Summary ==='); + console.log(`Repository: ${owner}/${repo}`); + console.log(`Issue Number: ${issue.number}`); + console.log(`Label: ${labelName}`); + console.log(`Message: ${message}`); + } + + } catch (err) { + console.log('❌ Error:', err.message); + } +}; \ No newline at end of file diff --git a/.github/scripts/inactivity_bot.sh b/.github/scripts/inactivity_bot.sh old mode 100644 new mode 100755 index 25e8e449e..ecb43babb --- a/.github/scripts/inactivity_bot.sh +++ b/.github/scripts/inactivity_bot.sh @@ -72,9 +72,9 @@ for ISSUE in $ISSUES; do echo " [INFO] Issue created at: ${ISSUE_CREATED_AT:-(unknown)}" echo - # Fetch timeline once (used for assignment events + PR links). - TIMELINE=$(gh api -H "Accept: application/vnd.github.mockingbird-preview+json" "repos/$REPO/issues/$ISSUE/timeline" 2>/dev/null || echo "[]") - TIMELINE=${TIMELINE:-'[]'} # defensive default + # Fetch full timeline with pagination and flatten array + TIMELINE=$(gh api --paginate -H "Accept: application/vnd.github.mockingbird-preview+json" "repos/$REPO/issues/$ISSUE/timeline" 2>/dev/null | jq -s 'add' || echo "[]") + TIMELINE=${TIMELINE:-'[]'} if [[ -z "${ASSIGNEES// }" ]]; then echo " [INFO] No assignees for this issue, skipping." @@ -93,15 +93,18 @@ for ISSUE in $ISSUES; do ASSIGNED_AT=$(echo "$ASSIGN_EVENT_JSON" | jq -r '.created_at // empty') ASSIGN_SOURCE="assignment_event" else - ASSIGNED_AT="${ISSUE_CREATED_AT:-}" - ASSIGN_SOURCE="issue_created_at (no explicit assignment event)" + # FIX: Do not fallback to issue creation date + ASSIGNED_AT="" + ASSIGN_SOURCE="not_found" fi if [[ -n "$ASSIGNED_AT" ]]; then ASSIGNED_TS=$(parse_ts "$ASSIGNED_AT") ASSIGNED_AGE_DAYS=$(( (NOW_TS - ASSIGNED_TS) / 86400 )) else - ASSIGNED_AGE_DAYS=0 + # Safety valve: if assignment event is missing, skip checking to prevent false positives + echo " [WARN] Could not find 'assigned' event in timeline. Skipping inactivity check for safety." + continue fi echo " [INFO] Assignment source: $ASSIGN_SOURCE" @@ -125,7 +128,6 @@ for ISSUE in $ISSUES; do echo " [RESULT] Phase 1 -> stale assignment (>= $DAYS days, no PR)" if (( DRY_RUN == 0 )); then - # NOTE: The EOF must be at the very start of the line! MESSAGE=$(cat < PR #$PR_NUM is stale (>= $DAYS days since last commit)" if (( DRY_RUN == 0 )); then - # NOTE: The EOF must be at the very start of the line! MESSAGE=$(cat </dev/null 2>&1; then + date -d "$ts" +%s # GNU date (Linux) + else + date -j -f "%Y-%m-%dT%H:%M:%SZ" "$ts" +"%s" # macOS/BSD + fi +} + +# Fetch open ISSUES (not PRs) that have assignees +ISSUES=$(gh api "repos/$REPO/issues" \ + --paginate \ + --jq '.[] | select(.state=="open" and (.assignees | length > 0) and (.pull_request | not)) | .number') + +if [ -z "$ISSUES" ]; then + echo "No open issues with assignees found." + exit 0 +fi + +for ISSUE in $ISSUES; do + echo "============================================================" + echo " ISSUE #$ISSUE" + echo "============================================================" + + ISSUE_JSON=$(gh api "repos/$REPO/issues/$ISSUE") + ASSIGNEES=$(echo "$ISSUE_JSON" | jq -r '.assignees[].login') + + if [ -z "$ASSIGNEES" ]; then + echo "[INFO] No assignees? Skipping." + echo + continue + fi + + echo "[INFO] Assignees: $ASSIGNEES" + echo + + # Check if this issue already has a reminder comment from ReminderBot + EXISTING_COMMENT=$(gh api "repos/$REPO/issues/$ISSUE/comments" \ + --jq ".[] | select(.user.login == \"github-actions[bot]\") | select(.body | contains(\"ReminderBot\")) | .id" \ + | head -n1) + + if [ -n "$EXISTING_COMMENT" ]; then + echo "[INFO] Reminder comment already posted on this issue." + echo + continue + fi + + # Get assignment time (use the last assigned event) + ASSIGN_TS=$(gh api "repos/$REPO/issues/$ISSUE/events" \ + --jq ".[] | select(.event==\"assigned\") | .created_at" \ + | tail -n1) + + if [ -z "$ASSIGN_TS" ]; then + echo "[WARN] No assignment event found. Skipping." + continue + fi + + ASSIGN_TS_SEC=$(parse_ts "$ASSIGN_TS") + DIFF_DAYS=$(( (NOW_TS - ASSIGN_TS_SEC) / 86400 )) + + echo "[INFO] Assigned at: $ASSIGN_TS" + echo "[INFO] Days since assignment: $DIFF_DAYS" + + # Check if any open PRs are linked to this issue + PR_NUMBERS=$(gh api \ + -H "Accept: application/vnd.github.mockingbird-preview+json" \ + "repos/$REPO/issues/$ISSUE/timeline" \ + --jq ".[] + | select(.event == \"cross-referenced\") + | select(.source.issue.pull_request != null) + | .source.issue.number" 2>/dev/null || true) + + OPEN_PR_FOUND="" + if [ -n "$PR_NUMBERS" ]; then + for PR_NUM in $PR_NUMBERS; do + PR_STATE=$(gh pr view "$PR_NUM" --repo "$REPO" --json state --jq '.state' 2>/dev/null || true) + if [ "$PR_STATE" = "OPEN" ]; then + OPEN_PR_FOUND="$PR_NUM" + break + fi + done + fi + + if [ -n "$OPEN_PR_FOUND" ]; then + echo "[KEEP] An OPEN PR #$OPEN_PR_FOUND is linked to this issue β†’ skip reminder." + echo + continue + fi + + echo "[RESULT] No OPEN PRs linked to this issue." + + # Check if threshold has been reached + if [ "$DIFF_DAYS" -lt "$DAYS" ]; then + echo "[WAIT] Only $DIFF_DAYS days (< $DAYS) β†’ not yet time for reminder." + echo + continue + fi + + echo "[REMIND] Issue #$ISSUE assigned for $DIFF_DAYS days, posting reminder." + + # Post reminder comment + MESSAGE="Hi, this is ReminderBot. This issue has been assigned but has had no pull request created. Are you still planning on working on the issue? + +From the Python SDK Team" + + if [ "$DRY_RUN" = "true" ]; then + echo "[DRY RUN] Would post comment on issue #$ISSUE:" + echo "$MESSAGE" + else + gh issue comment "$ISSUE" --repo "$REPO" --body "$MESSAGE" + echo "[DONE] Posted reminder comment on issue #$ISSUE." + fi + echo +done + +echo "------------------------------------------------------------" +echo " Issue Reminder Bot (No PR) complete." +echo "------------------------------------------------------------" diff --git a/.github/scripts/p0_issues_notify_team.js b/.github/scripts/p0_issues_notify_team.js new file mode 100644 index 000000000..8958994c2 --- /dev/null +++ b/.github/scripts/p0_issues_notify_team.js @@ -0,0 +1,57 @@ +// Script to notify the team when a P0 issue is created. + +const marker = ''; + + async function notifyTeam(github, owner, repo, issue, marker) { + const comment = `${marker} :rotating_light: Attention Team :rotating_light: +@hiero-ledger/hiero-sdk-python-maintainers @hiero-ledger/hiero-sdk-python-committers @hiero-ledger/hiero-sdk-python-triage + +A new P0 issue has been created: #${issue.number} - ${issue.title || '(no title)'} +Please prioritize this issue accordingly. + +Best Regards, +Automated Notification System`; + + try { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issue.number, + body: comment, + }); + console.log(`Notified team about P0 issue #${issue.number}`); + return true; + } catch (commentErr) { + console.log(`Failed to notify team about P0 issue #${issue.number}:`, commentErr.message || commentErr); + return false; + } + } + +module.exports = async ({ github, context }) => { + try { + const { owner, repo } = context.repo; + const { issue, label } = context.payload; + + // Validations + if (!issue?.number) return console.log('No issue in payload'); + if (label?.name?.toLowerCase() !== 'p0') return; + if (!issue.labels?.some(l => l?.name?.toLowerCase() === 'p0')) return; + + // Check for existing comment + const comments = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number: issue.number, per_page: 100 + }); + if (comments.some(c => c.body?.includes(marker))) { + return console.log(`Notification already exists for #${issue.number}`); + } + // Post notification + await notifyTeam(github, owner, repo, issue, marker); + + console.log('=== Summary ==='); + console.log(`Repository: ${owner}/${repo}`); + console.log(`Issue Number: ${issue.number}`); + console.log(`Issue Title: ${issue.title || '(no title)'}`); + } catch (err) { + console.log('❌ Error:', err.message); + } +}; \ No newline at end of file diff --git a/.github/workflows/bot-gfi-notify-team.yml b/.github/workflows/bot-gfi-notify-team.yml new file mode 100644 index 000000000..22f3ae2a1 --- /dev/null +++ b/.github/workflows/bot-gfi-notify-team.yml @@ -0,0 +1,37 @@ +# This workflow notifies the GFI support team when an issue is labeled as a GFI or GFI Candidate. +name: GFI Issue Notification +on: + issues: + types: + - labeled + +permissions: + issues: write + contents: read + +jobs: + gfi_notify_team: + runs-on: ubuntu-latest + if: > + (github.event_name == 'issues' && ( + github.event.label.name == 'Good First Issue' || + github.event.label.name == 'Good First Issue Candidate' + )) + + steps: + - name: Harden the runner + uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1 + + - name: Notify team of GFI issues + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 + with: + script: | + const script = require('./.github/scripts/gfi_notify_team.js'); + await script({ github, context}); \ No newline at end of file diff --git a/.github/workflows/bot-issue-reminder-no-pr.yml b/.github/workflows/bot-issue-reminder-no-pr.yml new file mode 100644 index 000000000..0c945d951 --- /dev/null +++ b/.github/workflows/bot-issue-reminder-no-pr.yml @@ -0,0 +1,39 @@ +name: bot-issue-reminder-no-pr + +on: + workflow_dispatch: + inputs: + dry_run: + description: "Dry run (log only, do not post comments)" + required: false + default: false + type: boolean + schedule: + - cron: "*/5 * * * *" + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + reminder: + runs-on: ubuntu-latest + + steps: + - name: Harden the runner + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #6.0.1 + + - name: Post reminder on assigned issues with no PRs + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + DAYS: 0 + DRY_RUN: false + # DRY_RUN: ${{ inputs.dry_run } + run: bash .github/scripts/issue_reminder_no_pr.sh diff --git a/.github/workflows/merge-conflict-bot.yml b/.github/workflows/bot-merge-conflict.yml similarity index 100% rename from .github/workflows/merge-conflict-bot.yml rename to .github/workflows/bot-merge-conflict.yml diff --git a/.github/workflows/bot-office-hours.yml b/.github/workflows/bot-office-hours.yml index 2a8cda84a..7ffed6248 100644 --- a/.github/workflows/bot-office-hours.yml +++ b/.github/workflows/bot-office-hours.yml @@ -37,9 +37,9 @@ jobs: echo "Meeting week detected. Proceeding to notify open PRs." REPO="${{ github.repository }}" - PR_LIST=$(gh pr list --repo $REPO --state open --json number --jq '.[].number') + PR_DATA=$(gh pr list --repo $REPO --state open --json number,author,createdAt) - if [ -z "$PR_LIST" ]; then + if [ -z "$PR_DATA" ] || [ "$PR_DATA" = "[]" ]; then echo "No open PRs found." exit 0 fi @@ -59,8 +59,8 @@ jobs: EOF ) - for PR_NUM in $PR_LIST; do - echo "Processing PR #$PR_NUM" + echo "$PR_DATA" | jq -r 'group_by(.author.login) | .[] | sort_by(.createdAt) | reverse | .[0] | .number' | while read PR_NUM; do + echo "Processing most recent PR #$PR_NUM for user" ALREADY_COMMENTED=$(gh pr view $PR_NUM --repo $REPO --json comments --jq '.comments[].body' | grep -F "Office Hour Bot" || true) diff --git a/.github/workflows/bot-p0-issues-notify-team.yml b/.github/workflows/bot-p0-issues-notify-team.yml new file mode 100644 index 000000000..93f8398ef --- /dev/null +++ b/.github/workflows/bot-p0-issues-notify-team.yml @@ -0,0 +1,38 @@ +# This workflow warns the team about P0 issues immediately when they are created. +name: P0 Issue Alert +on: + + issues: + types: + - labeled + +permissions: + issues: write + contents: read + +jobs: + p0_notify_team: + runs-on: ubuntu-latest + # Only run for issues labeled with 'p0' (case-insensitive) + if: > + (github.event_name == 'issues' && ( + (github.event.label.name == 'p0' || github.event.label.name == 'P0')) + ) + + steps: + - name: Harden the runner + uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1 + + - name: Notify team of P0 issues + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 + with: + script: | + const script = require('./.github/scripts/p0_issues_notify_team.js'); + await script({ github, context}); \ No newline at end of file diff --git a/.github/workflows/bot-pr-auto-draft-on-changes.yml b/.github/workflows/bot-pr-auto-draft-on-changes.yml new file mode 100644 index 000000000..619a3a5c3 --- /dev/null +++ b/.github/workflows/bot-pr-auto-draft-on-changes.yml @@ -0,0 +1,115 @@ +name: PythonBot - Auto Draft on Changes Requested + +on: + pull_request_review: + types: [submitted] + +permissions: + contents: read + pull-requests: write + +concurrency: + group: "auto-draft-${{ github.event.pull_request.number }}" + cancel-in-progress: true + +jobs: + convert-to-draft: + if: ${{ github.event.review.state == 'changes_requested' }} + runs-on: ubuntu-latest + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 + with: + egress-policy: audit + + - name: Convert PR to draft and notify author + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + REVIEWBOT_TOKEN: ${{ secrets.REVIEWBOT_TOKEN || '' }} + with: + github-token: ${{ secrets.REVIEWBOT_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + const owner = context.repo.owner; + const repo = context.repo.repo; + const prNodeId = context.payload.pull_request.node_id; + const isDraft = context.payload.pull_request.draft; + + const marker = ''; + const usingCustomToken = !!process.env.REVIEWBOT_TOKEN; + console.log(`ReviewBot using ${usingCustomToken ? 'REVIEWBOT_TOKEN secret' : 'GITHUB_TOKEN'} for API calls.`); + + // Skip if already a draft + if (isDraft) { + console.log(`PR #${prNumber} is already a draft. Skipping conversion.`); + return; + } + + // Convert to draft using GraphQL (REST API doesn't support this) + let convertedToDraft = false; + try { + await github.graphql(` + mutation($pullRequestId: ID!) { + convertPullRequestToDraft(input: { pullRequestId: $pullRequestId }) { + pullRequest { + isDraft + } + } + } + `, { pullRequestId: prNodeId }); + console.log(`Converted PR #${prNumber} to draft.`); + convertedToDraft = true; + } catch (err) { + console.log(`Failed to convert PR #${prNumber} to draft:`, err.message || err); + if (!usingCustomToken) { + console.log('Hint: Add a repo secret named REVIEWBOT_TOKEN with "repo" scope so ReviewBot can convert PRs from forked branches.'); + } + // Continue to post comment even if conversion fails + } + + // Check for existing bot comment + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number: prNumber, + per_page: 100, + }); + + const existingComment = comments.find(c => c.body && c.body.includes(marker)); + if (existingComment) { + console.log(`PR #${prNumber} already has a ReviewBot comment. Skipping.`); + return; + } + + // Post the notification comment + let comment; + if (convertedToDraft) { + comment = `${marker} + Hi @${context.payload.pull_request.user.login}, this is **ReviewBot**. + + I moved this PR to **draft** after a reviewer requested changes. + + Please push your updates and click **"Ready for review"** when it's readyβ€”this helps us focus on PRs that are prepared for another pass. + + Thanks! β€” The Hiero Python SDK Team`; + } else { + comment = `${marker} + Hi @${context.payload.pull_request.user.login}, this is **ReviewBot**. + + A reviewer requested changes, but I couldn't switch this PR to **draft** automatically. + + Please set it to draft while you work and click **"Ready for review"** once it's ready. + + Maintainers: add a **REVIEWBOT_TOKEN** secret (with \`repo\` scope) so I can handle this automatically next time. + + Thanks! β€” The Hiero Python SDK Team`; + } + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: prNumber, + body: comment, + }); + console.log(`Posted ReviewBot comment on PR #${prNumber}.`); diff --git a/.github/workflows/pr-inactivity-reminder-bot.yml b/.github/workflows/bot-pr-inactivity-reminder.yml similarity index 100% rename from .github/workflows/pr-inactivity-reminder-bot.yml rename to .github/workflows/bot-pr-inactivity-reminder.yml diff --git a/.github/workflows/pr-check-codecov.yml b/.github/workflows/pr-check-codecov.yml new file mode 100644 index 000000000..5ec92de3e --- /dev/null +++ b/.github/workflows/pr-check-codecov.yml @@ -0,0 +1,48 @@ +name: Code Coverage + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + coverage: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 2 + + - name: Set up Python + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: "3.11" + + - name: Install uv + uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 + + - name: Install dependencies + run: | + uv sync --all-extras --dev + + - name: Generate Proto Files + run: uv run python generate_proto.py + + - name: Run unit tests and generate coverage report + run: | + uv run pytest tests/unit \ + --cov=src \ + --cov-report=xml \ + --cov-report=term-missing + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.xml + fail_ci_if_error: true diff --git a/.github/workflows/examples.yml b/.github/workflows/pr-check-examples.yml similarity index 96% rename from .github/workflows/examples.yml rename to .github/workflows/pr-check-examples.yml index 11aa315a7..3e8c88cd9 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/pr-check-examples.yml @@ -24,7 +24,7 @@ jobs: fetch-depth: 0 - name: Install uv - uses: astral-sh/setup-uv@1781c6eb42f98d67097986d30130a8ff6879fda2 + uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 - name: Install dependencies run: uv sync diff --git a/.github/workflows/pr-check-test-files.yml b/.github/workflows/pr-check-test-files.yml new file mode 100644 index 000000000..36a2e90b4 --- /dev/null +++ b/.github/workflows/pr-check-test-files.yml @@ -0,0 +1,34 @@ +name: Test Files Naming Check + +on: + push: + branches-ignore: + - main + +permissions: + contents: read + +concurrency: + group: pr-checks-${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: true + +jobs: + check-test-files: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + + - name: Set up Hiero SDK Upstream + run: | + git remote set-url origin https://github.com/hiero-ledger/hiero-sdk-python.git + + - name: Fetch main branch + run: | + git fetch origin main + + - name: Check added test file names + run: | + chmod +x .github/scripts/check_test_files.sh + .github/scripts/check_test_files.sh diff --git a/.github/workflows/test.yml b/.github/workflows/pr-check-test.yml similarity index 98% rename from .github/workflows/test.yml rename to .github/workflows/pr-check-test.yml index 455892104..a11958bfe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/pr-check-test.yml @@ -36,7 +36,7 @@ jobs: cache: "pip" - name: Install uv - uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 + uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 - name: Install setuptools wheel run: pip install --upgrade pip setuptools wheel diff --git a/.zencoder/rules/repo.md b/.zencoder/rules/repo.md new file mode 100644 index 000000000..ce51a642a --- /dev/null +++ b/.zencoder/rules/repo.md @@ -0,0 +1,67 @@ +--- +description: Repository Information Overview +alwaysApply: true +--- + +# hiero-sdk-python Information + +## Summary +A Hiero SDK in pure Python for interacting with the Hedera Hashgraph platform. It allows developers to interact with the Hedera network, including managing accounts, tokens, smart contracts, and files. + +## Structure +- **src/hiero_sdk_python**: Main source code for the SDK, organized by functionality (account, contract, token, etc.). +- **tests**: Contains `unit` and `integration` tests. +- **examples**: Ready-to-run example scripts demonstrating various SDK operations. +- **docs**: Documentation for SDK users and developers. +- **.github**: CI/CD workflows and templates. + +## Language & Runtime +**Language**: Python +**Version**: >=3.10, <3.14 +**Build System**: pdm (backend), setuptools +**Package Manager**: pip, uv (for dev/tests) + +## Dependencies +**Main Dependencies**: +- grpcio-tools +- protobuf +- grpcio +- cryptography +- python-dotenv +- requests +- pycryptodome +- eth-abi +- rlp +- eth-keys + +**Development Dependencies**: +- pytest +- ruff +- mypy +- typing-extensions + +## Build & Installation +```bash +# Install from PyPI +pip install hiero-sdk-python + +# Install for development +pip install -e . +``` + +## Testing + +**Framework**: pytest +**Test Location**: `tests/` (split into `unit` and `integration`) +**Configuration**: `pytest.ini`, `pyproject.toml` + +**Run Command**: + +```bash +uv run pytest +``` + +## Validation + +**Linting**: ruff, mypy +**Configuration**: `pyproject.toml`, `mypy.ini` diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c294aa9a..69ca34163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,15 +6,21 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ## [Unreleased] - - ### Added +- Added `__str__` and `__repr__` methods to `AccountInfo` class for improved logging and debugging experience (#1098) +- Added Good First Issue (GFI) management and frequency documentation to clarify maintainer expectations and SDK-level GFI governance. +- Added SDK-level Good First Issue (GFI) guidelines for maintainers to clarify what qualifies as a good first issue. +- Codecov workflow +- Added unit tests for `key_format.py` to improve coverage. +- Fix inactivity bot execution for local dry-run testing. +- Added Good First Issue candidate guidelines documentation (`docs/maintainers/good_first_issue_candidate_guidelines.md`) and Good First Issues guidelines documentation (`docs/maintainers/good_first_issues_guidelines.md`) (#1066) - Added documentation: "Testing GitHub Actions using Forks" (`docs/sdk_developers/training/testing_forks.md`). - Unified the inactivity-unassign bot into a single script with `DRY_RUN` support, and fixed handling of cross-repo PR references for stale detection. - Added unit tests for `SubscriptionHandle` class covering cancellation state, thread management, and join operations. - Refactored `account_create_transaction_create_with_alias.py` example by splitting monolithic function into modular functions: `generate_main_and_alias_keys()`, `create_account_with_ecdsa_alias()`, `fetch_account_info()`, `print_account_summary()` (#1016) -- +- Added `.github/workflows/bot-pr-auto-draft-on-changes.yml` to automatically convert PRs to draft and notify authors when reviewers request changes. +- - Modularized `transfer_transaction_fungible` example by introducing `account_balance_query()` & `transfer_transaction()`.Renamed `transfer_tokens()` β†’ `main()` - Phase 2 of the inactivity-unassign bot: Automatically detects stale open pull requests (no commit activity for 21+ days), comments with a helpful InactivityBot message, closes the stale PR, and unassigns the contributor from the linked issue. - Added `__str__()` to CustomFixedFee and updated examples and tests accordingly. @@ -33,23 +39,63 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Support selecting specific node account ID(s) for queries and transactions and added `Network._get_node()` with updated execution flow (#362) - Add TLS support with two-stage control (`set_transport_security()` and `set_verify_certificates()`) for encrypted connections to Hedera networks. TLS is enabled by default for hosted networks (mainnet, testnet, previewnet) and disabled for local networks (solo, localhost) (#855) - Add PR inactivity reminder bot for stale pull requests `.github/workflows/pr-inactivity-reminder-bot.yml` -- Add comprehensive training documentation for _Executable class `docs/sdk_developers/training/executable.md` +- Add comprehensive training documentation for \_Executable class `docs/sdk_developers/training/executable.md` - Added empty `docs/maintainers/good_first_issues.md` file for maintainers to write Good First Issue guidelines (#1034) +- Added new `.github/ISSUE_TEMPLATE/04_good_first_issue_candidate.yml` file (1068)(https://github.com/hiero-ledger/hiero-sdk-python/issues/1068) +- Enhanced `.github/ISSUE_TEMPLATE/01_good_first_issue.yml` with welcoming message and acceptance criteria sections to guide contributors in creating quality GFIs (#1052) +- Add workflow to notify team about P0 issues `bot-p0-issues-notify-team.yml` +- Added Issue Reminder (no-PR) bot, .github/scripts/issue_reminder_no_pr.sh and .github/workflows/bot-issue-reminder-no-pr.yml to automatically detect assigned issues with no linked pull requests for 7+ days and post a gentle ReminderBot comment.(#951) +- Add support for include_children in TransactionGetReceiptQuery (#1100)(https://github.com/hiero-ledger/hiero-sdk-python/issues/1100) +- Add new `.github/ISSUE_TEMPLATE/05_intermediate_issue.yml` file (1072)(https://github.com/hiero-ledger/hiero-sdk-python/issues/1072) +- Add a workflow to notify the team when issues are labeled as β€œgood first issues” or identified as candidates for that label: `bot-gfi-notify-team.yml`(#1115) +- Added **str** and **repr** to AccountBalance +- Added GitHub workflow that makes sure newly added test files follow pytest test files naming conventions (#1054) +- Added advanced issue template for contributors `.github/ISSUE_TEMPLATE/06_advanced_issue.yml`. +- Add new tests to `tests/unit/topic_info_query_test.py` (#1124) ### Changed +- Reduce office-hours reminder spam by posting only on each user's most recent open PR, grouping by author and sorting by creation time (#1121) +- Pylint cleanup for token_airdrop_transaction_cancel.py (#1081) [@tiya-15](https://github.com/tiya-15) +- Move `account_allowance_delete_transaction_hbar.py` from `examples/` to `examples/account/` for better organization (#1003) +- Added `add_hbar_transfer`, `add_approved_hbar_transfer`, and internal `_add_hbar_transfer` to accept `Hbar` objects in addition to raw tinybar integers, with internal normalization to tinybars. Added tests validating the new behavior. +- Improved consistency of transaction examples (#1120) +- Refactored `account_create_transaction_with_fallback_alias.py` by splitting the monolithic `create_account_with_fallback_alias` function into modular functions: `generate_fallback_key`, `fetch_account_info`, and `print_account_summary`. The existing `setup_client()` function was reused for improved readability and structure (#1018) +- Allow `PublicKey` for batch_key in `Transaction`, enabling both `PrivateKey` and `PublicKey` for batched transactions - Allow `PublicKey` for `TokenUpdateKeys` in `TokenUpdateTransaction`, enabling non-custodial workflows where operators can build transactions using only public keys (#934). - Bump protobuf toml to protobuf==6.33.2 +- chore: Move account allowance example to correct folder - Added more tests to the CustomFee class for different functionalities (#991) - Changed messaged for test failure summaries so it is clearer by extracting test failure names into summary - Renamed example files to match src naming (#1053) +- Updated bot naming conventions in `.github/workflows` to be consistent (#1042) +- Renamed workflow files for consistent PR check naming: + `examples.yml` β†’ `pr-check-examples.yml`, + `test.yml` β†’ `pr-check-test.yml` (#1043) +- Cleaned up `token_airdrop_claim_auto` example for pylint compliance (no functional changes). (#1079) +- Formatted `examples/query` using black (#1082)(https://github.com/hiero-ledger/hiero-sdk-python/issues/1082) +- Update team notification script and workflow for P0 issues 'p0_issues_notify_team.js' +- Rename test files across the repository to ensure they consistently end with \_test.py (#1055) +- Cleaned up `token_airdrop_claim_signature_required` example for pylint compliance (no functional changes). (#1080) +- Rename the file 'test_token_fee_schedule_update_transaction_e2e.py' to make it ends with \_test.py as all other test files.(#1117) +- Format token examples with Black for consistent code style and improved readability (#1119) +- Transformed `examples/tokens/custom_fee_fixed.py` to be an end-to-end example, that interacts with the Hedera network, rather than a static object demo. +- Replaced `ResponseCode.get_name(receipt.status)` with the `ResponseCode(receipt.status).name` across examples and integration tests for consistency. (#1136) +- Moved helpful references to Additional Context section and added clickable links. +- Transformed `examples\tokens\custom_royalty_fee.py` to be an end-to-end example, that interacts with the Hedera network, rather than a static object demo. ### Fixed +- Reverted validation logic in `AbstractTokenTransferTransaction` to correctly handle zero amounts with `ValueError` and message "Amount must be a non-zero integer". + +- Fix token association verification in `token_airdrop_transaction.py` to correctly check if tokens are associated by using `token_id in token_balances` instead of incorrectly displaying zero balances which was misleading (#[815]) - Fixed inactivity bot workflow not checking out repository before running (#964) - Fixed the topic_message_query integarion test - good first issue template yaml rendering - Fixed solo workflow defaulting to zero +- Fix unit test tet_query.py +- TLS Hostname Mismatch & Certificate Verification Failure for Nodes +- Workflow does not contain permissions for `pr-check-test-files` and `pr-check-codecov` ### Breaking Change @@ -67,7 +113,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Added `.github/workflows/merge-conflict-bot.yml` to automatically detect and notify users of merge conflicts in Pull Requests. - Added `.github/workflows/bot-office-hours.yml` to automate the Weekly Office Hour Reminder. - feat: Implement account creation with EVM-style alias transaction example. -- Added validation logic in `.github/workflows/pr-checks.yml` to detect when no new chnagelog entries are added under [Unreleased]. +- Added validation logic in `.github/workflows/pr-checks.yml` to detect when no new changelog entries are added under [Unreleased]. - Support for message chunking in `TopicSubmitMessageTransaction`. ### Changed @@ -82,6 +128,10 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - fixed workflow: changelog check with improved sensitivity to deletions, additions, new releases +### Breaking Changes + +- Changed error message in `TransferTransaction._add_hbar_transfer()` and `AbstractTokenTransferTransaction._add_token_transfer()` when amount is zero from "Amount must be a non-zero integer" to "Amount must be a non-zero value." for clarity and consistency. + ## [0.1.9] - 2025-11-26 ### Added @@ -616,3 +666,4 @@ contract_call_local_pb2.ContractLoginfo -> contract_types_pb2.ContractLoginfo ### Removed - N/A + diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..1c5dea4a5 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,13 @@ +# codecov.yml +coverage: + status: + project: + default: + target: 70% + patch: + default: + target: 80% + threshold: 1% + +comment: + layout: "reach, diff, flags" diff --git a/docs/GFI/GFI-Frequency.md b/docs/GFI/GFI-Frequency.md new file mode 100644 index 000000000..948542206 --- /dev/null +++ b/docs/GFI/GFI-Frequency.md @@ -0,0 +1,79 @@ +# Good First Issue (GFI) Frequency Guidelines + +## Purpose + +This document defines **how frequently Good First Issues (GFIs)** should be created and maintained for an SDK. + +Clear expectations around GFI frequency help: +- Maintain a healthy contributor pipeline +- Prevent contributor overload or abandonment +- Align GFI availability with maintainer capacity and review bandwidth + +These guidelines are **SDK-specific** and may differ across SDKs depending on team size, activity, and release cadence. + +--- + +## What Is GFI Frequency? + +**GFI frequency** refers to the number of open, actively maintained Good First Issues an SDK aims to support over time. + +This is not a hard requirement, but a **maintainer-defined target** that helps ensure: +- Issues labeled as GFI receive timely responses +- Contributors are not left waiting without feedback +- Maintainers do not overcommit beyond their review capacity + +--- + +## Recommended Frequency Models + +Maintainers may choose one of the following models, or define a custom approach that fits their SDK. + +### 1. Fixed Cadence + +A predictable schedule for creating or refreshing GFIs. + +**Examples:** +- One GFI per month +- One GFI every two weeks +- Two GFIs per week + +This model works well for SDKs with: +- Stable maintainer availability +- Regular contributor interest +- Predictable release cycles + +--- + +### 2. Capacity-Based (Burn Rate) + +GFIs are created based on **maintainer capacity**, not time. + +**Examples:** +- Maintain 1–3 open GFIs at any given time +- Only open a new GFI when an existing one is closed or inactive +- Limit GFIs to what can be reviewed within a defined SLA (e.g., 5 business days) + +This model works well for: +- Small maintainer teams +- Periods of high operational load +- SDKs with fluctuating contributor demand + +--- + +### 3. Hybrid Model + +A combination of cadence and capacity. + +**Examples:** +- One GFI per month, up to a maximum of 3 open GFIs +- Weekly GFI creation, paused when review backlog exceeds a threshold + +--- + +## SDK-Specific Declaration + +Each SDK is encouraged to **explicitly declare its GFI frequency policy**, for example: + +```text +This SDK aims to maintain up to two active Good First Issues at any given time, +subject to maintainer availability. diff --git a/docs/GFI/GFI-Guidelines.md b/docs/GFI/GFI-Guidelines.md new file mode 100644 index 000000000..e6a4e5758 --- /dev/null +++ b/docs/GFI/GFI-Guidelines.md @@ -0,0 +1,137 @@ +# Good First Issue (GFI) Guidelines + +## Purpose + +This document defines what qualifies as a **Good First Issue (GFI)** for SDKs in this repository. + +Good First Issues are intended to: +- Help new contributors get started successfully +- Reduce ambiguity for maintainers when labeling issues +- Provide **SDK-level clarity**, beyond organization- or project-level guidance + +While higher-level guidance exists (e.g., at the Hiero level), this document focuses specifically on **SDK expectations**. + +--- + +## What Is a Good First Issue? + +A Good First Issue should be: +- Narrow in scope +- Low risk +- Easy to test or validate +- Achievable without deep domain knowledge of the SDK + +Below are examples of changes that are generally considered good first issues. + +--- + +## Examples of Good First Issues + +### 1. Small, Focused Code Changes + +- Narrow changes or additions to existing `src` functionality +- Uses common Python skills +- Can be tested by adding to an existing test + +**Examples:** +- Adding or improving `__str__` functions +- Adding or improving `__repr__` functions +- Simple typing fixes (e.g., return type hints, resolving basic type conflicts) + +--- + +### 2. Refactoring Existing Examples + +- Separating existing examples into smaller helper functions +- Consolidating overly split examples into a single, clearer function +- Improving readability without changing behavior + +--- + +### 3. Documentation Improvements + +Documentation-only changes are excellent GFIs. + +**Examples:** +- Adding or improving module docstrings +- Adding or improving function docstrings +- Adding inline comments to clarify logic +- Improving or adding `print` statements for clarity in examples + +--- + +### 4. Improvements to Existing Examples + +- Adding small, incremental steps to examples to better demonstrate functionality +- Clarifying edge cases or expected outputs +- Improving naming or structure for educational purposes + +--- + +### 5. Test Enhancements (Limited Scope) + +- Small additions to **existing** unit or integration tests +- Improving clarity or coverage of an existing test + +--- + +## What Is *Not* a Good First Issue + +The following are **generally not considered Good First Issues**: + +- Creation of entirely new examples +- Creation of entirely new unit tests +- Creation of entirely new integration tests +- Large refactors +- Changes requiring deep architectural or domain knowledge +- Features that introduce new public APIs + +These may still be valuable contributions, but they are better suited for more experienced contributors. + +--- + +## SDK-Specific Guidelines + +Each SDK **may extend or refine** these guidelines based on its own needs. + +SDK maintainers are encouraged to: +- Add a short SDK-specific section +- Call out any exceptions or special cases +- Document SDK-specific tooling or constraints + +This approach allows: +- Consistent contributor experience across SDKs +- Easy updates without changing organization-wide guidance + +--- + +## Maintainer Responsibility + +Maintainers should: +- Label issues as `good first issue` only when they meet these guidelines +- Provide clear issue descriptions and acceptance criteria +- Include pointers to relevant files, examples, or tests when possible + +Clear labeling and descriptions significantly improve the contributor experience. + +--- + +## Updating These Guidelines + +These guidelines are expected to evolve. + +If you believe changes are needed: +- Open a discussion or pull request +- Propose SDK-specific additions rather than broad changes when possible + +--- + +## Summary + +Good First Issues should be: +- Small +- Clear +- Low-risk +- Educational + +When in doubt, favor issues that help contributors learn the SDK without overwhelming them. diff --git a/docs/GFI/GFI-Management.md b/docs/GFI/GFI-Management.md new file mode 100644 index 000000000..a97e72480 --- /dev/null +++ b/docs/GFI/GFI-Management.md @@ -0,0 +1,176 @@ +# Good First Issue (GFI) Management Guidelines + +## Purpose + +This document describes **how Good First Issues (GFIs)** should be managed once they are created. + +Clear management practices help: +- Ensure GFIs remain welcoming and actionable +- Provide consistent contributor experiences +- Balance maintainer workload with community engagement + +These guidelines focus on **maintainer responsibilities and discretion** and are intended to be applied at the **SDK level**. + +--- + +## What Is GFI Management? + +**GFI management** refers to the ongoing actions maintainers take to ensure that: +- Issues labeled as `good first issue` remain suitable for new contributors +- Contributors receive timely guidance and feedback +- GFIs do not become stale, abandoned, or misleading + +Management is not micromanagement; it is **lightweight stewardship**. + +--- + +## GFI Lifecycle Overview + +A Good First Issue typically moves through the following stages: + +1. Identified as a potential GFI (GFIC β€” Good First Issue Candidate) +2. Reviewed and labeled as a GFI +3. Assigned or claimed by a contributor +4. Actively supported through contribution +5. Completed, relabeled, or closed + +Not all GFIs must follow this exact flow, but clarity around expectations helps contributors succeed. + +--- + +## Labeling and Promotion (GFIC β†’ GFI) + +Maintainers may use an intermediate **Good First Issue Candidate (GFIC)** state. + +### Recommended Practices + +- Use a `good first issue candidate` (or similar) label to flag potential GFIs +- Promote an issue from GFIC β†’ GFI once: + - Scope and acceptance criteria are clear + - The issue meets `GFI-guidelines.md` + - Maintainer capacity exists to support it + +Maintainers are not required to promote every GFIC to a GFI. + +--- + +## Assignment and Claiming + +Maintainers may choose how contributors engage with GFIs. + +### Common Approaches + +- Allow contributors to self-assign +- Assign first-time contributors upon request +- Limit the number of simultaneous GFI assignments per contributor + +### Recommended Guidance + +- Prefer assigning **one contributor per GFI** +- Avoid long-term assignment without activity +- Reclaim or unassign issues after prolonged inactivity, with a clear comment + +This ensures fair access and avoids stalled issues. + +--- + +## Mentorship and Support + +Mentorship is encouraged but **not mandatory**. + +### Examples of Supportive Management + +- Answering clarification questions +- Pointing contributors to relevant files or examples +- Clarifying acceptance criteria +- Suggesting incremental approaches + +### What Is *Not* Expected + +- Writing the solution for the contributor +- Providing extensive code reviews beyond GFI scope +- Guaranteeing real-time or synchronous support + +Light guidance is often sufficient for a successful first contribution. + +--- + +## Review Expectations (β€œSoft Review”) + +Maintainers may choose to apply a **lighter review standard** for GFIs. + +### Soft Review May Include + +- Prioritizing clarity and correctness over optimization +- Offering constructive, educational feedback +- Allowing minor follow-up improvements after merge + +### Still Required + +- Correctness +- Tests where applicable +- Compliance with project standards + +GFIs should be welcoming, not lower-quality. + +--- + +## Maintainer Discretion and Control + +Maintainers retain full control over: + +- Labeling or removing the `good first issue` label +- Assignment decisions +- Pausing or closing GFIs +- Adjusting management practices over time + +Discretion allows maintainers to adapt to workload, contributor behavior, and project priorities. + +--- + +## Transparency and Community Engagement + +Maintainers are encouraged to: +- Leave brief comments when relabeling or closing GFIs +- Explain pauses or changes in availability +- Encourage contributors to ask questions + +Transparency builds trust and helps the community understand expectations. + +--- + +## Updating GFI Management Practices + +This document is intended to be **easy to update**. + +Maintainers may: +- Adjust management practices at any time +- Experiment with different levels of mentorship or review +- Align management with `GFI-frequency.md` and maintainer capacity + +Updates should be treated as **process guidance**, not rigid policy. + +--- + +## Relationship to Other GFI Documents + +This document complements: +- `GFI-guidelines.md` β€” what qualifies as a GFI +- `GFI-frequency.md` β€” how many GFIs to support +- Issue templates and labeling conventions + +Together, these documents provide a complete view of: +- GFI definition +- GFI availability +- GFI stewardship + +--- + +## Summary + +Effective GFI management is: +- Supportive +- Sustainable +- Transparent + +A well-managed Good First Issue benefits contributors, maintainers, and the SDK as a whole. diff --git a/docs/maintainers/good_first_issue_candidate_guidelines.md b/docs/maintainers/good_first_issue_candidate_guidelines.md new file mode 100644 index 000000000..d230aa647 --- /dev/null +++ b/docs/maintainers/good_first_issue_candidate_guidelines.md @@ -0,0 +1,199 @@ +# Good First Issue β€” Candidate Guidelines + +This document explains the purpose of the **`good first issue: candidate`** label, when to use it, and when an issue should be promoted to a full **Good First Issue**. + +## Table of Contents + +- [Why We Use a "Candidate" Label](#-why-we-use-a-candidate-label) +- [When to Use the Candidate Label](#️-when-to-use-good-first-issue-candidate) +- [What a Candidate Is NOT](#-what-a-candidate-is-not) +- [Promoting a Candidate to GFI](#-promoting-a-candidate-to-gfi) +- [Workflow Summary](#-workflow-summary) +- [Important Considerations](#important-considerations) + +--- + +## 🎯 Why We Use a "Candidate" Label + +Labeling an issue as a **Good First Issue (GFI)** signals to new contributors that the issue is: + +- βœ… **Well-scoped** β€” clear boundaries and deliverables +- βœ… **Low risk** β€” minimal chance of breaking changes +- βœ… **Clearly defined** β€” unambiguous requirements +- βœ… **Ready to be picked up** β€” with minimal guidance needed + +However, **not all issues start in that state**. + +The **`good first issue: candidate`** label exists to: + +| Purpose | Description | +|---------|-------------| +| 🚫 **Avoid premature labeling** | Prevent issues from being labeled as GFIs before they're ready | +| πŸ” **Allow refinement time** | Give maintainers space to clarify scope and requirements | +| πŸ“Š **Set accurate expectations** | Ensure new contributors know exactly what to do | +| πŸ“‹ **Create a clear pipeline** | Establish a workflow for curating high-quality GFIs | + +This approach helps us prioritize **quality over quantity** when advertising beginner-friendly work. + +--- + +## 🏷️ When to Use `good first issue: candidate` + +Apply the **candidate** label when an issue: + +### βœ… Fits the General Criteria + +- *Might* be suitable as a GFI based on initial assessment +- Fits within the [allowed categories](./good_first_issues_guidelines.md#allowed-categories) of GFI work +- Appears to be small in scope and low risk + +### ⏳ Still Needs Work + +- **Needs clarification** β€” requirements are ambiguous or incomplete +- **Needs refinement** β€” scope could be narrowed or better defined +- **Needs confirmation** β€” maintainer review required to verify suitability +- **Needs acceptance criteria** β€” clear success conditions not yet defined + +### πŸ“ Example Scenarios + +| Scenario | Why Use Candidate? | +|----------|-------------------| +| User reports a documentation gap | Needs scoping to determine exact changes required | +| Bug in example code identified | Need to confirm it's isolated and straightforward to fix | +| Type annotation improvement suggested | Need to verify it doesn't affect runtime behavior | +| Test assertion missing | Need to confirm it extends existing tests only | + +--- + +## 🚦 What a Candidate Is NOT + +The **candidate** label should **NOT** be used for: + +### ❌ Large or Cross-Cutting Changes + +Issues that span multiple modules, packages, or require architectural understanding. + +### ❌ Core Protocol or SDK Logic + +Changes to: +- `to_proto` / `from_proto` methods +- Serialization/deserialization logic +- Network or wire-level behavior + +### ❌ Exploratory or Investigative Work + +Issues where the solution path is unclear or requires research. + +### ❌ Blocked Issues + +Issues that depend on external decisions, other PRs, or upstream changes. + +--- + +> ⚠️ **Important:** If an issue clearly does *not* meet GFI criteria, it should **not** be labeled as a candidate either. The candidate label is for issues that *might* qualify, not for issues that definitely won't. + +--- + +## ✨ Promoting a Candidate to GFI + +A candidate should be promoted to a full **Good First Issue** when: + +### Readiness Checklist + +- [ ] **Clear description** β€” the problem and solution are well-defined +- [ ] **Scoped appropriately** β€” changes are localized and low-risk +- [ ] **Acceptance criteria defined** β€” clear conditions for success +- [ ] **Documentation linked** β€” relevant guides are referenced +- [ ] **No blockers** β€” no dependencies on other work +- [ ] **Maintainer approved** β€” a maintainer has reviewed and confirmed suitability + +### Promotion Process + +1. **Review the candidate issue** against [GFI guidelines](./good_first_issues_guidelines.md) +2. **Add missing details** β€” clarify requirements, add acceptance criteria +3. **Remove `good first issue: candidate`** label +4. **Add `Good First Issue`** label +5. **Optionally notify** in comments that the issue is ready for contributors + +--- + +## πŸ“Š Workflow Summary + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Issue Created β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Initial Assessment by Maintainer β”‚ +β”‚ β”‚ +β”‚ Is this potentially a Good First Issue? β”‚ +β”‚ β”‚ +β”‚ β€’ Small scope? β”‚ +β”‚ β€’ Low risk? β”‚ +β”‚ β€’ Fits allowed categories? β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ β”‚ + β–Ό β–Ό β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ No β”‚ β”‚ Maybe β”‚ β”‚ Yes β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ β”‚ + β–Ό β–Ό β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Label normally β”‚ β”‚ Label as β”‚ β”‚ Label as β”‚ + β”‚ (not GFI) β”‚ β”‚ `candidate` β”‚ β”‚ Good First β”‚ + β”‚ β”‚ β”‚ β”‚ β”‚ Issue β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Refine & Review β”‚ + β”‚ β”‚ + β”‚ β€’ Add details β”‚ + β”‚ β€’ Define scope β”‚ + β”‚ β€’ Set criteria β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Promote to GFI β”‚ + β”‚ when ready β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Important Considerations + +### Why This Matters + +1. **Good First Issues are automatically promoted** by GitHub and Hiero, making them highly visible to potential contributors worldwide + +2. **New contributors trust the GFI label** β€” they expect issues to be ready and achievable + +3. **Poorly scoped GFIs waste contributor time** β€” and can discourage future contributions + +4. **Quality GFIs build community** β€” successful first contributions lead to long-term contributors + +### Best Practices + +| Do | Don't | +|----|-------| +| βœ… Use candidate for uncertain issues | ❌ Rush issues to GFI status | +| βœ… Take time to refine candidates | ❌ Label obviously unsuitable issues as candidates | +| βœ… Add clear acceptance criteria before promotion | ❌ Promote candidates without review | +| βœ… Link to relevant documentation | ❌ Assume contributors know the codebase | + +--- + +## Additional Resources + +- [Good First Issue Guidelines](./good_first_issues_guidelines.md) β€” what qualifies as a GFI +- [Contributing Guide](../../CONTRIBUTING.md) β€” how to contribute +- [DCO Signing Guide](../sdk_developers/signing.md) β€” commit signing requirements +- [Discord Community](../discord.md) β€” get help from the community +- [Community Calls](https://zoom-lfx.platform.linuxfoundation.org/meetings/hiero?view=week) β€” weekly office hours diff --git a/docs/maintainers/good_first_issues.md b/docs/maintainers/good_first_issues.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/maintainers/good_first_issues_guidelines.md b/docs/maintainers/good_first_issues_guidelines.md new file mode 100644 index 000000000..7e744ae79 --- /dev/null +++ b/docs/maintainers/good_first_issues_guidelines.md @@ -0,0 +1,249 @@ +# Good First Issue Guidelines + +This document defines what we **do** and **do not** consider a *Good First Issue (GFI)* in the Hiero Python SDK. + +## Table of Contents + +- [Purpose](#purpose) +- [Allowed Categories](#allowed-categories) + - [Small, Focused Source Changes](#-small-focused-source-changes) + - [Typing Improvements](#-typing-improvements) + - [Refactors of Existing Examples](#-refactors-of-existing-examples) + - [Documentation Improvements](#-documentation-improvements) + - [Print and Output Clarity](#️-print-and-output-clarity-examples-only) + - [Functional Improvements to Examples](#️-functional-improvements-to-examples) + - [Test Improvements](#-test-improvements-additive-only) +- [What We Do NOT Consider Good First Issues](#-what-we-do-not-consider-good-first-issues) +- [Maintainer Guidance](#-maintainer-guidance) + +--- + +## Purpose + +The goal of a Good First Issue is to: + +- **Help new contributors get onboarded successfully** β€” providing a clear, achievable starting point +- **Build confidence with a meaningful but low-risk contribution** β€” ensuring success without overwhelming complexity +- **Reduce maintainer overhead during first-time contributions** β€” making review and guidance straightforward + +These issues are intentionally: + +- βœ… Small +- βœ… Low risk +- βœ… Easy to review +- βœ… Safe for first-time contributors + +--- + +## Allowed Categories + +### 🧡 Small, Focused Source Changes + +Limited, localized changes to existing source files that do not alter public behavior or SDK contracts. + +#### Allowed + +- Adding or improving simple string helper functions +- Implementing or improving `__str__` or `__repr__` methods +- Fixing or clarifying edge cases in existing utility functions + +#### Examples + +- Improve formatting of a `__repr__` output +- Make a string helper more robust or readable +- Clarify handling of empty or `None` inputs in a utility function + +--- + +### 🧩 Typing Improvements + +Improvements to type annotations that increase correctness or clarity without changing runtime behavior. + +#### Allowed + +- Adding missing return type hints +- Fixing incorrect or overly broad type annotations +- Resolving basic type conflicts flagged by type checkers + +#### Examples + +- Change `-> Any` to a more specific return type +- Fix mismatched return types in conditional branches +- Tighten a `Dict[str, Any]` to a more precise type + +--- + +### πŸ”„ Refactors of Existing Examples + +Refactors that improve clarity, structure, or readability of **existing examples only**. + +#### Allowed + +- Refactoring an example for clarity or readability +- Extracting repeated logic into helper functions +- Renaming variables to be more descriptive + +#### Allowed Directions + +- Split a large example into smaller, named functions +- Combine a split example back into a single monolithic function for simplicity + +> ⚠️ **Note:** This category applies **only** to existing examples. +> Creating new examples is **out of scope** for GFIs. + +--- + +### πŸ“š Documentation Improvements + +Improvements to documentation that clarify intent or behavior without changing functionality. + +#### Includes + +- Module-level docstrings +- Function and method docstrings +- Inline comments that explain *why* (not what) code does something + +#### Examples + +- Clarify a confusing or outdated docstring +- Add explanation for non-obvious behavior +- Improve wording or structure for readability + +--- + +### πŸ–¨οΈ Print and Output Clarity (Examples Only) + +Improvements to output clarity in example code. + +#### Allowed + +- Improving clarity of `print()` statements +- Making output more descriptive or user-friendly +- Standardizing message formatting (prefixes, spacing, context) + +#### Examples + +- Replace ambiguous prints like `"Done"` with meaningful context +- Add explanatory text before printing values +- Make output ordering easier to follow + +--- + +### βš™οΈ Functional Improvements to Examples + +Small functional improvements that better illustrate **existing behavior** in examples. + +#### Allowed + +- Adding missing steps that improve understanding +- Improving ordering or structure of example code +- Clarifying error-handling paths + +#### Examples + +- Add an explicit setup step that was previously implied +- Improve error-handling clarity in an example +- Make control flow easier to follow + +--- + +### πŸ§ͺ Test Improvements (Additive Only) + +Small, additive improvements to **existing** tests. + +#### Allowed + +- Adding specific assertions to existing tests +- Extending tests to cover an obvious edge case +- Improving test names or failure messages + +#### Examples + +- Add an assertion for a previously untested branch +- Improve test failure messages for clarity + +> ⚠️ Tests must extend **existing test files**. +> Creating new test suites or frameworks is **out of scope**. + +--- + +## 🚫 What We Do NOT Consider Good First Issues + +The following types of changes are **explicitly out of scope** for GFIs. + +--- + +### ❌ New Examples + +- Creating entirely new examples +- Adding new example files or workflows + +These require deeper understanding of intended usage patterns. + +--- + +### ❌ New Unit or Integration Tests + +- Creating new test files +- Designing new test strategies or frameworks + +Test creation often requires broader architectural context. + +--- + +### ❌ Core DLT or Protocol Logic + +- Changes to `to_proto` / `from_proto` +- Modifying serialization or deserialization logic +- Any change affecting network or wire-level behavior + +These areas are sensitive and require domain expertise. + +--- + +### ❌ Cross-Cutting or Architectural Changes + +- Refactors spanning multiple modules or packages +- Changes requiring understanding of multiple subsystems +- Performance optimizations or concurrency changes + +These are better suited for experienced contributors. + +--- + +## πŸ“Œ Maintainer Guidance + +When evaluating whether to label an issue as a Good First Issue, consider: + +### Label as GFI if the issue: + +- βœ… Touches a **single file or module** +- βœ… Has **clear, well-defined scope** +- βœ… Requires **no domain or protocol knowledge** +- βœ… Can be **reviewed quickly** +- βœ… Has **low risk of breaking changes** + +### Do NOT label as GFI if the issue: + +- ❌ Touches **multiple subsystems** +- ❌ Changes **SDK behavior or contracts** +- ❌ Requires **domain or protocol knowledge** +- ❌ Could have **unintended side effects** +- ❌ Needs **extensive review or testing** + +### Important Reminders + +1. **Good First Issues are promoted automatically** by GitHub and Hiero, making them highly visible to new contributors +2. **Quality over quantity** β€” we prefer fewer, clearly safe GFIs over many ambiguous ones +3. **Clear acceptance criteria** β€” every GFI should have well-defined success conditions +4. **Link to documentation** β€” include relevant guides to help contributors succeed + +--- + +## Additional Resources + +- [Contributing Guide](../../CONTRIBUTING.md) +- [DCO Signing Guide](../sdk_developers/signing.md) +- [Changelog Entry Guide](../sdk_developers/changelog_entry.md) +- [Discord Community](../discord.md) +- [Community Calls](https://zoom-lfx.platform.linuxfoundation.org/meetings/hiero?view=week) diff --git a/docs/sdk_developers/testing.md b/docs/sdk_developers/testing.md index 679a69384..23bbe4ecc 100644 --- a/docs/sdk_developers/testing.md +++ b/docs/sdk_developers/testing.md @@ -110,29 +110,30 @@ from hiero_sdk_python.account.account_create_transaction import AccountCreateTra from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import IntegrationTestEnv +from tests.integration.utils import IntegrationTestEnv + @pytest.mark.integration def test_integration_account_create_transaction_can_execute(): - """Test that an account can be created on the network.""" - env = IntegrationTestEnv() - try: - new_account_private_key = PrivateKey.generate() - new_account_public_key = new_account_private_key.public_key() - initial_balance = Hbar(2) - - transaction = AccountCreateTransaction( - key=new_account_public_key, - initial_balance=initial_balance, - memo="Test Account" - ) - transaction.freeze_with(env.client) - receipt = transaction.execute(env.client) - - assert receipt.account_id is not None, "Account ID should be present" - assert receipt.status == ResponseCode.SUCCESS - finally: - env.close() + """Test that an account can be created on the network.""" + env = IntegrationTestEnv() + try: + new_account_private_key = PrivateKey.generate() + new_account_public_key = new_account_private_key.public_key() + initial_balance = Hbar(2) + + transaction = AccountCreateTransaction( + key=new_account_public_key, + initial_balance=initial_balance, + memo="Test Account" + ) + transaction.freeze_with(env.client) + receipt = transaction.execute(env.client) + + assert receipt.account_id is not None, "Account ID should be present" + assert receipt.status == ResponseCode.SUCCESS + finally: + env.close() ``` ### When to Write Integration Tests @@ -328,13 +329,13 @@ uv run pytest -m integration #### Run Specific Test File ```bash -uv run pytest tests/unit/test_hbar.py +uv run pytest tests/unit/hbar_test.py ``` #### Run Specific Test Function ```bash -uv run pytest tests/unit/test_hbar.py::test_hbar_conversion_to_tinybars +uv run pytest tests/unit/hbar_test.py::test_hbar_conversion_to_tinybars ``` #### Run Tests with Verbose Output @@ -457,7 +458,7 @@ You may look at an already-created unit test file for better clarity: ```bash # Run unit tests -uv run pytest tests/unit/tokens/test_token_transfer.py -v +uv run pytest tests/unit/tokens/token_transfer_test.py -v # Run integration tests uv run pytest tests/integration/token_transfer_e2e_test.py -v @@ -607,15 +608,16 @@ def test_with_fixtures(sample_account_id, sample_token_id): The `env` fixture from `utils_for_test.py` provides a configured test environment: ```python -from tests.integration.utils_for_test import env +from tests.integration.utils import env + @pytest.mark.integration def test_with_env_fixture(env): - """Test using the env fixture.""" - # env.client is already configured - # env.operator_id and env.operator_key are available - account = env.create_account() # Helper method - assert account.id is not None + """Test using the env fixture.""" + # env.client is already configured + # env.operator_id and env.operator_key are available + account = env.create_account() # Helper method + assert account.id is not None ``` #### 2. **Always Clean Up Resources** @@ -720,21 +722,22 @@ The `tests/integration/utils_for_test.py` file provides essential testing utilit #### IntegrationTestEnv Class ```python -from tests.integration.utils_for_test import IntegrationTestEnv, env +from tests.integration.utils import IntegrationTestEnv, env # Create environment manually env = IntegrationTestEnv() try: - # Use env.client, env.operator_id, env.operator_key - pass + # Use env.client, env.operator_id, env.operator_key + pass finally: - env.close() + env.close() + # Or use the pytest fixture (recommended) @pytest.mark.integration def test_example(env): - # env is automatically created and cleaned up - account = env.create_account() + # env is automatically created and cleaned up + account = env.create_account() ``` **Key Methods:** @@ -747,25 +750,26 @@ def test_example(env): #### Helper Functions ```python -from tests.integration.utils_for_test import ( - create_fungible_token, - create_nft_token, - env +from tests.integration.utils import ( + create_fungible_token, + create_nft_token, + env ) + @pytest.mark.integration def test_with_helpers(env): - # Create a fungible token with default settings - token_id = create_fungible_token(env) - - # Create an NFT token - nft_id = create_nft_token(env) - - # Use custom configuration with lambdas - token_id = create_fungible_token(env, [ - lambda tx: tx.set_decimals(8), - lambda tx: tx.set_initial_supply(1000000) - ]) + # Create a fungible token with default settings + token_id = create_fungible_token(env) + + # Create an NFT token + nft_id = create_nft_token(env) + + # Use custom configuration with lambdas + token_id = create_fungible_token(env, [ + lambda tx: tx.set_decimals(8), + lambda tx: tx.set_initial_supply(1000000) + ]) ``` ### Pytest Markers diff --git a/examples/account/account_allowance_approve_transaction_hbar.py b/examples/account/account_allowance_approve_transaction_hbar.py index ef63c1d46..f3ccbaca7 100644 --- a/examples/account/account_allowance_approve_transaction_hbar.py +++ b/examples/account/account_allowance_approve_transaction_hbar.py @@ -1,7 +1,5 @@ """ Example demonstrating hbar allowance approval and usage. -Run: -uv run examples/account/account_allowance_approve_transaction_hbar.py """ import os @@ -18,24 +16,25 @@ from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() -def setup_client(): - """Initialize and set up the client with operator account""" + +def setup_client() -> Client: + """Initialize and set up the client with operator account using env vars.""" network = Network(network_name) print(f"Connecting to Hedera {network_name} network!") client = Client(network) - operator_id = AccountId.from_string(os.getenv("OPERATOR_ID","")) - operator_key = PrivateKey.from_string(os.getenv("OPERATOR_KEY","")) + 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 -def create_account(client): - """Create an account""" +def create_account(client: Client): + """Create a new Hedera account with initial balance.""" account_private_key = PrivateKey.generate_ed25519() account_public_key = account_private_key.public_key() @@ -48,7 +47,10 @@ def create_account(client): ) if account_receipt.status != ResponseCode.SUCCESS: - print(f"Account creation failed with status: {ResponseCode(account_receipt.status).name}") + print( + "Account creation failed with status: " + f"{ResponseCode(account_receipt.status).name}" + ) sys.exit(1) account_account_id = account_receipt.account_id @@ -56,8 +58,13 @@ def create_account(client): return account_account_id, account_private_key -def approve_hbar_allowance(client, owner_account_id, spender_account_id, amount): - """Approve Hbar allowance for spender""" +def approve_hbar_allowance( + client: Client, + owner_account_id: AccountId, + spender_account_id: AccountId, + amount: Hbar, +): + """Approve Hbar allowance for spender.""" receipt = ( AccountAllowanceApproveTransaction() .approve_hbar_allowance(owner_account_id, spender_account_id, amount) @@ -65,60 +72,55 @@ def approve_hbar_allowance(client, owner_account_id, spender_account_id, amount) ) if receipt.status != ResponseCode.SUCCESS: - print(f"Hbar allowance approval failed with status: {ResponseCode(receipt.status).name}") + print( + "Hbar allowance approval failed with status: " + f"{ResponseCode(receipt.status).name}" + ) sys.exit(1) print(f"Hbar allowance of {amount} approved for spender {spender_account_id}") return receipt -def delete_hbar_allowance(client, owner_account_id, spender_account_id): - """Delete hbar allowance by setting amount to 0""" +def transfer_hbar_with_allowance( + client: Client, + owner_account_id: AccountId, + spender_account_id: AccountId, + spender_private_key: PrivateKey, + receiver_account_id: AccountId, + amount: Hbar, +): + """Transfer hbars using a previously approved allowance.""" receipt = ( - AccountAllowanceApproveTransaction() - .approve_hbar_allowance(owner_account_id, spender_account_id, Hbar(0)) + TransferTransaction() + # Transaction is paid for / initiated by spender + .set_transaction_id(TransactionId.generate(spender_account_id)) + .add_approved_hbar_transfer(owner_account_id, -amount.to_tinybars()) + .add_approved_hbar_transfer(receiver_account_id, amount.to_tinybars()) + .freeze_with(client) + .sign(spender_private_key) .execute(client) ) if receipt.status != ResponseCode.SUCCESS: - print(f"Hbar allowance deletion failed with status: {ResponseCode(receipt.status).name}") + print(f"Hbar transfer failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - print(f"Hbar allowance deleted for spender {spender_account_id}") - return receipt - - -def transfer_hbar_without_allowance(client, spender_account_id, spender_private_key, amount): - """Transfer hbars without allowance""" - print("Trying to transfer hbars without allowance...") - owner_account_id = client.operator_account_id - client.set_operator(spender_account_id, spender_private_key) # Set operator to spender - - receipt = ( - TransferTransaction() - .add_approved_hbar_transfer(owner_account_id, -amount.to_tinybars()) - .add_approved_hbar_transfer(spender_account_id, amount.to_tinybars()) - .execute(client) + print( + f"Successfully transferred {amount} from {owner_account_id} " + f"to {receiver_account_id} using allowance" ) - if receipt.status != ResponseCode.SPENDER_DOES_NOT_HAVE_ALLOWANCE: - print( - f"Hbar transfer should have failed with SPENDER_DOES_NOT_HAVE_ALLOWANCE " - f"status but got: {ResponseCode(receipt.status).name}" - ) - - print(f"Hbar transfer successfully failed with {ResponseCode(receipt.status).name} status") + return receipt -def hbar_allowance(): +def main(): """ Demonstrates hbar allowance functionality by: 1. Setting up client with operator account 2. Creating spender and receiver accounts 3. Approving hbar allowance for spender 4. Transferring hbars using the allowance - 5. Deleting the allowance - 6. Attempting to transfer hbars without allowance (should fail) """ client = setup_client() @@ -131,33 +133,20 @@ def hbar_allowance(): # Approve hbar allowance for spender allowance_amount = Hbar(2) + owner_account_id = client.operator_account_id - approve_hbar_allowance(client, client.operator_account_id, spender_id, allowance_amount) + approve_hbar_allowance(client, owner_account_id, spender_id, allowance_amount) # Transfer hbars using the allowance - receipt = ( - TransferTransaction() - .set_transaction_id(TransactionId.generate(spender_id)) - .add_approved_hbar_transfer(client.operator_account_id, -allowance_amount.to_tinybars()) - .add_approved_hbar_transfer(receiver_id, allowance_amount.to_tinybars()) - .freeze_with(client) - .sign(spender_private_key) - .execute(client) + transfer_hbar_with_allowance( + client=client, + owner_account_id=owner_account_id, + spender_account_id=spender_id, + spender_private_key=spender_private_key, + receiver_account_id=receiver_id, + amount=allowance_amount, ) - if receipt.status != ResponseCode.SUCCESS: - print(f"Hbar transfer failed with status: {ResponseCode(receipt.status).name}") - sys.exit(1) - - print(f"Successfully transferred {allowance_amount} from", end=" ") - print(f"{client.operator_account_id} to {receiver_id} using allowance") - - # Delete allowance - delete_hbar_allowance(client, client.operator_account_id, spender_id) - - # Try to transfer hbars without allowance - transfer_hbar_without_allowance(client, spender_id, spender_private_key, allowance_amount) - if __name__ == "__main__": - hbar_allowance() + main() diff --git a/examples/account_allowance_delete_transaction_hbar.py b/examples/account/account_allowance_delete_transaction_hbar.py similarity index 100% rename from examples/account_allowance_delete_transaction_hbar.py rename to examples/account/account_allowance_delete_transaction_hbar.py diff --git a/examples/account/account_create_transaction_with_fallback_alias.py b/examples/account/account_create_transaction_with_fallback_alias.py index 38cb74c01..979160048 100644 --- a/examples/account/account_create_transaction_with_fallback_alias.py +++ b/examples/account/account_create_transaction_with_fallback_alias.py @@ -48,70 +48,75 @@ def setup_client(): 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) - ) +def generate_fallback_key() -> PrivateKey: + """Generate an ECDSA key pair and validate its EVM address.""" + 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) - # Freeze & sign with the account key as well - transaction = ( - transaction.freeze_with(client) - .sign(account_private_key) - ) + print(f"βœ… Account ECDSA public key: {account_public_key}") + print(f"βœ… Derived EVM address: {evm_address}") + return 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." - ) +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", + ) + .set_key_with_alias(account_private_key) + ) + + transaction = transaction.freeze_with(client).sign(account_private_key) - print(f"βœ… Account created with ID: {new_account_id}\n") + response = transaction.execute(client) + new_account_id = response.account_id - account_info = ( - AccountInfoQuery() - .set_account_id(new_account_id) - .execute(client) + if new_account_id is None: + raise RuntimeError( + "AccountID not found in receipt. Account may not have been created." ) - out = info_to_dict(account_info) - print("Account Info:") - print(json.dumps(out, indent=2) + "\n") + print(f"βœ… Account created with ID: {new_account_id}\n") + return new_account_id - 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 fetch_account_info(client: Client, account_id: AccountId): + """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: + """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}" + ) def main(): """Main entry point.""" client = setup_client() - create_account_with_fallback_alias(client) + try: + account_private_key = generate_fallback_key() + 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) if __name__ == "__main__": main() diff --git a/examples/account/account_info.py b/examples/account/account_info.py index b81c1fb46..8432ccde6 100644 --- a/examples/account/account_info.py +++ b/examples/account/account_info.py @@ -53,14 +53,8 @@ def build_mock_account_info() -> AccountInfo: def print_account_info(info: AccountInfo) -> None: """Pretty-print key AccountInfo fields.""" - print("πŸ“œ AccountInfo Example (Mock Data)") - print(f"Account ID: {info.account_id}") - print(f"Key: {info.key}") - print(f"Balance: {info.balance}") - print(f"Expiration Time: {info.expiration_time}") - print(f"Auto Renew Period: {info.auto_renew_period}") - print(f"Token Relationships: {info.token_relationships}") - print(f"Memo: {info.account_memo}") + print("πŸ“œ AccountInfo String Representation:") + print(info) def main(): """Run the AccountInfo example.""" @@ -68,4 +62,4 @@ def main(): print_account_info(info) if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/examples/account_allowance_approve_transaction_hbar.py b/examples/account_allowance_approve_transaction_hbar.py deleted file mode 100644 index f3ccbaca7..000000000 --- a/examples/account_allowance_approve_transaction_hbar.py +++ /dev/null @@ -1,152 +0,0 @@ -""" -Example demonstrating hbar allowance approval and usage. -""" - -import os -import sys - -from dotenv import load_dotenv - -from hiero_sdk_python import AccountId, Client, Hbar, Network, PrivateKey, TransactionId -from hiero_sdk_python.account.account_allowance_approve_transaction import ( - AccountAllowanceApproveTransaction, -) -from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction -from hiero_sdk_python.response_code import ResponseCode -from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction - -load_dotenv() -network_name = os.getenv("NETWORK", "testnet").lower() - - -def setup_client() -> Client: - """Initialize and set up the client with operator account using env vars.""" - network = Network(network_name) - print(f"Connecting to Hedera {network_name} network!") - client = Client(network) - - 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 - - -def create_account(client: Client): - """Create a new Hedera account with initial balance.""" - account_private_key = PrivateKey.generate_ed25519() - account_public_key = account_private_key.public_key() - - account_receipt = ( - AccountCreateTransaction() - .set_key(account_public_key) - .set_initial_balance(Hbar(1)) - .set_account_memo("Account for hbar allowance") - .execute(client) - ) - - if account_receipt.status != ResponseCode.SUCCESS: - print( - "Account creation failed with status: " - f"{ResponseCode(account_receipt.status).name}" - ) - sys.exit(1) - - account_account_id = account_receipt.account_id - - return account_account_id, account_private_key - - -def approve_hbar_allowance( - client: Client, - owner_account_id: AccountId, - spender_account_id: AccountId, - amount: Hbar, -): - """Approve Hbar allowance for spender.""" - receipt = ( - AccountAllowanceApproveTransaction() - .approve_hbar_allowance(owner_account_id, spender_account_id, amount) - .execute(client) - ) - - if receipt.status != ResponseCode.SUCCESS: - print( - "Hbar allowance approval failed with status: " - f"{ResponseCode(receipt.status).name}" - ) - sys.exit(1) - - print(f"Hbar allowance of {amount} approved for spender {spender_account_id}") - return receipt - - -def transfer_hbar_with_allowance( - client: Client, - owner_account_id: AccountId, - spender_account_id: AccountId, - spender_private_key: PrivateKey, - receiver_account_id: AccountId, - amount: Hbar, -): - """Transfer hbars using a previously approved allowance.""" - receipt = ( - TransferTransaction() - # Transaction is paid for / initiated by spender - .set_transaction_id(TransactionId.generate(spender_account_id)) - .add_approved_hbar_transfer(owner_account_id, -amount.to_tinybars()) - .add_approved_hbar_transfer(receiver_account_id, amount.to_tinybars()) - .freeze_with(client) - .sign(spender_private_key) - .execute(client) - ) - - if receipt.status != ResponseCode.SUCCESS: - print(f"Hbar transfer failed with status: {ResponseCode(receipt.status).name}") - sys.exit(1) - - print( - f"Successfully transferred {amount} from {owner_account_id} " - f"to {receiver_account_id} using allowance" - ) - - return receipt - - -def main(): - """ - Demonstrates hbar allowance functionality by: - 1. Setting up client with operator account - 2. Creating spender and receiver accounts - 3. Approving hbar allowance for spender - 4. Transferring hbars using the allowance - """ - client = setup_client() - - # Create spender and receiver accounts - spender_id, spender_private_key = create_account(client) - print(f"Spender account created with ID: {spender_id}") - - receiver_id, _ = create_account(client) - print(f"Receiver account created with ID: {receiver_id}") - - # Approve hbar allowance for spender - allowance_amount = Hbar(2) - owner_account_id = client.operator_account_id - - approve_hbar_allowance(client, owner_account_id, spender_id, allowance_amount) - - # Transfer hbars using the allowance - transfer_hbar_with_allowance( - client=client, - owner_account_id=owner_account_id, - spender_account_id=spender_id, - spender_private_key=spender_private_key, - receiver_account_id=receiver_id, - amount=allowance_amount, - ) - - -if __name__ == "__main__": - main() diff --git a/examples/query/account_balance_query.py b/examples/query/account_balance_query.py index dfd56bc12..419f1773f 100644 --- a/examples/query/account_balance_query.py +++ b/examples/query/account_balance_query.py @@ -12,6 +12,7 @@ python examples/query/account_balance_query.py """ + import os import sys import time @@ -30,7 +31,8 @@ ) load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """ @@ -47,12 +49,13 @@ def setup_client(): print(f"Connecting to Hedera {network_name} network!") client = Client(network) - operator_id_str = os.getenv('OPERATOR_ID') - operator_key_str = os.getenv('OPERATOR_KEY') + operator_id_str = os.getenv("OPERATOR_ID") + operator_key_str = os.getenv("OPERATOR_KEY") if not operator_id_str or not operator_key_str: raise ValueError( - "OPERATOR_ID and OPERATOR_KEY environment variables must be set") + "OPERATOR_ID and OPERATOR_KEY environment variables must be set" + ) operator_id = AccountId.from_string(operator_id_str) operator_key = PrivateKey.from_string(operator_key_str) @@ -82,9 +85,7 @@ def create_account(client, operator_key, initial_balance=Hbar(10)): # Create the account creation transaction transaction = AccountCreateTransaction( - key=new_account_public_key, - initial_balance=initial_balance, - memo="New Account" + key=new_account_public_key, initial_balance=initial_balance, memo="New Account" ).freeze_with(client) # Sign and execute the transaction @@ -95,7 +96,8 @@ def create_account(client, operator_key, initial_balance=Hbar(10)): print(f"βœ“ Account created successfully") print(f" Account ID: {new_account_id}") print( - f" Initial balance: {initial_balance.to_hbars()} hbars ({initial_balance.to_tinybars()} tinybars)\n") + f" Initial balance: {initial_balance.to_hbars()} hbars ({initial_balance.to_tinybars()} tinybars)\n" + ) return new_account_id, new_account_private_key @@ -136,7 +138,8 @@ def transfer_hbars(client, operator_id, operator_key, recipient_id, amount): str: The status of the transfer transaction. """ print( - f"Transferring {amount.to_tinybars()} tinybars ({amount.to_hbars()} hbars) from {operator_id} to {recipient_id}...") + f"Transferring {amount.to_tinybars()} tinybars ({amount.to_hbars()} hbars) from {operator_id} to {recipient_id}..." + ) # Create transfer transaction transfer_transaction = ( @@ -166,7 +169,8 @@ def main(): # Create a new account with initial balance new_account_id, new_account_private_key = create_account( - client, operator_key, initial_balance=Hbar(10)) + client, operator_key, initial_balance=Hbar(10) + ) # Query and display the initial balance print("=" * 60) @@ -182,7 +186,8 @@ def main(): print("=" * 60) transfer_amount = Hbar(5) transfer_status = transfer_hbars( - client, operator_id, operator_key, new_account_id, transfer_amount) + client, operator_id, operator_key, new_account_id, transfer_amount + ) print(f"Transfer transaction status: {transfer_status}") print("=" * 60 + "\n") diff --git a/examples/query/account_balance_query_2.py b/examples/query/account_balance_query_2.py index bcadb8a12..b618d3d06 100644 --- a/examples/query/account_balance_query_2.py +++ b/examples/query/account_balance_query_2.py @@ -19,7 +19,6 @@ TokenInfoQuery, TokenType, TokenMintTransaction, - ) from hiero_sdk_python.query.account_balance_query import CryptoGetAccountBalanceQuery from hiero_sdk_python.tokens.token_id import TokenId @@ -27,19 +26,20 @@ # Load environment variables from .env file load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() -key_type = os.getenv('KEY_TYPE', 'ecdsa') +network_name = os.getenv("NETWORK", "testnet").lower() +key_type = os.getenv("KEY_TYPE", "ecdsa") + def setup_client(): - """Setup Client """ + """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 try: - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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) print(f"Client set up with operator id {client.operator_account_id}") @@ -48,6 +48,7 @@ def setup_client(): print("Error: Please check OPERATOR_ID and OPERATOR_KEY in your .env file.") sys.exit(1) + def create_account(client, name, initial_balance=Hbar(10)): """Create a test account with initial balance""" account_private_key = PrivateKey.generate(key_type) @@ -70,6 +71,7 @@ def create_account(client, name, initial_balance=Hbar(10)): print(f"{name} account created with id: {account_id}") return account_id, account_private_key + def create_and_mint_token(treasury_account_id, treasury_account_key, client): """Create an NFT collection and mint metadata_list (default 3 items).""" metadata_list = [b"METADATA_A", b"METADATA_B", b"METADATA_C"] @@ -79,51 +81,54 @@ def create_and_mint_token(treasury_account_id, treasury_account_key, client): token_id = ( TokenCreateTransaction() - .set_token_name("My Awesome NFT").set_token_symbol("MANFT") + .set_token_name("My Awesome NFT") + .set_token_symbol("MANFT") .set_token_type(TokenType.NON_FUNGIBLE_UNIQUE) .set_treasury_account_id(treasury_account_id) .set_initial_supply(0) .set_supply_key(supply_key) .freeze_with(client) - .sign(treasury_account_key).sign(supply_key).execute(client) + .sign(treasury_account_key) + .sign(supply_key) + .execute(client) ).token_id - TokenMintTransaction() \ - .set_token_id(token_id).set_metadata(metadata_list) \ - .freeze_with(client).sign(supply_key).execute(client) + TokenMintTransaction().set_token_id(token_id).set_metadata( + metadata_list + ).freeze_with(client).sign(supply_key).execute(client) - total_supply = TokenInfoQuery().set_token_id(token_id).execute(client).total_supply + total_supply = ( + TokenInfoQuery().set_token_id(token_id).execute(client).total_supply + ) print(f"βœ… Created NFT {token_id} β€” total supply: {total_supply}") return token_id except (ValueError, TypeError, RuntimeError, ConnectionError) as error: print(f"❌ Error creating token: {error}") sys.exit(1) + def get_account_balance(client: Client, account_id: AccountId): """Get account balance using CryptoGetAccountBalanceQuery""" print(f"Retrieving account balance for account id: {account_id} ...") try: # Use CryptoGetAccountBalanceQuery to get the account balance account_balance = ( - CryptoGetAccountBalanceQuery() - .set_account_id(account_id) - .execute(client) + CryptoGetAccountBalanceQuery().set_account_id(account_id).execute(client) ) print("βœ… Account balance retrieved successfully!") + # Print account balance with account_id context print(f"πŸ’° HBAR Balance for {account_id}: {account_balance.hbars} hbars") - # Display token balances - print("πŸ’Ž Token Balances:") - for token_id, balance in account_balance.token_balances.items(): - print(f" - Token ID {token_id}: {balance} units") + # Alternatively, you can use: print(account_balance) return account_balance except (ValueError, TypeError, RuntimeError, ConnectionError) as error: print(f"Error retrieving account balance: {error}") sys.exit(1) -#OPTIONAL comparison function -def compare_token_balances(client, treasury_id: AccountId, - receiver_id: AccountId, - token_id: TokenId): + +# OPTIONAL comparison function +def compare_token_balances( + client, treasury_id: AccountId, receiver_id: AccountId, token_id: TokenId +): """Compare token balances between two accounts""" print( f"\nπŸ”Ž Comparing token balances for Token ID {token_id} " @@ -139,6 +144,7 @@ def compare_token_balances(client, treasury_id: AccountId, print(f"🏷️ Token balance for Treasury ({treasury_id}): {treasury_token_balance}") print(f"🏷️ Token balance for Receiver ({receiver_id}): {receiver_token_balance}") + def main(): """Main function to run the account balance query example 1-Create test account with intial balance @@ -151,14 +157,14 @@ def main(): test_account_id, test_account_key = create_account(client, "Test Account") # Create the tokens with the test account as the treasury so minted tokens # will be owned by the test account and show up in its token balances. - token_id = create_and_mint_token( - test_account_id, - test_account_key, - client) + token_id = create_and_mint_token(test_account_id, test_account_key, client) # Retrieve and display account balance for the test account get_account_balance(client, test_account_id) - #OPTIONAL comparison of token balances between test account and operator account - compare_token_balances(client, test_account_id, client.operator_account_id, token_id) + # OPTIONAL comparison of token balances between test account and operator account + compare_token_balances( + client, test_account_id, client.operator_account_id, token_id + ) + if __name__ == "__main__": main() diff --git a/examples/query/account_info_query.py b/examples/query/account_info_query.py index 78b85ca54..fb61c79b0 100644 --- a/examples/query/account_info_query.py +++ b/examples/query/account_info_query.py @@ -3,6 +3,7 @@ python examples/query/account_info_query.py """ + import os import sys from dotenv import load_dotenv @@ -18,7 +19,9 @@ ) from hiero_sdk_python.query.account_info_query import AccountInfoQuery from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction -from hiero_sdk_python.tokens.token_associate_transaction import TokenAssociateTransaction +from hiero_sdk_python.tokens.token_associate_transaction import ( + TokenAssociateTransaction, +) from hiero_sdk_python.tokens.token_grant_kyc_transaction import TokenGrantKycTransaction from hiero_sdk_python.tokens.supply_type import SupplyType from hiero_sdk_python.tokens.token_type import TokenType @@ -27,7 +30,8 @@ load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -35,18 +39,19 @@ def setup_client(): print(f"Connecting to the Hedera {network} network!") client = Client(network) - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key + def create_test_account(client, operator_key): """Create a new test account for demonstration""" new_account_private_key = PrivateKey.generate_ed25519() new_account_public_key = new_account_private_key.public_key() - + receipt = ( AccountCreateTransaction() .set_key(new_account_public_key) @@ -56,16 +61,19 @@ def create_test_account(client, operator_key): .sign(operator_key) .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: - print(f"Account creation failed with status: {ResponseCode(receipt.status).name}") + print( + f"Account creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + new_account_id = receipt.account_id print(f"\nTest account created with ID: {new_account_id}") - + return new_account_id, new_account_private_key + def create_fungible_token(client, operator_id, operator_key): """Create a fungible token for association with test account""" receipt = ( @@ -83,16 +91,17 @@ def create_fungible_token(client, operator_id, operator_key): .set_kyc_key(operator_key) .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: print(f"Token creation failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + token_id = receipt.token_id print(f"\nFungible token created with ID: {token_id}") - + return token_id + def create_nft(client, account_id, account_private_key): """Create a non-fungible token""" receipt = ( @@ -109,21 +118,22 @@ def create_nft(client, account_id, account_private_key): .set_supply_key(account_private_key) .set_freeze_key(account_private_key) .freeze_with(client) - .sign(account_private_key) # Sign with the account private key + .sign(account_private_key) # Sign with the account private key .execute(client) ) - + # Check if nft creation was successful if receipt.status != ResponseCode.SUCCESS: print(f"NFT creation failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + # Get token ID from receipt nft_token_id = receipt.token_id print(f"\nNFT created with ID: {nft_token_id}") - + return nft_token_id + def mint_nft(client, nft_token_id, account_private_key): """Mint a non-fungible token""" receipt = ( @@ -131,18 +141,19 @@ def mint_nft(client, nft_token_id, account_private_key): .set_token_id(nft_token_id) .set_metadata(b"My NFT Metadata 1") .freeze_with(client) - .sign(account_private_key) # Sign with the account private key + .sign(account_private_key) # Sign with the account private key .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: print(f"NFT minting failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + print(f"\nNFT minted with serial number: {receipt.serial_numbers[0]}") - + return NftId(nft_token_id, receipt.serial_numbers[0]) + def associate_token_with_account(client, token_id, account_id, account_key): """Associate the token with the test account""" receipt = ( @@ -153,13 +164,16 @@ def associate_token_with_account(client, token_id, account_id, account_key): .sign(account_key) .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: - print(f"Token association failed with status: {ResponseCode(receipt.status).name}") + print( + f"Token association failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + print(f"Token {token_id} associated with account {account_id}") + def grant_kyc_for_token(client, account_id, token_id): """Grant KYC for the token to the account""" receipt = ( @@ -168,13 +182,14 @@ def grant_kyc_for_token(client, account_id, token_id): .set_token_id(token_id) .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: print(f"KYC grant failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + print(f"\nKYC granted for token_id: {token_id}") + def display_account_info(info): """Display basic account information""" print(f"\nAccount ID: {info.account_id}") @@ -184,17 +199,20 @@ def display_account_info(info): print(f"Is Deleted: {info.is_deleted}") print(f"Receiver Signature Required: {info.receiver_signature_required}") print(f"Owned NFTs: {info.owned_nfts}") - + print(f"Public Key: {info.key.to_string()}") - + print(f"Expiration Time: {info.expiration_time}") print(f"Auto Renew Period: {info.auto_renew_period}") - + print(f"Proxy Received: {info.proxy_received}") + def display_token_relationships(info): """Display token relationships information""" - print(f"\nToken Relationships ({len(info.token_relationships)} total) for account {info.account_id}:") + print( + f"\nToken Relationships ({len(info.token_relationships)} total) for account {info.account_id}:" + ) if info.token_relationships: for i, relationship in enumerate(info.token_relationships, 1): print(f" Token {i}:") @@ -208,6 +226,7 @@ def display_token_relationships(info): else: print(" No token relationships found") + def query_account_info(): """ Demonstrates the account info query functionality by: @@ -222,41 +241,41 @@ def query_account_info(): 9. Querying final account info to see complete token relationships and NFT ownership """ client, operator_id, operator_key = setup_client() - + # Create a new account account_id, account_private_key = create_test_account(client, operator_key) - + # Query the account info and display account information info = AccountInfoQuery(account_id).execute(client) print("\nAccount info query completed successfully!") display_account_info(info) - + # Create a fungible token token_id = create_fungible_token(client, operator_id, operator_key) - + # Associate the token with the account associate_token_with_account(client, token_id, account_id, account_private_key) - + # Query the account info and display token relationships info = AccountInfoQuery(account_id).execute(client) print("\nToken info query completed successfully!") display_token_relationships(info) - + # Grant KYC for the token print(f"\nGrant KYC for token: {token_id}") grant_kyc_for_token(client, account_id, token_id) - + # Query the account info again and see the kyc status has been updated to GRANTED info = AccountInfoQuery(account_id).execute(client) print("\nAccount info query completed successfully!") display_token_relationships(info) - + # Create an NFT token with the new account as the owner nft_token_id = create_nft(client, account_id, account_private_key) - + # Mint an NFT to the account mint_nft(client, nft_token_id, account_private_key) - + # Query the account info again and see that the account has 1 owned NFT # and the token relationship has been updated to include the NFT # NOTE: the newest token is the first in the list @@ -264,5 +283,6 @@ def query_account_info(): display_account_info(info) display_token_relationships(info) + if __name__ == "__main__": - query_account_info() \ No newline at end of file + query_account_info() diff --git a/examples/query/payment_query.py b/examples/query/payment_query.py index 5b8d9eb66..ef7269721 100644 --- a/examples/query/payment_query.py +++ b/examples/query/payment_query.py @@ -3,6 +3,7 @@ python examples/query/payment_query.py """ + import os import sys from dotenv import load_dotenv @@ -23,7 +24,8 @@ load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -31,16 +33,17 @@ def setup_client(): print(f"Connecting to Hedera {network_name} network!") client = Client(network) - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key + def create_fungible_token(client, operator_id, operator_key): """Create a fungible token""" - + receipt = ( TokenCreateTransaction() .set_token_name("MyExampleFT") @@ -55,41 +58,44 @@ def create_fungible_token(client, operator_id, operator_key): .set_supply_key(operator_key) .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: - print(f"Fungible token creation failed with status: {ResponseCode.get_name(receipt.status)}") + print( + f"Fungible token creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + token_id = receipt.token_id print(f"Fungible token created with ID: {token_id}") - + return token_id + def demonstrate_zero_cost_balance_query(client, account_id): """ Demonstrate cost calculation for queries that don't require payment. - + CryptoGetAccountBalanceQuery is an example of a query that doesn't require payment. For such queries: - get_cost() returns 0 Hbar when no payment is set - get_cost() returns the set payment amount when payment is set """ print("\nQueries that DON'T require payment:\n") - + # Case 1: No payment set - should return 0 Hbar cost print("When no payment is set:") query_no_payment = CryptoGetAccountBalanceQuery().set_account_id(account_id) - + cost_no_payment = query_no_payment.get_cost(client) print(f"Cost: {cost_no_payment} Hbar") print("Expected: 0 Hbar (payment not required)") - + # Execute the query (should work without payment) print("\nExecuting query without payment...") result = query_no_payment.execute(client) print(f"Query executed successfully!") print(f" Account balance (only hbars): {result.hbars}") - + # Case 2: Payment set - should return the set payment amount print("\nWhen custom payment is set:") custom_payment = Hbar(2) @@ -98,67 +104,67 @@ def demonstrate_zero_cost_balance_query(client, account_id): .set_account_id(account_id) .set_query_payment(custom_payment) ) - + cost_with_payment = query_with_payment.get_cost(client) print(f"Cost: {cost_with_payment} Hbar") print(f"Expected: {custom_payment} Hbar") - + # Execute the query (should work with custom payment) print("\nExecuting query with custom payment...") result = query_with_payment.execute(client) print(f"Query executed successfully!") print(f" Account balance (only hbars): {result.hbars}") + def demonstrate_payment_required_queries(client, token_id): """ Demonstrate cost calculation for queries that require payment. - + TokenInfoQuery is an example of a query that requires payment. For such queries: - get_cost() asks the network for the actual cost when no payment is set - get_cost() returns the set payment amount when payment is set """ print("\nQueries that DO require payment:\n") - + # Case 1: No payment set - should ask network for cost print("When no payment is set:") query_no_payment = TokenInfoQuery().set_token_id(token_id) - + print("Asking network for query cost...") cost_from_network = query_no_payment.get_cost(client) print(f"Cost: {cost_from_network} Hbar") print("This is the actual cost calculated by the network") - + # Execute the query (should work with network-determined cost) print("\nExecuting query with network-determined cost...") result = query_no_payment.execute(client) print(f"Query executed successfully!") print(f" Token info: {result}") - + # Case 2: Payment set - should return the set payment amount print("\nWhen custom payment is set:") custom_payment = Hbar(2) query_with_payment = ( - TokenInfoQuery() - .set_token_id(token_id) - .set_query_payment(custom_payment) + TokenInfoQuery().set_token_id(token_id).set_query_payment(custom_payment) ) - + cost_with_payment = query_with_payment.get_cost(client) print(f"Cost: {cost_with_payment} Hbar") print(f"Expected: {custom_payment} Hbar") - + # Execute the query (should work with custom payment) print("\nExecuting query with custom payment...") result = query_with_payment.execute(client) print(f"Query executed successfully!") print(f" Token info: {result}") - + # Case 3: Compare network cost vs custom payment print("\nCost comparison:") print(f"Network-determined cost: {cost_from_network} Hbar") print(f"Custom payment: {custom_payment} Hbar") + def query_payment(): """ Demonstrates the query payment by: @@ -170,9 +176,10 @@ def query_payment(): """ client, operator_id, operator_key = setup_client() token_id = create_fungible_token(client, operator_id, operator_key) - + demonstrate_zero_cost_balance_query(client, operator_id) demonstrate_payment_required_queries(client, token_id) + if __name__ == "__main__": query_payment() diff --git a/examples/query/token_info_query_fungible.py b/examples/query/token_info_query_fungible.py index dd640893c..aee3b615f 100644 --- a/examples/query/token_info_query_fungible.py +++ b/examples/query/token_info_query_fungible.py @@ -22,7 +22,8 @@ load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -30,13 +31,14 @@ def setup_client(): print(f"Connecting to Hedera {network_name} network!") client = Client(network) - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key + def create_fungible_token(client, operator_id, operator_key): """Create a fungible token""" receipt = ( @@ -54,18 +56,21 @@ def create_fungible_token(client, operator_id, operator_key): .set_freeze_key(operator_key) .execute(client) ) - + # Check if token creation was successful if receipt.status != ResponseCode.SUCCESS: - print(f"Fungible token creation failed with status: {ResponseCode(receipt.status).name}") + print( + f"Fungible token creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + # Get token ID from receipt token_id = receipt.token_id print(f"Fungible token created with ID: {token_id}") - + return token_id + def query_token_info(): """ Demonstrates the token info query functionality by: @@ -75,9 +80,10 @@ def query_token_info(): """ client, operator_id, operator_key = setup_client() token_id = create_fungible_token(client, operator_id, operator_key) - + info = TokenInfoQuery().set_token_id(token_id).execute(client) print(f"Fungible token info: {info}") + if __name__ == "__main__": query_token_info() diff --git a/examples/query/token_info_query_nft.py b/examples/query/token_info_query_nft.py index c0eac6596..694ce386b 100644 --- a/examples/query/token_info_query_nft.py +++ b/examples/query/token_info_query_nft.py @@ -3,6 +3,7 @@ python examples/token_info_query_nft.py """ + import os import sys @@ -22,7 +23,8 @@ load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -30,13 +32,14 @@ def setup_client(): print(f"Connecting to Hedera {network_name} network!") client = Client(network) - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key + def create_nft(client, operator_id, operator_key): """Create a non-fungible token""" receipt = ( @@ -54,18 +57,19 @@ def create_nft(client, operator_id, operator_key): .set_freeze_key(operator_key) .execute(client) ) - + # Check if nft creation was successful if receipt.status != ResponseCode.SUCCESS: print(f"NFT creation failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + # Get token ID from receipt nft_token_id = receipt.token_id print(f"NFT created with ID: {nft_token_id}") - + return nft_token_id + def query_token_info(): """ Demonstrates the token info query functionality by: @@ -75,9 +79,10 @@ def query_token_info(): """ client, operator_id, operator_key = setup_client() token_id = create_nft(client, operator_id, operator_key) - + info = TokenInfoQuery().set_token_id(token_id).execute(client) print(f"Non-fungible token info: {info}") + if __name__ == "__main__": query_token_info() diff --git a/examples/query/token_nft_info_query.py b/examples/query/token_nft_info_query.py index 160194266..b95ad6d1a 100644 --- a/examples/query/token_nft_info_query.py +++ b/examples/query/token_nft_info_query.py @@ -3,6 +3,7 @@ python examples/query/token_nft_info_query.py """ + import os import sys from dotenv import load_dotenv @@ -23,7 +24,8 @@ load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -32,13 +34,14 @@ def setup_client(): client = Client(network) # Set up operator account - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key + def create_nft(client, operator_id, operator_key): """Create a non-fungible token""" receipt = ( @@ -56,18 +59,19 @@ def create_nft(client, operator_id, operator_key): .set_freeze_key(operator_key) .execute(client) ) - + # Check if nft creation was successful if receipt.status != ResponseCode.SUCCESS: print(f"NFT creation failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + # Get token ID from receipt nft_token_id = receipt.token_id print(f"NFT created with ID: {nft_token_id}") - + return nft_token_id + def mint_nft(client, nft_token_id): """Mint a non-fungible token""" receipt = ( @@ -76,15 +80,16 @@ def mint_nft(client, nft_token_id): .set_metadata(b"My NFT Metadata 1") .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: print(f"NFT minting failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + print(f"NFT minted with serial number: {receipt.serial_numbers[0]}") - + return NftId(nft_token_id, receipt.serial_numbers[0]) + def query_nft_info(): """ Demonstrates the nft info query functionality by: @@ -95,9 +100,10 @@ def query_nft_info(): client, operator_id, operator_key = setup_client() token_id = create_nft(client, operator_id, operator_key) nft_id = mint_nft(client, token_id) - + info = TokenNftInfoQuery(nft_id=nft_id).execute(client) print(f"NFT info: {info}") + if __name__ == "__main__": query_nft_info() diff --git a/examples/query/topic_info_query.py b/examples/query/topic_info_query.py index 827d58128..71471246b 100644 --- a/examples/query/topic_info_query.py +++ b/examples/query/topic_info_query.py @@ -3,6 +3,7 @@ python examples/query/topic_info_query.py """ + import os import sys from dotenv import load_dotenv @@ -13,11 +14,12 @@ AccountId, PrivateKey, TopicInfoQuery, - TopicCreateTransaction + TopicCreateTransaction, ) load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -26,24 +28,24 @@ def setup_client(): 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}") + 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, operator_id, operator_key + return client, operator_id, operator_key except (TypeError, ValueError): print("❌ Error: Creating client, Please check your .env file") sys.exit(1) + def create_topic(client, operator_key): """Create a new topic""" print("\nSTEP 1: Creating a Topic...") try: topic_tx = ( TopicCreateTransaction( - memo="Python SDK created topic", - admin_key=operator_key.public_key() + memo="Python SDK created topic", admin_key=operator_key.public_key() ) .freeze_with(client) .sign(operator_key) @@ -57,13 +59,14 @@ def create_topic(client, operator_key): print(f"❌ Error: Creating topic: {e}") sys.exit(1) + def query_topic_info(): """ A full example that create a topic and query topic info for that topic. """ # Config Client client, _, operator_key = setup_client() - + # Create a new Topic topic_id = create_topic(client, operator_key) @@ -73,5 +76,6 @@ def query_topic_info(): topic_info = query.execute(client) print("βœ… Success! Topic Info:", topic_info) + if __name__ == "__main__": query_topic_info() diff --git a/examples/query/topic_message_query.py b/examples/query/topic_message_query.py index 0341667c7..7ca6b382a 100644 --- a/examples/query/topic_message_query.py +++ b/examples/query/topic_message_query.py @@ -1,8 +1,9 @@ """ -uv run examples/query/topic_message_query.py -python examples/query/topic_message_query.py +uv run examples/query/topic_message_query.py +python examples/query/topic_message_query.py """ + import os import time import sys @@ -15,11 +16,13 @@ AccountId, PrivateKey, TopicCreateTransaction, - TopicMessageQuery + TopicMessageQuery, ) load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + + def setup_client(): """Initialize and set up the client with operator account""" network = Network(network_name) @@ -28,8 +31,8 @@ def setup_client(): client = Client(network) try: - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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}") @@ -38,14 +41,14 @@ def setup_client(): print("❌ Error: Creating client, Please check your .env file") sys.exit(1) + def create_topic(client, operator_key): """Create a new topic""" print("\nSTEP 1: Creating a Topic...") try: topic_tx = ( TopicCreateTransaction( - memo="Python SDK created topic", - admin_key=operator_key.public_key() + memo="Python SDK created topic", admin_key=operator_key.public_key() ) .freeze_with(client) .sign(operator_key) @@ -59,18 +62,20 @@ def create_topic(client, operator_key): print(f"❌ Error: Creating topic: {e}") sys.exit(1) + def query_topic_messages(): """ A full example that creates a topic and perform query topic messages. """ # Config Client client, _, operator_key = setup_client() - + # Create Topic topic_id = create_topic(client, operator_key) # Query Topic Messages print("\nSTEP 2: Query Topic Messages...") + def on_message_handler(topic_message): print(f"Received topic message: {topic_message}") @@ -81,25 +86,24 @@ def on_error_handler(e): topic_id=topic_id, start_time=datetime.now(timezone.utc), limit=0, - chunking_enabled=True + chunking_enabled=True, ) handle = query.subscribe( - client, - on_message=on_message_handler, - on_error=on_error_handler + client, on_message=on_message_handler, on_error=on_error_handler ) print("Subscription started. Will auto-cancel after 10 seconds or on Ctrl+C...") try: - startTime = time.time(); + startTime = time.time() while time.time() - startTime < 10: - time.sleep(1); + time.sleep(1) except KeyboardInterrupt: print("βœ‹ Ctrl+C detected. Cancelling subscription...") finally: handle.cancel() print("βœ… Subscription cancelled. Exiting.") + if __name__ == "__main__": query_topic_messages() diff --git a/examples/query/transaction_get_receipt_query.py b/examples/query/transaction_get_receipt_query.py index 74b5f5fee..e0d3023f2 100644 --- a/examples/query/transaction_get_receipt_query.py +++ b/examples/query/transaction_get_receipt_query.py @@ -3,6 +3,7 @@ python examples/query/transaction_get_receipt_query.py """ + import os import sys from dotenv import load_dotenv @@ -16,11 +17,12 @@ Hbar, TransactionGetReceiptQuery, ResponseCode, - AccountCreateTransaction + AccountCreateTransaction, ) load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -29,8 +31,8 @@ def setup_client(): client = Client(network) try: - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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}") @@ -54,14 +56,32 @@ def create_account(client, operator_key): recipient_id = receipt.account_id print(f"βœ… Success! Created a new recipient account with ID: {recipient_id}") return recipient_id, recipient_key - + except Exception as e: print(f"Error creating new account: {e}") sys.exit(1) + +def _print_receipt_with_children(queried_receipt): + """Pretty-print receipt status and any child receipts.""" + print(f"βœ… Queried transaction status: {ResponseCode(queried_receipt.status).name}") + + children = queried_receipt.children + print(f"Child receipts count: {len(children)}") + + if not children: + print("No child receipts returned (this can be normal depending on transaction type).") + return + + print("Child receipts:") + for idx, child in enumerate(children, start=1): + print(f" {idx}. status={ResponseCode(child.status).name}") + + def query_receipt(): """ - A full example that include account creation, Hbar transfer, and receipt querying + A full example that include account creation, Hbar transfer, and receipt querying. + Demonstrates include_child_receipts support (SDK API: set_include_children). """ # Config Client client, operator_id, operator_key = setup_client() @@ -77,19 +97,26 @@ def query_receipt(): .add_hbar_transfer(operator_id, -Hbar(amount).to_tinybars()) .add_hbar_transfer(recipient_id, Hbar(amount).to_tinybars()) .freeze_with(client) - .sign(operator_key) + .sign(operator_key) ) receipt = transaction.execute(client) transaction_id = transaction.transaction_id print(f"Transaction ID: {transaction_id}") - print(f"βœ… Success! Transfer transaction status: {ResponseCode(receipt.status).name}") + print( + f"βœ… Success! Transfer transaction status: {ResponseCode(receipt.status).name}" + ) # Query Transaction Receipt - print("\nSTEP 3: Querying transaction receipt...") - receipt_query = TransactionGetReceiptQuery().set_transaction_id(transaction_id) + print("\nSTEP 3: Querying transaction receipt (include child receipts)...") + receipt_query = TransactionGetReceiptQuery().set_transaction_id(transaction_id).set_include_children(True) queried_receipt = receipt_query.execute(client) - print(f"βœ… Success! Queried transaction status: {ResponseCode(queried_receipt.status).name}") + print( + f"βœ… Success! Queried transaction status: {ResponseCode(queried_receipt.status).name}" + ) + + + _print_receipt_with_children(queried_receipt) if __name__ == "__main__": query_receipt() diff --git a/examples/query/transaction_record_query.py b/examples/query/transaction_record_query.py index 0d8e3b2d8..cb7cde2ef 100644 --- a/examples/query/transaction_record_query.py +++ b/examples/query/transaction_record_query.py @@ -3,6 +3,7 @@ python examples/query/transaction_record_query.py """ + import os import sys from dotenv import load_dotenv @@ -18,14 +19,17 @@ from hiero_sdk_python.query.transaction_record_query import TransactionRecordQuery from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.tokens.supply_type import SupplyType -from hiero_sdk_python.tokens.token_associate_transaction import TokenAssociateTransaction +from hiero_sdk_python.tokens.token_associate_transaction import ( + TokenAssociateTransaction, +) from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction from hiero_sdk_python.tokens.token_type import TokenType from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -34,18 +38,19 @@ def setup_client(): client = Client(network) # Set up operator account - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key + def create_account_transaction(client): """Create a new account to get a transaction ID for record query""" # Generate a new key pair for the account new_account_key = PrivateKey.generate_ed25519() - + # Create the account receipt = ( AccountCreateTransaction() @@ -53,21 +58,24 @@ def create_account_transaction(client): .set_initial_balance(Hbar(1)) .execute(client) ) - + # Check if account creation was successful if receipt.status != ResponseCode.SUCCESS: - print(f"Account creation failed with status: {ResponseCode(receipt.status).name}") + print( + f"Account creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + # Get the new account ID and transaction ID from receipt new_account_id = receipt.account_id transaction_id = receipt.transaction_id - + print(f"Account created with ID: {new_account_id}") - + return new_account_id, new_account_key, transaction_id -def create_fungible_token(client: 'Client', account_id, account_private_key): + +def create_fungible_token(client: "Client", account_id, account_private_key): """Create a fungible token""" receipt = ( TokenCreateTransaction() @@ -83,16 +91,19 @@ def create_fungible_token(client: 'Client', account_id, account_private_key): .set_supply_key(account_private_key) .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: - print(f"Fungible token creation failed with status: {ResponseCode(receipt.status).name}") + print( + f"Fungible token creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + token_id = receipt.token_id print(f"\nFungible token created with ID: {token_id}") - + return token_id + def associate_token(client, token_id, receiver_id, receiver_private_key): """Associate token with an account""" # Associate the token_id with the new account @@ -101,17 +112,22 @@ def associate_token(client, token_id, receiver_id, receiver_private_key): .set_account_id(receiver_id) .add_token_id(token_id) .freeze_with(client) - .sign(receiver_private_key) # Has to be signed here by receiver's key + .sign(receiver_private_key) # Has to be signed here by receiver's key .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: - print(f"Token association failed with status: {ResponseCode(receipt.status).name}") + print( + f"Token association failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + print(f"Token successfully associated with account: {receiver_id}") -def transfer_tokens(client, treasury_id, treasury_private_key, receiver_id, token_id, amount=10): + +def transfer_tokens( + client, treasury_id, treasury_private_key, receiver_id, token_id, amount=10 +): """Transfer tokens to the receiver account so we can later reject them""" # Transfer tokens to the receiver account receipt = ( @@ -122,16 +138,17 @@ def transfer_tokens(client, treasury_id, treasury_private_key, receiver_id, toke .sign(treasury_private_key) .execute(client) ) - + # Check if transfer was successful if receipt.status != ResponseCode.SUCCESS: print(f"Transfer failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + print(f"Successfully transferred {amount} tokens to receiver account {receiver_id}") - + return receipt + def print_transaction_record(record): """Print the transaction record""" print(f"Transaction ID: {record.transaction_id}") @@ -139,11 +156,12 @@ def print_transaction_record(record): print(f"Transaction Hash: {record.transaction_hash.hex()}") print(f"Transaction Memo: {record.transaction_memo}") print(f"Transaction Account ID: {record.receipt.account_id}") - + print(f"\nTransfers made in the transaction:") for account_id, amount in record.transfers.items(): print(f" Account: {account_id}, Amount: {amount}") + def query_record(): """ Demonstrates the transaction record query functionality by performing the following steps: @@ -154,22 +172,20 @@ def query_record(): 5. Querying and displaying the transaction record for token transfer """ client, operator_id, operator_key = setup_client() - + # Create a transaction to get a transaction ID new_account_id, new_account_key, transaction_id = create_account_transaction(client) - - record = ( - TransactionRecordQuery() - .set_transaction_id(transaction_id) - .execute(client) - ) + + record = TransactionRecordQuery().set_transaction_id(transaction_id).execute(client) print("Transaction record for account creation:") print_transaction_record(record) - + token_id = create_fungible_token(client, operator_id, operator_key) associate_token(client, token_id, new_account_id, new_account_key) - transfer_receipt = transfer_tokens(client, operator_id, operator_key, new_account_id, token_id) - + transfer_receipt = transfer_tokens( + client, operator_id, operator_key, new_account_id, token_id + ) + transfer_record = ( TransactionRecordQuery() .set_transaction_id(transfer_receipt.transaction_id) @@ -177,12 +193,13 @@ def query_record(): ) print("Transaction record for token transfer:") print_transaction_record(transfer_record) - + print(f"\nToken Transfer Record:") for token_id, transfers in transfer_record.token_transfers.items(): print(f" Token ID: {token_id}") for account_id, amount in transfers.items(): print(f" Account: {account_id}, Amount: {amount}") + if __name__ == "__main__": query_record() diff --git a/examples/tokens/account_allowance_approve_transaction.py b/examples/tokens/account_allowance_approve_transaction.py index c3e0b74d2..fb83f5f9e 100644 --- a/examples/tokens/account_allowance_approve_transaction.py +++ b/examples/tokens/account_allowance_approve_transaction.py @@ -17,14 +17,17 @@ from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.tokens.supply_type import SupplyType -from hiero_sdk_python.tokens.token_associate_transaction import TokenAssociateTransaction +from hiero_sdk_python.tokens.token_associate_transaction import ( + TokenAssociateTransaction, +) from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction from hiero_sdk_python.tokens.token_type import TokenType from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -54,7 +57,9 @@ def create_account(client): ) if account_receipt.status != ResponseCode.SUCCESS: - print(f"Account creation failed with status: {ResponseCode(account_receipt.status).name}") + print( + f"Account creation failed with status: {ResponseCode(account_receipt.status).name}" + ) sys.exit(1) account_account_id = account_receipt.account_id @@ -99,13 +104,17 @@ def associate_token_with_account(client, account_id, account_private_key, token_ ) if receipt.status != ResponseCode.SUCCESS: - print(f"Token association failed with status: {ResponseCode(receipt.status).name}") + print( + f"Token association failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) print(f"Token {token_id} associated with account {account_id}") -def approve_token_allowance(client, token_id, owner_account_id, spender_account_id, amount): +def approve_token_allowance( + client, token_id, owner_account_id, spender_account_id, amount +): """Approve token allowance for spender""" receipt = ( AccountAllowanceApproveTransaction() @@ -114,7 +123,9 @@ def approve_token_allowance(client, token_id, owner_account_id, spender_account_ ) if receipt.status != ResponseCode.SUCCESS: - print(f"Token allowance approval failed with status: {ResponseCode(receipt.status).name}") + print( + f"Token allowance approval failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) print(f"Token allowance of {amount} approved for spender {spender_account_id}") @@ -130,7 +141,9 @@ def delete_token_allowance(client, token_id, owner_account_id, spender_account_i ) if receipt.status != ResponseCode.SUCCESS: - print(f"Token allowance deletion failed with status: {ResponseCode(receipt.status).name}") + print( + f"Token allowance deletion failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) print(f"Token allowance deleted for spender {spender_account_id}") @@ -138,12 +151,19 @@ def delete_token_allowance(client, token_id, owner_account_id, spender_account_i def transfer_token_without_allowance( - client, spender_account_id, spender_private_key, amount, receiver_account_id, token_id + client, + spender_account_id, + spender_private_key, + amount, + receiver_account_id, + token_id, ): """Transfer tokens without allowance""" print("Trying to transfer tokens without allowance...") owner_account_id = client.operator_account_id - client.set_operator(spender_account_id, spender_private_key) # Set operator to spender + client.set_operator( + spender_account_id, spender_private_key + ) # Set operator to spender receipt = ( TransferTransaction() @@ -158,7 +178,9 @@ def transfer_token_without_allowance( f"status but got: {ResponseCode(receipt.status).name}" ) - print(f"Token transfer successfully failed with {ResponseCode(receipt.status).name} status") + print( + f"Token transfer successfully failed with {ResponseCode(receipt.status).name} status" + ) def token_allowance(): @@ -198,7 +220,9 @@ def token_allowance(): receipt = ( TransferTransaction() .set_transaction_id(TransactionId.generate(spender_id)) - .add_approved_token_transfer(token_id, client.operator_account_id, -allowance_amount) + .add_approved_token_transfer( + token_id, client.operator_account_id, -allowance_amount + ) .add_approved_token_transfer(token_id, receiver_id, allowance_amount) .freeze_with(client) .sign(spender_private_key) diff --git a/examples/tokens/custom_fee_fixed.py b/examples/tokens/custom_fee_fixed.py index c55830cbf..a4a33a2e4 100644 --- a/examples/tokens/custom_fee_fixed.py +++ b/examples/tokens/custom_fee_fixed.py @@ -1,27 +1,122 @@ """ -Run with: +Run with: uv run examples/tokens/custom_fixed_fee.py python examples/tokens/custom_fixed_fee.py """ -from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee + +import os +import sys +from dotenv import load_dotenv + +from hiero_sdk_python.client.network import Network +from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.client.client import Client +from hiero_sdk_python.query.token_info_query import TokenInfoQuery from hiero_sdk_python.account.account_id import AccountId -from hiero_sdk_python.tokens.token_id import TokenId +from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.tokens.supply_type import SupplyType +from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee +from hiero_sdk_python.hbar import Hbar +from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction +from hiero_sdk_python.tokens.token_type import TokenType -def custom_fixed_fee(): - fixed_fee = CustomFixedFee( - amount=100, - denominating_token_id=TokenId(0, 0, 123), - fee_collector_account_id=AccountId(0, 0, 456), - all_collectors_are_exempt=False, - ) - print("\n--- Custom Fixed Fee ---") - print(fixed_fee) - # Convert to protobuf - fixed_fee_proto = fixed_fee._to_proto() +load_dotenv() +network_name = os.getenv("NETWORK", "testnet").lower() + +def setup_client(): + """Initialize and set up the client with operator account""" + # Initialize network and client + network = Network(network_name) + print(f"Connecting to Hedera {network_name} network!") + client = Client(network) - print("Fixed Fee Protobuf:", fixed_fee_proto) + # This disables the SSL error in the local development environment (Keep commented for production) # + # client.set_transport_security(False) + # client.set_verify_certificates(False) + + try: + operator_id_str = os.getenv('OPERATOR_ID') + operator_key_str = os.getenv('OPERATOR_KEY') + + if not operator_id_str or not operator_key_str: + raise ValueError("Environment variables OPERATOR_ID or OPERATOR_KEY are missing.") + + operator_id = AccountId.from_string(operator_id_str) + operator_key = PrivateKey.from_string(operator_key_str) + + 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) as e: + print(f"Error: {e}") + print("Please check OPERATOR_ID and OPERATOR_KEY in your .env file.") + sys.exit(1) + +def custom_fixed_fee_example(): + """ + + Demonstrates how to create a token with a Custom Fixed Fee. + + """ + + client, operator_id, operator_key = setup_client() + + print("\n--- Creating Custom Fixed Fee ---") + + fixed_fee = CustomFixedFee( + amount=Hbar(1).to_tinybars(), + fee_collector_account_id=operator_id, + all_collectors_are_exempt=False + ) + + print(f"Fee Definition: Pay 1 HBAR to {operator_id}") + + print("\n--- Creating Token with Fee ---") + transaction = ( + TokenCreateTransaction() + .set_token_name("Fixed Fee Example Token") + .set_token_symbol("FFET") + .set_decimals(2) + .set_treasury_account_id(operator_id) + .set_token_type(TokenType.FUNGIBLE_COMMON) + .set_supply_type(SupplyType.INFINITE) + .set_initial_supply(1000) + .set_admin_key(operator_key) + .set_custom_fees([fixed_fee]) + .freeze_with(client) + .sign(operator_key) + ) + + try: + receipt = transaction.execute(client) + + # Check if the status is explicitly SUCCESS + if receipt.status != ResponseCode.SUCCESS: + print(f"Transaction failed with status: {ResponseCode(receipt.status).name}") + + token_id = receipt.token_id + print(f"Token created successfully with ID: {token_id}") + + print("\n--- Verifying Fee on Network ---") + token_info = TokenInfoQuery().set_token_id(token_id).execute(client) + + retrieved_fees = token_info.custom_fees + if retrieved_fees: + print(f"Success! Found {len(retrieved_fees)} custom fee(s) on token.") + for fee in retrieved_fees: + print(f"Fee Collector: {fee.fee_collector_account_id}") + print(f"Fee Details: {fee}") + else: + print("Error: No custom fees found on the token.") + + except Exception as e: + print(f"Transaction failed: {e}") + sys.exit(1) + finally: + client.close() + if __name__ == "__main__": - custom_fixed_fee() \ No newline at end of file + custom_fixed_fee_example() \ No newline at end of file diff --git a/examples/tokens/custom_fee_fractional.py b/examples/tokens/custom_fee_fractional.py index 7154773e9..3ceb64d6c 100644 --- a/examples/tokens/custom_fee_fractional.py +++ b/examples/tokens/custom_fee_fractional.py @@ -1,5 +1,5 @@ """ -Run with: +Run with: uv run examples/tokens/custom_fractional_fee.py python examples/tokens/custom_fractional_fee.py """ @@ -8,7 +8,10 @@ import sys from dotenv import load_dotenv -from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction, TokenParams +from hiero_sdk_python.tokens.token_create_transaction import ( + TokenCreateTransaction, + TokenParams, +) from hiero_sdk_python.tokens.custom_fractional_fee import CustomFractionalFee from hiero_sdk_python.tokens.fee_assessment_method import FeeAssessmentMethod from hiero_sdk_python.query.token_info_query import TokenInfoQuery @@ -23,6 +26,7 @@ load_dotenv() + def setup_client(): network_name = os.getenv("NETWORK", "testnet") @@ -36,17 +40,18 @@ def setup_client(): print(f"Connecting to Hedera {network_name} network!") client = Client(network) - operator_id = AccountId.from_string(os.getenv("OPERATOR_ID", '')) - operator_key = PrivateKey.from_string(os.getenv("OPERATOR_KEY", '')) + 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}") except Exception as e: - raise ConnectionError(f'Error initializing client: {e}') from e + raise ConnectionError(f"Error initializing client: {e}") from e print(f"βœ… Connected to Hedera {network_name} network as operator: {operator_id}") return client, operator_id, operator_key + def build_fractional_fee(operator_account: AccountId) -> CustomFractionalFee: """Creates a CustomFractionalFee instance.""" return CustomFractionalFee( @@ -58,7 +63,8 @@ def build_fractional_fee(operator_account: AccountId) -> CustomFractionalFee: fee_collector_account_id=operator_account, all_collectors_are_exempt=True, ) - + + def create_token_with_fee_key(client, operator_id, fractional_fee: CustomFractionalFee): """Create a fungible token with a fee_schedule_key.""" print("Creating fungible token with fee_schedule_key...") @@ -74,19 +80,20 @@ def create_token_with_fee_key(client, operator_id, fractional_fee: CustomFractio supply_type=SupplyType.INFINITE, custom_fees=fractional_fee, ) - + tx = TokenCreateTransaction(token_params=token_params) tx.freeze_with(client) receipt = tx.execute(client) - + if receipt.status != ResponseCode.SUCCESS: print(f"Token creation failed: {ResponseCode(receipt.status).name}") sys.exit(1) - + token_id = receipt.token_id print(f"Token created with ID: {token_id}") return token_id + def print_fractional_fees(token_info, fractional_fee): """Print all CustomFractionalFee objects from a TokenInfo.""" if not token_info.custom_fees: @@ -96,22 +103,25 @@ def print_fractional_fees(token_info, fractional_fee): print("\n--- Custom Fractional Fee ---") print(fractional_fee) + def query_and_validate_fractional_fee(client: Client, token_id): """Fetch token info from Hedera and print the custom fractional fees.""" print("\nQuerying token info to validate fractional fee...") token_info = TokenInfoQuery(token_id=token_id).execute(client) return token_info + def main(): client, operator_id, _ = setup_client() # Build fractional fee fractional_fee = build_fractional_fee(operator_id) token_id = create_token_with_fee_key(client, operator_id, fractional_fee) - + # Query and validate fractional fee token_info = query_and_validate_fractional_fee(client, token_id) print_fractional_fees(token_info, fractional_fee) print("βœ… Example completed successfully.") - + + if __name__ == "__main__": main() diff --git a/examples/tokens/custom_fee_royalty.py b/examples/tokens/custom_fee_royalty.py deleted file mode 100644 index 7f1ec3173..000000000 --- a/examples/tokens/custom_fee_royalty.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Run with: -uv run examples/tokens/custom_royalty_fee.py -python examples/tokens/custom_royalty_fee.py -""" -from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee -from hiero_sdk_python.tokens.custom_royalty_fee import CustomRoyaltyFee -from hiero_sdk_python.account.account_id import AccountId -from hiero_sdk_python.tokens.token_id import TokenId - -def custom_royalty_fee(): - fallback_fee = CustomFixedFee( - amount=50, - denominating_token_id=TokenId(0, 0, 789), - ) - royalty_fee = CustomRoyaltyFee( - numerator=5, - denominator=100, - fallback_fee=fallback_fee, - fee_collector_account_id=AccountId(0, 0, 456), - all_collectors_are_exempt=True, - ) - print(royalty_fee) - - # Convert to protobuf - royalty_fee_proto = royalty_fee._to_proto() - - print("Royalty Fee Protobuf:", royalty_fee_proto) - -if __name__ == "__main__": - custom_royalty_fee() \ No newline at end of file diff --git a/examples/tokens/custom_royalty_fee.py b/examples/tokens/custom_royalty_fee.py new file mode 100644 index 000000000..b43371e5d --- /dev/null +++ b/examples/tokens/custom_royalty_fee.py @@ -0,0 +1,123 @@ +""" +Run with: +uv run examples/tokens/custom_royalty_fee.py +python examples/tokens/custom_royalty_fee.py +""" + +import os +import sys +from dotenv import load_dotenv + +from hiero_sdk_python.client.network import Network +from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.client.client import Client +from hiero_sdk_python.query.token_info_query import TokenInfoQuery +from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.tokens.supply_type import SupplyType +from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee +from hiero_sdk_python.tokens.custom_royalty_fee import CustomRoyaltyFee +from hiero_sdk_python.hbar import Hbar +from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction +from hiero_sdk_python.tokens.token_type import TokenType + +load_dotenv() +network_name = os.getenv("NETWORK", "testnet").lower() + +def setup_client(): + """Initialize and set up the client with operator account""" + + network = Network(network_name) + print(f"Connecting to the Hedera {network_name} network") + client = Client(network) + + try: + operator_id_str = os.getenv('OPERATOR_ID') + operator_key_str = os.getenv('OPERATOR_KEY') + + if not operator_id_str or not operator_key_str: + raise ValueError("Environment variables OPERATOR_ID or OPERATOR_KEY are missing.") + + operator_id = AccountId.from_string(operator_id_str) + operator_key = PrivateKey.from_string(operator_key_str) + + 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) as e: + print(f"Error: {e}") + print("Please check OPERATOR_ID and OPERATOR_KEY in your .env file.") + sys.exit(1) + +def custom_royalty_fee_example(): + """Demonstrates how to create a token with a custom royalty fee.""" + + client, operator_id, operator_key = setup_client() + + with client: + print("\n--- Creating Custom Royalty Fee ---") + + fallback_fee = CustomFixedFee( + amount=Hbar(1).to_tinybars(), + fee_collector_account_id=operator_id, + all_collectors_are_exempt=False + ) + + royalty_fee = CustomRoyaltyFee( + numerator=5, + denominator=100, + fallback_fee=fallback_fee, + fee_collector_account_id=operator_id, + all_collectors_are_exempt=False + ) + + print(f"Royalty Fee Configured: {royalty_fee.numerator}/{royalty_fee.denominator}") + print(f"Fallback Fee: {Hbar.from_tinybars(fallback_fee.amount)} HBAR") + + print("\n--- Creating Token with Royalty Fee ---") + transaction = ( + TokenCreateTransaction() + .set_token_name("Royalty NFT Collection") + .set_token_symbol("RNFT") + .set_treasury_account_id(operator_id) + .set_admin_key(operator_key) + .set_supply_key(operator_key) + .set_token_type(TokenType.NON_FUNGIBLE_UNIQUE) + .set_decimals(0) + .set_initial_supply(0) + .set_supply_type(SupplyType.FINITE) + .set_max_supply(100) + .set_custom_fees([royalty_fee]) + .freeze_with(client) + .sign(operator_key) + ) + + try: + receipt = transaction.execute(client) + + if receipt.status != ResponseCode.SUCCESS: + print(f"Transaction failed with status: {ResponseCode(receipt.status).name}") + return + + token_id = receipt.token_id + print(f"Token created successfully with ID: {token_id}") + + print("\n--- Verifying Fee on Network ---") + token_info = TokenInfoQuery().set_token_id(token_id).execute(client) + + retrieved_fees = token_info.custom_fees + if retrieved_fees: + print(f"Success! Found {len(retrieved_fees)} custom fee(s) on token.") + for fee in retrieved_fees: + print(f"Fee Collector: {fee.fee_collector_account_id}") + print(f"Fee Details: {fee}") + else: + print("Error: No custom fees found on the token.") + + except Exception as e: + print(f"Transaction execution failed: {e}") + sys.exit(1) + +if __name__ == "__main__": + custom_royalty_fee_example() \ No newline at end of file diff --git a/examples/tokens/token_airdrop_claim_auto.py b/examples/tokens/token_airdrop_claim_auto.py index 7199258bb..562d93c2d 100644 --- a/examples/tokens/token_airdrop_claim_auto.py +++ b/examples/tokens/token_airdrop_claim_auto.py @@ -1,7 +1,8 @@ """ Hedera Token Airdrop Example Script -This script demonstrates and end-to-end example for an account to automatically (no user action required) claim a set of airdrops. +This script demonstrates an end-to-end example for an account to +automatically (no user action required) claim a set of airdrops. Unique configurations of this account: - 10 auto-association slots. @@ -19,6 +20,13 @@ uv run examples/tokens/token_airdrop_claim_auto.py python examples/tokens/token_airdrop_claim_auto.py """ + +# pylint: disable=import-error, +# pylint: disable=too-many-arguments, +# pylint: disable=too-many-positional-arguments, +# pylint: disable=protected-access, +# pylint: disable=broad-exception-caught + import os import sys from typing import Iterable @@ -39,12 +47,14 @@ ResponseCode, Hbar, TokenId, - TokenNftInfoQuery + TokenNftInfoQuery, ) load_dotenv() + def setup_client(): + """Set up and return a Hedera client using environment configuration.""" network_name = os.getenv("NETWORK", "testnet") # Validate environment variables @@ -57,22 +67,22 @@ def setup_client(): print(f"Connecting to Hedera {network_name} network!") client = Client(network) - operator_id = AccountId.from_string(os.getenv("OPERATOR_ID", '')) - operator_key = PrivateKey.from_string(os.getenv("OPERATOR_KEY", '')) + 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}") except Exception as e: - raise ConnectionError(f"Error initializing client: {e}") + raise ConnectionError(f"Error initializing client: {e}") from e print(f"βœ… Connected to Hedera {network_name} network as operator: {operator_id}") return client, operator_id, operator_key + def create_receiver( - client: Client, - signature_required: bool =False, - max_auto_assoc: int =10 - ): + client: Client, signature_required: bool = False, max_auto_assoc: int = 10 +): + """Create and return a configured Hedera client.""" receiver_key = PrivateKey.generate() receiver_public_key = receiver_key.public_key() @@ -102,16 +112,17 @@ def create_receiver( def create_fungible_token( - client: Client, - operator_id: AccountId, - operator_key: PrivateKey, - name: str ="My Fungible Token", - symbol: str ="MFT", - initial_supply: int =50, - max_supply: int = 1000, - ): + client: Client, + operator_id: AccountId, + operator_key: PrivateKey, + name: str = "My Fungible Token", + symbol: str = "MFT", + initial_supply: int = 50, + max_supply: int = 1000, +): + """Create and return a fungible token on the Hedera network.""" try: - receipt = ( + receipt = ( TokenCreateTransaction() .set_token_name(name) .set_token_symbol(symbol) @@ -136,15 +147,16 @@ def create_fungible_token( def create_nft_token( - client: Client, - operator_id: AccountId, - operator_key: PrivateKey, - name: str ="My NFT Token", - symbol: str ="MNT", - max_supply: int = 100 - ): + client: Client, + operator_id: AccountId, + operator_key: PrivateKey, + name: str = "My NFT Token", + symbol: str = "MNT", + max_supply: int = 100, +): + """Create and return a non-fungible (NFT) token on the Hedera network.""" try: - receipt = ( + receipt = ( TokenCreateTransaction() .set_token_name(name) .set_token_symbol(symbol) @@ -170,12 +182,13 @@ def create_nft_token( def mint_nft_token( - client: Client, - operator_key: PrivateKey, - nft_token_id: TokenId, - ): + client: Client, + operator_key: PrivateKey, + nft_token_id: TokenId, +): + """Mint a new NFT for the given NFT token and return its serial number.""" try: - receipt = ( + receipt = ( TokenMintTransaction() .set_token_id(nft_token_id) .set_metadata([b"NFT Metadata Example"]) @@ -189,29 +202,38 @@ def mint_nft_token( if receipt.status != ResponseCode.SUCCESS: status_message = ResponseCode(receipt.status).name raise RuntimeError(f"❌ NFT token mint failed: {status_message}") - - print(f"βœ… NFT {nft_token_id} serial {serial} minted with NFT id of {nft_id}. Total NFT supply is {total_supply} ") + print(f"βœ… NFT {nft_token_id} serial {serial} minted with NFT id of {nft_id}.") + print(f"Total NFT supply is {total_supply}") return nft_id except Exception as e: raise RuntimeError(f"❌ Error minting NFT token: {e}") from e + + def log_balances( client: Client, operator_id: AccountId, receiver_id: AccountId, fungible_ids: Iterable[TokenId], nft_ids: Iterable[NftId], - prefix: str = "" + prefix: str = "", ): + """Fetch and log token balances for operator and receiver accounts.""" print(f"\n===== {prefix} Balances =====") try: - operator_balance = CryptoGetAccountBalanceQuery().set_account_id(operator_id).execute(client) - receiver_balance = CryptoGetAccountBalanceQuery().set_account_id(receiver_id).execute(client) - except Exception as e: + operator_balance = ( + CryptoGetAccountBalanceQuery().set_account_id(operator_id).execute(client) + ) + receiver_balance = ( + CryptoGetAccountBalanceQuery().set_account_id(receiver_id).execute(client) + ) + except Exception as e: # pylint: disable=broad-exception-caught print(f"❌ Failed to fetch balances: {e}") return - def log_fungible(account_id: AccountId, balances: dict, token_ids: Iterable[TokenId]): + def log_fungible( + _account_id: AccountId, balances: dict, token_ids: Iterable[TokenId] + ): print(" Fungible tokens:") for token_id in token_ids: print(f" {token_id}: {balances.get(token_id, 0)}") @@ -242,23 +264,29 @@ def log_nfts(account_id: AccountId, nft_ids: Iterable[NftId]): print("=============================================\n") -def perform_airdrop( - client: Client, - operator_id: AccountId, - operator_key: PrivateKey, - receiver_id: AccountId, - fungible_ids: Iterable[TokenId], - nft_ids: Iterable[NftId], - ft_amount: int = 100 - ): +def perform_airdrop( + client: Client, + operator_id: AccountId, + operator_key: PrivateKey, + receiver_id: AccountId, + fungible_ids: Iterable[TokenId], + nft_ids: Iterable[NftId], + ft_amount: int = 100, +): + """Perform a token airdrop from operator to receiver.""" try: tx = TokenAirdropTransaction() for fungible_id in fungible_ids: tx.add_token_transfer(fungible_id, operator_id, -ft_amount) tx.add_token_transfer(fungible_id, receiver_id, ft_amount) - print(f"πŸ“€ Transferring {ft_amount} of fungible token {fungible_id} from {operator_id} β†’ {receiver_id}") + + message = ( + f"πŸ“€ Transferring {ft_amount} of fungible token {fungible_id} " + f"from {operator_id} β†’ {receiver_id}" + ) + print(message) for nft_id in nft_ids: tx.add_nft_transfer(nft_id, operator_id, receiver_id) @@ -269,47 +297,94 @@ def perform_airdrop( if receipt.status != ResponseCode.SUCCESS: status_message = ResponseCode(receipt.status).name - raise RuntimeError(f"Airdrop transaction failed with status: {status_message}") + raise RuntimeError( + f"Airdrop transaction failed with status: {status_message}" + ) - print(f"βœ… Airdrop executed successfully! Transaction ID: {receipt.transaction_id}") + print( + f"βœ… Airdrop executed successfully! Transaction ID: {receipt.transaction_id}" + ) except Exception as e: print(f"❌ Airdrop failed: {e}") raise RuntimeError("Airdrop execution failed") from e + def main(): + """Run the token airdrop auto-claim example workflow.""" # Set up client and return client, operator_id, operator_key client, operator_id, operator_key = setup_client() - # Create and return a fungible token to airdrop + # Create and return a fungible token to airdrop print("Create 50 fungible tokens and 1 NFT to airdrop") - fungible_id = create_fungible_token(client, operator_id, operator_key, name="My Fungible Token", symbol="123", initial_supply=50, max_supply = 2000) - - # Create and return an nft token to airdrop - nft_token_id = create_nft_token(client, operator_id, operator_key, name="My NFT Token", symbol = "MNFT", max_supply=1000) + fungible_id = create_fungible_token( + client, + operator_id, + operator_key, + name="My Fungible Token", + symbol="123", + initial_supply=50, + max_supply=2000, + ) + + # Create and return an nft token to airdrop + nft_token_id = create_nft_token( + client, + operator_id, + operator_key, + name="My NFT Token", + symbol="MNFT", + max_supply=1000, + ) # Mint and return an nft to airdrop nft_serial = mint_nft_token(client, operator_key, nft_token_id) # Create a receiver that will test no signature is required to claim the auto-airdrop - # Ensure false for signature required - # Assume 10 max association slots + # Ensure false for signature required + # Assume 10 max association slots # Return the receiver id and receiver private key print("Creating the account that will automatically receive the airdropped tokens") receiver_id, receiver_key = create_receiver(client, False, 10) # Check pre-airdrop balances print("\nπŸ” Verifying sender has tokens to airdrop and receiver neither:") - log_balances(client, operator_id, receiver_id, [fungible_id], [nft_serial], prefix="Before airdrop") + log_balances( + client, + operator_id, + receiver_id, + [fungible_id], + [nft_serial], + prefix="Before airdrop", + ) # Initiate airdrop of 20 fungible tokens and 1 nft token id - perform_airdrop(client, operator_id, operator_key, receiver_id, [fungible_id], [nft_serial], 20) - - print("\nπŸ” Verifying receiver has received airdrop contents automatically and sender has sent:") - log_balances(client, operator_id, receiver_id, [fungible_id], [nft_serial], prefix="After airdrop") + perform_airdrop( + client, operator_id, operator_key, receiver_id, [fungible_id], [nft_serial], 20 + ) + + print( + "\nπŸ” Verifying receiver has received airdrop contents automatically" + "and sender has sent:" + ) + log_balances( + client, + operator_id, + receiver_id, + [fungible_id], + [nft_serial], + prefix="After airdrop", + ) + + print( + "βœ… Auto-association successful: Receiver accepted airdropped tokens " + "without pre-association." + ) + print( + "βœ… Airdrop successful: Receiver accepted new fungible tokens " + "without pre-association." + ) - print("βœ… Auto-association successful: Receiver accepted airdropped tokens without pre-association.") - print("βœ… Airdrop successful: Receiver accepted new fungible tokens without pre-association.") if __name__ == "__main__": main() diff --git a/examples/tokens/token_airdrop_claim_signature_required.py b/examples/tokens/token_airdrop_claim_signature_required.py index ddb2e2716..2abcb7ca6 100644 --- a/examples/tokens/token_airdrop_claim_signature_required.py +++ b/examples/tokens/token_airdrop_claim_signature_required.py @@ -22,6 +22,10 @@ python examples/tokens/token_airdrop_claim_signature_required.py """ +# pylint: disable=import-error, +# pylint: disable=too-many-arguments, +# pylint: disable=protected-access, +# pylint: disable=broad-except import os import sys from typing import Iterable, List, Dict @@ -48,12 +52,16 @@ PendingAirdropId, TransactionRecordQuery, TransactionRecord, - TransactionId + TransactionId, ) load_dotenv() + def setup_client(): + """ + Sets up the Hedera client using environment variables. + """ network_name = os.getenv("NETWORK", "testnet") # Validate environment variables @@ -66,22 +74,24 @@ def setup_client(): print(f"Connecting to Hedera {network_name} network!") client = Client(network) - operator_id = AccountId.from_string(os.getenv("OPERATOR_ID", '')) - operator_key = PrivateKey.from_string(os.getenv("OPERATOR_KEY", '')) + 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}") - except Exception as e: - raise ConnectionError(f'Error initializing client: {e}') from e + except Exception as exc: + raise ConnectionError(f"Error initializing client: {exc}") from exc print(f"βœ… Connected to Hedera {network_name} network as operator: {operator_id}") return client, operator_id, operator_key + def create_receiver( - client: Client, - signature_required: bool =True, - max_auto_assoc: int = 0 - ): + client: Client, signature_required: bool = True, max_auto_assoc: int = 0 +): + """ + Creates a receiver account with specific configurations. + """ receiver_key = PrivateKey.generate() receiver_public_key = receiver_key.public_key() @@ -106,18 +116,22 @@ def create_receiver( f"(auto-assoc={max_auto_assoc}, sig_required={signature_required})" ) return receiver_id, receiver_key - except Exception as e: - raise RuntimeError(f"❌ Error creating receiver account: {e}") from e + except Exception as exc: + raise RuntimeError(f"❌ Error creating receiver account: {exc}") from exc + def create_fungible_token( - client: Client, - operator_id: AccountId, - operator_key: PrivateKey, - name: str ="My Fungible Token", - symbol: str ="MFT", - initial_supply: int =50, - max_supply: int = 1000, - ): + client: Client, + operator_id: AccountId, + operator_key: PrivateKey, + name: str = "My Fungible Token", + symbol: str = "MFT", + initial_supply: int = 50, + max_supply: int = 1000, +): + """ + Creates a fungible token. + """ try: receipt = ( TokenCreateTransaction() @@ -139,17 +153,21 @@ def create_fungible_token( print(f"βœ… Fungible token created: {token_id}") return token_id - except Exception as e: - raise RuntimeError(f"❌ Error creating fungible token: {e}") from e + except Exception as exc: + raise RuntimeError(f"❌ Error creating fungible token: {exc}") from exc + def create_nft_token( - client: Client, - operator_id: AccountId, - operator_key: PrivateKey, - name: str ="My NFT Token", - symbol: str ="MNT", - max_supply: int = 100 - ): + client: Client, + operator_id: AccountId, + operator_key: PrivateKey, + name: str = "My NFT Token", + symbol: str = "MNT", + max_supply: int = 100, +): + """ + Creates an NFT token. + """ try: receipt = ( TokenCreateTransaction() @@ -172,14 +190,18 @@ def create_nft_token( print(f"βœ… NFT token created: {token_id}") return token_id - except Exception as e: - raise RuntimeError(f"❌ Error creating NFT token: {e}") from e + except Exception as exc: + raise RuntimeError(f"❌ Error creating NFT token: {exc}") from exc + def mint_nft_token( - client: Client, - operator_key: PrivateKey, - nft_token_id: TokenId, - ): + client: Client, + operator_key: PrivateKey, + nft_token_id: TokenId, +): + """ + Mints an NFT token. + """ try: receipt = ( TokenMintTransaction() @@ -199,22 +221,26 @@ def mint_nft_token( print(f"βœ… NFT {nft_token_id} serial {serial} minted with NFT id of {nft_id}.") print(f" Total NFT supply is {total_supply}") return nft_id - except Exception as e: - raise RuntimeError(f"❌ Error minting NFT token: {e}") from e + except Exception as exc: + raise RuntimeError(f"❌ Error minting NFT token: {exc}") from exc + def get_token_association_status( - client: Client, - receiver_id: AccountId, - token_ids: List[TokenId] - ) -> Dict[TokenId, bool]: + client: Client, receiver_id: AccountId, token_ids: List[TokenId] +) -> Dict[TokenId, bool]: + """ + Checks if the receiver account is associated with the given tokens. + """ try: # Query the receiver's balance, which includes token associations - balance = CryptoGetAccountBalanceQuery()\ - .set_account_id(receiver_id)\ - .execute(client) + balance = ( + CryptoGetAccountBalanceQuery().set_account_id(receiver_id).execute(client) + ) associated_tokens = set(balance.token_balances.keys()) - association_status = {token_id: token_id in associated_tokens for token_id in token_ids} + association_status = { + token_id: token_id in associated_tokens for token_id in token_ids + } print(f"βœ… Association status for account {receiver_id}:") for tid, associated in association_status.items(): @@ -222,10 +248,15 @@ def get_token_association_status( return association_status - except Exception as e: - print(f"❌ Failed to fetch token associations for account {receiver_id}: {e}") + except Exception as exc: + print(f"❌ Failed to fetch token associations for account {receiver_id}: {exc}") return {token_id: False for token_id in token_ids} -def log_fungible_balances(account_id: AccountId, balances: dict, token_ids: Iterable[TokenId]): + + +def log_fungible_balances(balances: dict, token_ids: Iterable[TokenId]): + """ + Logs the balances of fungible tokens. + """ print(" Fungible tokens:") for token_id in token_ids: amount = balances.get(token_id, 0) @@ -233,6 +264,9 @@ def log_fungible_balances(account_id: AccountId, balances: dict, token_ids: Iter def log_nft_balances(client: Client, account_id: AccountId, nft_ids: Iterable[NftId]): + """ + Logs the ownership of NFTs. + """ print(" NFTs:") owned_nfts = [] for nft_id in nft_ids: @@ -240,8 +274,8 @@ def log_nft_balances(client: Client, account_id: AccountId, nft_ids: Iterable[Nf info = TokenNftInfoQuery().set_nft_id(nft_id).execute(client) if info.account_id == account_id: owned_nfts.append(str(nft_id)) - except Exception as e: - print(f" ⚠️ Error fetching NFT {nft_id}: {e}") + except Exception as exc: + print(f" ⚠️ Error fetching NFT {nft_id}: {exc}") if owned_nfts: for nft in owned_nfts: @@ -256,15 +290,22 @@ def log_balances( receiver_id: AccountId, fungible_ids: Iterable[TokenId], nft_ids: Iterable[NftId], - prefix: str = "" + prefix: str = "", ): + """ + Logs the balances of both the operator and receiver accounts. + """ print(f"\n===== {prefix} Balances =====") try: - operator_balance = CryptoGetAccountBalanceQuery().set_account_id(operator_id).execute(client) - receiver_balance = CryptoGetAccountBalanceQuery().set_account_id(receiver_id).execute(client) - except Exception as e: - print(f"❌ Failed to fetch balances: {e}") + operator_balance = ( + CryptoGetAccountBalanceQuery().set_account_id(operator_id).execute(client) + ) + receiver_balance = ( + CryptoGetAccountBalanceQuery().set_account_id(receiver_id).execute(client) + ) + except Exception as exc: + print(f"❌ Failed to fetch balances: {exc}") return operator_balances = dict(operator_balance.token_balances) @@ -274,60 +315,66 @@ def log_balances( # SENDER BALANCES # ------------------------------ print(f"\nSender ({operator_id}):") - log_fungible_balances(operator_id, operator_balances, fungible_ids) + log_fungible_balances(operator_balances, fungible_ids) log_nft_balances(client, operator_id, nft_ids) # ------------------------------ # RECEIVER BALANCES # ------------------------------ print(f"\nReceiver ({receiver_id}):") - log_fungible_balances(receiver_id, receiver_balances, fungible_ids) + log_fungible_balances(receiver_balances, fungible_ids) log_nft_balances(client, receiver_id, nft_ids) print("=============================================\n") + def perform_airdrop( - client: Client, - operator_id: AccountId, - operator_key: PrivateKey, - receiver_id: AccountId, - fungible_ids: Iterable[TokenId], - nft_ids: Iterable[NftId], - ft_amount: int = 100 - ): + client: Client, + operator_id: AccountId, + operator_key: PrivateKey, + receiver_id: AccountId, + fungible_ids: Iterable[TokenId], + nft_ids: Iterable[NftId], + ft_amount: int = 100, +): + """ + Performs an airdrop of fungible and NFT tokens. + """ try: - tx = TokenAirdropTransaction() + transaction = TokenAirdropTransaction() for fungible_id in fungible_ids: - tx.add_token_transfer(fungible_id, operator_id, -ft_amount) - tx.add_token_transfer(fungible_id, receiver_id, ft_amount) + transaction.add_token_transfer(fungible_id, operator_id, -ft_amount) + transaction.add_token_transfer(fungible_id, receiver_id, ft_amount) print(f"πŸ“€ Transferring {ft_amount} of fungible token {fungible_id}") print(f" from {operator_id} β†’ {receiver_id}") for nft_id in nft_ids: - tx.add_nft_transfer(nft_id, operator_id, receiver_id) + transaction.add_nft_transfer(nft_id, operator_id, receiver_id) print(f"🎨 Transferring NFT {nft_id} from {operator_id} β†’ {receiver_id}") print("\n⏳ Submitting airdrop transaction...") - receipt = tx.freeze_with(client).sign(operator_key).execute(client) + receipt = transaction.freeze_with(client).sign(operator_key).execute(client) if receipt.status != ResponseCode.SUCCESS: status_message = ResponseCode(receipt.status).name - raise RuntimeError(f"Airdrop transaction failed with status: {status_message}") + raise RuntimeError( + f"Airdrop transaction failed with status: {status_message}" + ) transaction_id = receipt.transaction_id print(f"βœ… Airdrop executed successfully! Transaction ID: {transaction_id}") return transaction_id - except Exception as e: - print(f"❌ Airdrop failed: {e}") - raise RuntimeError("Airdrop execution failed") from e + except Exception as exc: + print(f"❌ Airdrop failed: {exc}") + raise RuntimeError("Airdrop execution failed") from exc + def fetch_pending_airdrops( - client: Client, - transaction_id: TransactionId - ) -> List[PendingAirdropId]: + client: Client, transaction_id: TransactionId +) -> List[PendingAirdropId]: """ Retrieve all pending airdrop IDs generated by a specific transaction. @@ -336,7 +383,9 @@ def fetch_pending_airdrops( `new_pending_airdrops` field. """ try: - record: TransactionRecord = TransactionRecordQuery(transaction_id).execute(client) + record: TransactionRecord = TransactionRecordQuery(transaction_id).execute( + client + ) pending_airdrops = record.new_pending_airdrops # List of PendingAirdropRecord pending_airdrop_ids = [p.pending_airdrop_id for p in pending_airdrops] @@ -347,15 +396,16 @@ def fetch_pending_airdrops( return pending_airdrop_ids - except Exception as e: - print(f"❌ Failed to fetch pending airdrops for transaction {transaction_id}: {e}") + except Exception as exc: + print( + f"❌ Failed to fetch pending airdrops for transaction {transaction_id}: {exc}" + ) return [] + def claim_airdrop( - client: Client, - receiver_key: PrivateKey, - pending_airdrops: List[PendingAirdropId] - ): + client: Client, receiver_key: PrivateKey, pending_airdrops: List[PendingAirdropId] +): """ Claims one or more pending airdrops on behalf of the receiver. @@ -364,26 +414,30 @@ def claim_airdrop( safely retrieve and display the list of pending airdrop IDs before execution """ try: - tx = ( + transaction = ( TokenClaimAirdropTransaction() .add_pending_airdrop_ids(pending_airdrops) .freeze_with(client) - .sign(receiver_key) # Signing with receiver is required + .sign(receiver_key) # Signing with receiver is required ) - print(f"{tx}") + print(f"{transaction}") - receipt = tx.execute(client) + receipt = transaction.execute(client) if receipt.status != ResponseCode.SUCCESS: status_message = ResponseCode(receipt.status).name raise RuntimeError(f"❌ Airdrop claim failed: {status_message}") - print(f"βœ… airdrop claimed") + print("βœ… airdrop claimed") return receipt - except Exception as e: - raise RuntimeError(f"❌ Error claiming airdrop: {e}") from e + except Exception as exc: + raise RuntimeError(f"❌ Error claiming airdrop: {exc}") from exc + def main(): + """ + Main function to execute the airdrop claim example. + """ # Set up client and return client, operator_id, operator_key client, operator_id, operator_key = setup_client() @@ -396,7 +450,7 @@ def main(): name="My Fungible Token", symbol="123", initial_supply=50, - max_supply = 2000 + max_supply=2000, ) # Create and return an nft token to airdrop @@ -405,35 +459,25 @@ def main(): operator_id, operator_key, name="My NFT Token", - symbol = "MNFT", - max_supply=1000 + symbol="MNFT", + max_supply=1000, ) # Mint and return an nft to airdrop - nft_serial = mint_nft_token( - client, - operator_key, - nft_token_id - ) + nft_serial = mint_nft_token(client, operator_key, nft_token_id) # Create a receiver that will require signature to claim the airdrop - # Ensure true for signature required (for the receiver) - # 0 max association slots + # Ensure true for signature required (for the receiver) + # 0 max association slots # Return the receiver id and receiver private key print("Creating the account that will receive the airdropped tokens on signing") - receiver_id, receiver_key = create_receiver( - client, - True, - 0 - ) + receiver_id, receiver_key = create_receiver(client, True, 0) # Verify this receiver does NOT have any of the fungible or NFT tokens associated # Claim airdrop will work regardless token_ids_to_check = [fungible_id, nft_token_id] association_status = get_token_association_status( - client, - receiver_id, - token_ids_to_check + client, receiver_id, token_ids_to_check ) print(association_status) @@ -445,23 +489,24 @@ def main(): receiver_id, [fungible_id], [nft_serial], - prefix="Before airdrop" + prefix="Before airdrop", ) # Initiate airdrop of 20 fungible tokens and 1 nft token id transaction_id = perform_airdrop( + client, operator_id, operator_key, receiver_id, [fungible_id], [nft_serial], 20 + ) + + print("\nπŸ” Verifying no balance change as airdrop is not yet claimed:") + log_balances( client, operator_id, - operator_key, receiver_id, [fungible_id], [nft_serial], - 20 + prefix="After airdrop", ) - print("\nπŸ” Verifying no balance change as airdrop is not yet claimed:") - log_balances(client,operator_id,receiver_id,[fungible_id],[nft_serial],prefix="After airdrop") - # Get a list of pending airdrops pending_airdrop_ids = fetch_pending_airdrops(client, transaction_id) print(pending_airdrop_ids) @@ -471,16 +516,28 @@ def main(): # Claiming will work even without association and available auto-association slots # This is because the signing itself causes the Hedera network to associate the tokens. print("Claiming airdrop:") - claim_airdrop(client,receiver_key,pending_airdrop_ids) #Pass the receiver key which is needed to sign + claim_airdrop( + client, receiver_key, pending_airdrop_ids + ) # Pass the receiver key which is needed to sign # Check airdrop has resulted in transfers print("\nπŸ” Verifying balances have now changed after claim:") - log_balances(client,operator_id,receiver_id,[fungible_id],[nft_serial],prefix="After claim") + log_balances( + client, + operator_id, + receiver_id, + [fungible_id], + [nft_serial], + prefix="After claim", + ) # Check Hedera network has associated these tokens behind the scenes token_ids_to_check = [fungible_id, nft_token_id] - association_status = get_token_association_status(client,receiver_id,token_ids_to_check) + association_status = get_token_association_status( + client, receiver_id, token_ids_to_check + ) print(association_status) + if __name__ == "__main__": main() diff --git a/examples/tokens/token_airdrop_transaction.py b/examples/tokens/token_airdrop_transaction.py index 46827420f..e411f111f 100644 --- a/examples/tokens/token_airdrop_transaction.py +++ b/examples/tokens/token_airdrop_transaction.py @@ -7,26 +7,27 @@ import sys from dotenv import load_dotenv from hiero_sdk_python import ( - Client, - Network, - AccountId, - PrivateKey, - Hbar, - AccountCreateTransaction, - TokenCreateTransaction, - TokenAirdropTransaction, - TokenAssociateTransaction, - TokenMintTransaction, - CryptoGetAccountBalanceQuery, - TokenType, - ResponseCode, - NftId, - TransactionRecordQuery, - TokenNftInfoQuery + Client, + Network, + AccountId, + PrivateKey, + Hbar, + AccountCreateTransaction, + TokenCreateTransaction, + TokenAirdropTransaction, + TokenAssociateTransaction, + TokenMintTransaction, + TokenType, + ResponseCode, + NftId, + TransactionRecordQuery, + TokenNftInfoQuery, ) +from hiero_sdk_python.query.account_info_query import AccountInfoQuery load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -35,18 +36,18 @@ def setup_client(): 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}") + 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, operator_id, operator_key + return client, operator_id, operator_key except (TypeError, ValueError): print("❌ Error: Creating client, Please check your .env file") sys.exit(1) + def create_account(client, operator_key): """Create a new recipient account""" print("\nCreating a new account...") @@ -58,7 +59,9 @@ def create_account(client, operator_key): .set_key(recipient_key.public_key()) .set_initial_balance(Hbar.from_tinybars(100_000_000)) ) - account_receipt = account_tx.freeze_with(client).sign(operator_key).execute(client) + account_receipt = ( + account_tx.freeze_with(client).sign(operator_key).execute(client) + ) recipient_id = account_receipt.account_id print(f"βœ… Success! Created a new recipient account with ID: {recipient_id}") @@ -67,6 +70,7 @@ def create_account(client, operator_key): print(f"❌ Error creating new recipient account: {e}") sys.exit(1) + def create_token(client, operator_id, operator_key): """Create a fungible token""" print("\nCreating a token...") @@ -89,6 +93,7 @@ def create_token(client, operator_id, operator_key): print(f"❌ Error creating token: {e}") sys.exit(1) + def create_nft(client, operator_key, operator_id): """Create a NFT""" print("\nCreating a nft...") @@ -111,6 +116,7 @@ def create_nft(client, operator_key, operator_id): print(f"❌ Error creating nft: {e}") sys.exit(1) + def mint_nft(client, operator_key, nft_id): """Mint the NFT with metadata""" print("\nMinting a nft...") @@ -127,26 +133,28 @@ def mint_nft(client, operator_key, nft_id): print(f"❌ Error minting nft: {e}") sys.exit(1) + def associate_tokens(client, recipient_id, recipient_key, tokens): """Associate the token and nft with the recipient""" print("\nAssociating tokens to recipient...") try: assocciate_tx = TokenAssociateTransaction( - account_id=recipient_id, - token_ids=tokens + account_id=recipient_id, token_ids=tokens ) assocciate_tx.freeze_with(client) assocciate_tx.sign(recipient_key) assocciate_tx.execute(client) - balance_before = ( - CryptoGetAccountBalanceQuery(account_id=recipient_id) - .execute(client) - .token_balances - ) - print("Tokens associated with recipient:") - print(f"{tokens[0]}: {balance_before.get(tokens[0])}") - print(f"{tokens[1]}: {balance_before.get(tokens[1])}") + # Use AccountInfoQuery to check token associations + info = AccountInfoQuery(account_id=recipient_id).execute(client) + + # Get the list of associated token IDs from token_relationships + associated_token_ids = [rel.token_id for rel in info.token_relationships] + + print("\nTokens associated with recipient:") + for token_id in tokens: + associated = token_id in associated_token_ids + print(f" {token_id}: {'Associated' if associated else 'NOT Associated'}") print("\nβœ… Success! Token association complete.") @@ -155,14 +163,18 @@ def associate_tokens(client, recipient_id, recipient_key, tokens): sys.exit(1) -def airdrop_tokens(client, operator_id, operator_key, recipient_id, token_id, nft_id, serial_number): +def airdrop_tokens( + client, operator_id, operator_key, recipient_id, token_id, nft_id, serial_number +): """ Build and execute a TokenAirdropTransaction that transfers one fungible token and the specified NFT serial. Returns the airdrop receipt. """ print(f"\nStep 6: Airdropping tokens to recipient {recipient_id}:") print(f" - 1 fungible token TKA ({token_id})") - print(f" - NFT from NFTA collection ({nft_id}) with serial number #{serial_number}") + print( + f" - NFT from NFTA collection ({nft_id}) with serial number #{serial_number}" + ) try: airdrop_tx = ( TokenAirdropTransaction() @@ -194,9 +206,15 @@ def _check_token_transfer_for_pair(record, token_id, operator_id, recipient_id): try: token_transfers = getattr(record, "token_transfers", {}) or {} # Try direct mapping lookup, otherwise fall back to equality-match in a generator - transfers = token_transfers.get(token_id) or next((v for k, v in token_transfers.items() if k == token_id), None) + transfers = token_transfers.get(token_id) or next( + (v for k, v in token_transfers.items() if k == token_id), None + ) # Validate shape and compare expected amounts in a single expression - return isinstance(transfers, dict) and transfers.get(operator_id) == -1 and transfers.get(recipient_id) == 1 + return ( + isinstance(transfers, dict) + and transfers.get(operator_id) == -1 + and transfers.get(recipient_id) == 1 + ) except Exception: return False @@ -213,7 +231,9 @@ def _extract_nft_transfers(record): if nft_transfers is None: continue # Skip non-iterable or string-like values - if not hasattr(nft_transfers, "__iter__") or isinstance(nft_transfers, (str, bytes)): + if not hasattr(nft_transfers, "__iter__") or isinstance( + nft_transfers, (str, bytes) + ): continue for nft_transfer in nft_transfers: @@ -245,7 +265,9 @@ def verify_transaction_record( } try: - record = TransactionRecordQuery(transaction_id=airdrop_receipt.transaction_id).execute(client) + record = TransactionRecordQuery( + transaction_id=airdrop_receipt.transaction_id + ).execute(client) except Exception as e: print(f"❌ Error fetching transaction record: {e}") return result @@ -263,7 +285,10 @@ def verify_transaction_record( # Single-pass confirmation using any() to keep branching minimal result["nft_transfer_confirmed"] = any( - token_key == nft_id and sender == operator_id and receiver == recipient_id and serial == serial_number + token_key == nft_id + and sender == operator_id + and receiver == recipient_id + and serial == serial_number for token_key, serial, sender, receiver in nft_serials ) @@ -282,6 +307,7 @@ def verify_post_airdrop_balances( ): """ Verify post-airdrop balances and NFT ownership to confirm successful transfer. + Uses AccountInfoQuery for robust token association and balance verification. Accepts a `balances_before` mapping with keys 'sender' and 'recipient'. Returns (fully_verified_bool, details_dict) """ @@ -298,18 +324,24 @@ def verify_post_airdrop_balances( "record_checks": record_verification, } - # Get current balances (fail-fast is not desired; capture errors in details) + # Get current account info and balances using AccountInfoQuery (more robust) try: - sender_current = CryptoGetAccountBalanceQuery(account_id=operator_id).execute(client).token_balances + sender_info = AccountInfoQuery(account_id=operator_id).execute(client) + sender_current = { + rel.token_id: rel.balance for rel in sender_info.token_relationships + } except Exception as e: - details["error"] = f"Error fetching sender balance: {e}" + details["error"] = f"Error fetching sender account info: {e}" details["fully_verified"] = False return False, details try: - recipient_current = CryptoGetAccountBalanceQuery(account_id=recipient_id).execute(client).token_balances + recipient_info = AccountInfoQuery(account_id=recipient_id).execute(client) + recipient_current = { + rel.token_id: rel.balance for rel in recipient_info.token_relationships + } except Exception as e: - details["error"] = f"Error fetching recipient balance: {e}" + details["error"] = f"Error fetching recipient account info: {e}" details["fully_verified"] = False return False, details @@ -321,7 +353,9 @@ def verify_post_airdrop_balances( recipient_nft_before = recipient_balances_before.get(nft_id, 0) recipient_nft_after = recipient_current.get(nft_id, 0) - print(f"\n - NFT balance changes: sender {sender_nft_before} -> {sender_nft_after}, recipient {recipient_nft_before} -> {recipient_nft_after}") + print( + f"\n - NFT balance changes: sender {sender_nft_before} -> {sender_nft_after}, recipient {recipient_nft_before} -> {recipient_nft_after}" + ) # Query NFT info for the serial to confirm ownership (optional) nft_serial_id = NftId(token_id=nft_id, serial_number=serial_number) @@ -349,36 +383,89 @@ def verify_post_airdrop_balances( def _log_balances_before(client, operator_id, recipient_id, token_id, nft_id): - """Fetch and print sender/recipient token balances before airdrop.""" - print("\nStep 5: Checking balances before airdrop...") - sender_balances_before = CryptoGetAccountBalanceQuery(account_id=operator_id).execute(client).token_balances - recipient_balances_before = CryptoGetAccountBalanceQuery(account_id=recipient_id).execute(client).token_balances + """Fetch and print sender/recipient token balances and associations before airdrop.""" + print("\nStep 5: Checking balances and associations before airdrop...") + + # Get account info to verify associations + sender_info = AccountInfoQuery(account_id=operator_id).execute(client) + recipient_info = AccountInfoQuery(account_id=recipient_id).execute(client) + + # Build dictionaries for balances from token_relationships (more robust than balance query) + sender_balances_before = { + rel.token_id: rel.balance for rel in sender_info.token_relationships + } + recipient_balances_before = { + rel.token_id: rel.balance for rel in recipient_info.token_relationships + } + print(f"Sender ({operator_id}) balances before airdrop:") print(f" {token_id}: {sender_balances_before.get(token_id, 0)}") print(f" {nft_id}: {sender_balances_before.get(nft_id, 0)}") print(f"Recipient ({recipient_id}) balances before airdrop:") print(f" {token_id}: {recipient_balances_before.get(token_id, 0)}") print(f" {nft_id}: {recipient_balances_before.get(nft_id, 0)}") + return sender_balances_before, recipient_balances_before -def _print_verification_summary(record_result, verification_details, operator_id, recipient_id, nft_id, serial_number): +def _print_verification_summary( + record_result, + verification_details, + operator_id, + recipient_id, + nft_id, + serial_number, +): """Print a concise verification summary based on results.""" - print(f" - Token transfer verification (fungible): {'OK' if record_result.get('expected_token_transfer') else 'MISSING'}") - print(f" - NFT transfer seen in record: {'OK' if record_result.get('nft_transfer_confirmed') else 'MISSING'}") - print(f" - NFT owner according to TokenNftInfoQuery: {verification_details.get('nft_owner')}") - print(f" - NFT owner matches recipient: {'YES' if verification_details.get('owner_matches') else 'NO'}") + print( + f" - Token transfer verification (fungible): {'OK' if record_result.get('expected_token_transfer') else 'MISSING'}" + ) + print( + f" - NFT transfer seen in record: {'OK' if record_result.get('nft_transfer_confirmed') else 'MISSING'}" + ) + print( + f" - NFT owner according to TokenNftInfoQuery: {verification_details.get('nft_owner')}" + ) + print( + f" - NFT owner matches recipient: {'YES' if verification_details.get('owner_matches') else 'NO'}" + ) - if verification_details.get('fully_verified'): - print(f"\n βœ… Success! NFT {nft_id} serial #{serial_number} was transferred from {operator_id} to {recipient_id} and verified by record + balances + TokenNftInfoQuery") + if verification_details.get("fully_verified"): + print( + f"\n βœ… Success! NFT {nft_id} serial #{serial_number} was transferred from {operator_id} to {recipient_id} and verified by record + balances + TokenNftInfoQuery" + ) else: - print(f"\n ⚠️ Warning: Could not fully verify NFT {nft_id} serial #{serial_number}. Combined checks result: {verification_details.get('fully_verified')}") + print( + f"\n ⚠️ Warning: Could not fully verify NFT {nft_id} serial #{serial_number}. Combined checks result: {verification_details.get('fully_verified')}" + ) -def _print_final_summary(client, operator_id, recipient_id, token_id, nft_id, serial_number, verification_details): +def _print_final_summary( + client, + operator_id, + recipient_id, + token_id, + nft_id, + serial_number, + verification_details, +): """Print final balances after airdrop and a small summary table.""" - sender_balances_after = verification_details.get("sender_balances_after") or CryptoGetAccountBalanceQuery(account_id=operator_id).execute(client).token_balances - recipient_balances_after = verification_details.get("recipient_balances_after") or CryptoGetAccountBalanceQuery(account_id=recipient_id).execute(client).token_balances + # Use AccountInfoQuery for accurate balance retrieval if not already cached + if verification_details.get("sender_balances_after") is None: + sender_info = AccountInfoQuery(account_id=operator_id).execute(client) + sender_balances_after = { + rel.token_id: rel.balance for rel in sender_info.token_relationships + } + else: + sender_balances_after = verification_details.get("sender_balances_after") + + if verification_details.get("recipient_balances_after") is None: + recipient_info = AccountInfoQuery(account_id=recipient_id).execute(client) + recipient_balances_after = { + rel.token_id: rel.balance for rel in recipient_info.token_relationships + } + else: + recipient_balances_after = verification_details.get("recipient_balances_after") print("\nBalances after airdrop:") print(f"Sender ({operator_id}):") @@ -389,18 +476,30 @@ def _print_final_summary(client, operator_id, recipient_id, token_id, nft_id, se print(f" {nft_id}: {recipient_balances_after.get(nft_id, 0)}") print("\nSummary Table:") - print("+----------------+----------------------+----------------------+----------------------+----------------------+") - print("| Token Type | Token ID | NFT Serial | Sender Balance | Recipient Balance |") - print("+----------------+----------------------+----------------------+----------------------+----------------------+") - print(f"| Fungible (TKA) | {str(token_id):20} | {'N/A':20} | {str(sender_balances_after.get(token_id, 0)):20} | {str(recipient_balances_after.get(token_id, 0)):20} |") - print(f"| NFT (NFTA) | {str(nft_id):20} | #{str(serial_number):19} | {str(sender_balances_after.get(nft_id, 0)):20} | {str(recipient_balances_after.get(nft_id, 0)):20} |") - print("+----------------+----------------------+----------------------+----------------------+----------------------+") + print( + "+----------------+----------------------+----------------------+----------------------+----------------------+" + ) + print( + "| Token Type | Token ID | NFT Serial | Sender Balance | Recipient Balance |" + ) + print( + "+----------------+----------------------+----------------------+----------------------+----------------------+" + ) + print( + f"| Fungible (TKA) | {str(token_id):20} | {'N/A':20} | {str(sender_balances_after.get(token_id, 0)):20} | {str(recipient_balances_after.get(token_id, 0)):20} |" + ) + print( + f"| NFT (NFTA) | {str(nft_id):20} | #{str(serial_number):19} | {str(sender_balances_after.get(nft_id, 0)):20} | {str(recipient_balances_after.get(nft_id, 0)):20} |" + ) + print( + "+----------------+----------------------+----------------------+----------------------+----------------------+" + ) print("\nβœ… Finished token airdrop example (see summary above).") def token_airdrop(): """ - A full example that creates an account, a token, associate token, and + A full example that creates an account, a token, associate token, and finally perform token airdrop. """ # Setup Client @@ -415,7 +514,7 @@ def token_airdrop(): # Create a nft nft_id = create_nft(client, operator_key, operator_id) - #Mint nft + # Mint nft serial_number = mint_nft(client, operator_key, nft_id) # Associate tokens @@ -433,11 +532,20 @@ def token_airdrop(): # 1) Verify record contents (token transfers and nft transfers) record_result = verify_transaction_record( - client, airdrop_receipt, operator_id, recipient_id, token_id, nft_id, serial_number + client, + airdrop_receipt, + operator_id, + recipient_id, + token_id, + nft_id, + serial_number, ) # 2) Verify post-airdrop balances and nft ownership - balances_before = {"sender": sender_balances_before, "recipient": recipient_balances_before} + balances_before = { + "sender": sender_balances_before, + "recipient": recipient_balances_before, + } fully_verified, verification_details = verify_post_airdrop_balances( client, operator_id, @@ -450,8 +558,23 @@ def token_airdrop(): ) # Print condensed verification summary and final table - _print_verification_summary(record_result, verification_details, operator_id, recipient_id, nft_id, serial_number) - _print_final_summary(client, operator_id, recipient_id, token_id, nft_id, serial_number, verification_details) + _print_verification_summary( + record_result, + verification_details, + operator_id, + recipient_id, + nft_id, + serial_number, + ) + _print_final_summary( + client, + operator_id, + recipient_id, + token_id, + nft_id, + serial_number, + verification_details, + ) if __name__ == "__main__": diff --git a/examples/tokens/token_airdrop_transaction_cancel.py b/examples/tokens/token_airdrop_transaction_cancel.py index 4d9dd049d..1eb927de1 100644 --- a/examples/tokens/token_airdrop_transaction_cancel.py +++ b/examples/tokens/token_airdrop_transaction_cancel.py @@ -95,28 +95,48 @@ def airdrop_tokens(client, operator_id, operator_key, recipient_id, token_ids): print("\nBalances before airdrop:") for t in token_ids: # token_ids elements are TokenId objects (not strings), so use them as dict keys - print(f" {str(t)}: sender={sender_balances_before.get(t, 0)} recipient={recipient_balances_before.get(t, 0)}") - + sender_balance = sender_balances_before.get(t, 0) + recipient_balance = recipient_balances_before.get(t, 0) + print(f" {str(t)}: sender={sender_balance} recipient={recipient_balance}") tx = TokenAirdropTransaction() for token_id in token_ids: - tx.add_token_transfer(token_id=token_id, account_id=operator_id, amount=-1) - tx.add_token_transfer(token_id=token_id, account_id=recipient_id, amount=1) - + tx.add_token_transfer( + token_id=token_id, + account_id=operator_id, + amount=-1 + ) + tx.add_token_transfer( + token_id=token_id, + account_id=recipient_id, + amount=1 + ) + receipt = tx.freeze_with(client).sign(operator_key).execute(client) - print(f"Token airdrop executed: status={receipt.status} transaction_id={receipt.transaction_id}") + print( + f"Token airdrop executed: status={receipt.status} " + f"transaction_id={receipt.transaction_id}" + ) # Get record to inspect pending airdrops - airdrop_record = TransactionRecordQuery(receipt.transaction_id).execute(client) + airdrop_record = TransactionRecordQuery( + receipt.transaction_id + ).execute(client) pending = getattr(airdrop_record, "new_pending_airdrops", []) or [] # Balances after airdrop - sender_balances_after = CryptoGetAccountBalanceQuery(account_id=operator_id).execute(client).token_balances - recipient_balances_after = CryptoGetAccountBalanceQuery(account_id=recipient_id).execute(client).token_balances - - print("\nBalances after airdrop:") + sender_balances_after = CryptoGetAccountBalanceQuery( + account_id=operator_id + ).execute(client).token_balances + recipient_balances_after = CryptoGetAccountBalanceQuery( + account_id=recipient_id + ).execute(client).token_balances + + print("\nBalances after airdrop:") for t in token_ids: # token_ids elements are TokenId objects (not strings), so use them as dict keys - print(f" {str(t)}: sender={sender_balances_after.get(t, 0)} recipient={recipient_balances_after.get(t, 0)}") + sender_balance = sender_balances_after.get(t, 0) + recipient_balance = recipient_balances_after.get(t, 0) + print(f" {str(t)}: sender={sender_balance} recipient={recipient_balance}") return pending except Exception as e: @@ -143,7 +163,9 @@ def cancel_airdrops(client, operator_key, pending_airdrops): cancel_airdrop_receipt = cancel_airdrop_tx.execute(client) if cancel_airdrop_receipt.status != ResponseCode.SUCCESS: - print(f"Failed to cancel airdrop: Status: {cancel_airdrop_receipt.status}") + print(f"Failed to cancel airdrop: " + f"Status: {cancel_airdrop_receipt.status}" + ) sys.exit(1) print("Airdrop cancel transaction successful") @@ -166,6 +188,5 @@ def token_airdrop_cancel(): # Cancel airdrops cancel_airdrops(client, operator_key, pending_airdrops) - if __name__ == "__main__": - token_airdrop_cancel() \ No newline at end of file + token_airdrop_cancel() diff --git a/examples/tokens/token_associate_transaction.py b/examples/tokens/token_associate_transaction.py index c19b3f9f7..3904baad4 100644 --- a/examples/tokens/token_associate_transaction.py +++ b/examples/tokens/token_associate_transaction.py @@ -26,7 +26,7 @@ # Load environment variables from .env file load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() def setup_client(): @@ -54,6 +54,7 @@ def setup_client(): print("❌ Error: Please check OPERATOR_ID and OPERATOR_KEY in your .env file.") sys.exit(1) + def create_test_account(client, operator_key): """ Create a new test account for demonstration. @@ -94,6 +95,7 @@ def create_test_account(client, operator_key): print(f"❌ Error creating new account: {e}") sys.exit(1) + def create_fungible_token(client, operator_id, operator_key): """ Create a fungible token for association with test account. @@ -135,6 +137,7 @@ def create_fungible_token(client, operator_id, operator_key): print(f"❌ Error creating token: {e}") sys.exit(1) + def associate_token_with_account(client, token_id, account_id, account_key): """ Associate the token with the test account. @@ -170,7 +173,9 @@ def associate_token_with_account(client, token_id, account_id, account_key): sys.exit(1) -def associate_two_tokens_mixed_types_with_set_token_ids(client, token_id_1, token_id_2, account_id, account_key): +def associate_two_tokens_mixed_types_with_set_token_ids( + client, token_id_1, token_id_2, account_id, account_key +): """ Associate two tokens using set_token_ids() with mixed types: - first as TokenId diff --git a/examples/tokens/token_burn_transaction_fungible.py b/examples/tokens/token_burn_transaction_fungible.py index 1591961ce..7df03bd01 100644 --- a/examples/tokens/token_burn_transaction_fungible.py +++ b/examples/tokens/token_burn_transaction_fungible.py @@ -1,8 +1,9 @@ """ -uv run examples/tokens/token_burn_transaction_fungible.py +uv run examples/tokens/token_burn_transaction_fungible.py python examples/tokens/token_burn_transaction_fungible.py """ + import os import sys from dotenv import load_dotenv @@ -22,7 +23,8 @@ load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -30,14 +32,15 @@ def setup_client(): print(f"Connecting to Hedera {network_name} network!") client = Client(network) - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + operator_id = AccountId.from_string(os.getenv("OPERATOR_ID", "")) + operator_key = PrivateKey.from_string(os.getenv("OPERATOR_KEY", "")) print(f"Client set up with operator id {client.operator_account_id}") client.set_operator(operator_id, operator_key) - + return client, operator_id, operator_key + def create_fungible_token(client, operator_id, operator_key): """Create a fungible token""" receipt = ( @@ -54,26 +57,26 @@ def create_fungible_token(client, operator_id, operator_key): .set_supply_key(operator_key) .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: - print(f"Fungible token creation failed with status: {ResponseCode(receipt.status).name}") + print( + f"Fungible token creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + token_id = receipt.token_id print(f"Fungible token created with ID: {token_id}") - + return token_id + def get_token_info(client, token_id): """Get token info for the token""" - token_info = ( - TokenInfoQuery() - .set_token_id(token_id) - .execute(client) - ) - + token_info = TokenInfoQuery().set_token_id(token_id).execute(client) + print(f"Token supply: {token_info.total_supply}") + def token_burn_fungible(): """ Demonstrates the fungible token burn functionality by: @@ -87,13 +90,13 @@ def token_burn_fungible(): # Create a fungible token with the treasury account as owner and signer token_id = create_fungible_token(client, operator_id, operator_key) - + # Get and print token supply before burn to show the initial state print("\nToken supply before burn:") get_token_info(client, token_id) - + burn_amount = 40 - + # Burn 40 tokens out of 100 receipt = ( TokenBurnTransaction() @@ -101,16 +104,17 @@ def token_burn_fungible(): .set_amount(burn_amount) .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: print(f"Token burn failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + print(f"Successfully burned {burn_amount} tokens from {token_id}") - + # Get and print token supply after burn to show the final state print("\nToken supply after burn:") get_token_info(client, token_id) - + + if __name__ == "__main__": token_burn_fungible() diff --git a/examples/tokens/token_burn_transaction_nft.py b/examples/tokens/token_burn_transaction_nft.py index b6841cd6e..2fe60e6e6 100644 --- a/examples/tokens/token_burn_transaction_nft.py +++ b/examples/tokens/token_burn_transaction_nft.py @@ -1,8 +1,9 @@ """ -uv run examples/tokens/token_burn_transaction_nft.py +uv run examples/tokens/token_burn_transaction_nft.py python examples/tokens/token_burn_transaction_nft.py """ + import os import sys from dotenv import load_dotenv @@ -23,7 +24,8 @@ load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -31,13 +33,14 @@ def setup_client(): print(f"Connecting to Hedera {network_name} network!") client = Client(network) - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key + def create_nft(client, operator_id, operator_key): """Create a non-fungible token""" receipt = ( @@ -54,18 +57,19 @@ def create_nft(client, operator_id, operator_key): .set_supply_key(operator_key) .execute(client) ) - + # Check if nft creation was successful if receipt.status != ResponseCode.SUCCESS: print(f"NFT creation failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + # Get token ID from receipt nft_token_id = receipt.token_id print(f"NFT created with ID: {nft_token_id}") - + return nft_token_id + def mint_nfts(client, nft_token_id, metadata_list): """Mint a non-fungible token""" receipt = ( @@ -74,25 +78,23 @@ def mint_nfts(client, nft_token_id, metadata_list): .set_metadata(metadata_list) .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: print(f"NFT minting failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + print(f"NFT minted with serial numbers: {receipt.serial_numbers}") - + return receipt.serial_numbers + def get_token_info(client, token_id): """Get token info for the token""" - token_info = ( - TokenInfoQuery() - .set_token_id(token_id) - .execute(client) - ) - + token_info = TokenInfoQuery().set_token_id(token_id).execute(client) + print(f"Token supply: {token_info.total_supply}") + def token_burn_nft(): """ Demonstrates the NFT burn functionality by: @@ -107,15 +109,15 @@ def token_burn_nft(): # Create a fungible token with the treasury account as owner and signer token_id = create_nft(client, operator_id, operator_key) - + # Mint 4 NFTs - metadata_list = [b'metadata1', b'metadata2', b'metadata3', b'metadata4'] + metadata_list = [b"metadata1", b"metadata2", b"metadata3", b"metadata4"] serial_numbers = mint_nfts(client, token_id, metadata_list) - + # Get and print token balances before burn to show the initial state print("\nToken balances before burn:") get_token_info(client, token_id) - + # Burn first 2 NFTs from the minted collection (serials 1 and 2) receipt = ( TokenBurnTransaction() @@ -123,16 +125,19 @@ def token_burn_nft(): .set_serials(serial_numbers[0:2]) .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: print(f"NFT burn failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - - print(f"Successfully burned NFTs with serial numbers {serial_numbers[0:2]} from {token_id}") + + print( + f"Successfully burned NFTs with serial numbers {serial_numbers[0:2]} from {token_id}" + ) # Get and print token balances after burn to show the final state print("\nToken balances after burn:") get_token_info(client, token_id) - + + if __name__ == "__main__": token_burn_nft() diff --git a/examples/tokens/token_create_transaction_admin_key.py b/examples/tokens/token_create_transaction_admin_key.py index 34af32093..5214be089 100644 --- a/examples/tokens/token_create_transaction_admin_key.py +++ b/examples/tokens/token_create_transaction_admin_key.py @@ -36,7 +36,7 @@ from hiero_sdk_python.tokens.supply_type import SupplyType load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() def setup_client(): @@ -46,8 +46,8 @@ def setup_client(): client = Client(network) try: - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key @@ -140,8 +140,12 @@ def demonstrate_failed_supply_key_addition(client, token_id, admin_key): try: receipt = transaction.execute(client) if receipt.status != ResponseCode.SUCCESS: - print(f"❌ As expected, adding supply key failed: {ResponseCode(receipt.status).name}") - print(" Admin key cannot authorize adding keys that were not present during token creation.") + print( + f"❌ As expected, adding supply key failed: {ResponseCode(receipt.status).name}" + ) + print( + " Admin key cannot authorize adding keys that were not present during token creation." + ) return True # Expected failure else: print("⚠️ Unexpectedly succeeded - this shouldn't happen") @@ -169,7 +173,9 @@ def demonstrate_admin_key_update(client, token_id, admin_key, operator_key): receipt = transaction.execute(client) if receipt.status != ResponseCode.SUCCESS: - print(f"Admin key update failed with status: {ResponseCode(receipt.status).name}") + print( + f"Admin key update failed with status: {ResponseCode(receipt.status).name}" + ) return False print("βœ… Admin key updated successfully") @@ -202,11 +208,7 @@ def demonstrate_token_deletion(client, token_id, operator_key): def get_token_info(client, token_id): """Query and display token information.""" try: - info = ( - TokenInfoQuery() - .set_token_id(token_id) - .execute(client) - ) + info = TokenInfoQuery().set_token_id(token_id).execute(client) print(f"\nToken Info for {token_id}:") print(f" Name: {info.name}") print(f" Symbol: {info.symbol}") diff --git a/examples/tokens/token_create_transaction_freeze_key.py b/examples/tokens/token_create_transaction_freeze_key.py index 33d679ab6..712a434d8 100644 --- a/examples/tokens/token_create_transaction_freeze_key.py +++ b/examples/tokens/token_create_transaction_freeze_key.py @@ -196,11 +196,11 @@ def create_demo_account(client: Client, operator_key: PrivateKey) -> DemoAccount return DemoAccount(account_id, new_key) -def associate_token( - client: Client, token_id, account: DemoAccount, signer: PrivateKey -): +def associate_token(client: Client, token_id, account: DemoAccount, signer: PrivateKey): """Associate a token with a given account.""" - print(f"\nSTEP 5️⃣ Associating token {token_id} with account {account.account_id}...") + print( + f"\nSTEP 5️⃣ Associating token {token_id} with account {account.account_id}..." + ) receipt = ( TokenAssociateTransaction() .set_account_id(account.account_id) @@ -310,7 +310,9 @@ def demonstrate_freeze_default_flow( symbol="FDF", ) default_frozen_account = create_demo_account(client, operator_key) - associate_token(client, token_id, default_frozen_account, default_frozen_account.private_key) + associate_token( + client, token_id, default_frozen_account, default_frozen_account.private_key + ) success_before_unfreeze = attempt_transfer( client, @@ -321,9 +323,13 @@ def demonstrate_freeze_default_flow( note="freezeDefault=True (should FAIL)", ) if success_before_unfreeze: - print("⚠️ Unexpected success: account should start frozen when freezeDefault=True.") + print( + "⚠️ Unexpected success: account should start frozen when freezeDefault=True." + ) else: - print("βœ… Transfer blocked as expected because the account was frozen by default.") + print( + "βœ… Transfer blocked as expected because the account was frozen by default." + ) unfreeze_account(client, token_id, default_frozen_account.account_id, freeze_key) attempt_transfer( @@ -343,7 +349,9 @@ def main(): client, operator_id, operator_key = setup_client() # Token without a freeze key - token_without_key = create_token_without_freeze_key(client, operator_id, operator_key) + token_without_key = create_token_without_freeze_key( + client, operator_id, operator_key + ) demonstrate_missing_freeze_key(client, token_without_key, operator_id, operator_key) # Token with a freeze key @@ -391,9 +399,10 @@ def main(): # Bonus behavior demonstrate_freeze_default_flow(client, operator_id, operator_key, freeze_key) - print("\nπŸŽ‰ Freeze key demonstration completed! Review the log above for each step.") + print( + "\nπŸŽ‰ Freeze key demonstration completed! Review the log above for each step." + ) if __name__ == "__main__": main() - diff --git a/examples/tokens/token_create_transaction_fungible_finite.py b/examples/tokens/token_create_transaction_fungible_finite.py index e776c4105..8ca93e9aa 100644 --- a/examples/tokens/token_create_transaction_fungible_finite.py +++ b/examples/tokens/token_create_transaction_fungible_finite.py @@ -35,15 +35,17 @@ def parse_optional_key(key_str): """Parse an optional private key from environment variables.""" - if not key_str or key_str.startswith('<') or key_str.endswith('>'): + if not key_str or key_str.startswith("<") or key_str.endswith(">"): return None try: return PrivateKey.from_string_ed25519(key_str) except Exception: return None + load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -51,8 +53,8 @@ def setup_client(): print(f"Connecting to Hedera {network_name} network!") client = Client(network) - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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}") @@ -61,10 +63,10 @@ def setup_client(): def load_optional_keys(): """Load optional keys (admin, supply, freeze, pause).""" - admin_key = parse_optional_key(os.getenv('ADMIN_KEY')) - supply_key = parse_optional_key(os.getenv('SUPPLY_KEY')) - freeze_key = parse_optional_key(os.getenv('FREEZE_KEY')) - pause_key = parse_optional_key(os.getenv('PAUSE_KEY')) + admin_key = parse_optional_key(os.getenv("ADMIN_KEY")) + supply_key = parse_optional_key(os.getenv("SUPPLY_KEY")) + freeze_key = parse_optional_key(os.getenv("FREEZE_KEY")) + pause_key = parse_optional_key(os.getenv("PAUSE_KEY")) return admin_key, supply_key, freeze_key, pause_key @@ -108,7 +110,9 @@ def execute_transaction(transaction, client, operator_key, admin_key): if receipt and receipt.token_id: print(f"Finite fungible token created with ID: {receipt.token_id}") else: - print("Finite fungible token creation failed: Token ID not returned in receipt.") + print( + "Finite fungible token creation failed: Token ID not returned in receipt." + ) sys.exit(1) except Exception as e: print(f"Token creation failed: {str(e)}") diff --git a/examples/tokens/token_create_transaction_fungible_infinite.py b/examples/tokens/token_create_transaction_fungible_infinite.py index e8be92476..7d50f2081 100644 --- a/examples/tokens/token_create_transaction_fungible_infinite.py +++ b/examples/tokens/token_create_transaction_fungible_infinite.py @@ -29,7 +29,8 @@ ) load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -38,8 +39,8 @@ def setup_client(): client = Client(network) try: - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key @@ -88,7 +89,9 @@ def execute_transaction(transaction, client, operator_key, admin_key, supply_key try: receipt = transaction.execute(client) if receipt and receipt.token_id: - print(f"Success! Infinite fungible token created with ID: {receipt.token_id}") + print( + f"Success! Infinite fungible token created with ID: {receipt.token_id}" + ) else: print("Token creation failed: Token ID not returned in receipt.") sys.exit(1) diff --git a/examples/tokens/token_create_transaction_kyc_key.py b/examples/tokens/token_create_transaction_kyc_key.py index defd4e8ea..ce20ee22b 100644 --- a/examples/tokens/token_create_transaction_kyc_key.py +++ b/examples/tokens/token_create_transaction_kyc_key.py @@ -19,6 +19,7 @@ python examples/tokens/token_create_transaction_kyc_key.py """ + import os import sys import time @@ -35,16 +36,20 @@ from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.hapi.services.basic_types_pb2 import TokenType from hiero_sdk_python.tokens.supply_type import SupplyType -from hiero_sdk_python.tokens.token_associate_transaction import TokenAssociateTransaction +from hiero_sdk_python.tokens.token_associate_transaction import ( + TokenAssociateTransaction, +) from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction from hiero_sdk_python.tokens.token_grant_kyc_transaction import TokenGrantKycTransaction -from hiero_sdk_python.tokens.token_revoke_kyc_transaction import TokenRevokeKycTransaction +from hiero_sdk_python.tokens.token_revoke_kyc_transaction import ( + TokenRevokeKycTransaction, +) from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction from hiero_sdk_python.query.account_balance_query import CryptoGetAccountBalanceQuery load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() def setup_client(): @@ -58,8 +63,8 @@ def setup_client(): client = Client(Network(network=network_name)) try: - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY')) + 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 configured with operator: {operator_id}\n") return client, operator_id, operator_key @@ -95,7 +100,8 @@ def create_account(client, operator_key, initial_balance=Hbar(2)): if receipt.status != ResponseCode.SUCCESS: print( - f" Account creation failed with status: {ResponseCode(receipt.status).name}") + f" Account creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) account_id = receipt.account_id @@ -139,7 +145,8 @@ def create_token_without_kyc_key(client, operator_id, operator_key): if receipt.status != ResponseCode.SUCCESS: print( - f" Token creation failed with status: {ResponseCode(receipt.status).name}") + f" Token creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) token_id = receipt.token_id @@ -221,7 +228,8 @@ def create_token_with_kyc_key(client, operator_id, operator_key, kyc_private_key if receipt.status != ResponseCode.SUCCESS: print( - f" Token creation failed with status: {ResponseCode(receipt.status).name}") + f" Token creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) token_id = receipt.token_id @@ -250,7 +258,8 @@ def associate_token_to_account(client, token_id, account_id, account_private_key if receipt.status != ResponseCode.SUCCESS: print( - f" Token association failed with status: {ResponseCode(receipt.status).name}") + f" Token association failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) print(f" Token {token_id} associated with account {account_id}") @@ -259,7 +268,9 @@ def associate_token_to_account(client, token_id, account_id, account_private_key sys.exit(1) -def attempt_transfer_without_kyc(client, token_id, operator_id, recipient_id, operator_key): +def attempt_transfer_without_kyc( + client, token_id, operator_id, recipient_id, operator_key +): """ Attempt to transfer tokens to an account that has not been granted KYC. Depending on token configuration, this may fail. @@ -279,8 +290,7 @@ def attempt_transfer_without_kyc(client, token_id, operator_id, recipient_id, op .token_balances ) recipient_balance_before = balance_before.get(token_id, 0) - print( - f"Recipient's token balance before transfer: {recipient_balance_before}") + print(f"Recipient's token balance before transfer: {recipient_balance_before}") # Attempt transfer transfer_tx = ( @@ -308,7 +318,8 @@ def attempt_transfer_without_kyc(client, token_id, operator_id, recipient_id, op ) recipient_balance_after = balance_after.get(token_id, 0) print( - f"Recipient's token balance after transfer: {recipient_balance_after}\n") + f"Recipient's token balance after transfer: {recipient_balance_after}\n" + ) return True except Exception as e: print(f" Error during transfer attempt: {e}\n") @@ -335,8 +346,7 @@ def grant_kyc_to_account(client, token_id, account_id, kyc_private_key): ) if receipt.status != ResponseCode.SUCCESS: - print( - f" KYC grant failed with status: {ResponseCode(receipt.status).name}") + print(f" KYC grant failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) print(f" KYC granted for account {account_id} on token {token_id}\n") @@ -362,8 +372,7 @@ def transfer_token_after_kyc(client, token_id, operator_id, recipient_id, operat .token_balances ) recipient_balance_before = balance_before.get(token_id, 0) - print( - f"Recipient's token balance before transfer: {recipient_balance_before}") + print(f"Recipient's token balance before transfer: {recipient_balance_before}") # Perform transfer transfer_tx = ( @@ -390,8 +399,7 @@ def transfer_token_after_kyc(client, token_id, operator_id, recipient_id, operat .token_balances ) recipient_balance_after = balance_after.get(token_id, 0) - print( - f"Recipient's token balance after transfer: {recipient_balance_after}\n") + print(f"Recipient's token balance after transfer: {recipient_balance_after}\n") except Exception as e: print(f" Error transferring token: {e}") sys.exit(1) @@ -418,7 +426,8 @@ def revoke_kyc_from_account(client, token_id, account_id, kyc_private_key): if receipt.status != ResponseCode.SUCCESS: print( - f" KYC revoke failed with status: {ResponseCode(receipt.status).name}") + f" KYC revoke failed with status: {ResponseCode(receipt.status).name}" + ) return False print(f" KYC revoked for account {account_id} on token {token_id}") @@ -450,30 +459,31 @@ def main(): # ===== PART 1: Token WITHOUT KYC Key ===== token_without_kyc = create_token_without_kyc_key( - client, operator_id, operator_key) + client, operator_id, operator_key + ) # Create test account for failed KYC attempt - test_account_1, test_account_key_1 = create_account( - client, operator_key) + test_account_1, test_account_key_1 = create_account(client, operator_key) associate_token_to_account( - client, token_without_kyc, test_account_1, test_account_key_1) + client, token_without_kyc, test_account_1, test_account_key_1 + ) # Try to grant KYC (should fail) - attempt_kyc_without_key(client, token_without_kyc, - test_account_1, operator_key) + attempt_kyc_without_key(client, token_without_kyc, test_account_1, operator_key) # ===== PART 2: Token WITH KYC Key ===== token_with_kyc = create_token_with_kyc_key( - client, operator_id, operator_key, kyc_private_key) + client, operator_id, operator_key, kyc_private_key + ) # Create and associate an account for KYC testing print("\n" + "=" * 70) print("STEP 4: Creating a new account for KYC testing") print("=" * 70) - test_account_2, test_account_key_2 = create_account( - client, operator_key) + test_account_2, test_account_key_2 = create_account(client, operator_key) associate_token_to_account( - client, token_with_kyc, test_account_2, test_account_key_2) + client, token_with_kyc, test_account_2, test_account_key_2 + ) # Try to transfer without KYC (may fail) transfer_without_kyc_result = attempt_transfer_without_kyc( @@ -481,19 +491,18 @@ def main(): ) # Grant KYC - grant_kyc_to_account(client, token_with_kyc, - test_account_2, kyc_private_key) + grant_kyc_to_account(client, token_with_kyc, test_account_2, kyc_private_key) # Wait a moment for state to be consistent time.sleep(1) # Transfer after KYC (should succeed) transfer_token_after_kyc( - client, token_with_kyc, operator_id, test_account_2, operator_key) + client, token_with_kyc, operator_id, test_account_2, operator_key + ) # ===== BONUS: Revoke KYC ===== - revoke_kyc_from_account(client, token_with_kyc, - test_account_2, kyc_private_key) + revoke_kyc_from_account(client, token_with_kyc, test_account_2, kyc_private_key) # Print summary print("\n" + "=" * 70) diff --git a/examples/tokens/token_create_transaction_max_automatic_token_associations_0.py b/examples/tokens/token_create_transaction_max_automatic_token_associations_0.py index b22852b96..5660fd411 100644 --- a/examples/tokens/token_create_transaction_max_automatic_token_associations_0.py +++ b/examples/tokens/token_create_transaction_max_automatic_token_associations_0.py @@ -81,9 +81,13 @@ def create_demo_token( return receipt.token_id -def create_max_account(client: Client, operator_key: PrivateKey) -> Tuple[AccountId, PrivateKey]: +def create_max_account( + client: Client, operator_key: PrivateKey +) -> Tuple[AccountId, PrivateKey]: """Create an account whose max automatic associations equals zero.""" - print("\nSTEP 2: Creating account 'max' with max automatic associations set to 0...") + print( + "\nSTEP 2: Creating account 'max' with max automatic associations set to 0..." + ) max_key = PrivateKey.generate() # Configure the new account to require explicit associations before accepting tokens. tx = ( @@ -238,5 +242,6 @@ def main() -> None: "Verify balances, association status, and token configuration." ) + if __name__ == "__main__": main() diff --git a/examples/tokens/token_create_transaction_nft_finite.py b/examples/tokens/token_create_transaction_nft_finite.py index 2d550182a..6727209b7 100644 --- a/examples/tokens/token_create_transaction_nft_finite.py +++ b/examples/tokens/token_create_transaction_nft_finite.py @@ -29,7 +29,8 @@ ) load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -38,8 +39,8 @@ def setup_client(): client = Client(network) try: - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key @@ -87,7 +88,9 @@ def execute_transaction(transaction, client, operator_key, admin_key, supply_key try: receipt = transaction.execute(client) if receipt and receipt.token_id: - print(f"Success! Finite non-fungible token created with ID: {receipt.token_id}") + print( + f"Success! Finite non-fungible token created with ID: {receipt.token_id}" + ) else: print("Token creation failed: Token ID not returned in receipt.") sys.exit(1) diff --git a/examples/tokens/token_create_transaction_nft_infinite.py b/examples/tokens/token_create_transaction_nft_infinite.py index 4fe89bf9b..36f974747 100644 --- a/examples/tokens/token_create_transaction_nft_infinite.py +++ b/examples/tokens/token_create_transaction_nft_infinite.py @@ -19,7 +19,8 @@ # Load environment variables from .env file load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -36,9 +37,12 @@ def setup_client(): print("Error: Please check OPERATOR_ID and OPERATOR_KEY in your .env file.") sys.exit(1) + """ 2. Generate Keys On-the-Fly """ + + def keys_on_fly(): print("\nGenerating new admin and supply keys for the NFT...") admin_key = PrivateKey.generate_ed25519() @@ -46,9 +50,12 @@ def keys_on_fly(): print("Keys generated successfully.") return admin_key, supply_key + """ 3. Build and Execute Transaction """ + + def transaction(client, operator_id, operator_key, admin_key, supply_key): try: print("\nBuilding transaction to create an infinite NFT...") @@ -60,7 +67,7 @@ def transaction(client, operator_id, operator_key, admin_key, supply_key): .set_treasury_account_id(operator_id) .set_initial_supply(0) # NFTs must have an initial supply of 0 .set_supply_type(SupplyType.INFINITE) # Infinite supply - .set_admin_key(admin_key) # Generated admin key + .set_admin_key(admin_key) # Generated admin key .set_supply_key(supply_key) # Generated supply key .freeze_with(client) ) @@ -68,15 +75,17 @@ def transaction(client, operator_id, operator_key, admin_key, supply_key): # Sign the transaction with required keys print("Signing transaction...") transaction.sign(operator_key) # Treasury account must sign - transaction.sign(admin_key) # Admin key must sign - transaction.sign(supply_key) # Supply key must sign + transaction.sign(admin_key) # Admin key must sign + transaction.sign(supply_key) # Supply key must sign # Execute the transaction print("Executing transaction...") receipt = transaction.execute(client) if receipt and receipt.token_id: - print(f"Success! Infinite non-fungible token created with ID: {receipt.token_id}") + print( + f"Success! Infinite non-fungible token created with ID: {receipt.token_id}" + ) return receipt.token_id else: print("Token creation failed: Token ID not returned in receipt.") @@ -86,14 +95,18 @@ def transaction(client, operator_id, operator_key, admin_key, supply_key): print(f"Token creation failed: {e}") sys.exit(1) + """ Creates an infinite NFT by generating admin and supply keys on the fly. """ + + def create_token_nft_infinite(): client, operator_id, operator_key = setup_client() admin_key, supply_key = keys_on_fly() token_id = transaction(client, operator_id, operator_key, admin_key, supply_key) print(f"\nCreated token: {token_id}") + if __name__ == "__main__": create_token_nft_infinite() diff --git a/examples/tokens/token_create_transaction_pause_key.py b/examples/tokens/token_create_transaction_pause_key.py index 026040c42..31197a31d 100644 --- a/examples/tokens/token_create_transaction_pause_key.py +++ b/examples/tokens/token_create_transaction_pause_key.py @@ -106,7 +106,9 @@ def attempt_pause_should_fail(client, token_id, operator_key): receipt = tx.execute(client) if receipt.status == ResponseCode.TOKEN_HAS_NO_PAUSE_KEY: - print("βœ… Expected failure: token cannot be paused because no pause key exists.\n") + print( + "βœ… Expected failure: token cannot be paused because no pause key exists.\n" + ) else: print(f"❌ Unexpected status: {ResponseCode(receipt.status).name}\n") @@ -210,8 +212,9 @@ def create_temp_account(client, operator_key): return account_id, new_key - -def test_transfer_while_paused(client, operator_id, operator_key, recipient_id, token_id): +def test_transfer_while_paused( + client, operator_id, operator_key, recipient_id, token_id +): print("πŸ”Ή Attempting transfer WHILE token is paused (expected failure)...") tx = ( @@ -229,6 +232,7 @@ def test_transfer_while_paused(client, operator_id, operator_key, recipient_id, else: print(f"⚠️ Unexpected status: {ResponseCode(receipt.status).name}\n") + # ------------------------------------------------------- # MAIN # ------------------------------------------------------- diff --git a/examples/tokens/token_create_transaction_supply_key.py b/examples/tokens/token_create_transaction_supply_key.py index 0c4aec985..e0f1481fa 100644 --- a/examples/tokens/token_create_transaction_supply_key.py +++ b/examples/tokens/token_create_transaction_supply_key.py @@ -34,7 +34,7 @@ from hiero_sdk_python.tokens.supply_type import SupplyType load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() def setup_client(): @@ -44,8 +44,8 @@ def setup_client(): client = Client(network) try: - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key @@ -68,8 +68,8 @@ def create_token_no_supply_key(client, operator_id, operator_key): TokenCreateTransaction() .set_token_name("Fixed Supply Token") .set_token_symbol("FST") - .set_token_type(TokenType.FUNGIBLE_COMMON) - .set_initial_supply(1000) + .set_token_type(TokenType.FUNGIBLE_COMMON) + .set_initial_supply(1000) .set_decimals(0) .set_treasury_account_id(operator_id) .freeze_with(client) @@ -77,16 +77,18 @@ def create_token_no_supply_key(client, operator_id, operator_key): transaction.sign(operator_key) - try: + try: reciept = transaction.execute(client) if reciept.status != ResponseCode.SUCCESS: - print(f"Token creation failed with status: {ResponseCode(reciept.status).name}") + print( + f"Token creation failed with status: {ResponseCode(reciept.status).name}" + ) sys.exit(1) - + token_id = reciept.token_id print(f" βœ… Token created successfully with ID: {token_id}") return token_id - + except Exception as e: print(f"Error during token creation as: {e}.") sys.exit(1) @@ -102,17 +104,19 @@ def demonstrate_mint_fail(client, token_id): transaction = ( TokenMintTransaction() .set_token_id(token_id) - .set_amount(100) # Trying to mint 100 more fungible tokens + .set_amount(100) # Trying to mint 100 more fungible tokens .freeze_with(client) ) - + try: receipt = transaction.execute(client) if receipt.status == ResponseCode.TOKEN_HAS_NO_SUPPLY_KEY: - print(f" --> Mint failed as expected! Status: {ResponseCode(receipt.status).name}") + print( + f" --> Mint failed as expected! Status: {ResponseCode(receipt.status).name}" + ) else: - print(f"Mint failed with status: {ResponseCode(receipt.status).name}") - + print(f"Mint failed with status: {ResponseCode(receipt.status).name}") + except Exception as e: print(f"βœ… Mint failed as expected! Error: {e}") @@ -122,7 +126,7 @@ def create_token_with_supply_key(client, operator_id, operator_key): Create a Non-Fungible token (NFT) WITH a supply key. """ print("\n--- Scenario 2: Token WITH Supply Key ---") - + # Generate a specific supply key supply_key = PrivateKey.generate_ed25519() print(" ---> Generated new Supply Key.") @@ -141,21 +145,23 @@ def create_token_with_supply_key(client, operator_id, operator_key): .freeze_with(client) ) - # Sign with operator and supply key + # Sign with operator and supply key transaction.sign(operator_key) transaction.sign(supply_key) - + try: receipt = transaction.execute(client) if receipt.status != ResponseCode.SUCCESS: - print(f"Token creation failed with status: {ResponseCode(receipt.status).name}") + print( + f"Token creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) token_id = receipt.token_id print(f" βœ… Token created successfully with ID: {token_id}") return token_id, supply_key - - except Exception as e: + + except Exception as e: print(f"Error during token Creation as :{e}.") sys.exit(1) @@ -178,7 +184,7 @@ def demonstrate_mint_success(client, token_id, supply_key): transaction.sign(supply_key) receipt = transaction.execute(client) - + if receipt.status != ResponseCode.SUCCESS: print(f" ❌ Mint failed with status: {ResponseCode(receipt.status).name}") return @@ -190,7 +196,7 @@ def verify_token_info(client, token_id): """Query token info to see total supply.""" print(f"Querying Token Info for {token_id}...") info = TokenInfoQuery().set_token_id(token_id).execute(client) - + print(f" - Total Supply: {info.total_supply}") print(f" - Supply Key Set: {info.supply_key is not None}") @@ -207,7 +213,9 @@ def main(): demonstrate_mint_fail(client, token_id_no_key) # 2. Demonstrate Success (With Supply Key) - token_id_with_key, supply_key = create_token_with_supply_key(client, operator_id, operator_key) + token_id_with_key, supply_key = create_token_with_supply_key( + client, operator_id, operator_key + ) demonstrate_mint_success(client, token_id_with_key, supply_key) verify_token_info(client, token_id_with_key) @@ -215,4 +223,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/examples/tokens/token_create_transaction_token_fee_schedule_key.py b/examples/tokens/token_create_transaction_token_fee_schedule_key.py index 429f01304..b4a1ff977 100644 --- a/examples/tokens/token_create_transaction_token_fee_schedule_key.py +++ b/examples/tokens/token_create_transaction_token_fee_schedule_key.py @@ -19,10 +19,16 @@ from dotenv import load_dotenv from hiero_sdk_python import Client, AccountId, PrivateKey, Network -from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction, TokenParams, TokenKeys +from hiero_sdk_python.tokens.token_create_transaction import ( + TokenCreateTransaction, + TokenParams, + TokenKeys, +) from hiero_sdk_python.tokens.token_type import TokenType from hiero_sdk_python.tokens.supply_type import SupplyType -from hiero_sdk_python.tokens.token_fee_schedule_update_transaction import TokenFeeScheduleUpdateTransaction +from hiero_sdk_python.tokens.token_fee_schedule_update_transaction import ( + TokenFeeScheduleUpdateTransaction, +) from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.query.token_info_query import TokenInfoQuery @@ -30,21 +36,23 @@ # Load environment variables load_dotenv() + def setup_client(): """Initialize client and operator credentials from .env.""" - network = Network(os.getenv('NETWORK', 'testnet')) + network = Network(os.getenv("NETWORK", "testnet")) client = Client(network) 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) return client, operator_id, operator_key + def create_token_with_fee_key(client, operator_id): """Create a fungible token with a fee_schedule_key.""" print("Creating fungible token with fee_schedule_key...") fee_schedule_key = PrivateKey.generate_ed25519() initial_fees = [CustomFixedFee(amount=100, fee_collector_account_id=operator_id)] - + token_params = TokenParams( token_name="Fee Key Token", token_symbol="FKT", @@ -55,26 +63,27 @@ def create_token_with_fee_key(client, operator_id): supply_type=SupplyType.INFINITE, custom_fees=initial_fees, ) - + keys = TokenKeys(fee_schedule_key=fee_schedule_key) - + tx = TokenCreateTransaction(token_params=token_params, keys=keys) tx.freeze_with(client) receipt = tx.execute(client) - + if receipt.status != ResponseCode.SUCCESS: print(f"Token creation failed: {ResponseCode(receipt.status).name}") sys.exit(1) - + token_id = receipt.token_id print(f"Token created with ID: {token_id} (has fee_schedule_key)") return token_id, fee_schedule_key + def create_token_without_fee_key(client, operator_id): """Create a fungible token without a fee_schedule_key.""" print("Creating fungible token without fee_schedule_key...") initial_fees = [CustomFixedFee(amount=100, fee_collector_account_id=operator_id)] - + token_params = TokenParams( token_name="No Fee Key Token", token_symbol="NFKT", @@ -85,20 +94,21 @@ def create_token_without_fee_key(client, operator_id): supply_type=SupplyType.INFINITE, custom_fees=initial_fees, ) - + # No keys set, so no fee_schedule_key tx = TokenCreateTransaction(token_params=token_params) tx.freeze_with(client) receipt = tx.execute(client) - + if receipt.status != ResponseCode.SUCCESS: print(f"Token creation failed: {ResponseCode(receipt.status).name}") sys.exit(1) - + token_id = receipt.token_id print(f"Token created with ID: {token_id} (no fee_schedule_key)") return token_id + def query_token_fees(client, token_id, description): """Query and display the custom fees for a token.""" token_info = TokenInfoQuery(token_id=token_id).execute(client) @@ -107,16 +117,23 @@ def query_token_fees(client, token_id, description): for fee in fees: print(f" - Fixed fee: {fee.amount} to {fee.fee_collector_account_id}") + def attempt_fee_update(client, token_id, fee_schedule_key, description): """Attempt to update custom fees for a token.""" print(f"\nAttempting to update fees for {description}...") - new_fees = [CustomFixedFee(amount=200, fee_collector_account_id=client.operator_account_id)] - - tx = TokenFeeScheduleUpdateTransaction().set_token_id(token_id).set_custom_fees(new_fees) - + new_fees = [ + CustomFixedFee(amount=200, fee_collector_account_id=client.operator_account_id) + ] + + tx = ( + TokenFeeScheduleUpdateTransaction() + .set_token_id(token_id) + .set_custom_fees(new_fees) + ) + if fee_schedule_key: tx.freeze_with(client).sign(fee_schedule_key) - + try: receipt = tx.execute(client) if receipt.status == ResponseCode.SUCCESS: @@ -126,31 +143,45 @@ def attempt_fee_update(client, token_id, fee_schedule_key, description): except Exception as e: print(f"Fee update failed with exception: {e}") + def main(): client, operator_id, operator_key = setup_client() - + try: # Create token with fee_schedule_key token_with_key, fee_key = create_token_with_fee_key(client, operator_id) - query_token_fees(client, token_with_key, "Token with fee_schedule_key (initial)") - + query_token_fees( + client, token_with_key, "Token with fee_schedule_key (initial)" + ) + # Create token without fee_schedule_key token_without_key = create_token_without_fee_key(client, operator_id) - query_token_fees(client, token_without_key, "Token without fee_schedule_key (initial)") - + query_token_fees( + client, token_without_key, "Token without fee_schedule_key (initial)" + ) + # Attempt updates - attempt_fee_update(client, token_with_key, fee_key, "token with fee_schedule_key") - attempt_fee_update(client, token_without_key, None, "token without fee_schedule_key") - + attempt_fee_update( + client, token_with_key, fee_key, "token with fee_schedule_key" + ) + attempt_fee_update( + client, token_without_key, None, "token without fee_schedule_key" + ) + # Query final fees - query_token_fees(client, token_with_key, "Token with fee_schedule_key (after update)") - query_token_fees(client, token_without_key, "Token without fee_schedule_key (after update)") - + query_token_fees( + client, token_with_key, "Token with fee_schedule_key (after update)" + ) + query_token_fees( + client, token_without_key, "Token without fee_schedule_key (after update)" + ) + except Exception as e: print(f"Error during operations: {e}") finally: client.close() print("\nClient closed. Example complete.") + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/examples/tokens/token_create_transaction_token_metadata.py b/examples/tokens/token_create_transaction_token_metadata.py index 741a7b47e..b03227c85 100644 --- a/examples/tokens/token_create_transaction_token_metadata.py +++ b/examples/tokens/token_create_transaction_token_metadata.py @@ -115,7 +115,7 @@ def try_update_metadata_without_key(client, operator_key, token_id): ) sys.exit(1) else: - print(f"βœ… Expected failure: metadata update rejected -> status={status}") + print(f"βœ… Expected failure: metadata update rejected -> status={status}") except Exception as e: print(f"Failed: {e}") @@ -216,7 +216,7 @@ def demonstrate_metadata_length_validation(client, operator_key, operator_id): print( "Error: Expected ValueError for metadata > 100 bytes, but none was raised." ) - + sys.exit(1) except ValueError as exc: print("Expected error raised for metadata > 100 bytes") diff --git a/examples/tokens/token_create_transaction_wipe_key.py b/examples/tokens/token_create_transaction_wipe_key.py index 364fdb0a8..914958e18 100644 --- a/examples/tokens/token_create_transaction_wipe_key.py +++ b/examples/tokens/token_create_transaction_wipe_key.py @@ -35,7 +35,7 @@ TokenInfoQuery, TokenMintTransaction, Hbar, - NftId # <--- FIX 1: Added NftId import + NftId, # <--- FIX 1: Added NftId import ) from hiero_sdk_python.response_code import ResponseCode @@ -43,7 +43,8 @@ from hiero_sdk_python.tokens.supply_type import SupplyType load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """ @@ -52,18 +53,21 @@ def 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', '')) + 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, operator_id, operator_key - + except (TypeError, ValueError): - print("Error: please check OPERATOR_ID and OPERATOR_KEY in you environment file.") + print( + "Error: please check OPERATOR_ID and OPERATOR_KEY in you environment file." + ) sys.exit(1) - + + def create_recipient_account(client): """ Helper: Create a new account to hold tokens(wiped ones) @@ -76,15 +80,18 @@ def create_recipient_account(client): ) receipt = tx.execute(client) if receipt.status != ResponseCode.SUCCESS: - print(f"❌ Account creation failed with status: {ResponseCode(receipt.status).name}") + print( + f"❌ Account creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) print(f"βœ… Account created: {receipt.account_id}") return receipt.account_id, private_key + def associate_and_transfer(client, token_id, recipient_id, recipient_key, amount): """Helper: Associate token to recipient and transfer tokens to them""" - + associate_tx = ( TokenAssociateTransaction() .set_account_id(recipient_id) @@ -95,10 +102,12 @@ def associate_and_transfer(client, token_id, recipient_id, recipient_key, amount receipt_associate = associate_tx.execute(client) if receipt_associate.status != ResponseCode.SUCCESS: - print(f"❌ Token association failed with status: {ResponseCode(receipt_associate.status).name}") + print( + f"❌ Token association failed with status: {ResponseCode(receipt_associate.status).name}" + ) sys.exit(1) print(f" --> Associated token {token_id} to account {recipient_id}.") - + transfer_tx = ( TransferTransaction() .add_token_transfer(token_id, client.operator_account_id, -amount) @@ -108,7 +117,9 @@ def associate_and_transfer(client, token_id, recipient_id, recipient_key, amount receipt_transfer = transfer_tx.execute(client) if receipt_transfer.status != ResponseCode.SUCCESS: - print(f"❌ Token transfer failed with status: {ResponseCode(receipt_transfer.status).name}") + print( + f"❌ Token transfer failed with status: {ResponseCode(receipt_transfer.status).name}" + ) sys.exit(1) print(f" --> Transferred {amount} tokens to account {recipient_id}.") @@ -117,7 +128,7 @@ def create_token_no_wipe_key(client, operator_id, operator_key): """Create a token WITHOUT a wipe key.""" print("\n--- Scenario 1: Token WITHOUT Wipe Key ---") print("Creating token WITHOUT a wipe key...") - + transaction = ( TokenCreateTransaction() .set_token_name("No Wipe Token") @@ -133,9 +144,11 @@ def create_token_no_wipe_key(client, operator_id, operator_key): try: receipt = transaction.execute(client) if receipt.status != ResponseCode.SUCCESS: - print(f"❌ Token creation failed with status: {ResponseCode(receipt.status).name}") + print( + f"❌ Token creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + print(f"βœ… Token created: {receipt.token_id}") return receipt.token_id @@ -155,15 +168,19 @@ def demonstrate_wipe_fail(client, token_id, target_account_id): .set_amount(10) .freeze_with(client) ) - + try: # Since no wipe key exists on the token, Hiero will reject this. receipt = transaction.execute(client) if receipt.status == ResponseCode.TOKEN_HAS_NO_WIPE_KEY: - print(f"βœ… Wipe failed as expected! Token has no wipe key with status: {ResponseCode(receipt.status).name}.") + print( + f"βœ… Wipe failed as expected! Token has no wipe key with status: {ResponseCode(receipt.status).name}." + ) else: - print(f"❌ Wipe unexpectedly succeeded or failed with status: {ResponseCode(receipt.status).name}") - + print( + f"❌ Wipe unexpectedly succeeded or failed with status: {ResponseCode(receipt.status).name}" + ) + except Exception as e: print(f"βœ… Wipe failed as expected with error: {e}") @@ -173,7 +190,7 @@ def create_token_with_wipe_key(client, operator_id, operator_key): print("\n--- Scenario 2: Token WITH Wipe Key ---") print("Creating token WITH a wipe key...") wipe_key = PrivateKey.generate_ed25519() - + transaction = ( TokenCreateTransaction() .set_token_name("With Wipe Token") @@ -185,13 +202,15 @@ def create_token_with_wipe_key(client, operator_id, operator_key): .freeze_with(client) ) transaction.sign(operator_key) - + try: receipt = transaction.execute(client) if receipt.status != ResponseCode.SUCCESS: - print(f"❌ Token creation failed with status: {ResponseCode(receipt.status).name}") + print( + f"❌ Token creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + print(f"βœ… Token created: {receipt.token_id}") return receipt.token_id, wipe_key @@ -199,6 +218,7 @@ def create_token_with_wipe_key(client, operator_id, operator_key): print(f"❌ Token creation failed with error: {e}") sys.exit(1) + def demonstrate_wipe_success(client, token_id, target_account_id, wipe_key): """Wipe tokens using the valid wipe key.""" print(f"Wiping 10 tokens from {target_account_id} using Wipe Key...") @@ -210,7 +230,7 @@ def demonstrate_wipe_success(client, token_id, target_account_id, wipe_key): .set_amount(10) .freeze_with(client) ) - + # Critical: Sign with the wipe key transaction.sign(wipe_key) @@ -238,7 +258,7 @@ def demonstrate_nft_wipe_scenario(client, operator_id, operator_key, user_id, us print("Creating an NFT Collection with a Wipe Key...") wipe_key = PrivateKey.generate_ed25519() - supply_key = PrivateKey.generate_ed25519() # Needed to mint the NFT first + supply_key = PrivateKey.generate_ed25519() # Needed to mint the NFT first # 1. Create the NFT Token transaction = ( @@ -249,12 +269,12 @@ def demonstrate_nft_wipe_scenario(client, operator_id, operator_key, user_id, us .set_initial_supply(0) .set_treasury_account_id(operator_id) .set_admin_key(operator_key) - .set_supply_key(supply_key) # Required to mint - .set_wipe_key(wipe_key) # Required to wipe + .set_supply_key(supply_key) # Required to mint + .set_wipe_key(wipe_key) # Required to wipe .freeze_with(client) ) transaction.sign(operator_key) - + receipt = transaction.execute(client) nft_token_id = receipt.token_id print(f"βœ… NFT Token created: {nft_token_id}") @@ -274,7 +294,7 @@ def demonstrate_nft_wipe_scenario(client, operator_id, operator_key, user_id, us # 3. Associate User and Transfer NFT to them print(f"Transferring NFT #{serial_number} to user {user_id}...") - + # Associate associate_tx = ( TokenAssociateTransaction() @@ -295,20 +315,22 @@ def demonstrate_nft_wipe_scenario(client, operator_id, operator_key, user_id, us # 4. Wipe the NFT from the User print(f"Attempting to WIPE NFT #{serial_number} from user {user_id}...") - + wipe_tx = ( TokenWipeTransaction() .set_token_id(nft_token_id) .set_account_id(user_id) - .set_serial([serial_number]) + .set_serial([serial_number]) .freeze_with(client) ) - wipe_tx.sign(wipe_key) # Sign with Wipe Key + wipe_tx.sign(wipe_key) # Sign with Wipe Key wipe_receipt = wipe_tx.execute(client) - + if wipe_receipt.status == ResponseCode.SUCCESS: - print("βœ… NFT Wipe Successful! The NFT has been effectively burned from the user's account.") + print( + "βœ… NFT Wipe Successful! The NFT has been effectively burned from the user's account." + ) else: print(f"❌ NFT Wipe Failed: {ResponseCode(wipe_receipt.status).name}") @@ -335,9 +357,11 @@ def main(): demonstrate_wipe_fail(client, token_id_no_key, user_id) # --- Scenario 2: With Wipe Key (Fungible) --- - token_id_with_key, wipe_key = create_token_with_wipe_key(client, operator_id, operator_key) + token_id_with_key, wipe_key = create_token_with_wipe_key( + client, operator_id, operator_key + ) associate_and_transfer(client, token_id_with_key, user_id, user_key, 50) - + if demonstrate_wipe_success(client, token_id_with_key, user_id, wipe_key): verify_supply(client, token_id_with_key) @@ -346,5 +370,6 @@ def main(): print("\nπŸŽ‰ Wipe key demonstration completed!") + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/examples/tokens/token_delete_transaction.py b/examples/tokens/token_delete_transaction.py index ae43a7825..f202fd6bc 100644 --- a/examples/tokens/token_delete_transaction.py +++ b/examples/tokens/token_delete_transaction.py @@ -17,18 +17,19 @@ # Load environment variables from .env file load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): - """Setup Client """ + """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 try: - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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) print(f"Client set up with operator id {client.operator_account_id}") @@ -36,7 +37,7 @@ def setup_client(): except (TypeError, ValueError): print("Error: Please check OPERATOR_ID and OPERATOR_KEY in your .env file.") sys.exit(1) - + def generate_admin_key(): """Generate a new admin key within the script: @@ -44,12 +45,13 @@ def generate_admin_key(): """ print("\nGenerating a new admin key for the token...") - admin_key = PrivateKey.generate(os.getenv('KEY_TYPE', 'ed25519')) + admin_key = PrivateKey.generate(os.getenv("KEY_TYPE", "ed25519")) print("Admin key generated successfully.") return admin_key + def create_new_token(client, operator_id, operator_key, admin_key): - """ Create the Token""" + """Create the Token""" token_id_to_delete = None try: @@ -62,7 +64,7 @@ def create_new_token(client, operator_id, operator_key, admin_key): .set_treasury_account_id(operator_id) .set_admin_key(admin_key) # Use the newly generated admin key .freeze_with(client) - .sign(operator_key) # Operator (treasury) must sign + .sign(operator_key) # Operator (treasury) must sign .sign(admin_key) # The new admin key must also sign ) @@ -92,10 +94,10 @@ def delete_token(admin_key, token_id_to_delete, client, operator_key): print(f"\nSTEP 2: Deleting token {token_id_to_delete}...") delete_tx = ( TokenDeleteTransaction() - .set_token_id(token_id_to_delete) # Use the ID from the token we just made + .set_token_id(token_id_to_delete) # Use the ID from the token we just made .freeze_with(client) # Use the ID from the token we just made - .sign(operator_key) # Operator must sign - .sign(admin_key) # Sign with the same admin key used to create it + .sign(operator_key) # Operator must sign + .sign(admin_key) # Sign with the same admin key used to create it ) delete_receipt = delete_tx.execute(client) @@ -112,6 +114,7 @@ def delete_token(admin_key, token_id_to_delete, client, operator_key): print(f"❌ Error deleting token: {repr(e)}") sys.exit(1) + def main(): """ 1. Call create_new_token() to create a new token and get its admin key, token ID, client, and operator key. @@ -126,5 +129,6 @@ def main(): token_id_to_delete = create_new_token(client, operator_id, operator_key, admin_key) delete_token(admin_key, token_id_to_delete, client, operator_key) + if __name__ == "__main__": main() diff --git a/examples/tokens/token_dissociate_transaction.py b/examples/tokens/token_dissociate_transaction.py index 6def716ce..933329317 100644 --- a/examples/tokens/token_dissociate_transaction.py +++ b/examples/tokens/token_dissociate_transaction.py @@ -24,7 +24,8 @@ # Load environment variables from .env file load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Setup Client""" @@ -33,8 +34,8 @@ def setup_client(): client = Client(network) try: - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key @@ -47,14 +48,14 @@ def setup_client(): def create_new_account(client, operator_id, operator_key): """Create a new account to associate/dissociate with tokens""" print("\nSTEP 1: Creating a new account...") - recipient_key = PrivateKey.generate(os.getenv('KEY_TYPE', 'ed25519')) + recipient_key = PrivateKey.generate(os.getenv("KEY_TYPE", "ed25519")) try: # Build the transaction tx = ( AccountCreateTransaction() - .set_key(recipient_key.public_key()) # <-- THE FIX: Call as a method - .set_initial_balance(Hbar.from_tinybars(100_000_000)) # 1 Hbar + .set_key(recipient_key.public_key()) # <-- THE FIX: Call as a method + .set_initial_balance(Hbar.from_tinybars(100_000_000)) # 1 Hbar ) # Freeze the transaction, sign with the operator, then execute @@ -66,8 +67,10 @@ def create_new_account(client, operator_id, operator_key): except (ValueError, RuntimeError) as e: print(f"❌ Error creating new account: {e}") sys.exit(1) + + def create_token(client, operator_key, recipient_id, recipient_key, operator_id): - """Create two new tokens (one NFT and one fungible) for demonstration purposes. """ + """Create two new tokens (one NFT and one fungible) for demonstration purposes.""" print("\nSTEP 2: Creating two new tokens...") try: # Generate supply key for NFT @@ -93,17 +96,24 @@ def create_token(client, operator_key, recipient_id, recipient_key, operator_id) .set_initial_supply(1) .set_treasury_account_id(operator_id) ) - fungible_receipt = fungible_tx.freeze_with(client).sign(operator_key).execute(client) + fungible_receipt = ( + fungible_tx.freeze_with(client).sign(operator_key).execute(client) + ) fungible_token_id = fungible_receipt.token_id - print(f"βœ… Success! Created NFT token: {nft_token_id} and fungible token: {fungible_token_id}") + print( + f"βœ… Success! Created NFT token: {nft_token_id} and fungible token: {fungible_token_id}" + ) return client, nft_token_id, fungible_token_id, recipient_id, recipient_key except (ValueError, RuntimeError) as e: print(f"❌ Error creating tokens: {e}") sys.exit(1) -def token_associate(client, nft_token_id, fungible_token_id, recipient_id, recipient_key): + +def token_associate( + client, nft_token_id, fungible_token_id, recipient_id, recipient_key +): """ Associate the tokens with the new account. @@ -111,8 +121,12 @@ def token_associate(client, nft_token_id, fungible_token_id, recipient_id, recip Association is a prerequisite for holding, transferring, or later dissociating tokens. """ - print(f"\nSTEP 3: Associating NFT and fungible tokens with account {recipient_id}...") - print("Note: Tokens must be associated with an account before they can be used or dissociated.") + print( + f"\nSTEP 3: Associating NFT and fungible tokens with account {recipient_id}..." + ) + print( + "Note: Tokens must be associated with an account before they can be used or dissociated." + ) try: receipt = ( TokenAssociateTransaction() @@ -129,17 +143,26 @@ def token_associate(client, nft_token_id, fungible_token_id, recipient_id, recip print(f"❌ Error associating tokens: {e}") sys.exit(1) + def verify_dissociation(client, nft_token_id, fungible_token_id, recipient_id): """Verify that the specified tokens are dissociated from the account.""" print("\nVerifying token dissociation...") info = AccountInfoQuery().set_account_id(recipient_id).execute(client) - associated_tokens = [rel.token_id for rel in getattr(info, 'token_relationships', [])] - if nft_token_id not in associated_tokens and fungible_token_id not in associated_tokens: + associated_tokens = [ + rel.token_id for rel in getattr(info, "token_relationships", []) + ] + if ( + nft_token_id not in associated_tokens + and fungible_token_id not in associated_tokens + ): print("βœ… Verified: Both tokens are dissociated from the account.") else: print("❌ Verification failed: Some tokens are still associated.") -def token_dissociate(client, nft_token_id, fungible_token_id, recipient_id, recipient_key): + +def token_dissociate( + client, nft_token_id, fungible_token_id, recipient_id, recipient_key +): """ Dissociate the tokens from the new account. @@ -150,22 +173,27 @@ def token_dissociate(client, nft_token_id, fungible_token_id, recipient_id, reci - To comply with business or regulatory requirements """ - print(f"\nSTEP 4: Dissociating NFT and fungible tokens from account {recipient_id}...") + print( + f"\nSTEP 4: Dissociating NFT and fungible tokens from account {recipient_id}..." + ) try: receipt = ( TokenDissociateTransaction() .set_account_id(recipient_id) .set_token_ids([nft_token_id, fungible_token_id]) .freeze_with(client) - .sign(recipient_key) # Recipient must sign to approve + .sign(recipient_key) # Recipient must sign to approve .execute(client) ) - print(f"βœ… Success! Token dissociation complete for both NFT and fungible tokens, Status: {ResponseCode(receipt.status).name}") + print( + f"βœ… Success! Token dissociation complete for both NFT and fungible tokens, Status: {ResponseCode(receipt.status).name}" + ) except (ValueError, RuntimeError) as e: print(f"❌ Error dissociating tokens: {e}") sys.exit(1) + def main(): """ 1-create new account @@ -175,10 +203,18 @@ def main(): 5-verify dissociation """ client, operator_id, operator_key = setup_client() - client, operator_key, recipient_id, recipient_key, operator_id =create_new_account(client, operator_id, operator_key) - client, nft_token_id, fungible_token_id, recipient_id, recipient_key = create_token(client, operator_key, recipient_id, recipient_key, operator_id) - token_associate(client, nft_token_id, fungible_token_id, recipient_id, recipient_key) - token_dissociate(client, nft_token_id, fungible_token_id, recipient_id, recipient_key) + client, operator_key, recipient_id, recipient_key, operator_id = create_new_account( + client, operator_id, operator_key + ) + client, nft_token_id, fungible_token_id, recipient_id, recipient_key = create_token( + client, operator_key, recipient_id, recipient_key, operator_id + ) + token_associate( + client, nft_token_id, fungible_token_id, recipient_id, recipient_key + ) + token_dissociate( + client, nft_token_id, fungible_token_id, recipient_id, recipient_key + ) # Optional: Verify dissociation verify_dissociation(client, nft_token_id, fungible_token_id, recipient_id) diff --git a/examples/tokens/token_fee_schedule_update_transaction_fungible.py b/examples/tokens/token_fee_schedule_update_transaction_fungible.py index ffa1a9f23..d919c3b39 100644 --- a/examples/tokens/token_fee_schedule_update_transaction_fungible.py +++ b/examples/tokens/token_fee_schedule_update_transaction_fungible.py @@ -2,15 +2,22 @@ uv run examples/tokens/token_fee_schedule_update_transaction_fungible.py python examples/tokens/token_fee_schedule_update_transaction_fungible.py """ + import os import sys from dotenv import load_dotenv from hiero_sdk_python import Client, AccountId, PrivateKey, Network -from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction, TokenParams, TokenKeys +from hiero_sdk_python.tokens.token_create_transaction import ( + TokenCreateTransaction, + TokenParams, + TokenKeys, +) from hiero_sdk_python.tokens.token_type import TokenType from hiero_sdk_python.tokens.supply_type import SupplyType -from hiero_sdk_python.tokens.token_fee_schedule_update_transaction import TokenFeeScheduleUpdateTransaction +from hiero_sdk_python.tokens.token_fee_schedule_update_transaction import ( + TokenFeeScheduleUpdateTransaction, +) from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.query.token_info_query import TokenInfoQuery @@ -19,7 +26,7 @@ def setup_client(): """Initialize client and operator credentials from .env.""" load_dotenv() - network_name = os.getenv('NETWORK', 'testnet').lower() + network_name = os.getenv("NETWORK", "testnet").lower() try: network = Network(network_name) @@ -47,15 +54,13 @@ def create_fungible_token(client, operator_id, fee_schedule_key): token_type=TokenType.FUNGIBLE_COMMON, supply_type=SupplyType.FINITE, max_supply=2000, - custom_fees=[], # No custom fees at creation - ) - - keys = TokenKeys( - fee_schedule_key=fee_schedule_key + custom_fees=[], # No custom fees at creation ) + keys = TokenKeys(fee_schedule_key=fee_schedule_key) + tx = TokenCreateTransaction(token_params=token_params, keys=keys) - + tx.freeze_with(client) receipt = tx.execute(client) @@ -82,9 +87,9 @@ def update_custom_fixed_fee(client, token_id, fee_schedule_key, treasury_account .set_token_id(token_id) .set_custom_fees(new_fees) ) - + # The transaction MUST be signed by the fee_schedule_key - tx.freeze_with(client).sign(fee_schedule_key) + tx.freeze_with(client).sign(fee_schedule_key) try: receipt = tx.execute(client) @@ -104,7 +109,7 @@ def query_token_info(client, token_id): try: token_info = TokenInfoQuery(token_id=token_id).execute(client) print("Token Info Retrieved Successfully!\n") - + print(f"Name: {getattr(token_info, 'name', 'N/A')}") print(f"Symbol: {getattr(token_info, 'symbol', 'N/A')}") print(f"Total Supply: {getattr(token_info, 'total_supply', 'N/A')}") @@ -118,7 +123,9 @@ def query_token_info(client, token_id): print(f"Found {len(custom_fees)} custom fee(s):") for i, fee in enumerate(custom_fees, 1): print(f" Fee #{i}: {type(fee).__name__}") - print(f" Collector: {getattr(fee, 'fee_collector_account_id', 'N/A')}") + print( + f" Collector: {getattr(fee, 'fee_collector_account_id', 'N/A')}" + ) if isinstance(fee, CustomFixedFee): print(f" Amount: {getattr(fee, 'amount', 'N/A')}") else: @@ -134,15 +141,15 @@ def main(): token_id = None try: fee_key = operator_key - + token_id = create_fungible_token(client, operator_id, fee_key) - + if token_id: - query_token_info(client, token_id) + query_token_info(client, token_id) # Pass the operator_id as the fee collector (which is also the treasury) update_custom_fixed_fee(client, token_id, fee_key, operator_id) query_token_info(client, token_id) - + except Exception as e: print(f" Error during token operations: {e}") finally: @@ -152,4 +159,3 @@ def main(): if __name__ == "__main__": main() - diff --git a/examples/tokens/token_fee_schedule_update_transaction_nft.py b/examples/tokens/token_fee_schedule_update_transaction_nft.py index 82f93afb6..b1155ca9d 100644 --- a/examples/tokens/token_fee_schedule_update_transaction_nft.py +++ b/examples/tokens/token_fee_schedule_update_transaction_nft.py @@ -1,15 +1,22 @@ """Example: Update Custom Fees for an NFT uv run examples/tokens/token_fee_schedule_update_transaction_nft.py python examples/tokens/token_fee_schedule_update_transaction_nft.py""" + import os import sys from dotenv import load_dotenv from hiero_sdk_python import Client, AccountId, PrivateKey, Network -from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction, TokenParams, TokenKeys +from hiero_sdk_python.tokens.token_create_transaction import ( + TokenCreateTransaction, + TokenParams, + TokenKeys, +) from hiero_sdk_python.tokens.token_type import TokenType from hiero_sdk_python.tokens.supply_type import SupplyType -from hiero_sdk_python.tokens.token_fee_schedule_update_transaction import TokenFeeScheduleUpdateTransaction +from hiero_sdk_python.tokens.token_fee_schedule_update_transaction import ( + TokenFeeScheduleUpdateTransaction, +) from hiero_sdk_python.tokens.custom_royalty_fee import CustomRoyaltyFee from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.query.token_info_query import TokenInfoQuery @@ -18,7 +25,7 @@ def setup_client(): """Initialize client and operator credentials from .env.""" load_dotenv() - network_name = os.getenv('NETWORK', 'testnet').lower() + network_name = os.getenv("NETWORK", "testnet").lower() try: network = Network(network_name) @@ -46,19 +53,16 @@ def create_nft(client, operator_id, supply_key, fee_schedule_key): token_type=TokenType.NON_FUNGIBLE_UNIQUE, supply_type=SupplyType.FINITE, max_supply=1000, - custom_fees=[], + custom_fees=[], ) - + # A supply_key is REQUIRED for NFTs (to mint) # A fee_schedule_key is required to update fees - keys = TokenKeys( - supply_key=supply_key, - fee_schedule_key=fee_schedule_key - ) + keys = TokenKeys(supply_key=supply_key, fee_schedule_key=fee_schedule_key) tx = TokenCreateTransaction(token_params=token_params, keys=keys) # tx.set_fee_schedule_key(fee_schedule_key) - + # Sign with the supply key as well tx.freeze_with(client).sign(supply_key) receipt = tx.execute(client) @@ -78,9 +82,9 @@ def update_custom_royalty_fee(client, token_id, fee_schedule_key, collector_acco print(f" Updating custom royalty fee for token {token_id}...") new_fees = [ CustomRoyaltyFee( - numerator=5, - denominator=100, # 5% royalty - fee_collector_account_id=collector_account_id + numerator=5, + denominator=100, # 5% royalty + fee_collector_account_id=collector_account_id, ) ] print(f" Defined {len(new_fees)} new custom fees.\n") @@ -89,19 +93,19 @@ def update_custom_royalty_fee(client, token_id, fee_schedule_key, collector_acco .set_token_id(token_id) .set_custom_fees(new_fees) ) - - tx.freeze_with(client).sign(fee_schedule_key) + + tx.freeze_with(client).sign(fee_schedule_key) try: receipt = tx.execute(client) if receipt.status != ResponseCode.SUCCESS: print(f" Fee schedule update failed: {ResponseCode(receipt.status).name}\n") - sys.exit(1) + sys.exit(1) else: print(" Fee schedule updated successfully.\n") except Exception as e: print(f" Error during fee schedule update execution: {e}\n") - sys.exit(1) + sys.exit(1) def query_token_info(client, token_id): @@ -110,7 +114,7 @@ def query_token_info(client, token_id): try: token_info = TokenInfoQuery(token_id=token_id).execute(client) print("Token Info Retrieved Successfully!\n") - + print(f"Name: {getattr(token_info, 'name', 'N/A')}") print(f"Symbol: {getattr(token_info, 'symbol', 'N/A')}") print(f"Total Supply: {getattr(token_info, 'total_supply', 'N/A')}") @@ -124,7 +128,9 @@ def query_token_info(client, token_id): print(f"Found {len(custom_fees)} custom fee(s):") for i, fee in enumerate(custom_fees, 1): print(f" Fee #{i}: {type(fee).__name__}") - print(f" Collector: {getattr(fee, 'fee_collector_account_id', 'N/A')}") + print( + f" Collector: {getattr(fee, 'fee_collector_account_id', 'N/A')}" + ) if isinstance(fee, CustomRoyaltyFee): print(f" Royalty: {fee.numerator}/{fee.denominator}") else: @@ -134,7 +140,8 @@ def query_token_info(client, token_id): except Exception as e: print(f"Error querying token info: {e}") - sys.exit(1) + sys.exit(1) + def main(): client, operator_id, operator_key = setup_client() @@ -143,14 +150,14 @@ def main(): # Use operator key as both supply and fee key supply_key = operator_key fee_key = operator_key - + token_id = create_nft(client, operator_id, supply_key, fee_key) - + if token_id: - query_token_info(client, token_id) + query_token_info(client, token_id) update_custom_royalty_fee(client, token_id, fee_key, operator_id) query_token_info(client, token_id) - + except Exception as e: print(f" Error during token operations: {e}") finally: @@ -160,4 +167,3 @@ def main(): if __name__ == "__main__": main() - diff --git a/examples/tokens/token_freeze_transaction.py b/examples/tokens/token_freeze_transaction.py index e7e28517c..afa063ed1 100644 --- a/examples/tokens/token_freeze_transaction.py +++ b/examples/tokens/token_freeze_transaction.py @@ -23,7 +23,8 @@ # Load environment variables from .env file load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Setup Client""" @@ -32,8 +33,8 @@ def setup_client(): client = Client(network) try: - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key @@ -45,10 +46,11 @@ def setup_client(): def generate_freeze_key(): """Generate a Freeze Key""" print("\nSTEP 1: Generating a new freeze key...") - freeze_key = PrivateKey.generate(os.getenv('KEY_TYPE', 'ed25519')) + freeze_key = PrivateKey.generate(os.getenv("KEY_TYPE", "ed25519")) print("βœ… Freeze key generated.") return freeze_key + def create_freezeable_token(client, operator_id, operator_key): """Create a token with the freeze key""" freeze_key = generate_freeze_key() @@ -60,14 +62,14 @@ def create_freezeable_token(client, operator_id, operator_key): .set_token_symbol("FRZ") .set_initial_supply(1000) .set_treasury_account_id(operator_id) - .set_freeze_key(freeze_key) # <-- THE FIX: Pass the private key directly + .set_freeze_key(freeze_key) # <-- THE FIX: Pass the private key directly ) # Freeze, sign with BOTH operator and the new freeze key, then execute receipt = ( tx.freeze_with(client) .sign(operator_key) - .sign(freeze_key) # The new freeze key must sign to give consent + .sign(freeze_key) # The new freeze key must sign to give consent .execute(client) ) token_id = receipt.token_id @@ -87,17 +89,20 @@ def freeze_token(token_id, client, operator_id, freeze_key): receipt = ( TokenFreezeTransaction() .set_token_id(token_id) - .set_account_id(operator_id) # Target the operator account + .set_account_id(operator_id) # Target the operator account .freeze_with(client) - .sign(freeze_key) # Must be signed by the freeze key + .sign(freeze_key) # Must be signed by the freeze key .execute(client) ) - print(f"βœ… Success! Token freeze complete. Status: {ResponseCode(receipt.status).name}") - + print( + f"βœ… Success! Token freeze complete. Status: {ResponseCode(receipt.status).name}" + ) + except RuntimeError as e: print(f"❌ Error freezing token: {e}") sys.exit(1) + def verify_freeze(token_id, client, operator_id, operator_key): """Attempt a token transfer to confirm the account cannot perform the operation while frozen.""" @@ -116,14 +121,19 @@ def verify_freeze(token_id, client, operator_id, operator_key): status_code = transfer_receipt.status status_name = ResponseCode(status_code).name if status_name in ["ACCOUNT_FROZEN_FOR_TOKEN", "ACCOUNT_FROZEN"]: - print(f"βœ… Verified: Transfer blocked as expected due to freeze. Status: {status_name}") + print( + f"βœ… Verified: Transfer blocked as expected due to freeze. Status: {status_name}" + ) elif status_name == "SUCCESS": - print("❌ Error: Transfer succeeded, but should have failed because the account is frozen.") + print( + "❌ Error: Transfer succeeded, but should have failed because the account is frozen." + ) else: print(f"❌ Unexpected: Transfer result. Status: {status_name}") except RuntimeError as e: print(f"βœ… Verified: Transfer failed as expected due to freeze. Error: {e}") + def main(): """ 1. Create a freezeable token with a freeze key. @@ -131,9 +141,12 @@ def main(): 3. Attempt a token transfer to verify the freeze (should fail). 4. Return token details for further operations.""" client, operator_id, operator_key = setup_client() - freeze_key, token_id, client, operator_id, operator_key = create_freezeable_token(client, operator_id, operator_key) + freeze_key, token_id, client, operator_id, operator_key = create_freezeable_token( + client, operator_id, operator_key + ) freeze_token(token_id, client, operator_id, freeze_key) verify_freeze(token_id, client, operator_id, operator_key) + if __name__ == "__main__": main() diff --git a/examples/tokens/token_grant_kyc_transaction.py b/examples/tokens/token_grant_kyc_transaction.py index bcff2d0c8..35f841a94 100644 --- a/examples/tokens/token_grant_kyc_transaction.py +++ b/examples/tokens/token_grant_kyc_transaction.py @@ -3,6 +3,7 @@ python examples/tokens/token_grant_kyc_transaction.py """ + import os import sys from dotenv import load_dotenv @@ -18,12 +19,15 @@ from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.tokens.supply_type import SupplyType -from hiero_sdk_python.tokens.token_associate_transaction import TokenAssociateTransaction +from hiero_sdk_python.tokens.token_associate_transaction import ( + TokenAssociateTransaction, +) from hiero_sdk_python.tokens.token_grant_kyc_transaction import TokenGrantKycTransaction from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -31,13 +35,14 @@ def setup_client(): print(f"Connecting to Hedera {network_name} network!") client = Client(network) - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key + def create_fungible_token(client, operator_id, operator_key, kyc_private_key): """Create a fungible token""" receipt = ( @@ -52,19 +57,24 @@ def create_fungible_token(client, operator_id, operator_key, kyc_private_key): .set_max_supply(1000) .set_admin_key(operator_key) .set_supply_key(operator_key) - .set_kyc_key(kyc_private_key) # Required key for granting KYC approval to accounts + .set_kyc_key( + kyc_private_key + ) # Required key for granting KYC approval to accounts .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: - print(f"Fungible token creation failed with status: {ResponseCode(receipt.status).name}") + print( + f"Fungible token creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + token_id = receipt.token_id print(f"Fungible token created with ID: {token_id}") - + return token_id + def associate_token(client, token_id, account_id, account_private_key): """Associate a token with an account""" associate_transaction = ( @@ -72,23 +82,26 @@ def associate_token(client, token_id, account_id, account_private_key): .set_account_id(account_id) .add_token_id(token_id) .freeze_with(client) - .sign(account_private_key) # Has to be signed by new account's key + .sign(account_private_key) # Has to be signed by new account's key ) - + receipt = associate_transaction.execute(client) - + if receipt.status != ResponseCode.SUCCESS: - print(f"Token association failed with status: {ResponseCode(receipt.status).name}") + print( + f"Token association failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + print("Token successfully associated with account") + def create_test_account(client): """Create a new account for testing""" # Generate private key for new account new_account_private_key = PrivateKey.generate() new_account_public_key = new_account_private_key.public_key() - + # Create new account with initial balance of 1 HBAR transaction = ( AccountCreateTransaction() @@ -96,20 +109,23 @@ def create_test_account(client): .set_initial_balance(Hbar(1)) .freeze_with(client) ) - + receipt = transaction.execute(client) - + # Check if account creation was successful if receipt.status != ResponseCode.SUCCESS: - print(f"Account creation failed with status: {ResponseCode(receipt.status).name}") + print( + f"Account creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + # Get account ID from receipt account_id = receipt.account_id print(f"New account created with ID: {account_id}") - + return account_id, new_account_private_key + def token_grant_kyc(): """ Demonstrates the token grant KYC functionality by: @@ -120,19 +136,19 @@ def token_grant_kyc(): 5. Granting KYC to the new account """ client, operator_id, operator_key = setup_client() - + # Create KYC key kyc_private_key = PrivateKey.generate_ed25519() - + # Create a fungible token with KYC key token_id = create_fungible_token(client, operator_id, operator_key, kyc_private_key) - + # Create a new account account_id, account_private_key = create_test_account(client) - + # Associate the token with the new account associate_token(client, token_id, account_id, account_private_key) - + # Grant KYC to the new account receipt = ( TokenGrantKycTransaction() @@ -142,13 +158,16 @@ def token_grant_kyc(): .sign(kyc_private_key) # Has to be signed by the KYC key .execute(client) ) - + # Check if the transaction was successful if receipt.status != ResponseCode.SUCCESS: - print(f"Token grant KYC failed with status: {ResponseCode(receipt.status).name}") + print( + f"Token grant KYC failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + print(f"Granted KYC for account {account_id} on token {token_id}") + if __name__ == "__main__": token_grant_kyc() diff --git a/examples/tokens/token_mint_transaction_fungible.py b/examples/tokens/token_mint_transaction_fungible.py index f81c11bfc..bb24bb143 100644 --- a/examples/tokens/token_mint_transaction_fungible.py +++ b/examples/tokens/token_mint_transaction_fungible.py @@ -19,13 +19,13 @@ TokenCreateTransaction, TokenMintTransaction, TokenInfoQuery, - ResponseCode - + ResponseCode, ) # Load environment variables from .env file load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Setup Client""" @@ -34,8 +34,8 @@ def setup_client(): client = Client(network) try: - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key @@ -44,14 +44,14 @@ def setup_client(): sys.exit(1) - def generate_supply_key(): """Generate a new supply key for the token.""" print("\nSTEP 1: Generating a new supply key...") - supply_key = PrivateKey.generate(os.getenv('KEY_TYPE', 'ed25519')) + supply_key = PrivateKey.generate(os.getenv("KEY_TYPE", "ed25519")) print("βœ… Supply key generated.") return supply_key + def create_new_token(): """ Create a fungible token that can have its supply changed (minted or burned). @@ -83,7 +83,7 @@ def create_new_token(): # Confirm the token has a supply key set info = TokenInfoQuery().set_token_id(token_id).execute(client) - if getattr(info, 'supply_key', None): + if getattr(info, "supply_key", None): print("βœ… Verified: Token has a supply key set.") else: print("❌ Warning: Token does not have a supply key set.") @@ -102,7 +102,7 @@ def token_mint_fungible(client, token_id, supply_key): Only the holder of the supply key can perform these actions. """ - mint_amount = 5000 # This is 50.00 tokens because decimals is 2 + mint_amount = 5000 # This is 50.00 tokens because decimals is 2 print(f"\nSTEP 3: Minting {mint_amount} more tokens for {token_id}...") # Confirm total supply before minting @@ -120,7 +120,9 @@ def token_mint_fungible(client, token_id, supply_key): .sign(supply_key) # Must be signed by the supply key .execute(client) ) - print(f"βœ… Success! Token minting complete, Status: {ResponseCode(receipt.status).name}") + print( + f"βœ… Success! Token minting complete, Status: {ResponseCode(receipt.status).name}" + ) # Confirm total supply after minting info_after = TokenInfoQuery().set_token_id(token_id).execute(client) @@ -129,6 +131,7 @@ def token_mint_fungible(client, token_id, supply_key): print(f"❌ Error minting tokens: {e}") sys.exit(1) + def main(): """ 1. Create a new token with a supply key so its supply can be changed later @@ -139,5 +142,6 @@ def main(): client, token_id, supply_key = create_new_token() token_mint_fungible(client, token_id, supply_key) + if __name__ == "__main__": main() diff --git a/examples/tokens/token_mint_transaction_non_fungible.py b/examples/tokens/token_mint_transaction_non_fungible.py index cc8592471..3ab00218a 100644 --- a/examples/tokens/token_mint_transaction_non_fungible.py +++ b/examples/tokens/token_mint_transaction_non_fungible.py @@ -25,7 +25,8 @@ # Load environment variables from .env file load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Setup and return a Hedera client.""" @@ -34,8 +35,8 @@ def setup_client(): client = Client(network) try: - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key @@ -47,12 +48,13 @@ def setup_client(): def generate_supply_key(): """Generate a new supply key for the token.""" print("\nSTEP 1: Generating a new supply key...") - supply_key = PrivateKey.generate(os.getenv('HSDK_KEY_TYPE', 'ed25519')) + supply_key = PrivateKey.generate(os.getenv("HSDK_KEY_TYPE", "ed25519")) print("βœ… Supply key generated") return supply_key + def create_nft_collection(): - """ Create the NFT Collection (Token) """ + """Create the NFT Collection (Token)""" client, operator_id, operator_key = setup_client() supply_key = generate_supply_key() print("\nSTEP 2: Creating a new NFT collection...") @@ -79,6 +81,8 @@ def create_nft_collection(): except Exception as e: print(f"❌ Error creating token: {e}") sys.exit(1) + + def token_mint_non_fungible(client, token_id, supply_key): """ Mint new NFTs with metadata @@ -104,14 +108,16 @@ def token_mint_non_fungible(client, token_id, supply_key): receipt = ( TokenMintTransaction() .set_token_id(token_id) - .set_metadata(metadata_list) # Set the list of metadata + .set_metadata(metadata_list) # Set the list of metadata .freeze_with(client) .sign(supply_key) # Must be signed by the supply key .execute(client) ) # THE FIX: The receipt confirms status, it does not contain serial numbers. - print(f"βœ… Success! NFT minting complete, Status: {ResponseCode(receipt.status).name}") + print( + f"βœ… Success! NFT minting complete, Status: {ResponseCode(receipt.status).name}" + ) # Confirm total supply after minting info_after = TokenInfoQuery().set_token_id(token_id).execute(client) print(f"Total supply after minting: {info_after.total_supply}") @@ -119,6 +125,7 @@ def token_mint_non_fungible(client, token_id, supply_key): print(f"❌ Error minting NFTs: {e}") sys.exit(1) + def main(): """ 1. Create a new NFT collection (token) with a supply key @@ -130,5 +137,6 @@ def main(): client, token_id, supply_key = create_nft_collection() token_mint_non_fungible(client, token_id, supply_key) + if __name__ == "__main__": main() diff --git a/examples/tokens/token_pause_transaction.py b/examples/tokens/token_pause_transaction.py index d17b85398..dba4aed8e 100644 --- a/examples/tokens/token_pause_transaction.py +++ b/examples/tokens/token_pause_transaction.py @@ -3,16 +3,12 @@ python examples/tokens/token_pause_transaction.py """ + import os import sys from dotenv import load_dotenv -from hiero_sdk_python import ( - Client, - AccountId, - PrivateKey, - Network -) +from hiero_sdk_python import Client, AccountId, PrivateKey, Network from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.tokens.supply_type import SupplyType from hiero_sdk_python.tokens.token_type import TokenType @@ -22,7 +18,8 @@ from hiero_sdk_python.query.token_info_query import TokenInfoQuery load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -32,13 +29,14 @@ def setup_client(): client = Client(network) # Set up operator account - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key + def assert_success(receipt, action: str): """ Verify that a transaction or query succeeded, else raise. @@ -55,6 +53,7 @@ def assert_success(receipt, action: str): name = ResponseCode(receipt.status).name raise RuntimeError(f"{action!r} failed with status {name}") + def create_token(client, operator_id, admin_key, pause_key): """Create a fungible token""" # Create fungible token @@ -68,20 +67,21 @@ def create_token(client, operator_id, admin_key, pause_key): .set_token_type(TokenType.FUNGIBLE_COMMON) .set_supply_type(SupplyType.FINITE) .set_max_supply(100) - .set_admin_key(admin_key) # Required for token delete - .set_pause_key(pause_key) # Required for pausing tokens + .set_admin_key(admin_key) # Required for token delete + .set_pause_key(pause_key) # Required for pausing tokens .freeze_with(client) ) - + receipt = create_token_transaction.execute(client) assert_success(receipt, "Token creation") - + # Get token ID from receipt token_id = receipt.token_id print(f"Token created with ID: {token_id}") - + return token_id + def pause_token(client, token_id, pause_key): """Pause token""" # Note: This requires the pause key that was specified during token creation @@ -91,19 +91,21 @@ def pause_token(client, token_id, pause_key): .freeze_with(client) .sign(pause_key) ) - + receipt = pause_transaction.execute(client) - assert_success(receipt, "Token pause") + assert_success(receipt, "Token pause") print(f"Successfully paused token {token_id}") + def check_pause_status(client, token_id): """ Query and print the current paused/unpaused status of a token. """ info = TokenInfoQuery().set_token_id(token_id).execute(client) print(f"Token status is now: {info.pause_status.name}") - + + def delete_token(client, token_id, admin_key): """Delete token""" # Note: This requires the admin key that was specified during token creation @@ -115,10 +117,11 @@ def delete_token(client, token_id, admin_key): ) receipt = delete_transaction.execute(client) - assert_success(receipt, "Token delete") + assert_success(receipt, "Token delete") print(f"Successfully deleted token {token_id}") + def token_pause(): """ Demonstrates the token pause functionality by: @@ -129,12 +132,12 @@ def token_pause(): """ client, operator_id, operator_key = setup_client() - pause_key = operator_key # for token pause - admin_key = operator_key # for token delete + pause_key = operator_key # for token pause + admin_key = operator_key # for token delete # Create token with required keys for pause and delete. token_id = create_token(client, operator_id, admin_key, pause_key) - + # Pause token using pause key – should succeed pause_token(client, token_id, pause_key) @@ -144,9 +147,12 @@ def token_pause(): # Try deleting token with admin key – should fail with TOKEN_IS_PAUSED try: delete_token(client, token_id, admin_key) - print("❌ Whoops, delete succeededβ€”but it should have failed on a paused token!") + print( + "❌ Whoops, delete succeededβ€”but it should have failed on a paused token!" + ) except RuntimeError as e: print(f"βœ… Unable to delete token as expected as it is paused: {e}") + if __name__ == "__main__": token_pause() diff --git a/examples/tokens/token_reject_transaction_fungible_token.py b/examples/tokens/token_reject_transaction_fungible_token.py index 86887525e..2a9171b4d 100644 --- a/examples/tokens/token_reject_transaction_fungible_token.py +++ b/examples/tokens/token_reject_transaction_fungible_token.py @@ -3,6 +3,7 @@ python examples/tokens/token_reject_transaction_fungible_token.py """ + import os import sys from dotenv import load_dotenv @@ -20,12 +21,15 @@ from hiero_sdk_python.query.account_balance_query import CryptoGetAccountBalanceQuery from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.tokens.supply_type import SupplyType -from hiero_sdk_python.tokens.token_associate_transaction import TokenAssociateTransaction +from hiero_sdk_python.tokens.token_associate_transaction import ( + TokenAssociateTransaction, +) from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction from hiero_sdk_python.tokens.token_reject_transaction import TokenRejectTransaction load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -33,19 +37,20 @@ def setup_client(): print(f"Connecting to Hedera {network_name} network!") client = Client(network) - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID','')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY','')) + 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 + def create_test_account(client): """Create a new account for testing""" # Generate private key for new account new_account_private_key = PrivateKey.generate_ed25519() new_account_public_key = new_account_private_key.public_key() - + # Create new account with initial balance of 1 HBAR receipt = ( AccountCreateTransaction() @@ -53,19 +58,22 @@ def create_test_account(client): .set_initial_balance(Hbar(1)) .execute(client) ) - + # Check if account creation was successful if receipt.status != ResponseCode.SUCCESS: - print(f"Account creation failed with status: {ResponseCode(receipt.status).name}") + print( + f"Account creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + # Get account ID from receipt account_id = receipt.account_id print(f"New account created with ID: {account_id}") - + return account_id, new_account_private_key -def create_fungible_token(client: 'Client', treasury_id, treasury_private_key): + +def create_fungible_token(client: "Client", treasury_id, treasury_private_key): """Create a fungible token""" receipt = ( TokenCreateTransaction() @@ -81,19 +89,24 @@ def create_fungible_token(client: 'Client', treasury_id, treasury_private_key): .set_supply_key(treasury_private_key) .set_freeze_key(treasury_private_key) .freeze_with(client) - .sign(treasury_private_key) # Has to be signed by treasury account's key as we set the treasury_account_id to be the treasury_id + .sign( + treasury_private_key + ) # Has to be signed by treasury account's key as we set the treasury_account_id to be the treasury_id .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: - print(f"Fungible token creation failed with status: {ResponseCode(receipt.status).name}") + print( + f"Fungible token creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + token_id = receipt.token_id print(f"Fungible token created with ID: {token_id}") - + return token_id + def associate_token(client, receiver_id, token_id, receiver_private_key): """Associate token with an account""" # Associate the token_id with the new account @@ -102,17 +115,22 @@ def associate_token(client, receiver_id, token_id, receiver_private_key): .set_account_id(receiver_id) .add_token_id(token_id) .freeze_with(client) - .sign(receiver_private_key) # Has to be signed here by receiver's key + .sign(receiver_private_key) # Has to be signed here by receiver's key .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: - print(f"Token association failed with status: {ResponseCode(receipt.status).name}") + print( + f"Token association failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + print(f"Token successfully associated with account: {receiver_id}") -def transfer_tokens(client, treasury_id, treasury_private_key, receiver_id, token_id, amount=10): + +def transfer_tokens( + client, treasury_id, treasury_private_key, receiver_id, token_id, amount=10 +): """Transfer tokens to the receiver account so we can later reject them""" # Transfer tokens to the receiver account receipt = ( @@ -123,29 +141,31 @@ def transfer_tokens(client, treasury_id, treasury_private_key, receiver_id, toke .sign(treasury_private_key) .execute(client) ) - + # Check if transfer was successful if receipt.status != ResponseCode.SUCCESS: print(f"Transfer failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + print(f"Successfully transferred {amount} tokens to receiver account {receiver_id}") - + + def get_token_balances(client, treasury_id, receiver_id, token_id): """Get token balances for both accounts""" token_balance = ( - CryptoGetAccountBalanceQuery() - .set_account_id(treasury_id) - .execute(client) + CryptoGetAccountBalanceQuery().set_account_id(treasury_id).execute(client) ) - print(f"Token balance of treasury {treasury_id}: {token_balance.token_balances[token_id]}") - + print( + f"Token balance of treasury {treasury_id}: {token_balance.token_balances[token_id]}" + ) + receiver_token_balance = ( - CryptoGetAccountBalanceQuery() - .set_account_id(receiver_id) - .execute(client) + CryptoGetAccountBalanceQuery().set_account_id(receiver_id).execute(client) ) - print(f"Token balance of receiver {receiver_id}: {receiver_token_balance.token_balances[token_id]}") + print( + f"Token balance of receiver {receiver_id}: {receiver_token_balance.token_balances[token_id]}" + ) + def token_reject_fungible(): """ @@ -162,20 +182,20 @@ def token_reject_fungible(): treasury_id, treasury_private_key = create_test_account(client) # Create receiver account that will receive and later reject tokens receiver_id, receiver_private_key = create_test_account(client) - + # Create a fungible token with the treasury account as owner and signer token_id = create_fungible_token(client, treasury_id, treasury_private_key) - + # Associate token with the receiver account so they can receive the tokens from the treasury associate_token(client, receiver_id, token_id, receiver_private_key) - + # Transfer tokens to the receiver account transfer_tokens(client, treasury_id, treasury_private_key, receiver_id, token_id) # Get and print token balances before rejection to show the initial state print("\nToken balances before rejection:") get_token_balances(client, treasury_id, receiver_id, token_id) - + # Receiver rejects the fungible tokens that were previously transferred to them receipt = ( TokenRejectTransaction() @@ -185,16 +205,19 @@ def token_reject_fungible(): .sign(receiver_private_key) .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: - print(f"Token rejection failed with status: {ResponseCode(receipt.status).name}") + print( + f"Token rejection failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + print(f"Successfully rejected token {token_id} from account {receiver_id}") - + # Get and print token balances after rejection to show the final state print("\nToken balances after rejection:") get_token_balances(client, treasury_id, receiver_id, token_id) - + + if __name__ == "__main__": token_reject_fungible() diff --git a/examples/tokens/token_reject_transaction_nft.py b/examples/tokens/token_reject_transaction_nft.py index 4da01a512..7be75b082 100644 --- a/examples/tokens/token_reject_transaction_nft.py +++ b/examples/tokens/token_reject_transaction_nft.py @@ -3,6 +3,7 @@ python examples/tokens/token_reject_transaction_nft.py """ + import os import sys from dotenv import load_dotenv @@ -21,13 +22,16 @@ from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.tokens.nft_id import NftId from hiero_sdk_python.tokens.supply_type import SupplyType -from hiero_sdk_python.tokens.token_associate_transaction import TokenAssociateTransaction +from hiero_sdk_python.tokens.token_associate_transaction import ( + TokenAssociateTransaction, +) from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction from hiero_sdk_python.tokens.token_reject_transaction import TokenRejectTransaction load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -35,19 +39,20 @@ def setup_client(): print(f"Connecting to Hedera {network_name} network!") client = Client(network) - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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 + def create_test_account(client): """Create a new account for testing""" # Generate private key for new account new_account_private_key = PrivateKey.generate_ed25519() new_account_public_key = new_account_private_key.public_key() - + # Create new account with initial balance of 1 HBAR receipt = ( AccountCreateTransaction() @@ -55,18 +60,21 @@ def create_test_account(client): .set_initial_balance(Hbar(1)) .execute(client) ) - + # Check if account creation was successful if receipt.status != ResponseCode.SUCCESS: - print(f"Account creation failed with status: {ResponseCode(receipt.status).name}") + print( + f"Account creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + # Get account ID from receipt account_id = receipt.account_id print(f"New account created with ID: {account_id}") - + return account_id, new_account_private_key + def create_nft(client, treasury_id, treasury_private_key): """Create a non-fungible token""" receipt = ( @@ -83,21 +91,24 @@ def create_nft(client, treasury_id, treasury_private_key): .set_supply_key(treasury_private_key) .set_freeze_key(treasury_private_key) .freeze_with(client) - .sign(treasury_private_key) # Has to be signed here by treasury's key as we set the treasury_account_id to be the treasury_id + .sign( + treasury_private_key + ) # Has to be signed here by treasury's key as we set the treasury_account_id to be the treasury_id .execute(client) ) - + # Check if nft creation was successful if receipt.status != ResponseCode.SUCCESS: print(f"NFT creation failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + # Get token ID from receipt nft_token_id = receipt.token_id print(f"NFT created with ID: {nft_token_id}") - + return nft_token_id + def mint_nfts(client, nft_token_id, metadata_list, treasury_private_key): """Mint a non-fungible token""" receipt = ( @@ -105,17 +116,22 @@ def mint_nfts(client, nft_token_id, metadata_list, treasury_private_key): .set_token_id(nft_token_id) .set_metadata(metadata_list) .freeze_with(client) - .sign(treasury_private_key) # Has to be signed here by treasury's key because they own the supply key + .sign( + treasury_private_key + ) # Has to be signed here by treasury's key because they own the supply key .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: print(f"NFT minting failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + print(f"NFT minted with serial numbers: {receipt.serial_numbers}") - - return [NftId(nft_token_id, serial_number) for serial_number in receipt.serial_numbers] + + return [ + NftId(nft_token_id, serial_number) for serial_number in receipt.serial_numbers + ] + def associate_token(client, receiver_id, nft_token_id, receiver_private_key): """Associate token with an account""" @@ -125,16 +141,19 @@ def associate_token(client, receiver_id, nft_token_id, receiver_private_key): .set_account_id(receiver_id) .add_token_id(nft_token_id) .freeze_with(client) - .sign(receiver_private_key) # Has to be signed here by receiver's key + .sign(receiver_private_key) # Has to be signed here by receiver's key .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: - print(f"Token association failed with status: {ResponseCode(receipt.status).name}") + print( + f"Token association failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + print(f"Token successfully associated with account: {receiver_id}") + def transfer_nfts(client, treasury_id, treasury_private_key, receiver_id, nft_ids): """Transfer NFTs to the receiver account so we can later reject them""" # Transfer NFTs to the receiver account @@ -146,29 +165,31 @@ def transfer_nfts(client, treasury_id, treasury_private_key, receiver_id, nft_id .sign(treasury_private_key) .execute(client) ) - + # Check if transfer was successful if receipt.status != ResponseCode.SUCCESS: print(f"Transfer failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + print(f"Successfully transferred NFTs to receiver account {receiver_id}") - + + def get_nft_balances(client, treasury_id, receiver_id, nft_token_id): """Get NFT balances for both accounts""" token_balance = ( - CryptoGetAccountBalanceQuery() - .set_account_id(treasury_id) - .execute(client) + CryptoGetAccountBalanceQuery().set_account_id(treasury_id).execute(client) + ) + print( + f"NFT balance of treasury {treasury_id}: {token_balance.token_balances[nft_token_id]}" ) - print(f"NFT balance of treasury {treasury_id}: {token_balance.token_balances[nft_token_id]}") - + receiver_token_balance = ( - CryptoGetAccountBalanceQuery() - .set_account_id(receiver_id) - .execute(client) + CryptoGetAccountBalanceQuery().set_account_id(receiver_id).execute(client) + ) + print( + f"NFT balance of receiver {receiver_id}: {receiver_token_balance.token_balances[nft_token_id]}" ) - print(f"NFT balance of receiver {receiver_id}: {receiver_token_balance.token_balances[nft_token_id]}") + def token_reject_nft(): """ @@ -186,22 +207,27 @@ def token_reject_nft(): treasury_id, treasury_private_key = create_test_account(client) # Create receiver account that will receive and later reject tokens receiver_id, receiver_private_key = create_test_account(client) - + # Create a new NFT collection with the treasury account as owner nft_token_id = create_nft(client, treasury_id, treasury_private_key) # Mint 2 NFTs in the collection with example metadata and get their unique IDs that we will send and reject - nft_ids = mint_nfts(client, nft_token_id, [b"ExampleMetadata 1", b"ExampleMetadata 2"], treasury_private_key) - + nft_ids = mint_nfts( + client, + nft_token_id, + [b"ExampleMetadata 1", b"ExampleMetadata 2"], + treasury_private_key, + ) + # Associate the NFT token with the receiver account so they can receive the NFTs associate_token(client, receiver_id, nft_token_id, receiver_private_key) - + # Transfer NFTs to the receiver account transfer_nfts(client, treasury_id, treasury_private_key, receiver_id, nft_ids) # Get and print NFT balances before rejection to show the initial state print("\nNFT balances before rejection:") get_nft_balances(client, treasury_id, receiver_id, nft_token_id) - + # Receiver rejects the NFTs that were previously transferred to them receipt = ( TokenRejectTransaction() @@ -211,16 +237,19 @@ def token_reject_nft(): .sign(receiver_private_key) .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: print(f"NFT rejection failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - - print(f"Successfully rejected NFTs {nft_ids[0]} and {nft_ids[1]} from account {receiver_id}") - + + print( + f"Successfully rejected NFTs {nft_ids[0]} and {nft_ids[1]} from account {receiver_id}" + ) + # Get and print NFT balances after rejection to show the final state print("\nNFT balances after rejection:") get_nft_balances(client, treasury_id, receiver_id, nft_token_id) - + + if __name__ == "__main__": token_reject_nft() diff --git a/examples/tokens/token_revoke_kyc_transaction.py b/examples/tokens/token_revoke_kyc_transaction.py index 96eac0729..122d19df7 100644 --- a/examples/tokens/token_revoke_kyc_transaction.py +++ b/examples/tokens/token_revoke_kyc_transaction.py @@ -3,6 +3,7 @@ python examples/tokens/token_revoke_kyc_transaction.py.py """ + import os import sys from dotenv import load_dotenv @@ -18,13 +19,18 @@ from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.tokens.supply_type import SupplyType -from hiero_sdk_python.tokens.token_associate_transaction import TokenAssociateTransaction +from hiero_sdk_python.tokens.token_associate_transaction import ( + TokenAssociateTransaction, +) from hiero_sdk_python.tokens.token_grant_kyc_transaction import TokenGrantKycTransaction -from hiero_sdk_python.tokens.token_revoke_kyc_transaction import TokenRevokeKycTransaction +from hiero_sdk_python.tokens.token_revoke_kyc_transaction import ( + TokenRevokeKycTransaction, +) from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -32,13 +38,14 @@ def setup_client(): print(f"Connecting to Hedera {network_name} network!") client = Client(network) - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key + def create_fungible_token(client, operator_id, operator_key, kyc_private_key): """Create a fungible token""" receipt = ( @@ -53,19 +60,24 @@ def create_fungible_token(client, operator_id, operator_key, kyc_private_key): .set_max_supply(1000) .set_admin_key(operator_key) .set_supply_key(operator_key) - .set_kyc_key(kyc_private_key) # Required key for granting/revoking KYC approval to accounts + .set_kyc_key( + kyc_private_key + ) # Required key for granting/revoking KYC approval to accounts .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: - print(f"Fungible token creation failed with status: {ResponseCode(receipt.status).name}") + print( + f"Fungible token creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + token_id = receipt.token_id print(f"Fungible token created with ID: {token_id}") - + return token_id + def associate_token(client, token_id, account_id, account_private_key): """Associate a token with an account""" associate_transaction = ( @@ -73,23 +85,26 @@ def associate_token(client, token_id, account_id, account_private_key): .set_account_id(account_id) .add_token_id(token_id) .freeze_with(client) - .sign(account_private_key) # Has to be signed by new account's key + .sign(account_private_key) # Has to be signed by new account's key ) - + receipt = associate_transaction.execute(client) - + if receipt.status != ResponseCode.SUCCESS: - print(f"Token association failed with status: {ResponseCode.get_name(receipt.status)}") + print( + f"Token association failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + print("Token successfully associated with account") + def create_test_account(client): """Create a new account for testing""" # Generate private key for new account new_account_private_key = PrivateKey.generate() new_account_public_key = new_account_private_key.public_key() - + # Create new account with initial balance of 1 HBAR transaction = ( AccountCreateTransaction() @@ -97,20 +112,23 @@ def create_test_account(client): .set_initial_balance(Hbar(1)) .freeze_with(client) ) - + receipt = transaction.execute(client) - + # Check if account creation was successful if receipt.status != ResponseCode.SUCCESS: - print(f"Account creation failed with status: {ResponseCode.get_name(receipt.status)}") + print( + f"Account creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + # Get account ID from receipt account_id = receipt.account_id print(f"New account created with ID: {account_id}") - + return account_id, new_account_private_key + def grant_kyc(client, token_id, account_id, kyc_private_key): """Grant KYC to an account""" receipt = ( @@ -121,13 +139,16 @@ def grant_kyc(client, token_id, account_id, kyc_private_key): .sign(kyc_private_key) # Has to be signed by the KYC key .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: - print(f"Token grant KYC failed with status: {ResponseCode.get_name(receipt.status)}") + print( + f"Token grant KYC failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + print(f"Granted KYC for account {account_id} on token {token_id}") + def token_revoke_kyc(): """ Demonstrates the token revoke KYC functionality by: @@ -139,22 +160,22 @@ def token_revoke_kyc(): 6. Revoking KYC from the new account """ client, operator_id, operator_key = setup_client() - + # Create KYC key kyc_private_key = PrivateKey.generate_ed25519() - + # Create a fungible token with KYC key token_id = create_fungible_token(client, operator_id, operator_key, kyc_private_key) - + # Create a new account account_id, account_private_key = create_test_account(client) - + # Associate the token with the new account associate_token(client, token_id, account_id, account_private_key) - + # Grant KYC to the new account first grant_kyc(client, token_id, account_id, kyc_private_key) - + # Revoke KYC from the new account receipt = ( TokenRevokeKycTransaction() @@ -164,13 +185,16 @@ def token_revoke_kyc(): .sign(kyc_private_key) # Has to be signed by the KYC key .execute(client) ) - + # Check if the transaction was successful if receipt.status != ResponseCode.SUCCESS: - print(f"Token revoke KYC failed with status: {ResponseCode.get_name(receipt.status)}") + print( + f"Token revoke KYC failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + print(f"Revoked KYC for account {account_id} on token {token_id}") + if __name__ == "__main__": - token_revoke_kyc() \ No newline at end of file + token_revoke_kyc() diff --git a/examples/tokens/token_unfreeze_transaction.py b/examples/tokens/token_unfreeze_transaction.py index aa4a6c7ac..fa8858c3a 100644 --- a/examples/tokens/token_unfreeze_transaction.py +++ b/examples/tokens/token_unfreeze_transaction.py @@ -5,6 +5,7 @@ python examples/tokens/token_unfreeze_transaction.py """ + import os import sys from dotenv import load_dotenv @@ -23,7 +24,8 @@ # Load environment variables from .env file load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Setup Client""" @@ -32,8 +34,8 @@ def setup_client(): client = Client(network) try: - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID','')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY','')) + 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, operator_id, operator_key @@ -46,10 +48,11 @@ def setup_client(): def generate_freeze_key(): """Generate a Freeze Key on the fly""" print("\nSTEP 1: Generating a new freeze key...") - freeze_key = PrivateKey.generate(os.getenv('KEY_TYPE', 'ed25519')) + freeze_key = PrivateKey.generate(os.getenv("KEY_TYPE", "ed25519")) print("βœ… Freeze key generated.") return freeze_key + def create_freezable_token(): """Create a token with the freeze key""" client, operator_id, operator_key = setup_client() @@ -67,10 +70,7 @@ def create_freezable_token(): # FIX: The .execute() method returns the receipt directly. receipt = ( - tx.freeze_with(client) - .sign(operator_key) - .sign(freeze_key) - .execute(client) + tx.freeze_with(client).sign(operator_key).sign(freeze_key).execute(client) ) token_id = receipt.token_id print(f"βœ… Success! Created token with ID: {token_id}") @@ -79,6 +79,7 @@ def create_freezable_token(): print(f"❌ Error creating token: {e}") sys.exit(1) + def freeze_token(token_id, client, operator_id, freeze_key): """Freeze the token for the operator account""" print(f"\nSTEP 3: Freezing token {token_id} for operator account {operator_id}...") @@ -91,15 +92,20 @@ def freeze_token(token_id, client, operator_id, freeze_key): .sign(freeze_key) .execute(client) ) - print(f"βœ… Success! Token freeze complete, Status: {ResponseCode(receipt.status).name}") + print( + f"βœ… Success! Token freeze complete, Status: {ResponseCode(receipt.status).name}" + ) except (RuntimeError, ValueError) as e: print(f"❌ Error freezing token: {e}") sys.exit(1) + def unfreeze_token(token_id, client, operator_id, freeze_key, operator_key): """Unfreeze the token for the operator account""" # Step 1: Unfreeze the token for the operator account - print(f"\nSTEP 4: Unfreezing token {token_id} for operator account {operator_id}...") + print( + f"\nSTEP 4: Unfreezing token {token_id} for operator account {operator_id}..." + ) try: receipt = ( TokenUnfreezeTransaction() @@ -109,7 +115,9 @@ def unfreeze_token(token_id, client, operator_id, freeze_key, operator_key): .sign(freeze_key) .execute(client) ) - print(f"βœ… Success! Token unfreeze complete, Status: {ResponseCode(receipt.status).name}") + print( + f"βœ… Success! Token unfreeze complete, Status: {ResponseCode(receipt.status).name}" + ) # Step 2: Attempt a test transfer of 1 unit of token to self print(f"Attempting a test transfer of 1 unit of token {token_id} to self...") @@ -122,15 +130,18 @@ def unfreeze_token(token_id, client, operator_id, freeze_key, operator_key): .sign(operator_key) .execute(client) ) - print(f"βœ… Test transfer succeeded. Token is unfrozen and usable, Status: {ResponseCode(transfer_receipt.status).name}") + print( + f"βœ… Test transfer succeeded. Token is unfrozen and usable, Status: {ResponseCode(transfer_receipt.status).name}" + ) except (RuntimeError, ValueError) as transfer_error: print(f"❌ Test transfer failed: {transfer_error}") except (RuntimeError, ValueError) as e: print(f"❌ Error unfreezing token: {e}") sys.exit(1) + def main(): - """ Unfreeze the token for the operator account. + """Unfreeze the token for the operator account. 1. Freeze the token for the operator account (calls freeze_token()). 2. Unfreeze the token for the operator account using TokenUnfreezeTransaction. 3. Attempt a test transfer of 1 unit of the token to self to verify unfreeze. @@ -139,5 +150,6 @@ def main(): freeze_token(token_id, client, operator_id, freeze_key) unfreeze_token(token_id, client, operator_id, freeze_key, operator_key) + if __name__ == "__main__": main() diff --git a/examples/tokens/token_unpause_transaction.py b/examples/tokens/token_unpause_transaction.py index c925ad3a2..857cf0b13 100644 --- a/examples/tokens/token_unpause_transaction.py +++ b/examples/tokens/token_unpause_transaction.py @@ -3,6 +3,7 @@ python examples/tokens/token_unpause_transaction.py """ + import os import sys from dotenv import load_dotenv @@ -18,11 +19,12 @@ TokenCreateTransaction, TokenUnpauseTransaction, TokenPauseTransaction, - TokenInfoQuery + TokenInfoQuery, ) load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -31,9 +33,9 @@ def setup_client(): network = Network(network_name) print(f"Connecting to Hedera {network_name} network!") client = Client(network) - - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + + 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}") @@ -42,7 +44,12 @@ def setup_client(): print("❌ Error: Creating client, Please check your .env file") sys.exit(1) -def create_token(client: Client, operator_id: AccountId, pause_key: PrivateKey,): + +def create_token( + client: Client, + operator_id: AccountId, + pause_key: PrivateKey, +): """Create a fungible token""" print("\nCreating a token...") @@ -54,21 +61,22 @@ def create_token(client: Client, operator_id: AccountId, pause_key: PrivateKey,) .set_initial_supply(1) .set_treasury_account_id(operator_id) .set_token_type(TokenType.FUNGIBLE_COMMON) - .set_pause_key(pause_key) # Required for pausing tokens + .set_pause_key(pause_key) # Required for pausing tokens .freeze_with(client) ) - + receipt = token_tx.sign(pause_key).execute(client) - + token_id = receipt.token_id print(f"βœ… Success! Created token: {token_id}") check_pause_status(client, token_id) - + return token_id except Exception as e: print(f"❌ Error creating token: {e}") sys.exit(1) + def pause_token(client: Client, token_id: TokenId, pause_key: PrivateKey): """Pause token""" print("\nAttempting to pause the token...") @@ -80,7 +88,7 @@ def pause_token(client: Client, token_id: TokenId, pause_key: PrivateKey): .freeze_with(client) .sign(pause_key) ) - + receipt = pause_tx.execute(client) if receipt.status == ResponseCode.SUCCESS: @@ -93,11 +101,12 @@ def pause_token(client: Client, token_id: TokenId, pause_key: PrivateKey): print(f"❌ Error pausing token: {e}") sys.exit(1) + def check_pause_status(client, token_id: TokenId): """Query and print the current paused/unpaused status of a token.""" info = TokenInfoQuery().set_token_id(token_id).execute(client) print(f"Token status is now: {info.pause_status.name}") - + def unpause_token(): pause_key = PrivateKey.generate() @@ -128,5 +137,6 @@ def unpause_token(): print(f"❌ Error pausing token: {e}") sys.exit(1) + if __name__ == "__main__": unpause_token() diff --git a/examples/tokens/token_update_nfts_transaction_nfts.py b/examples/tokens/token_update_nfts_transaction_nfts.py index cef1d1eec..1bc8caa2b 100644 --- a/examples/tokens/token_update_nfts_transaction_nfts.py +++ b/examples/tokens/token_update_nfts_transaction_nfts.py @@ -3,6 +3,7 @@ python examples/tokens/token_update_transaction_nfts.py """ + import os import sys from dotenv import load_dotenv @@ -19,11 +20,14 @@ from hiero_sdk_python.tokens.supply_type import SupplyType from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction -from hiero_sdk_python.tokens.token_update_nfts_transaction import TokenUpdateNftsTransaction +from hiero_sdk_python.tokens.token_update_nfts_transaction import ( + TokenUpdateNftsTransaction, +) from hiero_sdk_python.query.token_nft_info_query import TokenNftInfoQuery load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -31,13 +35,14 @@ def setup_client(): print(f"Connecting to Hedera {network_name} network!") client = Client(network) - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key + def create_nft(client, operator_id, operator_key, metadata_key): """Create a non-fungible token""" receipt = ( @@ -56,18 +61,19 @@ def create_nft(client, operator_id, operator_key, metadata_key): .set_metadata_key(metadata_key) # Needed to update NFTs .execute(client) ) - + # Check if nft creation was successful if receipt.status != ResponseCode.SUCCESS: print(f"NFT creation failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + # Get token ID from receipt nft_token_id = receipt.token_id print(f"NFT created with ID: {nft_token_id}") - + return nft_token_id + def mint_nfts(client, nft_token_id, metadata_list): """Mint a non-fungible token""" receipt = ( @@ -76,31 +82,33 @@ def mint_nfts(client, nft_token_id, metadata_list): .set_metadata(metadata_list) .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: print(f"NFT minting failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + print(f"NFT minted with serial numbers: {receipt.serial_numbers}") - - return [NftId(nft_token_id, serial_number) for serial_number in receipt.serial_numbers], receipt.serial_numbers + + return [ + NftId(nft_token_id, serial_number) for serial_number in receipt.serial_numbers + ], receipt.serial_numbers + def get_nft_info(client, nft_id): """Get information about an NFT""" - info = ( - TokenNftInfoQuery() - .set_nft_id(nft_id) - .execute(client) - ) - + info = TokenNftInfoQuery().set_nft_id(nft_id).execute(client) + return info -def update_nft_metadata(client, nft_token_id, serial_numbers, new_metadata, metadata_private_key): + +def update_nft_metadata( + client, nft_token_id, serial_numbers, new_metadata, metadata_private_key +): """Update metadata for NFTs in a collection""" receipt = ( TokenUpdateNftsTransaction() .set_token_id(nft_token_id) - .set_serial_numbers(serial_numbers) + .set_serial_numbers(serial_numbers) .set_metadata(new_metadata) .freeze_with(client) .sign(metadata_private_key) # Has to be signed here by metadata_key @@ -108,10 +116,15 @@ def update_nft_metadata(client, nft_token_id, serial_numbers, new_metadata, meta ) if receipt.status != ResponseCode.SUCCESS: - print(f"NFT metadata update failed with status: {ResponseCode(receipt.status).name}") + print( + f"NFT metadata update failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - print(f"Successfully updated metadata for NFTs with serial numbers: {serial_numbers}") + print( + f"Successfully updated metadata for NFTs with serial numbers: {serial_numbers}" + ) + def token_update_nfts(): """ @@ -124,38 +137,45 @@ def token_update_nfts(): 6. Verifying the updated NFT metadata """ client, operator_id, operator_key = setup_client() - + # Create metadata key metadata_private_key = PrivateKey.generate_ed25519() - + # Create a new NFT collection with the treasury account as owner nft_token_id = create_nft(client, operator_id, operator_key, metadata_private_key) - + # Initial metadata for our NFTs initial_metadata = [b"Initial metadata 1", b"Initial metadata 2"] - + # New metadata to update the first NFT new_metadata = b"Updated metadata1" - + # Mint 2 NFTs in the collection with initial metadata nft_ids, serial_numbers = mint_nfts(client, nft_token_id, initial_metadata) - + # Get and print information about the NFTs print("\nCheck that the NFTs have the initial metadata") for nft_id in nft_ids: nft_info = get_nft_info(client, nft_id) print(f"NFT ID: {nft_info.nft_id}, Metadata: {nft_info.metadata}") - + # Update metadata for specific NFTs by providing their id and serial numbers # Only the NFTs with the provided serial numbers will have their metadata updated serial_numbers_to_update = [serial_numbers[0]] - update_nft_metadata(client, nft_token_id, serial_numbers_to_update, new_metadata, metadata_private_key) - + update_nft_metadata( + client, + nft_token_id, + serial_numbers_to_update, + new_metadata, + metadata_private_key, + ) + # Get and print information about the NFTs print("\nCheck that only the first NFT has the updated metadata") for nft_id in nft_ids: nft_info = get_nft_info(client, nft_id) print(f"NFT ID: {nft_info.nft_id}, Metadata: {nft_info.metadata}") - + + if __name__ == "__main__": token_update_nfts() diff --git a/examples/tokens/token_update_transaction_fungible.py b/examples/tokens/token_update_transaction_fungible.py index 5d13575ac..0471bf804 100644 --- a/examples/tokens/token_update_transaction_fungible.py +++ b/examples/tokens/token_update_transaction_fungible.py @@ -3,6 +3,7 @@ python examples/tokens/token_update_transaction_fungible.py """ + import os import sys from dotenv import load_dotenv @@ -21,7 +22,8 @@ from hiero_sdk_python.tokens.token_update_transaction import TokenUpdateTransaction load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -29,22 +31,23 @@ def setup_client(): print(f"Connecting to Hedera {network_name} network!") client = Client(network) - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key + def create_fungible_token(client, operator_id, operator_key, metadata_key): """ Create a fungible token - + If we want to update metadata later using TokenUpdateTransaction: 1. Set a metadata_key and sign the update transaction with it, or 2. Sign the update transaction with the admin_key - - Note: If no Admin Key was assigned during token creation (immutable token), + + Note: If no Admin Key was assigned during token creation (immutable token), token updates will fail with TOKEN_IS_IMMUTABLE. """ receipt = ( @@ -63,29 +66,36 @@ def create_fungible_token(client, operator_id, operator_key, metadata_key): .set_metadata_key(metadata_key) .execute(client) ) - + # Check if token creation was successful if receipt.status != ResponseCode.SUCCESS: - print(f"Fungible token creation failed with status: {ResponseCode.get_name(receipt.status)}") + print( + f"Fungible token creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + # Get token ID from receipt token_id = receipt.token_id print(f"Fungible token created with ID: {token_id}") - + return token_id + def get_token_info(client, token_id): """Get information about a fungible token""" - info = ( - TokenInfoQuery() - .set_token_id(token_id) - .execute(client) - ) - + info = TokenInfoQuery().set_token_id(token_id).execute(client) + return info -def update_token_data(client, token_id, update_metadata, update_token_name, update_token_symbol, update_token_memo): + +def update_token_data( + client, + token_id, + update_metadata, + update_token_name, + update_token_symbol, + update_token_memo, +): """Update metadata for a fungible token""" receipt = ( TokenUpdateTransaction() @@ -96,13 +106,16 @@ def update_token_data(client, token_id, update_metadata, update_token_name, upda .set_token_memo(update_token_memo) .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: - print(f"Token metadata update failed with status: {ResponseCode.get_name(receipt.status)}") + print( + f"Token metadata update failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + print(f"Successfully updated token data") + def token_update_fungible(): """ Demonstrates the fungible token update functionality by: @@ -113,27 +126,37 @@ def token_update_fungible(): 5. Verifying the updated token info """ client, operator_id, operator_key = setup_client() - + # Create metadata key metadata_private_key = PrivateKey.generate_ed25519() - - token_id = create_fungible_token(client, operator_id, operator_key, metadata_private_key) - + + token_id = create_fungible_token( + client, operator_id, operator_key, metadata_private_key + ) + print("\nToken info before update:") token_info = get_token_info(client, token_id) print(token_info) - + # New data to update the fungible token update_metadata = b"Updated metadata" update_token_name = "Updated Token" update_token_symbol = "UPD" update_token_memo = "Updated memo" - - update_token_data(client, token_id, update_metadata, update_token_name, update_token_symbol, update_token_memo) - + + update_token_data( + client, + token_id, + update_metadata, + update_token_name, + update_token_symbol, + update_token_memo, + ) + print("\nToken info after update:") token_info = get_token_info(client, token_id) print(token_info) - + + if __name__ == "__main__": token_update_fungible() diff --git a/examples/tokens/token_update_transaction_key.py b/examples/tokens/token_update_transaction_key.py index f32852445..8d37bc30a 100644 --- a/examples/tokens/token_update_transaction_key.py +++ b/examples/tokens/token_update_transaction_key.py @@ -1,9 +1,9 @@ - """ uv run examples/tokens/token_update_transaction_key.py python examples/tokens/token_update_transaction_key.py """ + import os import sys from dotenv import load_dotenv @@ -23,7 +23,8 @@ from hiero_sdk_python.tokens.token_update_transaction import TokenUpdateTransaction load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -31,13 +32,14 @@ def setup_client(): print(f"Connecting to Hedera {network_name} network!") client = Client(network) - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id + def create_fungible_token(client, operator_id, admin_key, wipe_key): """Create a fungible token""" receipt = ( @@ -56,32 +58,32 @@ def create_fungible_token(client, operator_id, admin_key, wipe_key): .sign(admin_key) .execute(client) ) - + # Check if token creation was successful if receipt.status != ResponseCode.SUCCESS: - print(f"Fungible token creation failed with status: {ResponseCode.get_name(receipt.status)}") + print( + f"Fungible token creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + # Get token ID from receipt token_id = receipt.token_id print(f"Fungible token created with ID: {token_id}") - + return token_id + def get_token_info(client, token_id): """Get information about a fungible token""" - info = ( - TokenInfoQuery() - .set_token_id(token_id) - .execute(client) - ) - + info = TokenInfoQuery().set_token_id(token_id).execute(client) + return info + def update_wipe_key_full_validation(client, token_id, old_wipe_key): """ Update token wipe key with full validation mode. - + This demonstrates using FULL_VALIDATION mode (default) which requires both old and new key signatures. This ensures that there cannot be an accidental update to a public key for which the user does not possess the private key. @@ -90,7 +92,7 @@ def update_wipe_key_full_validation(client, token_id, old_wipe_key): """ # Generate new wipe key new_wipe_key = PrivateKey.generate_ed25519() - + receipt = ( TokenUpdateTransaction() .set_token_id(token_id) @@ -101,17 +103,20 @@ def update_wipe_key_full_validation(client, token_id, old_wipe_key): .sign(old_wipe_key) .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: - print(f"Token update failed with status: {ResponseCode.get_name(receipt.status)}") + print( + f"Token update failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + print(f"Successfully updated wipe key") # Query token info to verify wipe key update info = get_token_info(client, token_id) print(f"Token's wipe key after update: {info.wipe_key}") + def token_update_key(): """ Demonstrates updating keys on a fungible token by: @@ -121,19 +126,20 @@ def token_update_key(): 4. Updating the wipe key with full validation """ client, operator_id = setup_client() - + admin_key = PrivateKey.generate_ed25519() wipe_key = PrivateKey.generate_ed25519() - + token_id = create_fungible_token(client, operator_id, admin_key, wipe_key) - + print("\nToken info before update:") token_info = get_token_info(client, token_id) - + print(f"Token's wipe key after creation: {token_info.wipe_key}") print(f"Token's admin key after creation: {token_info.admin_key}") - + update_wipe_key_full_validation(client, token_id, wipe_key) - + + if __name__ == "__main__": token_update_key() diff --git a/examples/tokens/token_update_transaction_nft.py b/examples/tokens/token_update_transaction_nft.py index 19e4464a5..1f18388eb 100644 --- a/examples/tokens/token_update_transaction_nft.py +++ b/examples/tokens/token_update_transaction_nft.py @@ -3,6 +3,7 @@ python examples/tokens/token_update_transaction_nft.py """ + import os import sys from dotenv import load_dotenv @@ -21,7 +22,8 @@ from hiero_sdk_python.tokens.token_update_transaction import TokenUpdateTransaction load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -29,22 +31,23 @@ def setup_client(): print(f"Connecting to Hedera {network_name} network!") client = Client(network) - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key + def create_nft(client, operator_id, operator_key, metadata_key): """ Create a non-fungible token - + If we want to update metadata later using TokenUpdateTransaction: 1. Set a metadata_key and sign the update transaction with it, or 2. Sign the update transaction with the admin_key - - Note: If no Admin Key was assigned during token creation (immutable token), + + Note: If no Admin Key was assigned during token creation (immutable token), token updates will fail with TOKEN_IS_IMMUTABLE. """ receipt = ( @@ -63,29 +66,36 @@ def create_nft(client, operator_id, operator_key, metadata_key): .set_metadata_key(metadata_key) .execute(client) ) - + # Check if nft creation was successful if receipt.status != ResponseCode.SUCCESS: - print(f"NFT creation failed with status: {ResponseCode.get_name(receipt.status)}") + print( + f"NFT creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + # Get token ID from receipt nft_token_id = receipt.token_id print(f"NFT created with ID: {nft_token_id}") - + return nft_token_id + def get_nft_info(client, nft_token_id): """Get information about an NFT""" - info = ( - TokenInfoQuery() - .set_token_id(nft_token_id) - .execute(client) - ) - + info = TokenInfoQuery().set_token_id(nft_token_id).execute(client) + return info -def update_nft_data(client, nft_token_id, update_metadata, update_token_name, update_token_symbol, update_token_memo): + +def update_nft_data( + client, + nft_token_id, + update_metadata, + update_token_name, + update_token_symbol, + update_token_memo, +): """Update data for an NFT""" receipt = ( TokenUpdateTransaction() @@ -96,13 +106,16 @@ def update_nft_data(client, nft_token_id, update_metadata, update_token_name, up .set_token_memo(update_token_memo) .execute(client) ) - + if receipt.status != ResponseCode.SUCCESS: - print(f"NFT data update failed with status: {ResponseCode.get_name(receipt.status)}") + print( + f"NFT data update failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + print(f"Successfully updated NFT data") + def token_update_nft(): """ Demonstrates the NFT token update functionality by: @@ -113,27 +126,35 @@ def token_update_nft(): 5. Verifying the updated NFT info """ client, operator_id, operator_key = setup_client() - + # Create metadata key metadata_private_key = PrivateKey.generate_ed25519() - + nft_token_id = create_nft(client, operator_id, operator_key, metadata_private_key) - + print("\nNFT info before update:") nft_info = get_nft_info(client, nft_token_id) print(nft_info) - + # New data to update the NFT update_metadata = b"Updated metadata" update_token_name = "Updated NFT" update_token_symbol = "UPD" update_token_memo = "Updated memo" - - update_nft_data(client, nft_token_id, update_metadata, update_token_name, update_token_symbol, update_token_memo) - + + update_nft_data( + client, + nft_token_id, + update_metadata, + update_token_name, + update_token_symbol, + update_token_memo, + ) + print("\nNFT info after update:") nft_info = get_nft_info(client, nft_token_id) print(nft_info) - + + if __name__ == "__main__": token_update_nft() diff --git a/examples/tokens/token_wipe_transaction.py b/examples/tokens/token_wipe_transaction.py index 12ae5614f..f0a2c3816 100644 --- a/examples/tokens/token_wipe_transaction.py +++ b/examples/tokens/token_wipe_transaction.py @@ -2,6 +2,7 @@ uv run examples/tokens/token_wipe_transaction.py python examples/tokens/token_wipe_transaction.py """ + import os import sys from dotenv import load_dotenv @@ -23,7 +24,8 @@ from hiero_sdk_python.tokens.token_wipe_transaction import TokenWipeTransaction load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -33,19 +35,20 @@ def setup_client(): client = Client(network) # Set up operator account - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key + def create_test_account(client): """Create a new account for testing""" # Generate private key for new account new_account_private_key = PrivateKey.generate() new_account_public_key = new_account_private_key.public_key() - + # Create new account with initial balance of 1 HBAR transaction = ( AccountCreateTransaction() @@ -53,20 +56,23 @@ def create_test_account(client): .set_initial_balance(Hbar(1)) .freeze_with(client) ) - + receipt = transaction.execute(client) - + # Check if account creation was successful if receipt.status != ResponseCode.SUCCESS: - print(f"Account creation failed with status: {ResponseCode(receipt.status).name}") + print( + f"Account creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + # Get account ID from receipt account_id = receipt.account_id print(f"New account created with ID: {account_id}") - + return account_id, new_account_private_key + def create_token(client, operator_id, operator_key): """Create a fungible token""" # Create fungible token @@ -81,26 +87,27 @@ def create_token(client, operator_id, operator_key): .set_token_type(TokenType.FUNGIBLE_COMMON) .set_supply_type(SupplyType.FINITE) .set_max_supply(100) - .set_admin_key(operator_key) # For token management - .set_supply_key(operator_key) # For minting/burning - .set_freeze_key(operator_key) # For freezing accounts - .set_wipe_key(operator_key) # Required for wiping tokens + .set_admin_key(operator_key) # For token management + .set_supply_key(operator_key) # For minting/burning + .set_freeze_key(operator_key) # For freezing accounts + .set_wipe_key(operator_key) # Required for wiping tokens .freeze_with(client) ) - + receipt = transaction.execute(client) - + # Check if token creation was successful if receipt.status != ResponseCode.SUCCESS: print(f"Token creation failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + # Get token ID from receipt token_id = receipt.token_id print(f"Token created with ID: {token_id}") - + return token_id + def associate_token(client, account_id, token_id, account_private_key): """Associate a token with an account""" # Associate the token with the new account @@ -110,17 +117,20 @@ def associate_token(client, account_id, token_id, account_private_key): .set_account_id(account_id) .add_token_id(token_id) .freeze_with(client) - .sign(account_private_key) # Has to be signed by new account's key + .sign(account_private_key) # Has to be signed by new account's key ) - + receipt = associate_transaction.execute(client) - + if receipt.status != ResponseCode.SUCCESS: - print(f"Token association failed with status: {ResponseCode(receipt.status).name}") + print( + f"Token association failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + print("Token successfully associated with account") + def transfer_tokens(client, token_id, operator_id, account_id, amount): """Transfer tokens from operator to the specified account""" # Transfer tokens to the new account @@ -128,19 +138,20 @@ def transfer_tokens(client, token_id, operator_id, account_id, amount): transfer_transaction = ( TransferTransaction() .add_token_transfer(token_id, operator_id, -amount) # From operator - .add_token_transfer(token_id, account_id, amount) # To new account + .add_token_transfer(token_id, account_id, amount) # To new account .freeze_with(client) ) - + receipt = transfer_transaction.execute(client) - + # Check if token transfer was successful if receipt.status != ResponseCode.SUCCESS: print(f"Token transfer failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + print(f"Successfully transferred {amount} tokens to account {account_id}") + def wipe_tokens(client, token_id, account_id, amount): """Wipe tokens from the specified account""" # Wipe the tokens from the account @@ -152,15 +163,16 @@ def wipe_tokens(client, token_id, account_id, amount): .set_amount(amount) .freeze_with(client) ) - + receipt = transaction.execute(client) - + if receipt.status != ResponseCode.SUCCESS: print(f"Token wipe failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + print(f"Successfully wiped {amount} tokens from account {account_id}") + def token_wipe(): """ Demonstrates the token wipe functionality by: @@ -174,10 +186,11 @@ def token_wipe(): account_id, new_account_private_key = create_test_account(client) token_id = create_token(client, operator_id, operator_key) associate_token(client, account_id, token_id, new_account_private_key) - + amount = 10 transfer_tokens(client, token_id, operator_id, account_id, amount) wipe_tokens(client, token_id, account_id, amount) + if __name__ == "__main__": token_wipe() diff --git a/examples/transaction/batch_transaction.py b/examples/transaction/batch_transaction.py index 77e20d060..f2aa80ed7 100644 --- a/examples/transaction/batch_transaction.py +++ b/examples/transaction/batch_transaction.py @@ -1,6 +1,7 @@ """ uv run examples/transaction/batch_transaction.py """ + import os import sys @@ -19,11 +20,12 @@ TokenType, TokenUnfreezeTransaction, BatchTransaction, - TransferTransaction + TransferTransaction, ) load_dotenv() + def get_balance(client, account_id, token_id): tokens_balance = ( CryptoGetAccountBalanceQuery(account_id=account_id) @@ -33,21 +35,22 @@ def get_balance(client, account_id, token_id): print(f"Account: {account_id}: {tokens_balance[token_id] if tokens_balance else 0}") + def setup_client(): """ Set up and configure a Hedera client for testnet operations. """ - network_name = os.getenv('NETWORK', 'testnet').lower() + network_name = os.getenv("NETWORK", "testnet").lower() print(f"Connecting to Hedera {network_name} network!") - try : + try: network = Network(network_name) client = Client(network) - - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID','')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY','')) - + + 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 initialized with operator: {operator_id}") return client @@ -55,6 +58,7 @@ def setup_client(): print(f"Failed to set up client: {e}") sys.exit(1) + def create_account(client): """ Create a new recipient account. @@ -65,19 +69,22 @@ def create_account(client): tx = ( AccountCreateTransaction() .set_key_without_alias(key.public_key()) - .set_max_automatic_token_associations(2) # to transfer token without associating it + .set_max_automatic_token_associations( + 2 + ) # to transfer token without associating it .set_initial_balance(1) ) - + receipt = tx.freeze_with(client).execute(client) recipient_id = receipt.account_id - + print(f"New account created: {receipt.account_id}") return recipient_id except Exception as e: print(f"Error creating new account: {e}") sys.exit(1) + def create_fungible_token(client, freeze_key): """ Create a fungible token with freeze_key. @@ -106,6 +113,7 @@ def create_fungible_token(client, freeze_key): print(f"Error creating token: {e}") sys.exit(1) + def freeze_token(client, account_id, token_id, freeze_key): """ Freeze token for an account. @@ -121,16 +129,17 @@ def freeze_token(client, account_id, token_id, freeze_key): ) receipt = tx.execute(client) - + if receipt.status != ResponseCode.SUCCESS: print(f"Freeze failed: {ResponseCode(receipt.status).name})") sys.exit(1) - + print("Token freeze successful!") except Exception as e: print(f"Error freezing token for account: {e}") sys.exit(1) + def transfer_token(client, sender, recipient, token_id): """ Perform a token trasfer transaction. @@ -150,11 +159,12 @@ def transfer_token(client, sender, recipient, token_id): print(f"Error transfering token: {e}") sys.exit(1) + def perform_batch_tx(client, sender, recipient, token_id, freeze_key): """ - Perform a batch transaction. + Perform a batch transaction using PrivateKey as batch_key. """ - print("\nPerforming batch transaction (unfreeze β†’ transfer β†’ freeze)...") + print("\nPerforming batch transaction with PrivateKey (unfreeze β†’ transfer β†’ freeze)...") batch_key = PrivateKey.generate() unfreeze_tx = ( @@ -193,6 +203,59 @@ def perform_batch_tx(client, sender, recipient, token_id, freeze_key): receipt = batch.execute(client) print(f"Batch transaction status: {ResponseCode(receipt.status).name}") + +def perform_batch_tx_with_public_key(client, sender, recipient, token_id, freeze_key): + """ + Perform a batch transaction using PublicKey as batch_key. + Demonstrates that batch_key can accept both PrivateKey and PublicKey. + """ + print("\n✨ Performing batch transaction with PublicKey (unfreeze β†’ transfer β†’ freeze)...") + + # Generate a key pair - we'll use the PublicKey as batch_key + batch_private_key = PrivateKey.generate() + batch_public_key = batch_private_key.public_key() + + print(f"Using PublicKey as batch_key: {batch_public_key}") + + # Create inner transactions using PublicKey as batch_key + unfreeze_tx = ( + TokenUnfreezeTransaction() + .set_account_id(sender) + .set_token_id(token_id) + .batchify(client, batch_public_key) # Using PublicKey! + .sign(freeze_key) + ) + + transfer_tx = ( + TransferTransaction() + .add_token_transfer(token_id, sender, -1) + .add_token_transfer(token_id, recipient, 1) + .batchify(client, batch_public_key) # Using PublicKey! + ) + + freeze_tx = ( + TokenFreezeTransaction() + .set_account_id(sender) + .set_token_id(token_id) + .batchify(client, batch_public_key) # Using PublicKey! + .sign(freeze_key) + ) + + # Assemble the batch transaction + batch = ( + BatchTransaction() + .add_inner_transaction(unfreeze_tx) + .add_inner_transaction(transfer_tx) + .add_inner_transaction(freeze_tx) + .freeze_with(client) + .sign(batch_private_key) # Sign with PrivateKey for execution + ) + + receipt = batch.execute(client) + print(f"Batch transaction with PublicKey status: {ResponseCode(receipt.status).name}") + print(" This demonstrates that batch_key now accepts both PrivateKey and PublicKey!") + + def main(): client = setup_client() freeze_key = PrivateKey.generate() @@ -210,27 +273,46 @@ def main(): else: print("\nExpected freeze to block transfer!") sys.exit(1) - + # Show balances print("\nBalances before batch:") get_balance(client, client.operator_account_id, token_id) get_balance(client, recipient_id, token_id) - # Batch unfreeze β†’ transfer β†’ freeze + # Batch unfreeze β†’ transfer β†’ freeze (using PrivateKey) perform_batch_tx(client, client.operator_account_id, recipient_id, token_id, freeze_key) - print("\nBalances after batch:") + print("\nBalances after first batch:") + get_balance(client, client.operator_account_id, token_id) + get_balance(client, recipient_id, token_id) + + # Verify that token is frozen again + receipt = transfer_token(client, client.operator_account_id, recipient_id, token_id) + if receipt.status == ResponseCode.ACCOUNT_FROZEN_FOR_TOKEN: + print("\nβœ… Correct: Account is frozen again after first batch") + else: + print("\nAccount should be frozen again!") + sys.exit(1) + + # Now demonstrate using PublicKey as batch_key + print("\n" + "="*80) + print("Demonstrating PublicKey support for batch_key") + print("="*80) + + perform_batch_tx_with_public_key(client, client.operator_account_id, recipient_id, token_id, freeze_key) + + print("\nBalances after second batch (with PublicKey):") get_balance(client, client.operator_account_id, token_id) - get_balance(client, recipient_id,token_id) + get_balance(client, recipient_id, token_id) - # Should fail again Verify that token is again freeze for account + # Verify that token is frozen again receipt = transfer_token(client, client.operator_account_id, recipient_id, token_id) if receipt.status == ResponseCode.ACCOUNT_FROZEN_FOR_TOKEN: - print("\nCorrect: Account is frozen again") + print("\nβœ… Success! Account is frozen again, PublicKey batch_key works correctly!") else: print("\nAccount should be frozen again!") sys.exit(1) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/examples/transaction/custom_fee_limit.py b/examples/transaction/custom_fee_limit.py index a7250cb0b..631ea27dc 100644 --- a/examples/transaction/custom_fee_limit.py +++ b/examples/transaction/custom_fee_limit.py @@ -78,7 +78,7 @@ def create_revenue_generating_topic(client: Client, operator_id: AccountId): print("This topic charges a fixed fee of 1 HBAR per message.") return topic_id - except Exception as e: # noqa: BLE001 + except Exception as e: # noqa: BLE001 print(f"Failed to create topic: {e}") return None @@ -124,7 +124,7 @@ def submit_message_with_custom_fee_limit( print("Message submitted successfully!") print(f"Transaction status: {submit_receipt.status}") - except Exception as e: # noqa: BLE001 + except Exception as e: # noqa: BLE001 print(f"Transaction failed: {e}") diff --git a/examples/transaction/transaction_to_bytes.py b/examples/transaction/transaction_to_bytes.py index 47e25218f..b727c1d93 100644 --- a/examples/transaction/transaction_to_bytes.py +++ b/examples/transaction/transaction_to_bytes.py @@ -2,14 +2,14 @@ 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 +- Sign a deserialized transaction Run with: - uv run examples/transaction/transaction_to_bytes.py - python examples/transaction/transaction_to_bytes.py + uv run examples/transaction/transaction_to_bytes.py + python examples/transaction/transaction_to_bytes.py """ import os @@ -26,77 +26,104 @@ ) 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 network '{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 as e: + print(f"❌ Error initializing client: {e}") + 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) + # print a concise confirmation for the user + print(f"βœ… Transaction frozen with ID: {tx.transaction_id}") + return tx + + +def serialize_transaction(transaction: Transaction) -> bytes: + """Serialize transaction to bytes.""" + tx_bytes = transaction.to_bytes() + print(f"βœ… Transaction serialized: {len(tx_bytes)} bytes") + print(f" Preview (first 40 bytes hex): {tx_bytes[:40].hex()}") + return tx_bytes + + +def deserialize_transaction(bytes_data: bytes) -> Transaction: + """Restore a transaction from its byte representation.""" + restored = Transaction.from_bytes(bytes_data) + print("βœ… Transaction restored from bytes") + print(f" Restored ID: {restored.transaction_id}") + print(f" Memo: {restored.memo}") + return restored + + +def main(): + # Initialize client (exits with message if fails) + client = setup_client() + + # obtain operator information from the client + operator_id = client.operator_account_id + operator_key = client.operator_private_key + + # receiver example (adjust as needed) + receiver_id = AccountId.from_string("0.0.3") + + try: + print("\nSTEP 1 β€” Creating and freezing transaction...") + tx = create_and_freeze_transaction(client, operator_id, receiver_id) + + print("\nSTEP 2 β€” Serializing transaction...") + tx_bytes = serialize_transaction(tx) + + print("\nSTEP 3 β€” Deserializing transaction...") + restored_tx = deserialize_transaction(tx_bytes) + + print("\nSTEP 4 β€” Signing restored transaction...") + restored_tx.sign(operator_key) + print("βœ… Signed restored transaction successfully.") + + print("\nSTEP 5 β€” Verifying round-trip (signed bytes comparison)...") + # Sign the original transaction as well to compare the signed bytes + 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.") + + except Exception as e: + print(f"❌ Error in example flow: {e}") if __name__ == "__main__": - transaction_bytes_example() + main() diff --git a/examples/transaction/transfer_transaction_fungible.py b/examples/transaction/transfer_transaction_fungible.py index aad72a1b7..1ed099a1d 100644 --- a/examples/transaction/transfer_transaction_fungible.py +++ b/examples/transaction/transfer_transaction_fungible.py @@ -3,6 +3,7 @@ python examples/transaction/transfer_transaction_fungible.py """ + import os import sys from dotenv import load_dotenv @@ -17,11 +18,12 @@ Hbar, TokenCreateTransaction, CryptoGetAccountBalanceQuery, - TokenAssociateTransaction + TokenAssociateTransaction, ) load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + # -------------------------- # CLIENT SETUP @@ -33,8 +35,8 @@ def setup_client(): client = Client(network) try: - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID','')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY','')) + 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}") @@ -43,6 +45,7 @@ def setup_client(): print("❌ Error: Creating client, Please check your .env file") sys.exit(1) + # -------------------------- # ACCOUNT CREATION # -------------------------- @@ -60,11 +63,12 @@ def create_account(client, operator_key): recipient_id = receipt.account_id print(f"βœ… Success! Created a new recipient account with ID: {recipient_id}") return recipient_id, recipient_key - + except Exception as e: print(f"Error creating new account: {e}") sys.exit(1) - + + # -------------------------- # TOKEN CREATION # -------------------------- @@ -89,6 +93,7 @@ def create_token(client, operator_id, operator_key): print(f"❌ Error creating token: {e}") sys.exit(1) + # -------------------------- # TOKEN ASSOCIATION # -------------------------- @@ -107,8 +112,9 @@ def associate_token(client, recipient_id, recipient_key, token_id): print(f"❌ Error associating token: {e}") sys.exit(1) + # -------------------------- -# ACCOUNT BALANCE QUERY +# ACCOUNT BALANCE QUERY # -------------------------- def account_balance_query(client, account_id): """Query and return token balances for an account.""" @@ -119,8 +125,9 @@ def account_balance_query(client, account_id): print(f"❌ Error fetching account balance: {e}") sys.exit(1) + # -------------------------- -# TRANSFER TRANSACTION +# TRANSFER TRANSACTION # -------------------------- def transfer_transaction(client, operator_id, operator_key, recipient_id, token_id): """Execute a token transfer transaction.""" @@ -141,6 +148,7 @@ def transfer_transaction(client, operator_id, operator_key, recipient_id, token_ print(f"❌ Error transferring token: {e}") sys.exit(1) + # -------------------------- # MAIN ORCHESTRATOR (RENAMED) # -------------------------- @@ -173,7 +181,6 @@ def main(): balance_after = account_balance_query(client, recipient_id) print("Token balance AFTER transfer:") print(f"{token_id}: {balance_after.get(token_id)}") - if __name__ == "__main__": diff --git a/examples/transaction/transfer_transaction_hbar.py b/examples/transaction/transfer_transaction_hbar.py index 32e2ff6df..cd65b6e8f 100644 --- a/examples/transaction/transfer_transaction_hbar.py +++ b/examples/transaction/transfer_transaction_hbar.py @@ -3,6 +3,7 @@ python examples/transaction/transfer_transaction_hbar.py """ + import os import sys from dotenv import load_dotenv @@ -15,11 +16,13 @@ TransferTransaction, AccountCreateTransaction, Hbar, - CryptoGetAccountBalanceQuery + CryptoGetAccountBalanceQuery, ) load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + + def setup_client(): """Initialize and set up the client with operator account""" network = Network(network_name) @@ -27,8 +30,8 @@ def setup_client(): client = Client(network) try: - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID','')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY','')) + 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}") @@ -52,11 +55,12 @@ def create_account(client, operator_key): recipient_id = receipt.account_id print(f"βœ… Success! Created a new recipient account with ID: {recipient_id}") return recipient_id, recipient_key - + except Exception as e: print(f"Error creating new account: {e}") sys.exit(1) + def transfer_hbar(client, operator_id, recipient_id): """Transfer HBAR from operator account to recipient account""" print("\nSTEP 2: Transfering HBAR...") @@ -69,7 +73,7 @@ def transfer_hbar(client, operator_id, recipient_id): .freeze_with(client) ) transfer_tx.execute(client) - + print("\nβœ… Success! HBAR transfer successful.\n") except Exception as e: print(f"❌ HBAR transfer failed: {str(e)}") @@ -80,9 +84,7 @@ def account_balance_query(client, account_id, when=""): """Query and display account balance""" try: balance = ( - CryptoGetAccountBalanceQuery(account_id=account_id) - .execute(client) - .hbars + CryptoGetAccountBalanceQuery(account_id=account_id).execute(client).hbars ) print(f"Recipient account balance{when}: {balance} hbars") return balance @@ -110,5 +112,6 @@ def main(): # Check balance after HBAR transfer account_balance_query(client, recipient_id, " after transfer") + if __name__ == "__main__": main() diff --git a/examples/transaction/transfer_transaction_nft.py b/examples/transaction/transfer_transaction_nft.py index ffa5ee2b1..77adf8a5f 100644 --- a/examples/transaction/transfer_transaction_nft.py +++ b/examples/transaction/transfer_transaction_nft.py @@ -3,6 +3,7 @@ python examples/transaction/transfer_transaction_nft.py """ + import os import sys from dotenv import load_dotenv @@ -20,12 +21,15 @@ from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.tokens.nft_id import NftId from hiero_sdk_python.tokens.supply_type import SupplyType -from hiero_sdk_python.tokens.token_associate_transaction import TokenAssociateTransaction +from hiero_sdk_python.tokens.token_associate_transaction import ( + TokenAssociateTransaction, +) from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction load_dotenv() -network_name = os.getenv('NETWORK', 'testnet').lower() +network_name = os.getenv("NETWORK", "testnet").lower() + def setup_client(): """Initialize and set up the client with operator account""" @@ -35,19 +39,20 @@ def setup_client(): client = Client(network) # Set up operator account - operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', '')) - operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', '')) + 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, operator_id, operator_key + def create_test_account(client): """Create a new account for testing""" # Generate private key for new account new_account_private_key = PrivateKey.generate() new_account_public_key = new_account_private_key.public_key() - + # Create new account with initial balance of 1 HBAR transaction = ( AccountCreateTransaction() @@ -55,20 +60,23 @@ def create_test_account(client): .set_initial_balance(Hbar(1)) .freeze_with(client) ) - + receipt = transaction.execute(client) - + # Check if account creation was successful if receipt.status != ResponseCode.SUCCESS: - print(f"Account creation failed with status: {ResponseCode(receipt.status).name}") + print( + f"Account creation failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + # Get account ID from receipt account_id = receipt.account_id print(f"New account created with ID: {account_id}") - + return account_id, new_account_private_key + def create_nft(client, operator_id, operator_key): """Create a non-fungible token""" transaction = ( @@ -86,20 +94,21 @@ def create_nft(client, operator_id, operator_key): .set_freeze_key(operator_key) .freeze_with(client) ) - + receipt = transaction.execute(client) - + # Check if nft creation was successful if receipt.status != ResponseCode.SUCCESS: print(f"NFT creation failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + # Get token ID from receipt nft_token_id = receipt.token_id print(f"NFT created with ID: {nft_token_id}") - + return nft_token_id + def mint_nft(client, nft_token_id, operator_key): """Mint a non-fungible token""" transaction = ( @@ -110,15 +119,16 @@ def mint_nft(client, nft_token_id, operator_key): ) receipt = transaction.execute(client) - + if receipt.status != ResponseCode.SUCCESS: print(f"NFT minting failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + print(f"NFT minted with serial number: {receipt.serial_numbers[0]}") - + return NftId(nft_token_id, receipt.serial_numbers[0]) + def associate_nft(client, account_id, token_id, account_private_key): """Associate a non-fungible token with an account""" # Associate the token_id with the new account @@ -127,17 +137,20 @@ def associate_nft(client, account_id, token_id, account_private_key): .set_account_id(account_id) .add_token_id(token_id) .freeze_with(client) - .sign(account_private_key) # Has to be signed by new account's key + .sign(account_private_key) # Has to be signed by new account's key ) - + receipt = associate_transaction.execute(client) - + if receipt.status != ResponseCode.SUCCESS: - print(f"NFT association failed with status: {ResponseCode(receipt.status).name}") + print( + f"NFT association failed with status: {ResponseCode(receipt.status).name}" + ) sys.exit(1) - + print("NFT successfully associated with account") + def transfer_nft_token(client, nft_id, sender_id, receiver_id): """Transfer the NFT from the sender to the receiver account""" # Transfer nft to the new account @@ -146,16 +159,17 @@ def transfer_nft_token(client, nft_id, sender_id, receiver_id): .add_nft_transfer(nft_id, sender_id, receiver_id) .freeze_with(client) ) - + receipt = transfer_transaction.execute(client) - + # Check if nft transfer was successful if receipt.status != ResponseCode.SUCCESS: print(f"NFT transfer failed with status: {ResponseCode(receipt.status).name}") sys.exit(1) - + print(f"Successfully transferred NFT to account {receiver_id}") + def main(): """ Demonstrates the nft transfer functionality by: @@ -170,10 +184,10 @@ def main(): token_id = create_nft(client, operator_id, operator_key) nft_id = mint_nft(client, token_id, operator_key) associate_nft(client, account_id, token_id, new_account_private_key) - + # Transfer the NFT to the new account transfer_nft_token(client, nft_id, operator_id, account_id) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/pyproject.toml b/pyproject.toml index ac6929c14..237b9b1f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ classifiers = [ [dependency-groups] dev = [ "pytest>=8.3.4", + "pytest-cov>=7.0.0" ] lint = [ "ruff>=0.8.3", diff --git a/src/hiero_sdk_python/account/account_balance.py b/src/hiero_sdk_python/account/account_balance.py index b73746352..12af2e170 100644 --- a/src/hiero_sdk_python/account/account_balance.py +++ b/src/hiero_sdk_python/account/account_balance.py @@ -52,3 +52,31 @@ def _from_proto(cls, proto: CryptoGetAccountBalanceResponse) -> "AccountBalance" token_balances[token_id] = balance return cls(hbars=hbars, token_balances=token_balances) + + def __str__(self) -> str: + """ + Returns a human-friendly string representation of the account balance. + + Returns: + str: A string showing HBAR balance and token balances. + """ + lines = [f"HBAR Balance: {self.hbars} hbars"] + if self.token_balances: + lines.append("Token Balances:") + for token_id, balance in self.token_balances.items(): + lines.append(f" - Token ID {token_id}: {balance} units") + return "\n".join(lines) + + def __repr__(self) -> str: + """ + Returns a developer-friendly string representation of the account balance. + + Returns: + str: A string representation that shows the key attributes. + """ + token_balances_repr = ( + f"{{{', '.join(f'{token_id!r}: {balance}' for token_id, balance in self.token_balances.items())}}}" + if self.token_balances + else "{}" + ) + return f"AccountBalance(hbars={self.hbars!r}, token_balances={token_balances_repr})" diff --git a/src/hiero_sdk_python/account/account_info.py b/src/hiero_sdk_python/account/account_info.py index 36cf92fe5..e6844a9f8 100644 --- a/src/hiero_sdk_python/account/account_info.py +++ b/src/hiero_sdk_python/account/account_info.py @@ -153,3 +153,56 @@ def _to_proto(self) -> CryptoGetInfoResponse.AccountInfo: decline_reward=self.decline_staking_reward ), ) + + def __str__(self) -> str: + """Returns a user-friendly string representation of the AccountInfo.""" + # Define simple fields to print if they exist + # Format: (value_to_check, label) + simple_fields = [ + (self.account_id, "Account ID"), + (self.contract_account_id, "Contract Account ID"), + (self.balance, "Balance"), + (self.key, "Key"), + (self.account_memo, "Memo"), + (self.owned_nfts, "Owned NFTs"), + (self.max_automatic_token_associations, "Max Automatic Token Associations"), + (self.staked_account_id, "Staked Account ID"), + (self.staked_node_id, "Staked Node ID"), + (self.proxy_received, "Proxy Received"), + (self.expiration_time, "Expiration Time"), + (self.auto_renew_period, "Auto Renew Period"), + ] + + # Use a list comprehension to process simple fields (reduces complexity score) + lines = [f"{label}: {val}" for val, label in simple_fields if val is not None] + + # 2. Handle booleans and special cases explicitly + if self.is_deleted is not None: + lines.append(f"Deleted: {self.is_deleted}") + + if self.receiver_signature_required is not None: + lines.append(f"Receiver Signature Required: {self.receiver_signature_required}") + + if self.decline_staking_reward is not None: + lines.append(f"Decline Staking Reward: {self.decline_staking_reward}") + + if self.token_relationships: + lines.append(f"Token Relationships: {len(self.token_relationships)}") + + return "\n".join(lines) + + def __repr__(self) -> str: + """Returns a string representation of the AccountInfo object for debugging.""" + return ( + f"AccountInfo(" + f"account_id={self.account_id!r}, " + f"contract_account_id={self.contract_account_id!r}, " + f"is_deleted={self.is_deleted!r}, " + f"balance={self.balance!r}, " + f"receiver_signature_required={self.receiver_signature_required!r}, " + f"owned_nfts={self.owned_nfts!r}, " + f"account_memo={self.account_memo!r}, " + f"staked_node_id={self.staked_node_id!r}, " + f"staked_account_id={self.staked_account_id!r}" + f")" + ) \ No newline at end of file diff --git a/src/hiero_sdk_python/node.py b/src/hiero_sdk_python/node.py index 3ada104b8..68e0f93bd 100644 --- a/src/hiero_sdk_python/node.py +++ b/src/hiero_sdk_python/node.py @@ -2,7 +2,7 @@ import socket import ssl # Python's ssl module implements TLS (despite the name) import grpc -from typing import Optional, Callable +from typing import Optional from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.channels import _Channel from hiero_sdk_python.address_book.node_address import NodeAddress @@ -91,7 +91,7 @@ def __init__(self, account_id: AccountId, address: str, address_book: NodeAddres self._address: _ManagedNodeAddress = _ManagedNodeAddress._from_string(address) self._verify_certificates: bool = True self._root_certificates: Optional[bytes] = None - self._authority_override: Optional[str] = self._determine_authority_override() + self._node_pem_cert: Optional[bytes] = None def _close(self): """ @@ -115,13 +115,23 @@ def _get_channel(self): return self._channel if self._address._is_transport_security(): + if self._root_certificates: + # Use the certificate that is provided + self._node_pem_cert = self._root_certificates + else: + # Fetch pem_cert for the node + self._node_pem_cert = self._fetch_server_certificate_pem() + + if not self._node_pem_cert: + raise ValueError("No certificate available.") + # Validate certificate if verification is enabled if self._verify_certificates: - self._validate_tls_certificate_with_trust_manager() + self._validate_tls_certificate_with_trust_manager() options = self._build_channel_options() credentials = grpc.ssl_channel_credentials( - root_certificates=self._root_certificates, + root_certificates=self._node_pem_cert, private_key=None, certificate_chain=None, ) @@ -141,7 +151,9 @@ def _apply_transport_security(self, enabled: bool): return if not enabled and not self._address._is_transport_security(): return + self._close() + if enabled: self._address = self._address._to_secure() else: @@ -154,39 +166,48 @@ def _set_root_certificates(self, root_certificates: Optional[bytes]): self._root_certificates = root_certificates if self._channel and self._address._is_transport_security(): self._close() + def _set_verify_certificates(self, verify: bool): """ Set whether TLS certificates should be verified. """ if self._verify_certificates == verify: return + self._verify_certificates = verify + if verify and self._channel and self._address._is_transport_security(): # Force channel recreation to ensure certificates are revalidated. self._close() - def _determine_authority_override(self) -> Optional[str]: - """ - Determine the hostname to use for TLS authority override. - """ - if not self._address_book or not self._address_book._addresses: # pylint: disable=protected-access - return None - for endpoint in self._address_book._addresses: # pylint: disable=protected-access - domain = endpoint.get_domain_name() - if domain: - return domain - return None - def _build_channel_options(self): """ Build gRPC channel options for TLS connections. + + The options `grpc.default_authority` and `grpc.ssl_target_name_override` + are intentionally set to a fixed value ("127.0.0.1") to bypass standard + TLS hostname verification. + + This is REQUIRED because Hedera nodes are connected to via IP addresses + from the address book, while their TLS certificates are not issued for + those IPs. As a result, standard hostname verification would fail even + for legitimate nodes. + + Although hostname verification is disabled, transport security is NOT + weakened. Instead of relying on hostnames, the SDK validates the server + by performing certificate hash pinning. This guarantees the client is + communicating with the correct Hedera node regardless of the hostname + or IP address used to connect. """ - if not self._authority_override: - return None - host = self._address._get_host() - if host == self._authority_override: - return None - return [('grpc.ssl_target_name_override', self._authority_override)] + options = [ + ("grpc.default_authority", "127.0.0.1"), + ("grpc.ssl_target_name_override", "127.0.0.1"), + ("grpc.keepalive_time_ms", 100000), + ("grpc.keepalive_timeout_ms", 10000), + ("grpc.keepalive_permit_without_calls", 1) + ] + + return options def _validate_tls_certificate_with_trust_manager(self): """ @@ -197,9 +218,7 @@ def _validate_tls_certificate_with_trust_manager(self): Note: If verification is enabled but no cert hash is available (e.g., in unit tests without address books), validation is skipped rather than raising an error. """ - if not self._address._is_transport_security(): - return - if not self._verify_certificates: + if not self._address._is_transport_security() or not self._verify_certificates: return cert_hash = None @@ -214,10 +233,7 @@ def _validate_tls_certificate_with_trust_manager(self): # Create trust manager and validate certificate trust_manager = _HederaTrustManager(cert_hash, self._verify_certificates) - - # Fetch server certificate and validate - pem_cert = self._fetch_server_certificate_pem() - trust_manager.check_server_trusted(pem_cert) + trust_manager.check_server_trusted(self._node_pem_cert) @staticmethod def _normalize_cert_hash(cert_hash: bytes) -> str: @@ -228,6 +244,7 @@ def _normalize_cert_hash(cert_hash: bytes) -> str: decoded = cert_hash.decode('utf-8').strip().lower() if decoded.startswith("0x"): decoded = decoded[2:] + return decoded except UnicodeDecodeError: return cert_hash.hex() @@ -239,12 +256,22 @@ def _fetch_server_certificate_pem(self) -> bytes: Returns: bytes: PEM-encoded certificate bytes """ + if not self._address_book: + return None + host = self._address._get_host() port = self._address._get_port() - server_hostname = self._authority_override or host + server_hostname = host # Create TLS context that accepts any certificate (we validate hash ourselves) context = ssl.create_default_context() + # Restrict SSL/TLS versions to TLSv1.2+ only for security + if hasattr(context, 'minimum_version') and hasattr(ssl, 'TLSVersion'): + context.minimum_version = ssl.TLSVersion.TLSv1_2 + else: + # Backwards compatibility for Python <3.7 that lacks minimum_version + context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 + context.check_hostname = False context.verify_mode = ssl.CERT_NONE @@ -254,4 +281,4 @@ def _fetch_server_certificate_pem(self) -> bytes: # Convert DER to PEM format (matching Java's PEM encoding) pem_cert = ssl.DER_cert_to_PEM_cert(der_cert).encode('utf-8') - return pem_cert \ No newline at end of file + return pem_cert diff --git a/src/hiero_sdk_python/query/transaction_get_receipt_query.py b/src/hiero_sdk_python/query/transaction_get_receipt_query.py index aea4ced5b..6ec0f2b2c 100644 --- a/src/hiero_sdk_python/query/transaction_get_receipt_query.py +++ b/src/hiero_sdk_python/query/transaction_get_receipt_query.py @@ -24,7 +24,11 @@ class TransactionGetReceiptQuery(Query): """ - def __init__(self, transaction_id: Optional[TransactionId] = None) -> None: + def __init__( + self, + transaction_id: Optional[TransactionId] = None, + include_children: bool = False, + ) -> None: """ Initializes a new instance of the TransactionGetReceiptQuery class. @@ -34,6 +38,7 @@ def __init__(self, transaction_id: Optional[TransactionId] = None) -> None: super().__init__() self.transaction_id: Optional[TransactionId] = transaction_id self._frozen: bool = False + self.include_children = include_children def _require_not_frozen(self) -> None: """ @@ -62,6 +67,25 @@ def set_transaction_id(self, transaction_id: TransactionId) -> "TransactionGetRe self.transaction_id = transaction_id return self + def set_include_children( + self, include_children: bool + ) -> "TransactionGetReceiptQuery": + """ + Sets include_children for which to retrieve the child transaction receipts. + + Args: + include_children: bool. + + Returns: + TransactionGetReceiptQuery: The current instance for method chaining. + + Raises: + ValueError: If the query is frozen and cannot be modified. + """ + self._require_not_frozen() + self.include_children = include_children + return self + def freeze(self) -> "TransactionGetReceiptQuery": """ Marks the query as frozen, preventing further modification. @@ -100,6 +124,8 @@ def _make_request(self) -> query_pb2.Query: transaction_get_receipt.header.CopyFrom(query_header) transaction_get_receipt.transactionID.CopyFrom(self.transaction_id._to_proto()) + transaction_get_receipt.include_child_receipts = self.include_children + query = query_pb2.Query() if not hasattr(query, "transactionGetReceipt"): raise AttributeError("Query object has no attribute 'transactionGetReceipt'") @@ -219,8 +245,18 @@ def execute(self, client: Client) -> TransactionReceipt: """ self._before_execute(client) response = self._execute(client) + parent = TransactionReceipt._from_proto(response.transactionGetReceipt.receipt, self.transaction_id) + + if self.include_children: + children = [] + + for child_proto in response.transactionGetReceipt.child_transaction_receipts: + child_receipt = TransactionReceipt._from_proto(child_proto, self.transaction_id) + children.append(child_receipt) + + parent._set_children(children) - return TransactionReceipt._from_proto(response.transactionGetReceipt.receipt, self.transaction_id) + return parent def _get_query_response( self, response: response_pb2.Response diff --git a/src/hiero_sdk_python/tokens/abstract_token_transfer_transaction.py b/src/hiero_sdk_python/tokens/abstract_token_transfer_transaction.py index cd3910217..2d7f3271d 100644 --- a/src/hiero_sdk_python/tokens/abstract_token_transfer_transaction.py +++ b/src/hiero_sdk_python/tokens/abstract_token_transfer_transaction.py @@ -149,7 +149,7 @@ def _add_token_transfer( if not isinstance(account_id, AccountId): raise TypeError("account_id must be an AccountId instance.") if not isinstance(amount, int) or amount == 0: - raise ValueError("Amount must be a non-zero integer.") + raise ValueError("Amount must be a non-zero integer") if expected_decimals is not None and not isinstance(expected_decimals, int): raise TypeError("expected_decimals must be an integer.") if not isinstance(is_approved, bool): diff --git a/src/hiero_sdk_python/transaction/transaction.py b/src/hiero_sdk_python/transaction/transaction.py index c0102901b..5304c8c61 100644 --- a/src/hiero_sdk_python/transaction/transaction.py +++ b/src/hiero_sdk_python/transaction/transaction.py @@ -16,6 +16,7 @@ from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.transaction.transaction_id import TransactionId from hiero_sdk_python.transaction.transaction_response import TransactionResponse +from hiero_sdk_python.utils.key_utils import Key, key_to_proto if TYPE_CHECKING: from hiero_sdk_python.schedule.schedule_create_transaction import ( @@ -65,7 +66,7 @@ def __init__(self) -> None: # changed from int: 2_000_000 to Hbar: 0.02 self._default_transaction_fee = Hbar(0.02) self.operator_account_id = None - self.batch_key: Optional[PrivateKey] = None + self.batch_key: Optional[Key] = None def _make_request(self): """ @@ -434,7 +435,7 @@ def build_base_transaction_body(self) -> transaction_pb2.TransactionBody: transaction_body.max_custom_fees.extend(custom_fee_limits) if self.batch_key: - transaction_body.batch_key.CopyFrom(self.batch_key.public_key()._to_proto()) + transaction_body.batch_key.CopyFrom(key_to_proto(self.batch_key)) return transaction_body @@ -807,12 +808,12 @@ def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): return transaction - def set_batch_key(self, key: PrivateKey): + def set_batch_key(self, key: Key): """ Set the batch key required for batch transaction. Args: - batch_key (PrivateKey): Private key to use as batch key. + batch_key (Key): Key to use as batch key (accepts both PrivateKey and PublicKey). Returns: Transaction: A reconstructed transaction instance of the appropriate subclass. @@ -821,13 +822,13 @@ def set_batch_key(self, key: PrivateKey): self.batch_key = key return self - def batchify(self, client: Client, batch_key: PrivateKey): + def batchify(self, client: Client, batch_key: Key): """ Marks the current transaction as an inner (batched) transaction. Args: client (Client): The client instance to use for setting defaults. - batch_key (PrivateKey): Private key to use as batch key. + batch_key (Key): Key to use as batch key (accepts both PrivateKey and PublicKey). Returns: Transaction: A reconstructed transaction instance of the appropriate subclass. diff --git a/src/hiero_sdk_python/transaction/transaction_receipt.py b/src/hiero_sdk_python/transaction/transaction_receipt.py index 29e986d86..7744e8e40 100644 --- a/src/hiero_sdk_python/transaction/transaction_receipt.py +++ b/src/hiero_sdk_python/transaction/transaction_receipt.py @@ -16,8 +16,6 @@ from hiero_sdk_python.contract.contract_id import ContractId from hiero_sdk_python.schedule.schedule_id import ScheduleId from hiero_sdk_python.tokens.token_id import TokenId -from hiero_sdk_python.transaction.transaction_id import TransactionId - from hiero_sdk_python.transaction.transaction_id import TransactionId from hiero_sdk_python.hapi.services import transaction_receipt_pb2, response_code_pb2 from hiero_sdk_python.account.account_id import AccountId @@ -39,7 +37,8 @@ class TransactionReceipt: def __init__( self, receipt_proto: transaction_receipt_pb2.TransactionReceipt, - transaction_id: Optional[TransactionId] = None + transaction_id: Optional[TransactionId] = None, + children: Optional[list["TransactionReceipt"]] = None, ) -> None: """ Initializes the TransactionReceipt with the provided protobuf receipt. @@ -51,6 +50,7 @@ def __init__( self._transaction_id: Optional[TransactionId] = transaction_id self.status: Optional[response_code_pb2.ResponseCodeEnum] = receipt_proto.status self._receipt_proto: transaction_receipt_pb2.TransactionReceipt = receipt_proto + self._children: list["TransactionReceipt"] = children or [] @property def token_id(self) -> Optional[TokenId]: @@ -183,7 +183,7 @@ def node_id(self): int: The node ID if present; otherwise, 0. """ return self._receipt_proto.node_id - + @property def topic_sequence_number(self) -> int: """ @@ -207,6 +207,25 @@ def topic_running_hash(self) -> Optional[bytes]: return None + @property + def children(self) -> list["TransactionReceipt"]: + """ + Returns the child transaction receipts associated with this receipt. + + Returns: + list[TransactionReceipt]: Child receipts (empty if not requested or none exist). + """ + return self._children + + def _set_children(self, children: list["TransactionReceipt"]) -> None: + """ + Internal setter for child receipts (used by receipt queries). + + Args: + children (list[TransactionReceipt]): Child receipts. + """ + self._children = children + def _to_proto(self): """ Returns the underlying protobuf transaction receipt. diff --git a/src/hiero_sdk_python/transaction/transfer_transaction.py b/src/hiero_sdk_python/transaction/transfer_transaction.py index f630ed869..a84d06bd0 100644 --- a/src/hiero_sdk_python/transaction/transfer_transaction.py +++ b/src/hiero_sdk_python/transaction/transfer_transaction.py @@ -2,11 +2,12 @@ Defines TransferTransaction for transferring HBAR or tokens between accounts. """ -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Optional, Tuple, Union from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.channels import _Channel from hiero_sdk_python.executable import _Method +from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.hapi.services import basic_types_pb2, crypto_transfer_pb2, transaction_pb2 from hiero_sdk_python.hapi.services.schedulable_transaction_body_pb2 import ( SchedulableTransactionBody, @@ -29,7 +30,8 @@ def __init__( self, hbar_transfers: Optional[Dict[AccountId, int]] = None, token_transfers: Optional[Dict[TokenId, Dict[AccountId, int]]] = None, - nft_transfers: Optional[Dict[TokenId, List[Tuple[AccountId, AccountId, int, bool]]]] = None, + nft_transfers: Optional[Dict[TokenId, + List[Tuple[AccountId, AccountId, int, bool]]]] = None, ) -> None: """ Initializes a new TransferTransaction instance. @@ -59,24 +61,35 @@ def _init_hbar_transfers(self, hbar_transfers: Dict[AccountId, int]) -> None: self.add_hbar_transfer(account_id, amount) def _add_hbar_transfer( - self, account_id: AccountId, amount: int, is_approved: bool = False + self, account_id: AccountId, amount: Union[int, Hbar], is_approved: bool = False ) -> "TransferTransaction": """ Internal method to add a HBAR transfer to the transaction. Args: account_id (AccountId): The account ID of the sender or receiver. - amount (int): The amount of the HBAR to transfer. + amount (Union[int, Hbar]): The amount of the HBAR to transfer (in tinybars if int, or Hbar object). is_approved (bool, optional): Whether the transfer is approved. Defaults to False. Returns: TransferTransaction: The current instance of the transaction for chaining. """ self._require_not_frozen() + if not isinstance(account_id, AccountId): raise TypeError("account_id must be an AccountId instance.") - if not isinstance(amount, int) or amount == 0: - raise ValueError("Amount must be a non-zero integer.") + + if amount is None: + raise TypeError("amount cannot be None.") + + if isinstance(amount, Hbar): + amount = amount.to_tinybars() + elif not isinstance(amount, int): + raise TypeError("amount must be an integer or Hbar object.") + + if amount == 0: + raise ValueError("Amount must be a non-zero integer") + if not isinstance(is_approved, bool): raise TypeError("is_approved must be a boolean.") @@ -85,16 +98,17 @@ def _add_hbar_transfer( transfer.amount += amount return self - self.hbar_transfers.append(HbarTransfer(account_id, amount, is_approved)) + self.hbar_transfers.append( + HbarTransfer(account_id, amount, is_approved)) return self - def add_hbar_transfer(self, account_id: AccountId, amount: int) -> "TransferTransaction": + def add_hbar_transfer(self, account_id: AccountId, amount: Union[int, Hbar]) -> "TransferTransaction": """ Adds a HBAR transfer to the transaction. Args: account_id (AccountId): The account ID of the sender or receiver. - amount (int): The amount of the HBAR to transfer. + amount (Union[int, Hbar]): The amount of the HBAR to transfer (in tinybars if int, or Hbar object). Returns: TransferTransaction: The current instance of the transaction for chaining. @@ -103,14 +117,14 @@ def add_hbar_transfer(self, account_id: AccountId, amount: int) -> "TransferTran return self def add_approved_hbar_transfer( - self, account_id: AccountId, amount: int + self, account_id: AccountId, amount: Union[int, Hbar] ) -> "TransferTransaction": """ Adds a HBAR transfer with approval to the transaction. Args: account_id (AccountId): The account ID of the sender or receiver. - amount (int): The amount of the HBAR to transfer. + amount (Union[int, Hbar]): The amount of the HBAR to transfer (in tinybars if int, or Hbar object). Returns: TransferTransaction: The current instance of the transaction for chaining. @@ -190,7 +204,8 @@ def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): if crypto_transfer.HasField("transfers"): for account_amount in crypto_transfer.transfers.accountAmounts: - account_id = AccountId._from_proto(account_amount.accountID) + account_id = AccountId._from_proto( + account_amount.accountID) amount = account_amount.amount is_approved = account_amount.is_approval transaction.hbar_transfers.append( @@ -210,17 +225,21 @@ def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): expected_decimals = token_transfer_list.expected_decimals.value transaction.token_transfers[token_id].append( - TokenTransfer(token_id, account_id, amount, expected_decimals, is_approved) + TokenTransfer(token_id, account_id, amount, + expected_decimals, is_approved) ) for nft_transfer in token_transfer_list.nftTransfers: - sender_id = AccountId._from_proto(nft_transfer.senderAccountID) - receiver_id = AccountId._from_proto(nft_transfer.receiverAccountID) + sender_id = AccountId._from_proto( + nft_transfer.senderAccountID) + receiver_id = AccountId._from_proto( + nft_transfer.receiverAccountID) serial_number = nft_transfer.serialNumber is_approved = nft_transfer.is_approval transaction.nft_transfers[token_id].append( - TokenNftTransfer(token_id, sender_id, receiver_id, serial_number, is_approved) + TokenNftTransfer( + token_id, sender_id, receiver_id, serial_number, is_approved) ) return transaction diff --git a/tests/integration/account_allowance_e2e_test.py b/tests/integration/account_allowance_e2e_test.py index 9f7d763f3..4df17cd60 100644 --- a/tests/integration/account_allowance_e2e_test.py +++ b/tests/integration/account_allowance_e2e_test.py @@ -17,7 +17,7 @@ from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction from hiero_sdk_python.transaction.transaction_id import TransactionId from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction -from tests.integration.utils_for_test import create_fungible_token, create_nft_token, env +from tests.integration.utils import create_fungible_token, create_nft_token, env def _create_spender_and_receiver_accounts(env): diff --git a/tests/integration/account_balance_query_e2e_test.py b/tests/integration/account_balance_query_e2e_test.py index 325b7326c..fa39838a5 100644 --- a/tests/integration/account_balance_query_e2e_test.py +++ b/tests/integration/account_balance_query_e2e_test.py @@ -1,7 +1,7 @@ import pytest from hiero_sdk_python.query.account_balance_query import CryptoGetAccountBalanceQuery -from tests.integration.utils_for_test import IntegrationTestEnv +from tests.integration.utils import IntegrationTestEnv @pytest.mark.integration diff --git a/tests/integration/account_create_transaction_e2e_test.py b/tests/integration/account_create_transaction_e2e_test.py index 865337c66..3e4239c88 100644 --- a/tests/integration/account_create_transaction_e2e_test.py +++ b/tests/integration/account_create_transaction_e2e_test.py @@ -5,7 +5,7 @@ from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.query.account_info_query import AccountInfoQuery from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import env +from tests.integration.utils import env @pytest.mark.integration diff --git a/tests/integration/account_delete_transaction_e2e_test.py b/tests/integration/account_delete_transaction_e2e_test.py index 452f7b533..2edeea94b 100644 --- a/tests/integration/account_delete_transaction_e2e_test.py +++ b/tests/integration/account_delete_transaction_e2e_test.py @@ -14,7 +14,7 @@ from hiero_sdk_python.tokens.nft_id import NftId from hiero_sdk_python.tokens.token_airdrop_transaction import TokenAirdropTransaction from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction -from tests.integration.utils_for_test import ( +from tests.integration.utils import ( create_fungible_token, create_nft_token, env, diff --git a/tests/integration/account_info_query_e2e_test.py b/tests/integration/account_info_query_e2e_test.py index 234012d32..e16c6b989 100644 --- a/tests/integration/account_info_query_e2e_test.py +++ b/tests/integration/account_info_query_e2e_test.py @@ -15,7 +15,7 @@ from hiero_sdk_python.tokens.token_kyc_status import TokenKycStatus from hiero_sdk_python.tokens.token_unfreeze_transaction import TokenUnfreezeTransaction from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token, create_nft_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token, create_nft_token @pytest.mark.integration def test_integration_account_info_query_can_execute(): diff --git a/tests/integration/account_records_query_e2e_test.py b/tests/integration/account_records_query_e2e_test.py index e5eab7d95..bf1a765a5 100644 --- a/tests/integration/account_records_query_e2e_test.py +++ b/tests/integration/account_records_query_e2e_test.py @@ -10,7 +10,7 @@ from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction -from tests.integration.utils_for_test import env +from tests.integration.utils import env @pytest.mark.integration diff --git a/tests/integration/account_update_transaction_e2e_test.py b/tests/integration/account_update_transaction_e2e_test.py index 8d33e0389..b31a1aaa0 100644 --- a/tests/integration/account_update_transaction_e2e_test.py +++ b/tests/integration/account_update_transaction_e2e_test.py @@ -14,7 +14,7 @@ from hiero_sdk_python.query.account_info_query import AccountInfoQuery from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.timestamp import Timestamp -from tests.integration.utils_for_test import env +from tests.integration.utils import env @pytest.mark.integration diff --git a/tests/integration/batch_transaction_e2e_test.py b/tests/integration/batch_transaction_e2e_test.py index e290e6401..e401736d6 100644 --- a/tests/integration/batch_transaction_e2e_test.py +++ b/tests/integration/batch_transaction_e2e_test.py @@ -2,6 +2,7 @@ import pytest from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.crypto.public_key import PublicKey from hiero_sdk_python.file.file_id import FileId from hiero_sdk_python.query.account_info_query import AccountInfoQuery from hiero_sdk_python.query.transaction_get_receipt_query import TransactionGetReceiptQuery @@ -11,7 +12,7 @@ from hiero_sdk_python.timestamp import Timestamp from hiero_sdk_python.transaction.batch_transaction import BatchTransaction from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction -from tests.integration.utils_for_test import env +from tests.integration.utils import env def create_account_tx(key, client): """Helper transaction to create an account.""" @@ -352,3 +353,154 @@ def test_batch_transaction_with_inner_schedule_transaction(env): batch_receipt = batch_tx.execute(env.client) assert batch_receipt.status == ResponseCode.BATCH_TRANSACTION_IN_BLACKLIST + + +def test_batch_transaction_with_public_key_as_batch_key(env): + """Test batch transaction can use PublicKey as batch_key.""" + # Generate a key pair - we'll use the PublicKey as batch_key + batch_private_key = PrivateKey.generate() + batch_public_key = batch_private_key.public_key() + + receiver_id = create_account_tx(PrivateKey.generate().public_key(), env.client) + + # Use PublicKey in batchify + transfer_tx = ( + TransferTransaction() + .add_hbar_transfer(account_id=env.operator_id, amount=-1) + .add_hbar_transfer(account_id=receiver_id, amount=1) + .batchify(env.client, batch_public_key) # Using PublicKey! + ) + + # Verify batch_key was set to PublicKey + assert isinstance(transfer_tx.batch_key, PublicKey) + assert transfer_tx.batch_key == batch_public_key + + # Sign and execute with PrivateKey + batch_tx = ( + BatchTransaction() + .add_inner_transaction(transfer_tx) + .freeze_with(env.client) + .sign(batch_private_key) # Sign with corresponding PrivateKey + ) + + batch_receipt = batch_tx.execute(env.client) + assert batch_receipt.status == ResponseCode.SUCCESS + + # Inner Transaction Receipt + transfer_tx_id = batch_tx.get_inner_transaction_ids()[0] + transfer_tx_receipt = ( + TransactionGetReceiptQuery() + .set_transaction_id(transfer_tx_id) + .execute(env.client) + ) + assert transfer_tx_receipt.status == ResponseCode.SUCCESS + + +def test_batch_transaction_with_mixed_public_and_private_keys(env): + """Test batch transaction can handle inner transactions with mixed PrivateKey and PublicKey.""" + # Generate different keys + batch_key1_private = PrivateKey.generate() + batch_key2_private = PrivateKey.generate() + batch_key2_public = batch_key2_private.public_key() + batch_key3_private = PrivateKey.generate() + batch_key3_public = batch_key3_private.public_key() + + # Create receivers + receiver_id1 = create_account_tx(PrivateKey.generate().public_key(), env.client) + receiver_id2 = create_account_tx(PrivateKey.generate().public_key(), env.client) + receiver_id3 = create_account_tx(PrivateKey.generate().public_key(), env.client) + + # First inner transaction uses PrivateKey + transfer_tx1 = ( + TransferTransaction() + .add_hbar_transfer(account_id=env.operator_id, amount=-1) + .add_hbar_transfer(account_id=receiver_id1, amount=1) + .batchify(env.client, batch_key1_private) # PrivateKey + ) + + # Second inner transaction uses PublicKey + transfer_tx2 = ( + TransferTransaction() + .add_hbar_transfer(account_id=env.operator_id, amount=-1) + .add_hbar_transfer(account_id=receiver_id2, amount=1) + .batchify(env.client, batch_key2_public) # PublicKey + ) + + # Third inner transaction uses PublicKey + transfer_tx3 = ( + TransferTransaction() + .add_hbar_transfer(account_id=env.operator_id, amount=-1) + .add_hbar_transfer(account_id=receiver_id3, amount=1) + .batchify(env.client, batch_key3_public) # PublicKey + ) + + # Verify key types + assert isinstance(transfer_tx1.batch_key, PrivateKey) + assert isinstance(transfer_tx2.batch_key, PublicKey) + assert isinstance(transfer_tx3.batch_key, PublicKey) + + # Assemble and sign batch transaction + batch_tx = ( + BatchTransaction() + .add_inner_transaction(transfer_tx1) + .add_inner_transaction(transfer_tx2) + .add_inner_transaction(transfer_tx3) + .freeze_with(env.client) + .sign(batch_key1_private) + .sign(batch_key2_private) # Sign with PrivateKey for PublicKey batch_key + .sign(batch_key3_private) # Sign with PrivateKey for PublicKey batch_key + ) + + batch_receipt = batch_tx.execute(env.client) + assert batch_receipt.status == ResponseCode.SUCCESS + + # Verify all inner transactions succeeded + for transfer_tx_id in batch_tx.get_inner_transaction_ids(): + transfer_tx_receipt = ( + TransactionGetReceiptQuery() + .set_transaction_id(transfer_tx_id) + .execute(env.client) + ) + assert transfer_tx_receipt.status == ResponseCode.SUCCESS + + +def test_batch_transaction_set_batch_key_with_public_key(env): + """Test batch transaction inner transaction can use set_batch_key with PublicKey.""" + # Generate a key pair + batch_private_key = PrivateKey.generate() + batch_public_key = batch_private_key.public_key() + + receiver_id = create_account_tx(PrivateKey.generate().public_key(), env.client) + + # Use set_batch_key with PublicKey instead of batchify + transfer_tx = ( + TransferTransaction() + .add_hbar_transfer(account_id=env.operator_id, amount=-1) + .add_hbar_transfer(account_id=receiver_id, amount=1) + .set_batch_key(batch_public_key) # Using set_batch_key with PublicKey + .freeze_with(env.client) + .sign(env.operator_key) # Sign inner transaction with operator key + ) + + # Verify batch_key was set correctly + assert transfer_tx.batch_key == batch_public_key + assert isinstance(transfer_tx.batch_key, PublicKey) + + batch_tx = ( + BatchTransaction() + .add_inner_transaction(transfer_tx) + .freeze_with(env.client) + .sign(batch_private_key) # Sign batch transaction with batch key + ) + + batch_receipt = batch_tx.execute(env.client) + assert batch_receipt.status == ResponseCode.SUCCESS + + # Inner Transaction Receipt + transfer_tx_id = batch_tx.get_inner_transaction_ids()[0] + transfer_tx_receipt = ( + TransactionGetReceiptQuery() + .set_transaction_id(transfer_tx_id) + .execute(env.client) + ) + assert transfer_tx_receipt.status == ResponseCode.SUCCESS diff --git a/tests/integration/contract_bytecode_query_e2e_test.py b/tests/integration/contract_bytecode_query_e2e_test.py index 8450788b8..ee643651d 100644 --- a/tests/integration/contract_bytecode_query_e2e_test.py +++ b/tests/integration/contract_bytecode_query_e2e_test.py @@ -17,7 +17,7 @@ from hiero_sdk_python.exceptions import PrecheckError from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import env +from tests.integration.utils import env @pytest.mark.integration diff --git a/tests/integration/contract_call_query_e2e_test.py b/tests/integration/contract_call_query_e2e_test.py index ef1ccec20..cf5d63863 100644 --- a/tests/integration/contract_call_query_e2e_test.py +++ b/tests/integration/contract_call_query_e2e_test.py @@ -21,7 +21,7 @@ from hiero_sdk_python.file.file_create_transaction import FileCreateTransaction from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import env +from tests.integration.utils import env @pytest.mark.integration diff --git a/tests/integration/contract_create_transaction_e2e_test.py b/tests/integration/contract_create_transaction_e2e_test.py index 1ff74bf92..060ef97b8 100644 --- a/tests/integration/contract_create_transaction_e2e_test.py +++ b/tests/integration/contract_create_transaction_e2e_test.py @@ -17,7 +17,7 @@ ) from hiero_sdk_python.file.file_create_transaction import FileCreateTransaction from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import env +from tests.integration.utils import env @pytest.mark.integration diff --git a/tests/integration/contract_delete_transaction_e2e_test.py b/tests/integration/contract_delete_transaction_e2e_test.py index fd28837e9..9998374ff 100644 --- a/tests/integration/contract_delete_transaction_e2e_test.py +++ b/tests/integration/contract_delete_transaction_e2e_test.py @@ -18,7 +18,7 @@ from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.query.account_info_query import AccountInfoQuery from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import env +from tests.integration.utils import env @pytest.mark.integration diff --git a/tests/integration/contract_execute_transaction_e2e_test.py b/tests/integration/contract_execute_transaction_e2e_test.py index 5d1eac020..5b17f587f 100644 --- a/tests/integration/contract_execute_transaction_e2e_test.py +++ b/tests/integration/contract_execute_transaction_e2e_test.py @@ -22,7 +22,7 @@ from hiero_sdk_python.exceptions import PrecheckError from hiero_sdk_python.file.file_create_transaction import FileCreateTransaction from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import env +from tests.integration.utils import env @pytest.mark.integration diff --git a/tests/integration/contract_function_parameters_e2e_test.py b/tests/integration/contract_function_parameters_e2e_test.py index c7ad96343..f035d91e1 100644 --- a/tests/integration/contract_function_parameters_e2e_test.py +++ b/tests/integration/contract_function_parameters_e2e_test.py @@ -14,7 +14,7 @@ from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.file.file_create_transaction import FileCreateTransaction from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import env +from tests.integration.utils import env # Generate a new ECDSA key pair and extract the first 40 bytes of the public key # to use as a test address for the contract constructor diff --git a/tests/integration/contract_info_query_e2e_test.py b/tests/integration/contract_info_query_e2e_test.py index 519520fff..10259d591 100644 --- a/tests/integration/contract_info_query_e2e_test.py +++ b/tests/integration/contract_info_query_e2e_test.py @@ -18,7 +18,7 @@ from hiero_sdk_python.file.file_create_transaction import FileCreateTransaction from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import env +from tests.integration.utils import env @pytest.mark.integration diff --git a/tests/integration/contract_update_transaction_e2e_test.py b/tests/integration/contract_update_transaction_e2e_test.py index 702e6430a..81809a28f 100644 --- a/tests/integration/contract_update_transaction_e2e_test.py +++ b/tests/integration/contract_update_transaction_e2e_test.py @@ -19,7 +19,7 @@ from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.timestamp import Timestamp -from tests.integration.utils_for_test import env +from tests.integration.utils import env @pytest.mark.integration diff --git a/tests/integration/ethereum_transaction_e2e_test.py b/tests/integration/ethereum_transaction_e2e_test.py index a93f27b9c..aba1c5545 100644 --- a/tests/integration/ethereum_transaction_e2e_test.py +++ b/tests/integration/ethereum_transaction_e2e_test.py @@ -23,7 +23,7 @@ from hiero_sdk_python.query.transaction_record_query import TransactionRecordQuery from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction -from tests.integration.utils_for_test import env +from tests.integration.utils import env @pytest.mark.integration diff --git a/tests/integration/file_append_transaction_e2e_test.py b/tests/integration/file_append_transaction_e2e_test.py index 72a54e9ef..eb507412a 100644 --- a/tests/integration/file_append_transaction_e2e_test.py +++ b/tests/integration/file_append_transaction_e2e_test.py @@ -8,7 +8,7 @@ from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.exceptions import PrecheckError -from tests.integration.utils_for_test import env, IntegrationTestEnv +from tests.integration.utils import env, IntegrationTestEnv # Generate big contents for chunking tests - similar to JavaScript bigContents BIG_CONTENTS = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " * 250 # ~13,750 characters diff --git a/tests/integration/file_contents_query_e2e_test.py b/tests/integration/file_contents_query_e2e_test.py index 78d5d8d3f..cb0e0ebe8 100644 --- a/tests/integration/file_contents_query_e2e_test.py +++ b/tests/integration/file_contents_query_e2e_test.py @@ -9,7 +9,7 @@ from hiero_sdk_python.file.file_create_transaction import FileCreateTransaction from hiero_sdk_python.file.file_id import FileId from hiero_sdk_python.hbar import Hbar -from tests.integration.utils_for_test import env +from tests.integration.utils import env FILE_CONTENT = b"Hello, World" diff --git a/tests/integration/file_create_transaction_e2e_test.py b/tests/integration/file_create_transaction_e2e_test.py index 13e74cb18..ae345cf28 100644 --- a/tests/integration/file_create_transaction_e2e_test.py +++ b/tests/integration/file_create_transaction_e2e_test.py @@ -5,7 +5,7 @@ from hiero_sdk_python.file.file_create_transaction import FileCreateTransaction from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.timestamp import Timestamp -from tests.integration.utils_for_test import env, IntegrationTestEnv +from tests.integration.utils import env, IntegrationTestEnv @mark.integration def test_integration_file_create_transaction_can_execute(env): diff --git a/tests/integration/file_delete_transaction_e2e_test.py b/tests/integration/file_delete_transaction_e2e_test.py index 465b045b9..5907a0b5d 100644 --- a/tests/integration/file_delete_transaction_e2e_test.py +++ b/tests/integration/file_delete_transaction_e2e_test.py @@ -10,7 +10,7 @@ from hiero_sdk_python.file.file_id import FileId from hiero_sdk_python.file.file_info_query import FileInfoQuery from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import env +from tests.integration.utils import env @pytest.mark.integration diff --git a/tests/integration/file_info_query_e2e_test.py b/tests/integration/file_info_query_e2e_test.py index 28a627179..6c92891df 100644 --- a/tests/integration/file_info_query_e2e_test.py +++ b/tests/integration/file_info_query_e2e_test.py @@ -10,7 +10,7 @@ from hiero_sdk_python.file.file_id import FileId from hiero_sdk_python.file.file_info_query import FileInfoQuery from hiero_sdk_python.hbar import Hbar -from tests.integration.utils_for_test import env +from tests.integration.utils import env FILE_CONTENT = b"Hello, World" FILE_MEMO = "python sdk e2e tests" diff --git a/tests/integration/file_update_transaction_e2e_test.py b/tests/integration/file_update_transaction_e2e_test.py index 1681d248a..27ea491b4 100644 --- a/tests/integration/file_update_transaction_e2e_test.py +++ b/tests/integration/file_update_transaction_e2e_test.py @@ -10,7 +10,7 @@ from hiero_sdk_python.file.file_info_query import FileInfoQuery from hiero_sdk_python.file.file_update_transaction import FileUpdateTransaction from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import env +from tests.integration.utils import env @pytest.mark.integration @@ -29,7 +29,7 @@ def test_integration_file_update_transaction_can_execute(env): ) assert ( receipt.status == ResponseCode.SUCCESS - ), f"File creation failed with status: {ResponseCode.get_name(receipt.status)}" + ), f"File creation failed with status: {ResponseCode(receipt.status).name}" file_id = receipt.file_id assert file_id is not None, "File ID should not be None" @@ -53,7 +53,7 @@ def test_integration_file_update_transaction_can_execute(env): ) assert ( receipt.status == ResponseCode.SUCCESS - ), f"File update failed with status: {ResponseCode.get_name(receipt.status)}" + ), f"File update failed with status: {ResponseCode(receipt.status).name}" # Query file info and check if everything is updated info = FileInfoQuery().set_file_id(file_id).execute(env.client) @@ -85,7 +85,7 @@ def test_integration_file_update_transaction_cannot_update_immutable_file(env): receipt = FileCreateTransaction().set_contents("Immutable file").execute(env.client) assert ( receipt.status == ResponseCode.SUCCESS - ), f"File creation failed with status: {ResponseCode.get_name(receipt.status)}" + ), f"File creation failed with status: {ResponseCode(receipt.status).name}" file_id = receipt.file_id assert file_id is not None, "File ID should not be None" @@ -121,7 +121,7 @@ def test_integration_file_update_transaction_fails_when_key_is_invalid(env): ) assert ( receipt.status == ResponseCode.SUCCESS - ), f"File creation failed with status: {ResponseCode.get_name(receipt.status)}" + ), f"File creation failed with status: {ResponseCode(receipt.status).name}" file_id = receipt.file_id assert file_id is not None, "File ID should not be None" diff --git a/tests/integration/prng_transaction_e2e_test.py b/tests/integration/prng_transaction_e2e_test.py index 07971e131..e467bd68f 100644 --- a/tests/integration/prng_transaction_e2e_test.py +++ b/tests/integration/prng_transaction_e2e_test.py @@ -7,7 +7,7 @@ from hiero_sdk_python.prng_transaction import PrngTransaction from hiero_sdk_python.query.transaction_record_query import TransactionRecordQuery from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import env +from tests.integration.utils import env @pytest.mark.integration diff --git a/tests/integration/query_e2e_test.py b/tests/integration/query_e2e_test.py index 82b32846a..c72ecb971 100644 --- a/tests/integration/query_e2e_test.py +++ b/tests/integration/query_e2e_test.py @@ -8,7 +8,7 @@ from hiero_sdk_python.query.account_balance_query import CryptoGetAccountBalanceQuery from hiero_sdk_python.query.token_info_query import TokenInfoQuery from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token @pytest.mark.integration def test_integration_free_query_no_cost(): diff --git a/tests/integration/revenue_generating_topics_e2e_test.py b/tests/integration/revenue_generating_topics_e2e_test.py index 4d9b6fae4..8c87a751f 100644 --- a/tests/integration/revenue_generating_topics_e2e_test.py +++ b/tests/integration/revenue_generating_topics_e2e_test.py @@ -21,7 +21,7 @@ from hiero_sdk_python.tokens.token_associate_transaction import TokenAssociateTransaction from hiero_sdk_python.tokens.token_id import TokenId from hiero_sdk_python.transaction.custom_fee_limit import CustomFeeLimit -from tests.integration.utils_for_test import create_fungible_token, env +from tests.integration.utils import create_fungible_token, env TOPIC_MEMO = "Python SDK revenue generating topic" MESSAGE = "test_message" diff --git a/tests/integration/schedule_create_transaction_e2e_test.py b/tests/integration/schedule_create_transaction_e2e_test.py index c86f6df32..34f577ebd 100644 --- a/tests/integration/schedule_create_transaction_e2e_test.py +++ b/tests/integration/schedule_create_transaction_e2e_test.py @@ -20,7 +20,7 @@ from hiero_sdk_python.tokens.token_burn_transaction import TokenBurnTransaction from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction -from tests.integration.utils_for_test import create_fungible_token, env +from tests.integration.utils import create_fungible_token, env @pytest.mark.integration diff --git a/tests/integration/schedule_delete_transaction_e2e_test.py b/tests/integration/schedule_delete_transaction_e2e_test.py index aacba3548..53936f3f5 100644 --- a/tests/integration/schedule_delete_transaction_e2e_test.py +++ b/tests/integration/schedule_delete_transaction_e2e_test.py @@ -14,7 +14,7 @@ from hiero_sdk_python.schedule.schedule_info_query import ScheduleInfoQuery from hiero_sdk_python.timestamp import Timestamp from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction -from tests.integration.utils_for_test import env +from tests.integration.utils import env @pytest.mark.integration diff --git a/tests/integration/schedule_info_query_e2e_test.py b/tests/integration/schedule_info_query_e2e_test.py index 039a8f0b5..18b97693b 100644 --- a/tests/integration/schedule_info_query_e2e_test.py +++ b/tests/integration/schedule_info_query_e2e_test.py @@ -13,7 +13,7 @@ from hiero_sdk_python.schedule.schedule_info_query import ScheduleInfoQuery from hiero_sdk_python.timestamp import Timestamp from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction -from tests.integration.utils_for_test import env +from tests.integration.utils import env @pytest.mark.integration diff --git a/tests/integration/schedule_sign_transaction_e2e_test.py b/tests/integration/schedule_sign_transaction_e2e_test.py index dfb239f60..d4017bb09 100644 --- a/tests/integration/schedule_sign_transaction_e2e_test.py +++ b/tests/integration/schedule_sign_transaction_e2e_test.py @@ -9,7 +9,7 @@ from hiero_sdk_python.schedule.schedule_info_query import ScheduleInfoQuery from hiero_sdk_python.schedule.schedule_sign_transaction import ScheduleSignTransaction from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction -from tests.integration.utils_for_test import env +from tests.integration.utils import env @pytest.mark.integration diff --git a/tests/integration/tls_integration_test.py b/tests/integration/tls_integration_test.py index 618435982..f6cfc1fa9 100644 --- a/tests/integration/tls_integration_test.py +++ b/tests/integration/tls_integration_test.py @@ -8,7 +8,7 @@ from hiero_sdk_python.query.account_balance_query import CryptoGetAccountBalanceQuery from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.crypto.private_key import PrivateKey -from tests.integration.utils_for_test import IntegrationTestEnv +from tests.integration.utils import IntegrationTestEnv load_dotenv(override=True) diff --git a/tests/integration/token_airdrop_transaction_cancel_e2e_test.py b/tests/integration/token_airdrop_transaction_cancel_e2e_test.py index f68ada5ec..109428cca 100644 --- a/tests/integration/token_airdrop_transaction_cancel_e2e_test.py +++ b/tests/integration/token_airdrop_transaction_cancel_e2e_test.py @@ -10,7 +10,7 @@ from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token, create_nft_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token, create_nft_token #Mint NFT and return serial_number def mint_nft(env: IntegrationTestEnv, nft_id): diff --git a/tests/integration/token_airdrop_transaction_claim_e2e_test.py b/tests/integration/token_airdrop_transaction_claim_e2e_test.py index b7b95cf4d..7559e2d9d 100644 --- a/tests/integration/token_airdrop_transaction_claim_e2e_test.py +++ b/tests/integration/token_airdrop_transaction_claim_e2e_test.py @@ -10,7 +10,7 @@ from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.tokens.token_id import TokenId from hiero_sdk_python.query.transaction_record_query import TransactionRecordQuery -from tests.integration.utils_for_test import env, create_fungible_token, create_nft_token +from tests.integration.utils import env, create_fungible_token, create_nft_token from typing import List pytestmark = pytest.mark.integration diff --git a/tests/integration/token_airdrop_transaction_e2e_test.py b/tests/integration/token_airdrop_transaction_e2e_test.py index f936bb222..2ecfc17a9 100644 --- a/tests/integration/token_airdrop_transaction_e2e_test.py +++ b/tests/integration/token_airdrop_transaction_e2e_test.py @@ -10,7 +10,7 @@ from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token, create_nft_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token, create_nft_token def _mint_nft(env: IntegrationTestEnv, nft_id): token_mint_tx = TokenMintTransaction( diff --git a/tests/integration/token_associate_transaction_e2e_test.py b/tests/integration/token_associate_transaction_e2e_test.py index 648a54793..af2c3aeb0 100644 --- a/tests/integration/token_associate_transaction_e2e_test.py +++ b/tests/integration/token_associate_transaction_e2e_test.py @@ -6,7 +6,7 @@ from hiero_sdk_python.tokens.token_dissociate_transaction import TokenDissociateTransaction from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token @pytest.mark.integration diff --git a/tests/integration/token_burn_transaction_e2e_test.py b/tests/integration/token_burn_transaction_e2e_test.py index 15764223e..3aeff75ca 100644 --- a/tests/integration/token_burn_transaction_e2e_test.py +++ b/tests/integration/token_burn_transaction_e2e_test.py @@ -4,7 +4,7 @@ from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.tokens.token_burn_transaction import TokenBurnTransaction from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token, create_nft_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token, create_nft_token @pytest.mark.integration diff --git a/tests/integration/token_create_transaction_e2e_test.py b/tests/integration/token_create_transaction_e2e_test.py index 9629cadbb..bf471cd78 100644 --- a/tests/integration/token_create_transaction_e2e_test.py +++ b/tests/integration/token_create_transaction_e2e_test.py @@ -12,7 +12,7 @@ from hiero_sdk_python.query.token_info_query import TokenInfoQuery from hiero_sdk_python.timestamp import Timestamp from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction, TokenParams -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token, create_nft_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token, create_nft_token @pytest.mark.integration diff --git a/tests/integration/token_create_with_custom_fee_e2e_test.py b/tests/integration/token_create_with_custom_fee_e2e_test.py index 15af5edf5..329f7b099 100644 --- a/tests/integration/token_create_with_custom_fee_e2e_test.py +++ b/tests/integration/token_create_with_custom_fee_e2e_test.py @@ -5,7 +5,7 @@ from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee from hiero_sdk_python.tokens.token_id import TokenId from hiero_sdk_python.query.token_info_query import TokenInfoQuery -from tests.integration.utils_for_test import IntegrationTestEnv +from tests.integration.utils import IntegrationTestEnv @pytest.mark.integration def test_token_create_with_custom_fee_e2e(): diff --git a/tests/integration/token_delete_transaction_e2e_test.py b/tests/integration/token_delete_transaction_e2e_test.py index c829053b0..a7ddadc87 100644 --- a/tests/integration/token_delete_transaction_e2e_test.py +++ b/tests/integration/token_delete_transaction_e2e_test.py @@ -1,6 +1,6 @@ import pytest -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token from hiero_sdk_python.tokens.token_delete_transaction import TokenDeleteTransaction from hiero_sdk_python.response_code import ResponseCode diff --git a/tests/integration/token_dissociate_transaction_e2e_test.py b/tests/integration/token_dissociate_transaction_e2e_test.py index c9933adf2..99a7df68b 100644 --- a/tests/integration/token_dissociate_transaction_e2e_test.py +++ b/tests/integration/token_dissociate_transaction_e2e_test.py @@ -6,7 +6,7 @@ from hiero_sdk_python.tokens.token_associate_transaction import TokenAssociateTransaction from hiero_sdk_python.tokens.token_dissociate_transaction import TokenDissociateTransaction from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token @pytest.mark.integration diff --git a/tests/integration/test_token_fee_schedule_update_transaction_e2e.py b/tests/integration/token_fee_schedule_update_transaction_e2e_test.py similarity index 99% rename from tests/integration/test_token_fee_schedule_update_transaction_e2e.py rename to tests/integration/token_fee_schedule_update_transaction_e2e_test.py index fa1070135..13a33c1d4 100644 --- a/tests/integration/test_token_fee_schedule_update_transaction_e2e.py +++ b/tests/integration/token_fee_schedule_update_transaction_e2e_test.py @@ -1,7 +1,7 @@ import pytest from hiero_sdk_python.hbar import Hbar -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token, create_nft_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token, create_nft_token from hiero_sdk_python.tokens.token_create_transaction import ( TokenCreateTransaction, TokenParams, diff --git a/tests/integration/token_freeze_transaction_e2e_test.py b/tests/integration/token_freeze_transaction_e2e_test.py index 233174e1a..51b0d5c53 100644 --- a/tests/integration/token_freeze_transaction_e2e_test.py +++ b/tests/integration/token_freeze_transaction_e2e_test.py @@ -7,7 +7,7 @@ from hiero_sdk_python.tokens.token_freeze_transaction import TokenFreezeTransaction from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token @pytest.mark.integration diff --git a/tests/integration/token_grant_kyc_transaction_e2e_test.py b/tests/integration/token_grant_kyc_transaction_e2e_test.py index 057e20e78..82821e9b1 100644 --- a/tests/integration/token_grant_kyc_transaction_e2e_test.py +++ b/tests/integration/token_grant_kyc_transaction_e2e_test.py @@ -6,7 +6,7 @@ from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.tokens.token_grant_kyc_transaction import TokenGrantKycTransaction -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token @pytest.mark.integration def test_token_grant_kyc_transaction_can_execute(): diff --git a/tests/integration/token_info_query_e2e_test.py b/tests/integration/token_info_query_e2e_test.py index 34f4a1068..394073c32 100644 --- a/tests/integration/token_info_query_e2e_test.py +++ b/tests/integration/token_info_query_e2e_test.py @@ -6,7 +6,7 @@ from hiero_sdk_python.tokens.token_type import TokenType from hiero_sdk_python.tokens.token_id import TokenId from hiero_sdk_python.query.token_info_query import TokenInfoQuery -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token @pytest.mark.integration def test_integration_token_info_query_can_execute(): diff --git a/tests/integration/token_mint_transaction_e2e_test.py b/tests/integration/token_mint_transaction_e2e_test.py index 39b7f554a..c4589e01e 100644 --- a/tests/integration/token_mint_transaction_e2e_test.py +++ b/tests/integration/token_mint_transaction_e2e_test.py @@ -5,7 +5,7 @@ from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token @pytest.mark.integration diff --git a/tests/integration/token_nft_info_query_e2e_test.py b/tests/integration/token_nft_info_query_e2e_test.py index ab8cfcefd..216653847 100644 --- a/tests/integration/token_nft_info_query_e2e_test.py +++ b/tests/integration/token_nft_info_query_e2e_test.py @@ -5,7 +5,7 @@ from hiero_sdk_python.tokens.token_nft_info import TokenNftInfo from hiero_sdk_python.query.token_nft_info_query import TokenNftInfoQuery from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction -from tests.integration.utils_for_test import IntegrationTestEnv, create_nft_token +from tests.integration.utils import IntegrationTestEnv, create_nft_token from hiero_sdk_python.tokens.nft_id import NftId @pytest.mark.integration diff --git a/tests/integration/token_pause_transaction_e2e_test.py b/tests/integration/token_pause_transaction_e2e_test.py index bf862fa3c..d6ea48ce5 100644 --- a/tests/integration/token_pause_transaction_e2e_test.py +++ b/tests/integration/token_pause_transaction_e2e_test.py @@ -1,7 +1,7 @@ import pytest from pytest import mark, fixture -from tests.integration.utils_for_test import env, create_fungible_token, create_nft_token +from tests.integration.utils import env, create_fungible_token, create_nft_token from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.response_code import ResponseCode @@ -9,7 +9,7 @@ from hiero_sdk_python.tokens.token_pause_transaction import TokenPauseTransaction from hiero_sdk_python.tokens.token_id import TokenId -from tests.integration.utils_for_test import create_fungible_token, Account +from tests.integration.utils import create_fungible_token, Account from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction from hiero_sdk_python.tokens.token_associate_transaction import TokenAssociateTransaction from hiero_sdk_python.query.account_balance_query import CryptoGetAccountBalanceQuery diff --git a/tests/integration/token_reject_transaction_e2e_test.py b/tests/integration/token_reject_transaction_e2e_test.py index 25febad16..0afc62384 100644 --- a/tests/integration/token_reject_transaction_e2e_test.py +++ b/tests/integration/token_reject_transaction_e2e_test.py @@ -11,7 +11,7 @@ from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction from hiero_sdk_python.tokens.token_reject_transaction import TokenRejectTransaction from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token, create_nft_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token, create_nft_token from hiero_sdk_python.tokens.nft_id import NftId from hiero_sdk_python.query.token_nft_info_query import TokenNftInfoQuery diff --git a/tests/integration/token_revoke_kyc_transaction_e2e_test.py b/tests/integration/token_revoke_kyc_transaction_e2e_test.py index 3c007e0c2..01cb670be 100644 --- a/tests/integration/token_revoke_kyc_transaction_e2e_test.py +++ b/tests/integration/token_revoke_kyc_transaction_e2e_test.py @@ -7,7 +7,7 @@ from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.tokens.token_grant_kyc_transaction import TokenGrantKycTransaction from hiero_sdk_python.tokens.token_revoke_kyc_transaction import TokenRevokeKycTransaction -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token @pytest.mark.integration def test_token_revoke_kyc_transaction_can_execute(): @@ -24,7 +24,7 @@ def test_token_revoke_kyc_transaction_can_execute(): .set_initial_balance(Hbar(2)) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Account creation failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Account creation failed with status: {ResponseCode(receipt.status).name}" account_id = receipt.account_id # Create a new token and set the kyc key to be the operator's key @@ -39,7 +39,7 @@ def test_token_revoke_kyc_transaction_can_execute(): .sign(new_account_private_key) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token association failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token association failed with status: {ResponseCode(receipt.status).name}" # Grant KYC to the new account first receipt = ( @@ -48,7 +48,7 @@ def test_token_revoke_kyc_transaction_can_execute(): .set_token_id(token_id) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token grant KYC failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token grant KYC failed with status: {ResponseCode(receipt.status).name}" # Revoke KYC from the new account receipt = ( @@ -57,7 +57,7 @@ def test_token_revoke_kyc_transaction_can_execute(): .set_token_id(token_id) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token revoke KYC failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token revoke KYC failed with status: {ResponseCode(receipt.status).name}" finally: env.close() @@ -76,7 +76,7 @@ def test_token_revoke_kyc_transaction_fails_with_no_kyc_key(): .set_initial_balance(Hbar(2)) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Account creation failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Account creation failed with status: {ResponseCode(receipt.status).name}" account_id = receipt.account_id # Create a new token without KYC key @@ -91,7 +91,7 @@ def test_token_revoke_kyc_transaction_fails_with_no_kyc_key(): .sign(new_account_private_key) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token association failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token association failed with status: {ResponseCode(receipt.status).name}" # Try to revoke KYC for token without KYC key - should fail with TOKEN_HAS_NO_KYC_KEY receipt = ( @@ -100,7 +100,7 @@ def test_token_revoke_kyc_transaction_fails_with_no_kyc_key(): .set_token_id(token_id) .execute(env.client) ) - assert receipt.status == ResponseCode.TOKEN_HAS_NO_KYC_KEY, f"Token revoke KYC should have failed with TOKEN_HAS_NO_KYC_KEY status but got: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.TOKEN_HAS_NO_KYC_KEY, f"Token revoke KYC should have failed with TOKEN_HAS_NO_KYC_KEY status but got: {ResponseCode(receipt.status).name}" # Try to revoke KYC with non-KYC key - should fail with TOKEN_HAS_NO_KYC_KEY receipt = ( @@ -109,7 +109,7 @@ def test_token_revoke_kyc_transaction_fails_with_no_kyc_key(): .set_token_id(token_id) .execute(env.client) ) - assert receipt.status == ResponseCode.TOKEN_HAS_NO_KYC_KEY, f"Token revoke KYC should have failed with TOKEN_HAS_NO_KYC_KEY status but got: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.TOKEN_HAS_NO_KYC_KEY, f"Token revoke KYC should have failed with TOKEN_HAS_NO_KYC_KEY status but got: {ResponseCode(receipt.status).name}" finally: env.close() @@ -128,7 +128,7 @@ def test_token_revoke_kyc_transaction_fails_when_account_not_associated(): .set_initial_balance(Hbar(2)) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Account creation failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Account creation failed with status: {ResponseCode(receipt.status).name}" account_id = receipt.account_id # Create a new token and set the kyc key to be the operator's key @@ -141,6 +141,6 @@ def test_token_revoke_kyc_transaction_fails_when_account_not_associated(): .set_token_id(token_id) .execute(env.client) ) - assert receipt.status == ResponseCode.TOKEN_NOT_ASSOCIATED_TO_ACCOUNT, f"Token revoke KYC should have failed with TOKEN_NOT_ASSOCIATED_TO_ACCOUNT status but got: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.TOKEN_NOT_ASSOCIATED_TO_ACCOUNT, f"Token revoke KYC should have failed with TOKEN_NOT_ASSOCIATED_TO_ACCOUNT status but got: {ResponseCode(receipt.status).name}" finally: env.close() \ No newline at end of file diff --git a/tests/integration/token_unfreeze_transaction_e2e_test.py b/tests/integration/token_unfreeze_transaction_e2e_test.py index 2d08e6817..f6dbd1063 100644 --- a/tests/integration/token_unfreeze_transaction_e2e_test.py +++ b/tests/integration/token_unfreeze_transaction_e2e_test.py @@ -7,7 +7,7 @@ from hiero_sdk_python.tokens.token_unfreeze_transaction import TokenUnfreezeTransaction from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token @pytest.mark.integration diff --git a/tests/integration/token_unpause_transaction_e2e_test.py b/tests/integration/token_unpause_transaction_e2e_test.py index 421a868e0..7ed0512c4 100644 --- a/tests/integration/token_unpause_transaction_e2e_test.py +++ b/tests/integration/token_unpause_transaction_e2e_test.py @@ -6,7 +6,7 @@ from hiero_sdk_python.tokens.token_id import TokenId from hiero_sdk_python.tokens.token_pause_transaction import TokenPauseTransaction from hiero_sdk_python.tokens.token_unpause_transaction import TokenUnpauseTransaction -from tests.integration.utils_for_test import env, create_fungible_token +from tests.integration.utils import env, create_fungible_token pause_key = PrivateKey.generate() diff --git a/tests/integration/token_update_nfts_transaction_e2e_test.py b/tests/integration/token_update_nfts_transaction_e2e_test.py index 2c95a8174..30448d64c 100644 --- a/tests/integration/token_update_nfts_transaction_e2e_test.py +++ b/tests/integration/token_update_nfts_transaction_e2e_test.py @@ -5,7 +5,7 @@ from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction from hiero_sdk_python.tokens.token_update_nfts_transaction import TokenUpdateNftsTransaction -from tests.integration.utils_for_test import IntegrationTestEnv, create_nft_token +from tests.integration.utils import IntegrationTestEnv, create_nft_token from hiero_sdk_python.tokens.nft_id import NftId from hiero_sdk_python.query.token_nft_info_query import TokenNftInfoQuery diff --git a/tests/integration/token_update_transaction_e2e_test.py b/tests/integration/token_update_transaction_e2e_test.py index a79791be4..0402d245f 100644 --- a/tests/integration/token_update_transaction_e2e_test.py +++ b/tests/integration/token_update_transaction_e2e_test.py @@ -17,7 +17,7 @@ from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction from hiero_sdk_python.transaction.transaction import Transaction -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token, create_nft_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token, create_nft_token private_key = PrivateKey.generate() @@ -39,7 +39,7 @@ def test_integration_token_update_transaction_can_execute(): .freeze_with(env.client) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update transaction failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update transaction failed with status: {ResponseCode(receipt.status).name}" info = ( TokenInfoQuery() @@ -73,7 +73,7 @@ def test_integration_token_update_preserves_fields_without_updating_parameters() .set_token_id(token_id) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update transaction failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update transaction failed with status: {ResponseCode(receipt.status).name}" info = ( TokenInfoQuery() @@ -114,7 +114,7 @@ def test_integration_token_update_transaction_different_keys(): .set_initial_balance(Hbar(2)) ) receipt = tx.execute(env.client) - assert receipt.status == ResponseCode.SUCCESS, f"Account creation failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Account creation failed with status: {ResponseCode(receipt.status).name}" # Create fungible token with initial metadata and pause keys both set to the first key token_id = create_fungible_token(env, opts=[ @@ -141,7 +141,7 @@ def test_integration_token_update_transaction_different_keys(): .set_fee_schedule_key(keys[7]) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update transaction failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update transaction failed with status: {ResponseCode(receipt.status).name}" # Query token info and verify updates info = ( @@ -178,7 +178,7 @@ def test_integration_token_update_transaction_treasury(): .set_initial_balance(Hbar(2)) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Account creation failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Account creation failed with status: {ResponseCode(receipt.status).name}" account_id = receipt.account_id # Create fungible token @@ -191,7 +191,7 @@ def test_integration_token_update_transaction_treasury(): .freeze_with(env.client) ) receipt = tx.sign(new_private_key).execute(env.client) - assert receipt.status == ResponseCode.SUCCESS, f"Token association failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token association failed with status: {ResponseCode(receipt.status).name}" # Update token with new treasury account receipt = ( @@ -203,7 +203,7 @@ def test_integration_token_update_transaction_treasury(): .sign(new_private_key) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode(receipt.status).name}" # Query token info and verify updates info = ( @@ -227,7 +227,7 @@ def test_integration_token_update_transaction_fail_invalid_token_id(): receipt = tx.execute(env.client) - assert receipt.status == ResponseCode.INVALID_TOKEN_ID, f"Token update should have failed with INVALID_TOKEN_ID status but got: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.INVALID_TOKEN_ID, f"Token update should have failed with INVALID_TOKEN_ID status but got: {ResponseCode(receipt.status).name}" finally: env.close() @@ -256,7 +256,7 @@ def test_integration_token_update_transaction_fungible_metadata(): .set_metadata(new_metadata) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode(receipt.status).name}" # Query token info and verify updated metadata info = ( @@ -294,7 +294,7 @@ def test_integration_token_update_transaction_nft_metadata(): .set_metadata(new_metadata) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode(receipt.status).name}" # Query token info and verify updated metadata info = ( @@ -342,7 +342,7 @@ def test_integration_token_update_transaction_metadata_immutable_fungible_token( .sign(metadata_key) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode(receipt.status).name}" # Query token info and verify updated metadata info = ( @@ -390,7 +390,7 @@ def test_integration_token_update_transaction_metadata_immutable_nft(): .sign(metadata_key) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode(receipt.status).name}" # Query token info and verify updated metadata info = ( @@ -426,7 +426,7 @@ def test_token_update_transaction_cannot_update_metadata_fungible(): .set_token_memo("updated memo") .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode(receipt.status).name}" # Query token info and verify metadata remains unchanged info = ( @@ -462,7 +462,7 @@ def test_integration_token_update_transaction_cannot_update_metadata_nft(): .set_token_memo("asdf") .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode(receipt.status).name}" # Query token info and verify metadata remains unchanged info = ( @@ -498,7 +498,7 @@ def test_integration_token_update_transaction_erase_metadata_fungible_token(): .set_metadata(b"") .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode(receipt.status).name}" # Query token info and verify metadata was erased info = ( @@ -534,7 +534,7 @@ def test_integration_token_update_transaction_erase_metadata_nft(): .set_metadata(b"") .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode(receipt.status).name}" # Query token info and verify metadata was erased info = ( @@ -571,7 +571,7 @@ def test_integration_token_update_transaction_cannot_update_metadata_without_key .execute(env.client) ) - assert receipt.status == ResponseCode.INVALID_SIGNATURE, f"Token update should have failed with INVALID_SIGNATURE status but got: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.INVALID_SIGNATURE, f"Token update should have failed with INVALID_SIGNATURE status but got: {ResponseCode(receipt.status).name}" finally: env.close() @@ -600,7 +600,7 @@ def test_integration_token_update_transaction_cannot_update_metadata_without_key .execute(env.client) ) - assert receipt.status == ResponseCode.INVALID_SIGNATURE, f"Token update should have failed with INVALID_SIGNATURE status but got: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.INVALID_SIGNATURE, f"Token update should have failed with INVALID_SIGNATURE status but got: {ResponseCode(receipt.status).name}" finally: env.close() @@ -624,7 +624,7 @@ def test_integration_token_update_transaction_cannot_update_immutable_fungible_t .execute(env.client) ) - assert receipt.status == ResponseCode.TOKEN_IS_IMMUTABLE, f"Token update should have failed with TOKEN_IS_IMMUTABLE status but got: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.TOKEN_IS_IMMUTABLE, f"Token update should have failed with TOKEN_IS_IMMUTABLE status but got: {ResponseCode(receipt.status).name}" finally: env.close() @@ -647,7 +647,7 @@ def test_integration_token_update_transaction_cannot_update_immutable_nft(): .execute(env.client) ) - assert receipt.status == ResponseCode.TOKEN_IS_IMMUTABLE, f"Token update should have failed with TOKEN_IS_IMMUTABLE status but got: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.TOKEN_IS_IMMUTABLE, f"Token update should have failed with TOKEN_IS_IMMUTABLE status but got: {ResponseCode(receipt.status).name}" finally: env.close() @@ -675,7 +675,7 @@ def test_integration_token_update_auto_renew_account(): .sign(recipient.key) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update transaction failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update transaction failed with status: {ResponseCode(receipt.status).name}" new_info = ( TokenInfoQuery() @@ -710,7 +710,7 @@ def test_integration_token_update_expiration_time(): .freeze_with(env.client) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update transaction failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update transaction failed with status: {ResponseCode(receipt.status).name}" new_info = ( TokenInfoQuery() @@ -756,7 +756,7 @@ def test_integration_token_update_kyc_key_fungible_token(): .sign(new_kyc_key) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode(receipt.status).name}" token_info = TokenInfoQuery(token_id=token_id).execute(env.client) assert token_info.kyc_key.to_string() == new_kyc_key.public_key().to_string(), "Updated kyc_key mismatch" @@ -808,7 +808,7 @@ def test_integration_token_update_kyc_key_nft(): .sign(new_kyc_key) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode(receipt.status).name}" token_info = TokenInfoQuery(token_id=token_id).execute(env.client) assert token_info.kyc_key.to_string() == new_kyc_key.public_key().to_string(), "Updated kyc_key mismatch" @@ -849,7 +849,7 @@ def test_integation_token_update_fee_schedule_key_fungible_token(): .sign(admin_key) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode(receipt.status).name}" token_info = TokenInfoQuery(token_id=token_id).execute(env.client) assert token_info.fee_schedule_key.to_string() == new_fee_schedule_key.public_key().to_string(), "Updated fee_schedule_key mismatch" @@ -895,7 +895,7 @@ def test_integation_token_update_fee_schedule_key_nft(): .sign(admin_key) .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode(receipt.status).name}" token_info = TokenInfoQuery(token_id=token_id).execute(env.client) assert token_info.fee_schedule_key.to_string() == new_fee_schedule_key.public_key().to_string(), "Updated fee_schedule_key mismatch" @@ -942,7 +942,7 @@ def test_integration_token_update_with_public_key(): .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update transaction failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update transaction failed with status: {ResponseCode(receipt.status).name}" # Query the token and verify the freeze key matches the public key info = TokenInfoQuery().set_token_id(token_id).execute(env.client) @@ -1001,7 +1001,7 @@ def test_integration_token_update_non_custodial_workflow(): receipt = tx_from_bytes.execute(env.client) - assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update failed with status: {ResponseCode(receipt.status).name}" # PROOF: Query the token and check if the freeze key matches token_info = TokenInfoQuery(token_id=token_id).execute(env.client) @@ -1042,7 +1042,7 @@ def test_integration_token_update_with_ecdsa_public_key(): .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update transaction failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update transaction failed with status: {ResponseCode(receipt.status).name}" # Query the token and verify the admin key matches the public key token_info = TokenInfoQuery(token_id=token_id).execute(env.client) @@ -1085,7 +1085,7 @@ def test_integration_token_update_with_mixed_key_types(): .execute(env.client) ) - assert receipt.status == ResponseCode.SUCCESS, f"Token update transaction failed with status: {ResponseCode.get_name(receipt.status)}" + assert receipt.status == ResponseCode.SUCCESS, f"Token update transaction failed with status: {ResponseCode(receipt.status).name}" # Query the token and verify all keys are correctly set token_info = TokenInfoQuery(token_id=token_id).execute(env.client) diff --git a/tests/integration/token_wipe_account_transaction_e2e_test.py b/tests/integration/token_wipe_account_transaction_e2e_test.py index 8d24079ed..3e1911c4b 100644 --- a/tests/integration/token_wipe_account_transaction_e2e_test.py +++ b/tests/integration/token_wipe_account_transaction_e2e_test.py @@ -9,7 +9,7 @@ from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.tokens.token_wipe_transaction import TokenWipeTransaction from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token @pytest.mark.integration diff --git a/tests/integration/topic_create_transaction_e2e_test.py b/tests/integration/topic_create_transaction_e2e_test.py index 483f3a32c..ba1f53871 100644 --- a/tests/integration/topic_create_transaction_e2e_test.py +++ b/tests/integration/topic_create_transaction_e2e_test.py @@ -7,7 +7,7 @@ from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.crypto.public_key import PublicKey from hiero_sdk_python.transaction.transaction import Transaction -from tests.integration.utils_for_test import IntegrationTestEnv +from tests.integration.utils import IntegrationTestEnv topic_memo = "Python SDK created topic" diff --git a/tests/integration/topic_delete_transaction_e2e_test.py b/tests/integration/topic_delete_transaction_e2e_test.py index 9e4a92b28..baa08f701 100644 --- a/tests/integration/topic_delete_transaction_e2e_test.py +++ b/tests/integration/topic_delete_transaction_e2e_test.py @@ -5,7 +5,7 @@ from hiero_sdk_python.exceptions import PrecheckError from hiero_sdk_python.query.topic_info_query import TopicInfoQuery from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import IntegrationTestEnv +from tests.integration.utils import IntegrationTestEnv @pytest.mark.integration diff --git a/tests/integration/topic_info_query_e2e_test.py b/tests/integration/topic_info_query_e2e_test.py index b95d9a9fc..e79dfb4bc 100644 --- a/tests/integration/topic_info_query_e2e_test.py +++ b/tests/integration/topic_info_query_e2e_test.py @@ -4,7 +4,7 @@ from hiero_sdk_python.consensus.topic_delete_transaction import TopicDeleteTransaction from hiero_sdk_python.query.topic_info_query import TopicInfoQuery from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import IntegrationTestEnv +from tests.integration.utils import IntegrationTestEnv @pytest.mark.integration diff --git a/tests/integration/topic_message_query_e2e_test.py b/tests/integration/topic_message_query_e2e_test.py index bffca81c2..5749688b8 100644 --- a/tests/integration/topic_message_query_e2e_test.py +++ b/tests/integration/topic_message_query_e2e_test.py @@ -13,7 +13,7 @@ ) from hiero_sdk_python.query.topic_message_query import TopicMessageQuery from hiero_sdk_python.response_code import ResponseCode -from tests.integration.utils_for_test import env +from tests.integration.utils import env BIG_CONTENT = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur aliquam augue sem, ut mattis dui laoreet a. Curabitur consequat est euismod, scelerisque metus et, tristique dui. Nulla commodo mauris ut faucibus ultricies. Quisque venenatis nisl nec augue tempus, at efficitur elit eleifend. Duis pharetra felis metus, sed dapibus urna vehicula id. Duis non venenatis turpis, sit amet ornare orci. Donec non interdum quam. Sed finibus nunc et risus finibus, non sagittis lorem cursus. Proin pellentesque tempor aliquam. Sed congue nisl in enim bibendum, condimentum vehicula nisi feugiat. diff --git a/tests/integration/topic_message_submit_transaction_e2e_test.py b/tests/integration/topic_message_submit_transaction_e2e_test.py index a2ac199e1..b5d2c681c 100644 --- a/tests/integration/topic_message_submit_transaction_e2e_test.py +++ b/tests/integration/topic_message_submit_transaction_e2e_test.py @@ -16,7 +16,7 @@ from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee from hiero_sdk_python.transaction.custom_fee_limit import CustomFeeLimit -from tests.integration.utils_for_test import env +from tests.integration.utils import env def create_topic(client, admin_key=None, submit_key=None, custom_fees=None): """Helper transaction for creating a topic.""" diff --git a/tests/integration/topic_update_transaction_e2e_test.py b/tests/integration/topic_update_transaction_e2e_test.py index ca13bb798..1aa683a82 100644 --- a/tests/integration/topic_update_transaction_e2e_test.py +++ b/tests/integration/topic_update_transaction_e2e_test.py @@ -7,7 +7,7 @@ from hiero_sdk_python.query.topic_info_query import TopicInfoQuery from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee -from tests.integration.utils_for_test import IntegrationTestEnv +from tests.integration.utils import IntegrationTestEnv @pytest.mark.integration diff --git a/tests/integration/transaction_get_receipt_query_children_e2e_test.py b/tests/integration/transaction_get_receipt_query_children_e2e_test.py new file mode 100644 index 000000000..03d1411d3 --- /dev/null +++ b/tests/integration/transaction_get_receipt_query_children_e2e_test.py @@ -0,0 +1,184 @@ +""" +E2E integration tests for TransactionGetReceiptQuery include_child_receipts support. + +These tests validate the full SDK flow against a real Hedera network: +- submit a real transaction +- query its receipt +- verify children behavior with and without include_children flag + +NOTE: +The contract used in these tests (StatefulContract) does NOT deterministically +produce child receipts, so we only assert API correctness and stability, +not children count > 0. +""" + +import pytest + +from hiero_sdk_python.hbar import Hbar +from hiero_sdk_python.query.transaction_get_receipt_query import TransactionGetReceiptQuery +from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction + +from tests.integration.utils import env + + +def _extract_tx_id(tx, receipt): + """ + Best-effort extraction of TransactionId for E2E tests. + """ + tx_id = getattr(receipt, "transaction_id", None) + if tx_id is not None: + return tx_id + + tx_id = getattr(tx, "transaction_id", None) + if tx_id is not None: + return tx_id + + tx_id = getattr(tx, "_transaction_id", None) + if tx_id is not None: + return tx_id + + tx_ids = getattr(tx, "_transaction_ids", None) + if tx_ids: + return tx_ids[0] + + raise AssertionError( + "Unable to extract TransactionId from transaction or receipt." + ) + + +def _submit_simple_transfer(env): + """ + Submit a simple transfer transaction and return its TransactionId. + """ + receiver = env.create_account(initial_hbar=0.0) + + tx = ( + TransferTransaction() + .add_hbar_transfer(env.operator_id, Hbar(-0.01).to_tinybars()) + .add_hbar_transfer(receiver.id, Hbar(0.01).to_tinybars()) + ) + + receipt = tx.execute(env.client) + assert receipt.status == ResponseCode.SUCCESS + + return _extract_tx_id(tx, receipt) + + +@pytest.mark.integration +def test_get_receipt_query_children_empty_when_not_requested_e2e(env): + """ + E2E: + When include_children is NOT requested, receipt.children must be empty. + """ + tx_id = _submit_simple_transfer(env) + + receipt = ( + TransactionGetReceiptQuery() + .set_transaction_id(tx_id) + .execute(env.client) + ) + + assert receipt.status == ResponseCode.SUCCESS + assert receipt.children == [] + + +@pytest.mark.integration +def test_get_receipt_query_children_list_when_requested_e2e(env): + """ + E2E: + When include_children IS requested, receipt.children must exist and be a list. + The list may be empty depending on transaction type. + """ + tx_id = _submit_simple_transfer(env) + + receipt = ( + TransactionGetReceiptQuery() + .set_transaction_id(tx_id) + .set_include_children(True) + .execute(env.client) + ) + + assert receipt.status == ResponseCode.SUCCESS + assert isinstance(receipt.children, list) + + +@pytest.mark.integration +def test_get_receipt_query_children_with_contract_execute_e2e(env): + """ + E2E: + Execute a real contract transaction and query its receipt with include_children enabled. + + We assert: + - no crash + - receipt.children exists and is a list + """ + from examples.contract.contracts.contract_utils import ( + CONTRACT_DEPLOY_GAS, + STATEFUL_CONTRACT_BYTECODE, + ) + from hiero_sdk_python.file.file_create_transaction import FileCreateTransaction + from hiero_sdk_python.contract.contract_create_transaction import ContractCreateTransaction + from hiero_sdk_python.contract.contract_execute_transaction import ContractExecuteTransaction + from hiero_sdk_python.contract.contract_function_parameters import ContractFunctionParameters + + # Upload contract bytecode + file_receipt = ( + FileCreateTransaction() + .set_keys(env.operator_key.public_key()) + .set_contents(STATEFUL_CONTRACT_BYTECODE) + .set_file_memo("transaction receipt children test") + .execute(env.client) + ) + assert file_receipt.status == ResponseCode.SUCCESS + file_id = file_receipt.file_id + assert file_id is not None + + # Deploy contract + constructor_params = ContractFunctionParameters().add_bytes32( + b"Initial message from constructor" + ) + contract_receipt = ( + ContractCreateTransaction() + .set_admin_key(env.operator_key.public_key()) + .set_gas(CONTRACT_DEPLOY_GAS) + .set_constructor_parameters(constructor_params) + .set_bytecode_file_id(file_id) + .execute(env.client) + ) + assert contract_receipt.status == ResponseCode.SUCCESS + contract_id = contract_receipt.contract_id + assert contract_id is not None + + # Execute contract function + execute_tx = ( + ContractExecuteTransaction() + .set_contract_id(contract_id) + .set_gas(1_000_000) + .set_function( + "setMessage", + ContractFunctionParameters().add_bytes32(b"Updated message".ljust(32, b"\x00")), + ) + ) + execute_receipt = execute_tx.execute(env.client) + assert execute_receipt.status == ResponseCode.SUCCESS + + try: + tx_id = _extract_tx_id(execute_tx, execute_receipt) + except AssertionError as e: + pytest.skip(str(e)) + + queried = ( + TransactionGetReceiptQuery() + .set_transaction_id(tx_id) + .set_include_children(True) + .execute(env.client) + ) + + assert queried.status == ResponseCode.SUCCESS + + if len(queried.children) > 0: + for child in queried.children: + assert child.status is not None + + assert isinstance(queried.children, list) diff --git a/tests/integration/transaction_record_query_e2e_test.py b/tests/integration/transaction_record_query_e2e_test.py index 97e2519c2..7885b6c9c 100644 --- a/tests/integration/transaction_record_query_e2e_test.py +++ b/tests/integration/transaction_record_query_e2e_test.py @@ -8,7 +8,7 @@ from hiero_sdk_python.tokens.token_associate_transaction import TokenAssociateTransaction from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction -from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token, create_nft_token +from tests.integration.utils import IntegrationTestEnv, create_fungible_token, create_nft_token @pytest.mark.integration def test_transaction_record_query_can_execute(): diff --git a/tests/integration/transfer_transaction_e2e_test.py b/tests/integration/transfer_transaction_e2e_test.py index 1e037031b..7b0b2cb78 100644 --- a/tests/integration/transfer_transaction_e2e_test.py +++ b/tests/integration/transfer_transaction_e2e_test.py @@ -7,6 +7,7 @@ from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.exceptions import PrecheckError from hiero_sdk_python.hbar import Hbar +from hiero_sdk_python.hbar_unit import HbarUnit from hiero_sdk_python.query.account_balance_query import CryptoGetAccountBalanceQuery from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.tokens.nft_id import NftId @@ -14,7 +15,7 @@ from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction from hiero_sdk_python.transaction.transaction_id import TransactionId from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction -from tests.integration.utils_for_test import ( +from tests.integration.utils import ( IntegrationTestEnv, create_fungible_token, create_nft_token, @@ -448,3 +449,47 @@ def test_integration_transfer_transaction_approved_token_transfer(): finally: env.close() + + +@pytest.mark.integration +def test_integration_transfer_transaction_with_hbar_units(): + env = IntegrationTestEnv() + + try: + new_account_private_key = PrivateKey.generate() + new_account_public_key = new_account_private_key.public_key() + + initial_balance = Hbar(1) + + account_transaction = AccountCreateTransaction( + key=new_account_public_key, initial_balance=initial_balance, memo="Recipient Account" + ) + + receipt = account_transaction.execute(env.client) + + assert ( + receipt.status == ResponseCode.SUCCESS + ), f"Account creation failed with status: {ResponseCode(receipt.status).name}" + + account_id = receipt.account_id + assert account_id is not None + + transfer_transaction = TransferTransaction() + transfer_transaction.add_hbar_transfer(env.operator_id, Hbar(-1.5, HbarUnit.HBAR)) + transfer_transaction.add_hbar_transfer(account_id, Hbar(1.5, HbarUnit.HBAR)) + + receipt = transfer_transaction.execute(env.client) + + assert ( + receipt.status == ResponseCode.SUCCESS + ), f"Transfer failed with status: {ResponseCode(receipt.status).name}" + + query_transaction = CryptoGetAccountBalanceQuery(account_id) + balance = query_transaction.execute(env.client) + + expected_balance_tinybars = Hbar(1).to_tinybars() + Hbar(1.5, HbarUnit.HBAR).to_tinybars() + assert ( + balance and balance.hbars.to_tinybars() == expected_balance_tinybars + ), f"Expected balance: {expected_balance_tinybars}, actual balance: {balance.hbars.to_tinybars()}" + finally: + env.close() diff --git a/tests/integration/utils_for_test.py b/tests/integration/utils.py similarity index 100% rename from tests/integration/utils_for_test.py rename to tests/integration/utils.py diff --git a/tests/unit/test_account_allowance_approve_transaction.py b/tests/unit/account_allowance_approve_transaction_test.py similarity index 100% rename from tests/unit/test_account_allowance_approve_transaction.py rename to tests/unit/account_allowance_approve_transaction_test.py diff --git a/tests/unit/test_account_allowance_delete_transaction.py b/tests/unit/account_allowance_delete_transaction_test.py similarity index 100% rename from tests/unit/test_account_allowance_delete_transaction.py rename to tests/unit/account_allowance_delete_transaction_test.py diff --git a/tests/unit/test_account_balance_query.py b/tests/unit/account_balance_query_test.py similarity index 100% rename from tests/unit/test_account_balance_query.py rename to tests/unit/account_balance_query_test.py diff --git a/tests/unit/test_account_create_transaction.py b/tests/unit/account_create_transaction_test.py similarity index 100% rename from tests/unit/test_account_create_transaction.py rename to tests/unit/account_create_transaction_test.py diff --git a/tests/unit/test_account_delete_transaction.py b/tests/unit/account_delete_transaction_test.py similarity index 100% rename from tests/unit/test_account_delete_transaction.py rename to tests/unit/account_delete_transaction_test.py diff --git a/tests/unit/test_account_id.py b/tests/unit/account_id_test.py similarity index 100% rename from tests/unit/test_account_id.py rename to tests/unit/account_id_test.py diff --git a/tests/unit/test_account_info_query.py b/tests/unit/account_info_query_test.py similarity index 100% rename from tests/unit/test_account_info_query.py rename to tests/unit/account_info_query_test.py diff --git a/tests/unit/test_account_info.py b/tests/unit/account_info_test.py similarity index 92% rename from tests/unit/test_account_info.py rename to tests/unit/account_info_test.py index b75951de9..e645ffba2 100644 --- a/tests/unit/test_account_info.py +++ b/tests/unit/account_info_test.py @@ -169,3 +169,20 @@ def test_proto_conversion(account_info): assert converted_account_info.token_relationships == account_info.token_relationships assert converted_account_info.account_memo == account_info.account_memo assert converted_account_info.owned_nfts == account_info.owned_nfts + +def test_str_and_repr(account_info): + """Test the __str__ and __repr__ methods""" + info_str = str(account_info) + info_repr = repr(account_info) + + # __str__ checks (User-friendly output) + assert "Account ID: 0.0.100" in info_str + assert "Contract Account ID: 0.0.100" in info_str + assert "Balance: 0.05000000 ℏ" in info_str + assert "Memo: Test account memo" in info_str + + # __repr__ checks (Debug output) + assert info_repr.startswith("AccountInfo(") + assert "account_id=AccountId(shard=0, realm=0, num=100" in info_repr + assert "contract_account_id='0.0.100'" in info_repr + assert "account_memo='Test account memo'" in info_repr \ No newline at end of file diff --git a/tests/unit/test_account_records_query.py b/tests/unit/account_records_query_test.py similarity index 100% rename from tests/unit/test_account_records_query.py rename to tests/unit/account_records_query_test.py diff --git a/tests/unit/test_account_update_transaction.py b/tests/unit/account_update_transaction_test.py similarity index 100% rename from tests/unit/test_account_update_transaction.py rename to tests/unit/account_update_transaction_test.py diff --git a/tests/unit/test_batch_transaction.py b/tests/unit/batch_transaction_test.py similarity index 72% rename from tests/unit/test_batch_transaction.py rename to tests/unit/batch_transaction_test.py index 0394ae641..ff28a5045 100644 --- a/tests/unit/test_batch_transaction.py +++ b/tests/unit/batch_transaction_test.py @@ -15,6 +15,7 @@ ) from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.crypto.public_key import PublicKey from hiero_sdk_python.hapi.services.transaction_pb2 import AtomicBatchTransactionBody from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.system.freeze_transaction import FreezeTransaction @@ -366,3 +367,159 @@ def test_batch_transaction_execute_successful(mock_account_ids, mock_client): receipt = transaction.execute(client) assert receipt.status == ResponseCode.SUCCESS, f"Transaction should have succeeded, got {receipt.status}" + + +def test_batch_key_accepts_public_key(mock_client, mock_account_ids): + """Test that batch_key can accept PublicKey (not just PrivateKey).""" + sender, receiver, _, _, _ = mock_account_ids + + # Generate a key pair + private_key = PrivateKey.generate_ed25519() + public_key = private_key.public_key() + + # Test using PublicKey as batch_key + tx = ( + TransferTransaction() + .add_hbar_transfer(account_id=sender, amount=-1) + .add_hbar_transfer(account_id=receiver, amount=1) + .set_batch_key(public_key) # Using PublicKey instead of PrivateKey + ) + + # Verify batch_key was set correctly + assert tx.batch_key == public_key + assert isinstance(tx.batch_key, type(public_key)) + + +def test_batchify_with_public_key(mock_client, mock_account_ids): + """Test that batchify method accepts PublicKey.""" + sender, receiver, _, _, _ = mock_account_ids + + # Generate a key pair + private_key = PrivateKey.generate_ed25519() + public_key = private_key.public_key() + + # Test using PublicKey in batchify + tx = ( + TransferTransaction() + .add_hbar_transfer(account_id=sender, amount=-1) + .add_hbar_transfer(account_id=receiver, amount=1) + .batchify(mock_client, public_key) # Using PublicKey + ) + + # Verify batch_key was set and transaction was frozen + assert tx.batch_key == public_key + assert tx._transaction_body_bytes # Should be frozen + + +def test_batch_transaction_with_public_key_inner_transactions(mock_client, mock_account_ids): + """Test BatchTransaction can accept inner transactions with PublicKey batch_keys.""" + sender, receiver, _, _, _ = mock_account_ids + + # Generate key pairs + batch_key1 = PrivateKey.generate_ed25519() + public_key1 = batch_key1.public_key() + + batch_key2 = PrivateKey.generate_ecdsa() + public_key2 = batch_key2.public_key() + + # Create inner transactions with PublicKey batch_keys + inner_tx1 = ( + TransferTransaction() + .add_hbar_transfer(account_id=sender, amount=-1) + .add_hbar_transfer(account_id=receiver, amount=1) + .set_batch_key(public_key1) + .freeze_with(mock_client) + ) + + inner_tx2 = ( + TransferTransaction() + .add_hbar_transfer(account_id=sender, amount=-1) + .add_hbar_transfer(account_id=receiver, amount=1) + .set_batch_key(public_key2) + .freeze_with(mock_client) + ) + + # BatchTransaction should accept these inner transactions + batch_tx = BatchTransaction(inner_transactions=[inner_tx1, inner_tx2]) + + assert len(batch_tx.inner_transactions) == 2 + assert batch_tx.inner_transactions[0].batch_key == public_key1 + assert batch_tx.inner_transactions[1].batch_key == public_key2 + + +def test_batch_key_mixed_private_and_public_keys(mock_client, mock_account_ids): + """Test that BatchTransaction can handle inner transactions with mixed PrivateKey and PublicKey.""" + sender, receiver, _, _, _ = mock_account_ids + + # Generate keys + private_key = PrivateKey.generate_ed25519() + public_key = PrivateKey.generate_ecdsa().public_key() + + # Inner transaction with PrivateKey + inner_tx1 = ( + TransferTransaction() + .add_hbar_transfer(account_id=sender, amount=-1) + .add_hbar_transfer(account_id=receiver, amount=1) + .set_batch_key(private_key) + .freeze_with(mock_client) + ) + + # Inner transaction with PublicKey + inner_tx2 = ( + TransferTransaction() + .add_hbar_transfer(account_id=sender, amount=-1) + .add_hbar_transfer(account_id=receiver, amount=1) + .set_batch_key(public_key) + .freeze_with(mock_client) + ) + + # BatchTransaction should accept mixed key types + batch_tx = BatchTransaction(inner_transactions=[inner_tx1, inner_tx2]) + + assert len(batch_tx.inner_transactions) == 2 + assert isinstance(batch_tx.inner_transactions[0].batch_key, PrivateKey) + assert isinstance(batch_tx.inner_transactions[1].batch_key, type(public_key)) + + +def test_set_batch_key_with_private_key(): + """Test that batch_key can be set with PrivateKey.""" + private_key = PrivateKey.generate_ed25519() + transaction = TransferTransaction() + + result = transaction.set_batch_key(private_key) + + assert transaction.batch_key == private_key + assert result == transaction # Check method chaining + + +def test_set_batch_key_with_public_key(): + """Test that batch_key can be set with PublicKey.""" + private_key = PrivateKey.generate_ed25519() + public_key = private_key.public_key() + transaction = TransferTransaction() + + result = transaction.set_batch_key(public_key) + + assert transaction.batch_key == public_key + assert result == transaction # Check method chaining + + +def test_batch_key_type_annotation(): + """Test that batch_key accepts both PrivateKey and PublicKey types.""" + transaction = TransferTransaction() + + # Test with PrivateKey + private_key = PrivateKey.generate_ecdsa() + transaction.set_batch_key(private_key) + assert isinstance(transaction.batch_key, PrivateKey) + + # Test with PublicKey + public_key = private_key.public_key() + transaction.set_batch_key(public_key) + assert isinstance(transaction.batch_key, PublicKey) + + +def test_batch_key_none_by_default(): + """Test that batch_key is None by default.""" + transaction = TransferTransaction() + assert transaction.batch_key is None diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 6302b8966..96fc6674b 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,15 +1,16 @@ +import hashlib import time import pytest from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.address_book.node_address import NodeAddress from hiero_sdk_python.client.client import Client from hiero_sdk_python.client.network import Network from hiero_sdk_python.consensus.topic_id import TopicId from hiero_sdk_python.contract.contract_id import ContractId from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.file.file_id import FileId -from hiero_sdk_python.hapi.services import timestamp_pb2 from hiero_sdk_python.logger.log_level import LogLevel from hiero_sdk_python.node import _Node from hiero_sdk_python.tokens.token_id import TokenId @@ -18,6 +19,12 @@ from hiero_sdk_python.transaction.transaction_id import TransactionId +FAKE_CERT_PEM = b"""-----BEGIN CERTIFICATE----- +MIIBszCCAVmgAwIBAgIUQFakeFakeFakeFakeFakeFakeFakewCgYIKoZIzj0EAwIw +-----END CERTIFICATE-----""" + +FAKE_CERT_HASH = hashlib.sha384(FAKE_CERT_PEM).hexdigest().encode("utf-8") + @pytest.fixture def mock_account_ids(): """Fixture to provide mock account IDs and token IDs.""" @@ -78,7 +85,16 @@ def contract_id(): @pytest.fixture def mock_client(): """Fixture to provide a mock client with hardcoded nodes for testing purposes.""" - nodes = [_Node(AccountId(0, 0, 3), "node1.example.com:50211", None)] + # Mock Node + node = _Node( + AccountId(0, 0, 3), + "node1.example.com:50211", + address_book=NodeAddress(cert_hash=FAKE_CERT_HASH, addresses=[]) + ) + node._fetch_server_certificate_pem = lambda: FAKE_CERT_PEM + + nodes = [node] + network = Network(nodes=nodes) client = Client(network) client.logger.set_level(LogLevel.DISABLED) diff --git a/tests/unit/test_contract_bytecode_query.py b/tests/unit/contract_bytecode_query_test.py similarity index 100% rename from tests/unit/test_contract_bytecode_query.py rename to tests/unit/contract_bytecode_query_test.py diff --git a/tests/unit/test_contract_call_query.py b/tests/unit/contract_call_query_test.py similarity index 100% rename from tests/unit/test_contract_call_query.py rename to tests/unit/contract_call_query_test.py diff --git a/tests/unit/test_contract_create_transaction.py b/tests/unit/contract_create_transaction_test.py similarity index 100% rename from tests/unit/test_contract_create_transaction.py rename to tests/unit/contract_create_transaction_test.py diff --git a/tests/unit/test_contract_delete_transaction.py b/tests/unit/contract_delete_transaction_test.py similarity index 100% rename from tests/unit/test_contract_delete_transaction.py rename to tests/unit/contract_delete_transaction_test.py diff --git a/tests/unit/test_contract_execute_transaction.py b/tests/unit/contract_execute_transaction_test.py similarity index 100% rename from tests/unit/test_contract_execute_transaction.py rename to tests/unit/contract_execute_transaction_test.py diff --git a/tests/unit/test_contract_function_result.py b/tests/unit/contract_function_result_test.py similarity index 100% rename from tests/unit/test_contract_function_result.py rename to tests/unit/contract_function_result_test.py diff --git a/tests/unit/test_contract_id.py b/tests/unit/contract_id_test.py similarity index 100% rename from tests/unit/test_contract_id.py rename to tests/unit/contract_id_test.py diff --git a/tests/unit/test_contract_info_query.py b/tests/unit/contract_info_query_test.py similarity index 100% rename from tests/unit/test_contract_info_query.py rename to tests/unit/contract_info_query_test.py diff --git a/tests/unit/test_contract_info.py b/tests/unit/contract_info_test.py similarity index 100% rename from tests/unit/test_contract_info.py rename to tests/unit/contract_info_test.py diff --git a/tests/unit/test_contract_log_info.py b/tests/unit/contract_log_info_test.py similarity index 100% rename from tests/unit/test_contract_log_info.py rename to tests/unit/contract_log_info_test.py diff --git a/tests/unit/test_contract_nonce_info.py b/tests/unit/contract_nonce_info_test.py similarity index 100% rename from tests/unit/test_contract_nonce_info.py rename to tests/unit/contract_nonce_info_test.py diff --git a/tests/unit/test_contract_update_transaction.py b/tests/unit/contract_update_transaction_test.py similarity index 100% rename from tests/unit/test_contract_update_transaction.py rename to tests/unit/contract_update_transaction_test.py diff --git a/tests/unit/test_crypto_utils.py b/tests/unit/crypto_utils_test.py similarity index 100% rename from tests/unit/test_crypto_utils.py rename to tests/unit/crypto_utils_test.py diff --git a/tests/unit/test_custom_fee_limit.py b/tests/unit/custom_fee_limit_test.py similarity index 100% rename from tests/unit/test_custom_fee_limit.py rename to tests/unit/custom_fee_limit_test.py diff --git a/tests/unit/test_custom_fee.py b/tests/unit/custom_fee_test.py similarity index 100% rename from tests/unit/test_custom_fee.py rename to tests/unit/custom_fee_test.py diff --git a/tests/unit/test_entity_id_helper.py b/tests/unit/entity_id_helper_test.py similarity index 100% rename from tests/unit/test_entity_id_helper.py rename to tests/unit/entity_id_helper_test.py diff --git a/tests/unit/test_ethereum_transaction.py b/tests/unit/ethereum_transaction_test.py similarity index 100% rename from tests/unit/test_ethereum_transaction.py rename to tests/unit/ethereum_transaction_test.py diff --git a/tests/unit/test_evm_address.py b/tests/unit/evm_address_test.py similarity index 100% rename from tests/unit/test_evm_address.py rename to tests/unit/evm_address_test.py diff --git a/tests/unit/test_executable.py b/tests/unit/executable_test.py similarity index 100% rename from tests/unit/test_executable.py rename to tests/unit/executable_test.py diff --git a/tests/unit/test_file_append_transaction.py b/tests/unit/file_append_transaction_test.py similarity index 100% rename from tests/unit/test_file_append_transaction.py rename to tests/unit/file_append_transaction_test.py diff --git a/tests/unit/test_file_contents_query.py b/tests/unit/file_contents_query_test.py similarity index 100% rename from tests/unit/test_file_contents_query.py rename to tests/unit/file_contents_query_test.py diff --git a/tests/unit/test_file_create_transaction.py b/tests/unit/file_create_transaction_test.py similarity index 100% rename from tests/unit/test_file_create_transaction.py rename to tests/unit/file_create_transaction_test.py diff --git a/tests/unit/test_file_delete_transaction.py b/tests/unit/file_delete_transaction_test.py similarity index 100% rename from tests/unit/test_file_delete_transaction.py rename to tests/unit/file_delete_transaction_test.py diff --git a/tests/unit/test_file_id.py b/tests/unit/file_id_test.py similarity index 100% rename from tests/unit/test_file_id.py rename to tests/unit/file_id_test.py diff --git a/tests/unit/test_file_info_query.py b/tests/unit/file_info_query_test.py similarity index 100% rename from tests/unit/test_file_info_query.py rename to tests/unit/file_info_query_test.py diff --git a/tests/unit/test_file_info.py b/tests/unit/file_info_test.py similarity index 100% rename from tests/unit/test_file_info.py rename to tests/unit/file_info_test.py diff --git a/tests/unit/test_file_update_transaction.py b/tests/unit/file_update_transaction_test.py similarity index 100% rename from tests/unit/test_file_update_transaction.py rename to tests/unit/file_update_transaction_test.py diff --git a/tests/unit/test_freeze_transaction.py b/tests/unit/freeze_transaction_test.py similarity index 100% rename from tests/unit/test_freeze_transaction.py rename to tests/unit/freeze_transaction_test.py diff --git a/tests/unit/test_freeze_type.py b/tests/unit/freeze_type_test.py similarity index 100% rename from tests/unit/test_freeze_type.py rename to tests/unit/freeze_type_test.py diff --git a/tests/unit/get_receipt_query_test.py b/tests/unit/get_receipt_query_test.py new file mode 100644 index 000000000..5f5018d2c --- /dev/null +++ b/tests/unit/get_receipt_query_test.py @@ -0,0 +1,253 @@ +"""Tests for the TransactionGetReceiptQuery functionality.""" + +import pytest +from unittest.mock import patch + +from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.exceptions import MaxAttemptsError +from hiero_sdk_python.hapi.services import ( + basic_types_pb2, + response_header_pb2, + response_pb2, + transaction_get_receipt_pb2, + transaction_receipt_pb2, +) +from hiero_sdk_python.query.transaction_get_receipt_query import TransactionGetReceiptQuery +from hiero_sdk_python.response_code import ResponseCode + +from tests.unit.mock_server import mock_hedera_servers + +pytestmark = pytest.mark.unit + +# This test uses fixture transaction_id as parameter +def test_transaction_get_receipt_query(transaction_id): + """Test basic functionality of TransactionGetReceiptQuery with a mocked client.""" + response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.SUCCESS + ), + ) + ) + + response_sequences = [[response]] + + with mock_hedera_servers(response_sequences) as client: + query = TransactionGetReceiptQuery().set_transaction_id(transaction_id) + + try: + result = query.execute(client) + except Exception as e: + pytest.fail(f"Unexpected exception raised: {e}") + + assert result.status == ResponseCode.SUCCESS + + +# This test uses fixture transaction_id as parameter +def test_receipt_query_retry_on_receipt_not_found(transaction_id): + """Test that receipt query retries when the receipt status is RECEIPT_NOT_FOUND.""" + # First response has RECEIPT_NOT_FOUND, second has SUCCESS + not_found_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.RECEIPT_NOT_FOUND + ), + ) + ) + + success_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.SUCCESS, + accountID=basic_types_pb2.AccountID( + shardNum=0, realmNum=0, accountNum=1234 + ), + ), + ) + ) + + response_sequences = [[not_found_response, success_response]] + + with ( + mock_hedera_servers(response_sequences) as client, + patch("time.sleep") as mock_sleep, + ): + query = TransactionGetReceiptQuery().set_transaction_id(transaction_id) + + try: + result = query.execute(client) + except Exception as e: + pytest.fail(f"Should not raise exception, but raised: {e}") + + # Verify query was successful after retry + assert result.status == ResponseCode.SUCCESS + + # Verify we slept once for the retry + assert mock_sleep.call_count == 1, "Should have retried once" + + # Verify we didn't switch nodes (RECEIPT_NOT_FOUND is retriable without node switch) + assert client.network.current_node._account_id == AccountId(0, 0, 3) + + +# This test uses fixture transaction_id as parameter +def test_receipt_query_receipt_status_error(transaction_id): + """Test that receipt query fails on receipt status error.""" + # Create a response with a receipt status error + error_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.UNKNOWN + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.UNKNOWN + ), + ) + ) + + response_sequences = [[error_response]] + + with mock_hedera_servers(response_sequences) as client, patch("time.sleep"): + client.max_attempts = 1 + query = TransactionGetReceiptQuery().set_transaction_id(transaction_id) + + # Create the query and verify it fails with the expected error + with pytest.raises(MaxAttemptsError) as exc_info: + query.execute(client) + + assert str( + f"Receipt for transaction {transaction_id} contained error status: UNKNOWN ({ResponseCode.UNKNOWN})" + ) in str(exc_info.value) + + +def test_receipt_query_does_not_require_payment(): + """Test that the receipt query does not require payment.""" + query = TransactionGetReceiptQuery() + assert not query._is_payment_required() + + +def test_transaction_get_receipt_query_sets_include_child_receipts_in_request( + transaction_id, +): + """ + Test that _make_request() sets include_child_receipts correctly in the protobuf query. + """ + query = ( + TransactionGetReceiptQuery() + .set_transaction_id(transaction_id) + .set_include_children(True) + ) + + request = query._make_request() + + # request is query_pb2.Query and should contain transactionGetReceipt + assert request.transactionGetReceipt.include_child_receipts is True + + +def test_transaction_get_receipt_query_returns_child_receipts_when_requested( + transaction_id, +): + """ + Test that execute() maps child_transaction_receipts into TransactionReceipt.children + when include_child_receipts is enabled and the network returns child receipts. + """ + response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.SUCCESS + ), + child_transaction_receipts=[ + transaction_receipt_pb2.TransactionReceipt(status=ResponseCode.SUCCESS), + transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.FAIL_INVALID + ), + ], + ) + ) + + response_sequences = [[response]] + + with mock_hedera_servers(response_sequences) as client: + query = ( + TransactionGetReceiptQuery() + .set_transaction_id(transaction_id) + .set_include_children(True) + ) + + result = query.execute(client) + + assert query.include_children is True + assert len(response.transactionGetReceipt.child_transaction_receipts) == 2 + assert result.status == ResponseCode.SUCCESS + assert len(result.children) == 2 + assert result.children[0].status == ResponseCode.SUCCESS + assert result.children[1].status == ResponseCode.FAIL_INVALID + + +def test_transaction_get_receipt_query_children_empty_when_not_requested( + transaction_id, +): + """ + Test that execute() does not populate children by default (backward compatible behavior), + even if the network includes child_transaction_receipts in the response. + """ + response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.SUCCESS + ), + child_transaction_receipts=[ + transaction_receipt_pb2.TransactionReceipt(status=ResponseCode.SUCCESS), + ], + ) + ) + + response_sequences = [[response]] + + with mock_hedera_servers(response_sequences) as client: + query = ( + TransactionGetReceiptQuery().set_transaction_id(transaction_id) + ) + + result = query.execute(client) + + assert result.status == ResponseCode.SUCCESS + assert result.children == [] + + +def test_transaction_get_receipt_query_include_children_with_no_children(transaction_id): + """ Testing that nothing explode if no children ar passed""" + response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader(nodeTransactionPrecheckCode=ResponseCode.OK), + receipt=transaction_receipt_pb2.TransactionReceipt(status=ResponseCode.SUCCESS), + # no child_transaction_receipts + ) + ) + + response_sequences = [[response]] + + with mock_hedera_servers(response_sequences) as client: + query = ( + TransactionGetReceiptQuery() + .set_transaction_id(transaction_id) + .set_include_children(True) + ) + result = query.execute(client) + + assert result.status == ResponseCode.SUCCESS + assert result.children == [] diff --git a/tests/unit/test_hbar_allowance.py b/tests/unit/hbar_allowance_test.py similarity index 100% rename from tests/unit/test_hbar_allowance.py rename to tests/unit/hbar_allowance_test.py diff --git a/tests/unit/test_hbar.py b/tests/unit/hbar_test.py similarity index 100% rename from tests/unit/test_hbar.py rename to tests/unit/hbar_test.py diff --git a/tests/unit/test_hbar_transfer.py b/tests/unit/hbar_transfer_test.py similarity index 100% rename from tests/unit/test_hbar_transfer.py rename to tests/unit/hbar_transfer_test.py diff --git a/tests/unit/test_hedera_trust_manager.py b/tests/unit/hedera_trust_manager_test.py similarity index 100% rename from tests/unit/test_hedera_trust_manager.py rename to tests/unit/hedera_trust_manager_test.py diff --git a/tests/unit/test_key_utils.py b/tests/unit/key_utils_test.py similarity index 100% rename from tests/unit/test_key_utils.py rename to tests/unit/key_utils_test.py diff --git a/tests/unit/test_keys_private.py b/tests/unit/keys_private_test.py similarity index 100% rename from tests/unit/test_keys_private.py rename to tests/unit/keys_private_test.py diff --git a/tests/unit/test_keys_public.py b/tests/unit/keys_public_test.py similarity index 100% rename from tests/unit/test_keys_public.py rename to tests/unit/keys_public_test.py diff --git a/tests/unit/test_logger.py b/tests/unit/logger_test.py similarity index 100% rename from tests/unit/test_logger.py rename to tests/unit/logger_test.py diff --git a/tests/unit/test_managed_node_address.py b/tests/unit/managed_node_address_test.py similarity index 100% rename from tests/unit/test_managed_node_address.py rename to tests/unit/managed_node_address_test.py diff --git a/tests/unit/test_network_tls.py b/tests/unit/network_tls_test.py similarity index 100% rename from tests/unit/test_network_tls.py rename to tests/unit/network_tls_test.py diff --git a/tests/unit/test_node_address.py b/tests/unit/node_address_test.py similarity index 100% rename from tests/unit/test_node_address.py rename to tests/unit/node_address_test.py diff --git a/tests/unit/test_node_create_transaction.py b/tests/unit/node_create_transaction_test.py similarity index 100% rename from tests/unit/test_node_create_transaction.py rename to tests/unit/node_create_transaction_test.py diff --git a/tests/unit/test_node_delete_transaction.py b/tests/unit/node_delete_transaction_test.py similarity index 100% rename from tests/unit/test_node_delete_transaction.py rename to tests/unit/node_delete_transaction_test.py diff --git a/tests/unit/test_node_tls.py b/tests/unit/node_tls_test.py similarity index 65% rename from tests/unit/test_node_tls.py rename to tests/unit/node_tls_test.py index bc7d34d58..1898ba79d 100644 --- a/tests/unit/test_node_tls.py +++ b/tests/unit/node_tls_test.py @@ -1,11 +1,9 @@ """Unit tests for TLS functionality in _Node.""" import hashlib -import socket import ssl from unittest.mock import Mock, patch, MagicMock import pytest -import grpc -from src.hiero_sdk_python.node import _Node, _HederaTrustManager +from src.hiero_sdk_python.node import _Node from src.hiero_sdk_python.account.account_id import AccountId from src.hiero_sdk_python.address_book.node_address import NodeAddress from src.hiero_sdk_python.address_book.endpoint import Endpoint @@ -89,7 +87,7 @@ def test_node_apply_transport_security_closes_channel(mock_node_with_address_boo node._verify_certificates = False # Create a channel first - with patch('grpc.secure_channel') as mock_secure: + with patch('grpc.secure_channel') as mock_secure, patch.object(node, "_fetch_server_certificate_pem", return_value=b"dummy-cert"): mock_channel = Mock() mock_secure.return_value = mock_channel node._get_channel() @@ -121,7 +119,7 @@ def test_node_set_verify_certificates_idempotent(mock_node_with_address_book): assert node._verify_certificates == initial_state -def test_node_build_channel_options_with_hostname_override(mock_address_book): +def test_node_build_channel_options_with_hostname_not_override(): """Test channel options include hostname override when domain differs from address.""" endpoint = Endpoint(address=b"127.0.0.1", port=50212, domain_name="node.example.com") address_book = NodeAddress( @@ -133,10 +131,10 @@ def test_node_build_channel_options_with_hostname_override(mock_address_book): options = node._build_channel_options() assert options is not None - assert ('grpc.ssl_target_name_override', 'node.example.com') in options + assert ('grpc.ssl_target_name_override', 'node.example.com') not in options -def test_node_build_channel_options_no_override_when_same(mock_address_book): +def test_node_build_channel_options_no_override_when_same(): """Test channel options don't include override when hostname matches address.""" endpoint = Endpoint(address=b"node.example.com", port=50212, domain_name="node.example.com") address_book = NodeAddress( @@ -147,14 +145,26 @@ def test_node_build_channel_options_no_override_when_same(mock_address_book): node = _Node(AccountId(0, 0, 3), "node.example.com:50212", address_book) options = node._build_channel_options() - assert options is None + assert options == [ + ("grpc.default_authority", "127.0.0.1"), + ("grpc.ssl_target_name_override", "127.0.0.1"), + ("grpc.keepalive_time_ms", 100000), + ("grpc.keepalive_timeout_ms", 10000), + ("grpc.keepalive_permit_without_calls", 1) + ] -def test_node_build_channel_options_no_override_without_address_book(mock_node_without_address_book): +def test_node_build_channel_options_override_localhost_without_address_book(mock_node_without_address_book): """Test channel options don't include override without address book.""" node = mock_node_without_address_book options = node._build_channel_options() - assert options is None + assert options == [ + ("grpc.default_authority", "127.0.0.1"), + ("grpc.ssl_target_name_override", "127.0.0.1"), + ("grpc.keepalive_time_ms", 100000), + ("grpc.keepalive_timeout_ms", 10000), + ("grpc.keepalive_permit_without_calls", 1) + ] @patch('socket.create_connection') @@ -189,6 +199,7 @@ def test_node_validate_tls_certificate_with_trust_manager(mock_node_with_address # Update address book with matching hash node._address_book._cert_hash = cert_hash.encode('utf-8') + node._node_pem_cert = pem_cert with patch.object(node, '_fetch_server_certificate_pem', return_value=pem_cert): # Should not raise @@ -203,10 +214,10 @@ def test_node_validate_tls_certificate_hash_mismatch(mock_node_with_address_book pem_cert = b"-----BEGIN CERTIFICATE-----\nTEST\n-----END CERTIFICATE-----\n" wrong_hash = b"wrong_hash" node._address_book._cert_hash = wrong_hash + node._node_pem_cert = pem_cert - with patch.object(node, '_fetch_server_certificate_pem', return_value=pem_cert): - with pytest.raises(ValueError, match="Failed to confirm the server's certificate"): - node._validate_tls_certificate_with_trust_manager() + with pytest.raises(ValueError, match="Failed to confirm the server's certificate"): + node._validate_tls_certificate_with_trust_manager() def test_node_validate_tls_certificate_no_verification(mock_node_with_address_book): @@ -236,17 +247,18 @@ def test_node_get_channel_secure(mock_insecure, mock_secure, mock_node_with_addr node = mock_node_with_address_book node._address = node._address._to_secure() # Ensure TLS is enabled - mock_channel = Mock() - mock_secure.return_value = mock_channel + with patch.object(node, "_fetch_server_certificate_pem", return_value=b"dummy-cert"): + mock_channel = Mock() + mock_secure.return_value = mock_channel - # Skip certificate validation for this test - node._verify_certificates = False + # Skip certificate validation for this test + node._verify_certificates = False - channel = node._get_channel() + channel = node._get_channel() - mock_secure.assert_called_once() - mock_insecure.assert_not_called() - assert channel is not None + mock_secure.assert_called_once() + mock_insecure.assert_not_called() + assert channel is not None @patch('grpc.secure_channel') @@ -272,15 +284,16 @@ def test_node_get_channel_reuses_existing(mock_insecure, mock_secure, mock_node_ node = mock_node_with_address_book node._verify_certificates = False - mock_channel = Mock() - mock_secure.return_value = mock_channel - - channel1 = node._get_channel() - channel2 = node._get_channel() - - # Should only create channel once - assert mock_secure.call_count == 1 - assert channel1 is channel2 + with patch.object(node, "_fetch_server_certificate_pem", return_value=b"dummy-cert"): + mock_channel = Mock() + mock_secure.return_value = mock_channel + + channel1 = node._get_channel() + channel2 = node._get_channel() + + # Should only create channel once + assert mock_secure.call_count == 1 + assert channel1 is channel2 def test_node_set_root_certificates(mock_node_with_address_book): @@ -297,7 +310,8 @@ def test_node_set_root_certificates_closes_channel(mock_node_with_address_book): node = mock_node_with_address_book node._verify_certificates = False - with patch('grpc.secure_channel') as mock_secure: + with patch('grpc.secure_channel') as mock_secure, patch.object(node, "_fetch_server_certificate_pem", return_value=b"dummy-cert"): + mock_channel = Mock() mock_secure.return_value = mock_channel node._get_channel() @@ -307,3 +321,88 @@ def test_node_set_root_certificates_closes_channel(mock_node_with_address_book): # Channel should be closed to force recreation assert node._channel is None +def test_secure_connect_raise_error_if_no_certificate_is_available(mock_node_without_address_book): + """Test get channel raise error if no certificate available if transport security true.""" + node = mock_node_without_address_book + node._apply_transport_security(True) + + with pytest.raises(ValueError, match="No certificate available."): + node._get_channel() + + +@patch("grpc.secure_channel") +def test_node_get_channel_with_root_certificates(mock_secure, mock_node_with_address_book): + """Test secure channel uses provided root certificates.""" + node = mock_node_with_address_book + node._address = node._address._to_secure() + + # Skip certificate verification (consistent with other tests) + node._verify_certificates = False + + root_certs = b"custom_root_certificates" + node._set_root_certificates(root_certs) + + with patch.object(node, "_fetch_server_certificate_pem") as mock_fetch: + mock_channel = Mock() + mock_secure.return_value = mock_channel + + channel = node._get_channel() + + # Root certificates should be used directly + assert node._node_pem_cert == root_certs + + # Server certificate should not be fetched + mock_fetch.assert_not_called() + assert channel is not None + +@pytest.mark.parametrize( + "cert_hash, expected", + [ + (b"TestCertHashABC", "testcerthashabc"), + # Remove 0x prefix + (b"0xABCDEF1234", "abcdef1234"), + (b" AbCdEf ", "abcdef"), + (b"abcdef123456", "abcdef123456"), + (b"\xff\xfe\xfd\xfc", "fffefdfc") + ], +) +def test_normalize_cert_hash(cert_hash, expected): + """Test certificate hash normalization.""" + result = _Node._normalize_cert_hash(cert_hash) + assert result == expected + +def test_validate_tls_skipped_when_not_secure(mock_node_with_address_book): + """Test skip validate_certificate when insrcure connection is use""" + node = mock_node_with_address_book + # Force insecure transport + node._address = node._address._to_insecure() + node._verify_certificates = True + + # Should return early and NOT raise + node._validate_tls_certificate_with_trust_manager() + +@patch("socket.create_connection") +@patch("ssl.create_default_context") +def test_fetch_server_certificate_legacy_tls_path(mock_ssl_context, mock_socket): + """Test ssl_context with enforce TLS verison restiction.""" + node = _Node(AccountId(0, 0, 3), "127.0.0.1:50212", Mock()) + + mock_context = MagicMock() + # Simulate Python < 3.7 + delattr(mock_context, "minimum_version") + mock_context.options = 0 + + mock_ssl_context.return_value = mock_context + + mock_tls_socket = MagicMock() + mock_tls_socket.getpeercert.return_value = b"DER_CERT" + + mock_context.wrap_socket.return_value.__enter__.return_value = mock_tls_socket + mock_socket.return_value.__enter__.return_value = MagicMock() + + with patch("ssl.DER_cert_to_PEM_cert", return_value="PEM"): + node._fetch_server_certificate_pem() + + # Assert legacy flags applied + assert mock_context.options & ssl.OP_NO_TLSv1 + assert mock_context.options & ssl.OP_NO_TLSv1_1 \ No newline at end of file diff --git a/tests/unit/test_node_update_transaction.py b/tests/unit/node_update_transaction_test.py similarity index 100% rename from tests/unit/test_node_update_transaction.py rename to tests/unit/node_update_transaction_test.py diff --git a/tests/unit/test_prng_transaction.py b/tests/unit/prng_transaction_test.py similarity index 100% rename from tests/unit/test_prng_transaction.py rename to tests/unit/prng_transaction_test.py diff --git a/tests/unit/test_query_nodes.py b/tests/unit/query_nodes_test.py similarity index 100% rename from tests/unit/test_query_nodes.py rename to tests/unit/query_nodes_test.py diff --git a/tests/unit/test_query.py b/tests/unit/query_test.py similarity index 97% rename from tests/unit/test_query.py rename to tests/unit/query_test.py index 981155c0f..d6315487a 100644 --- a/tests/unit/test_query.py +++ b/tests/unit/query_test.py @@ -43,7 +43,9 @@ def test_before_execute_payment_not_required(query, mock_client): # payment_amount is None, should not set payment_amount query._before_execute(mock_client) - assert query.node_account_ids == mock_client.get_node_account_ids() + # since node_account_ids is not set it will be empty + # query internally use node form client + assert query.node_account_ids == [] assert query.operator == mock_client.operator assert query.payment_amount is None @@ -56,8 +58,10 @@ def test_before_execute_payment_required(query_requires_payment, mock_client): # payment_amount is None, should set payment_amount to 2 Hbars query_requires_payment._before_execute(mock_client) - - assert query_requires_payment.node_account_ids == mock_client.get_node_account_ids() + + # since node_account_ids is not set it will be empty + # query internally use node form client + assert query_requires_payment.node_account_ids == [] assert query_requires_payment.operator == mock_client.operator assert query_requires_payment.payment_amount.to_tinybars() == Hbar(2).to_tinybars() diff --git a/tests/unit/test_schedule_create_transaction.py b/tests/unit/schedule_create_transaction_test.py similarity index 100% rename from tests/unit/test_schedule_create_transaction.py rename to tests/unit/schedule_create_transaction_test.py diff --git a/tests/unit/test_schedule_delete_transaction.py b/tests/unit/schedule_delete_transaction_test.py similarity index 100% rename from tests/unit/test_schedule_delete_transaction.py rename to tests/unit/schedule_delete_transaction_test.py diff --git a/tests/unit/test_schedule_id.py b/tests/unit/schedule_id_test.py similarity index 100% rename from tests/unit/test_schedule_id.py rename to tests/unit/schedule_id_test.py diff --git a/tests/unit/test_schedule_info_query.py b/tests/unit/schedule_info_query_test.py similarity index 100% rename from tests/unit/test_schedule_info_query.py rename to tests/unit/schedule_info_query_test.py diff --git a/tests/unit/test_schedule_info.py b/tests/unit/schedule_info_test.py similarity index 100% rename from tests/unit/test_schedule_info.py rename to tests/unit/schedule_info_test.py diff --git a/tests/unit/test_schedule_sign_transaction.py b/tests/unit/schedule_sign_transaction_test.py similarity index 100% rename from tests/unit/test_schedule_sign_transaction.py rename to tests/unit/schedule_sign_transaction_test.py diff --git a/tests/unit/test_subscription_handle.py b/tests/unit/subscription_handle_test.py similarity index 100% rename from tests/unit/test_subscription_handle.py rename to tests/unit/subscription_handle_test.py diff --git a/tests/unit/test_supply_type.py b/tests/unit/supply_type_test.py similarity index 100% rename from tests/unit/test_supply_type.py rename to tests/unit/supply_type_test.py diff --git a/tests/unit/test_account_balance.py b/tests/unit/test_account_balance.py new file mode 100644 index 000000000..fb036172c --- /dev/null +++ b/tests/unit/test_account_balance.py @@ -0,0 +1,87 @@ +"""Tests for the AccountBalance class.""" + +import pytest + +from hiero_sdk_python.account.account_balance import AccountBalance +from hiero_sdk_python.hbar import Hbar +from hiero_sdk_python.tokens.token_id import TokenId + +pytestmark = pytest.mark.unit + + +def test_account_balance_str_with_hbars_only(): + """Test __str__ method with only hbars.""" + hbars = Hbar(10) + account_balance = AccountBalance(hbars=hbars) + + result = str(account_balance) + + assert "HBAR Balance:" in result + assert "10.00000000 ℏ" in result + assert "hbars" in result + # Should not include token balances section when empty + assert "Token Balances:" not in result + + +def test_account_balance_str_with_token_balances(): + """Test __str__ method with hbars and token balances.""" + hbars = Hbar(10) + token_id_1 = TokenId(0, 0, 100) + token_id_2 = TokenId(0, 0, 200) + token_balances = {token_id_1: 1000, token_id_2: 500} + account_balance = AccountBalance(hbars=hbars, token_balances=token_balances) + + result = str(account_balance) + + assert "HBAR Balance:" in result + assert "10.00000000 ℏ" in result + assert " hbars" in result + assert "Token Balances:" in result + assert " - Token ID 0.0.100: 1000 units" in result + assert " - Token ID 0.0.200: 500 units" in result + + +def test_account_balance_str_with_empty_token_balances(): + """Test __str__ method with empty token balances dict.""" + hbars = Hbar(5.5) + account_balance = AccountBalance(hbars=hbars, token_balances={}) + + result = str(account_balance) + + assert "HBAR Balance:" in result + assert "5.50000000 ℏ" in result + assert " hbars" in result + # Should not include token balances section when empty + assert "Token Balances:" not in result + + +def test_account_balance_repr_with_hbars_only(): + """Test __repr__ method with only hbars.""" + hbars = Hbar(10) + account_balance = AccountBalance(hbars=hbars) + + result = repr(account_balance) + + assert "AccountBalance" in result + assert "hbars=" in result + assert "token_balances={}" in result + assert "Hbar(" in result + + +def test_account_balance_repr_with_token_balances(): + """Test __repr__ method with hbars and token balances.""" + hbars = Hbar(10) + token_id_1 = TokenId(0, 0, 100) + token_id_2 = TokenId(0, 0, 200) + token_balances = {token_id_1: 1000, token_id_2: 500} + account_balance = AccountBalance(hbars=hbars, token_balances=token_balances) + + result = repr(account_balance) + + assert "AccountBalance" in result + assert "hbars=" in result + assert "token_balances=" in result + assert "0.0.100" in result or "TokenId" in result + assert "1000" in result + assert "500" in result + diff --git a/tests/unit/test_get_receipt_query.py b/tests/unit/test_get_receipt_query.py deleted file mode 100644 index c1912b0e8..000000000 --- a/tests/unit/test_get_receipt_query.py +++ /dev/null @@ -1,137 +0,0 @@ -"""Tests for the TransactionGetReceiptQuery functionality.""" - -import pytest -from unittest.mock import patch - -from hiero_sdk_python.account.account_id import AccountId -from hiero_sdk_python.exceptions import MaxAttemptsError -from hiero_sdk_python.hapi.services import ( - basic_types_pb2, - response_header_pb2, - response_pb2, - transaction_get_receipt_pb2, - transaction_receipt_pb2, -) -from hiero_sdk_python.query.transaction_get_receipt_query import TransactionGetReceiptQuery -from hiero_sdk_python.response_code import ResponseCode - -from tests.unit.mock_server import mock_hedera_servers - -pytestmark = pytest.mark.unit - -# This test uses fixture transaction_id as parameter -def test_transaction_get_receipt_query(transaction_id): - """Test basic functionality of TransactionGetReceiptQuery with a mocked client.""" - response = response_pb2.Response( - transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( - header=response_header_pb2.ResponseHeader( - nodeTransactionPrecheckCode=ResponseCode.OK - ), - receipt=transaction_receipt_pb2.TransactionReceipt( - status=ResponseCode.SUCCESS - ) - ) - ) - - response_sequences = [[response]] - - with mock_hedera_servers(response_sequences) as client: - query = ( - TransactionGetReceiptQuery() - .set_transaction_id(transaction_id) - ) - - try: - result = query.execute(client) - except Exception as e: - pytest.fail(f"Unexpected exception raised: {e}") - - assert result.status == ResponseCode.SUCCESS - -# This test uses fixture transaction_id as parameter -def test_receipt_query_retry_on_receipt_not_found(transaction_id): - """Test that receipt query retries when the receipt status is RECEIPT_NOT_FOUND.""" - # First response has RECEIPT_NOT_FOUND, second has SUCCESS - not_found_response = response_pb2.Response( - transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( - header=response_header_pb2.ResponseHeader( - nodeTransactionPrecheckCode=ResponseCode.OK - ), - receipt=transaction_receipt_pb2.TransactionReceipt( - status=ResponseCode.RECEIPT_NOT_FOUND - ) - ) - ) - - success_response = response_pb2.Response( - transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( - header=response_header_pb2.ResponseHeader( - nodeTransactionPrecheckCode=ResponseCode.OK - ), - receipt=transaction_receipt_pb2.TransactionReceipt( - status=ResponseCode.SUCCESS, - accountID=basic_types_pb2.AccountID( - shardNum=0, - realmNum=0, - accountNum=1234 - ) - ) - ) - ) - - response_sequences = [[not_found_response, success_response]] - - with mock_hedera_servers(response_sequences) as client, patch('time.sleep') as mock_sleep: - query = ( - TransactionGetReceiptQuery() - .set_transaction_id(transaction_id) - ) - - try: - result = query.execute(client) - except Exception as e: - pytest.fail(f"Should not raise exception, but raised: {e}") - - # Verify query was successful after retry - assert result.status == ResponseCode.SUCCESS - - # Verify we slept once for the retry - assert mock_sleep.call_count == 1, "Should have retried once" - - # Verify we didn't switch nodes (RECEIPT_NOT_FOUND is retriable without node switch) - assert client.network.current_node._account_id == AccountId(0, 0, 3) - -# This test uses fixture transaction_id as parameter -def test_receipt_query_receipt_status_error(transaction_id): - """Test that receipt query fails on receipt status error.""" - # Create a response with a receipt status error - error_response = response_pb2.Response( - transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( - header=response_header_pb2.ResponseHeader( - nodeTransactionPrecheckCode=ResponseCode.UNKNOWN - ), - receipt=transaction_receipt_pb2.TransactionReceipt( - status=ResponseCode.UNKNOWN - ) - ) - ) - - response_sequences = [[error_response]] - - with mock_hedera_servers(response_sequences) as client, patch('time.sleep'): - client.max_attempts = 1 - query = ( - TransactionGetReceiptQuery() - .set_transaction_id(transaction_id) - ) - - # Create the query and verify it fails with the expected error - with pytest.raises(MaxAttemptsError) as exc_info: - query.execute(client) - - assert str(f"Receipt for transaction {transaction_id} contained error status: UNKNOWN ({ResponseCode.UNKNOWN})") in str(exc_info.value) - -def test_receipt_query_does_not_require_payment(): - """Test that the receipt query does not require payment.""" - query = TransactionGetReceiptQuery() - assert not query._is_payment_required() \ No newline at end of file diff --git a/tests/unit/test_key_format.py b/tests/unit/test_key_format.py new file mode 100644 index 000000000..b11d5c98b --- /dev/null +++ b/tests/unit/test_key_format.py @@ -0,0 +1,73 @@ +"""Tests for the key_format module.""" + +import pytest + +from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.hapi.services import basic_types_pb2 +from hiero_sdk_python.utils.key_utils import key_to_proto +from hiero_sdk_python.utils.key_format import format_key + +pytestmark = pytest.mark.unit + +def test_format_key_ed25519(): + """Test formatting an Ed25519 key.""" + private_key = PrivateKey.generate_ed25519() + public_key = private_key.public_key() + + proto_key = key_to_proto(public_key) + formatted = format_key(proto_key) + + expected = f"ed25519({public_key.to_bytes_raw().hex()})" + + assert formatted == expected + +def test_format_key_none(): + """Test formatting a None key.""" + formatted = format_key(None) + + assert formatted == "None" + +def test_format_key_threshold_key(): + """Test formatting a ThresholdKey.""" + + key = basic_types_pb2.Key() + key.thresholdKey.threshold = 2 + + formatted = format_key(key) + + assert formatted == "thresholdKey(...)" + +def test_format_key_contract_id(): + """Test formatting a ContractID key.""" + + key = basic_types_pb2.Key() + key.contractID.shardNum = 0 + key.contractID.realmNum = 0 + key.contractID.contractNum = 5678 + + expected_inner = str(key.contractID) + expected = f"contractID({expected_inner})" + + formatted = format_key(key) + + assert formatted == expected + +def test_format_key_keylist(): + """Test formatting a KeyList.""" + + key = basic_types_pb2.Key() + key.keyList.keys.add() + + formatted = format_key(key) + + assert formatted == "keyList(...)" + +def test_format_key_unknown(): + """Test formatting an unknown key type.""" + key = basic_types_pb2.Key() + # Intentionally not setting any known key type + + formatted = format_key(key) + expected = str(key).replace("\n", " ") + + assert formatted == expected \ No newline at end of file diff --git a/tests/unit/test_transaction_receipt.py b/tests/unit/test_transaction_receipt.py new file mode 100644 index 000000000..9549ceca9 --- /dev/null +++ b/tests/unit/test_transaction_receipt.py @@ -0,0 +1,30 @@ +import pytest + +from hiero_sdk_python.hapi.services import transaction_receipt_pb2 +from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt + + +pytestmark = pytest.mark.unit + + +def test_transaction_receipt_children_default_empty(): + proto = transaction_receipt_pb2.TransactionReceipt() + receipt = TransactionReceipt(receipt_proto=proto, transaction_id=None) + + assert receipt.children == [] + + +def test_transaction_receipt_set_children_updates_property(): + parent_proto = transaction_receipt_pb2.TransactionReceipt() + child_proto_1 = transaction_receipt_pb2.TransactionReceipt() + child_proto_2 = transaction_receipt_pb2.TransactionReceipt() + + parent = TransactionReceipt(receipt_proto=parent_proto, transaction_id=None) + child1 = TransactionReceipt(receipt_proto=child_proto_1, transaction_id=None) + child2 = TransactionReceipt(receipt_proto=child_proto_2, transaction_id=None) + + parent._set_children([child1, child2]) + + assert len(parent.children) == 2 + assert parent.children[0] is child1 + assert parent.children[1] is child2 diff --git a/tests/unit/test_token_airdrop_claim.py b/tests/unit/token_airdrop_claim_test.py similarity index 100% rename from tests/unit/test_token_airdrop_claim.py rename to tests/unit/token_airdrop_claim_test.py diff --git a/tests/unit/test_token_airdrop_pending_id.py b/tests/unit/token_airdrop_pending_id_test.py similarity index 100% rename from tests/unit/test_token_airdrop_pending_id.py rename to tests/unit/token_airdrop_pending_id_test.py diff --git a/tests/unit/test_token_airdrop_pending_record.py b/tests/unit/token_airdrop_pending_record_test.py similarity index 100% rename from tests/unit/test_token_airdrop_pending_record.py rename to tests/unit/token_airdrop_pending_record_test.py diff --git a/tests/unit/test_token_airdrop_transaction_cancel.py b/tests/unit/token_airdrop_transaction_cancel_test.py similarity index 100% rename from tests/unit/test_token_airdrop_transaction_cancel.py rename to tests/unit/token_airdrop_transaction_cancel_test.py diff --git a/tests/unit/test_token_airdrop_transaction.py b/tests/unit/token_airdrop_transaction_test.py similarity index 100% rename from tests/unit/test_token_airdrop_transaction.py rename to tests/unit/token_airdrop_transaction_test.py diff --git a/tests/unit/test_token_allowance.py b/tests/unit/token_allowance_test.py similarity index 100% rename from tests/unit/test_token_allowance.py rename to tests/unit/token_allowance_test.py diff --git a/tests/unit/test_token_associate_transaction.py b/tests/unit/token_associate_transaction_test.py similarity index 100% rename from tests/unit/test_token_associate_transaction.py rename to tests/unit/token_associate_transaction_test.py diff --git a/tests/unit/test_token_burn_transaction.py b/tests/unit/token_burn_transaction_test.py similarity index 100% rename from tests/unit/test_token_burn_transaction.py rename to tests/unit/token_burn_transaction_test.py diff --git a/tests/unit/test_token_create_transaction.py b/tests/unit/token_create_transaction_test.py similarity index 99% rename from tests/unit/test_token_create_transaction.py rename to tests/unit/token_create_transaction_test.py index b8e679b71..45935b38c 100644 --- a/tests/unit/test_token_create_transaction.py +++ b/tests/unit/token_create_transaction_test.py @@ -1188,7 +1188,7 @@ def test_token_info_query_structure(): print("βœ… TokenInfoQuery structure test passed") # --- Tests for _to_proto_key (backward compatibility wrapper) --- -# Note: Core functionality tests for key_to_proto are in test_key_utils.py +# Note: Core functionality tests for key_to_proto are in key_utils_test.py def test_to_proto_key_wrapper_still_works(): """Tests that _to_proto_key wrapper method still works for backward compatibility.""" diff --git a/tests/unit/test_token_delete_transaction.py b/tests/unit/token_delete_transaction_test.py similarity index 100% rename from tests/unit/test_token_delete_transaction.py rename to tests/unit/token_delete_transaction_test.py diff --git a/tests/unit/test_token_dissociate_transaction.py b/tests/unit/token_dissociate_transaction_test.py similarity index 100% rename from tests/unit/test_token_dissociate_transaction.py rename to tests/unit/token_dissociate_transaction_test.py diff --git a/tests/unit/test_token_fee_schedule_update_transaction.py b/tests/unit/token_fee_schedule_update_transaction_test.py similarity index 100% rename from tests/unit/test_token_fee_schedule_update_transaction.py rename to tests/unit/token_fee_schedule_update_transaction_test.py diff --git a/tests/unit/test_token_freeze_transaction.py b/tests/unit/token_freeze_transaction_test.py similarity index 100% rename from tests/unit/test_token_freeze_transaction.py rename to tests/unit/token_freeze_transaction_test.py diff --git a/tests/unit/test_token_grant_kyc_transaction.py b/tests/unit/token_grant_kyc_transaction_test.py similarity index 100% rename from tests/unit/test_token_grant_kyc_transaction.py rename to tests/unit/token_grant_kyc_transaction_test.py diff --git a/tests/unit/test_token_id.py b/tests/unit/token_id_test.py similarity index 100% rename from tests/unit/test_token_id.py rename to tests/unit/token_id_test.py diff --git a/tests/unit/test_token_info_query.py b/tests/unit/token_info_query_test.py similarity index 100% rename from tests/unit/test_token_info_query.py rename to tests/unit/token_info_query_test.py diff --git a/tests/unit/test_token_info.py b/tests/unit/token_info_test.py similarity index 100% rename from tests/unit/test_token_info.py rename to tests/unit/token_info_test.py diff --git a/tests/unit/test_token_mint_transaction.py b/tests/unit/token_mint_transaction_test.py similarity index 100% rename from tests/unit/test_token_mint_transaction.py rename to tests/unit/token_mint_transaction_test.py diff --git a/tests/unit/test_token_nft_allowance.py b/tests/unit/token_nft_allowance_test.py similarity index 100% rename from tests/unit/test_token_nft_allowance.py rename to tests/unit/token_nft_allowance_test.py diff --git a/tests/unit/test_token_nft_info_query.py b/tests/unit/token_nft_info_query_test.py similarity index 100% rename from tests/unit/test_token_nft_info_query.py rename to tests/unit/token_nft_info_query_test.py diff --git a/tests/unit/test_token_nft_info.py b/tests/unit/token_nft_info_test.py similarity index 100% rename from tests/unit/test_token_nft_info.py rename to tests/unit/token_nft_info_test.py diff --git a/tests/unit/test_token_nft_transfer.py b/tests/unit/token_nft_transfer_test.py similarity index 100% rename from tests/unit/test_token_nft_transfer.py rename to tests/unit/token_nft_transfer_test.py diff --git a/tests/unit/test_token_pause_transaction.py b/tests/unit/token_pause_transaction_test.py similarity index 100% rename from tests/unit/test_token_pause_transaction.py rename to tests/unit/token_pause_transaction_test.py diff --git a/tests/unit/test_token_reject_transaction.py b/tests/unit/token_reject_transaction_test.py similarity index 100% rename from tests/unit/test_token_reject_transaction.py rename to tests/unit/token_reject_transaction_test.py diff --git a/tests/unit/test_token_relationship.py b/tests/unit/token_relationship_test.py similarity index 100% rename from tests/unit/test_token_relationship.py rename to tests/unit/token_relationship_test.py diff --git a/tests/unit/test_token_revoke_kyc_transaction.py b/tests/unit/token_revoke_kyc_transaction_test.py similarity index 100% rename from tests/unit/test_token_revoke_kyc_transaction.py rename to tests/unit/token_revoke_kyc_transaction_test.py diff --git a/tests/unit/test_token_transfer_list.py b/tests/unit/token_transfer_list_test.py similarity index 100% rename from tests/unit/test_token_transfer_list.py rename to tests/unit/token_transfer_list_test.py diff --git a/tests/unit/test_token_transfer.py b/tests/unit/token_transfer_test.py similarity index 100% rename from tests/unit/test_token_transfer.py rename to tests/unit/token_transfer_test.py diff --git a/tests/unit/test_token_type.py b/tests/unit/token_type_test.py similarity index 100% rename from tests/unit/test_token_type.py rename to tests/unit/token_type_test.py diff --git a/tests/unit/test_token_unfreeze_transaction.py b/tests/unit/token_unfreeze_transaction_test.py similarity index 100% rename from tests/unit/test_token_unfreeze_transaction.py rename to tests/unit/token_unfreeze_transaction_test.py diff --git a/tests/unit/test_token_unpause_transaction.py b/tests/unit/token_unpause_transaction_test.py similarity index 100% rename from tests/unit/test_token_unpause_transaction.py rename to tests/unit/token_unpause_transaction_test.py diff --git a/tests/unit/test_token_update_nfts_transaction.py b/tests/unit/token_update_nfts_transaction_test.py similarity index 100% rename from tests/unit/test_token_update_nfts_transaction.py rename to tests/unit/token_update_nfts_transaction_test.py diff --git a/tests/unit/test_token_update_transaction.py b/tests/unit/token_update_transaction_test.py similarity index 100% rename from tests/unit/test_token_update_transaction.py rename to tests/unit/token_update_transaction_test.py diff --git a/tests/unit/test_token_wipe_transaction.py b/tests/unit/token_wipe_transaction_test.py similarity index 100% rename from tests/unit/test_token_wipe_transaction.py rename to tests/unit/token_wipe_transaction_test.py diff --git a/tests/unit/test_topic_create_transaction.py b/tests/unit/topic_create_transaction_test.py similarity index 100% rename from tests/unit/test_topic_create_transaction.py rename to tests/unit/topic_create_transaction_test.py diff --git a/tests/unit/test_topic_delete_transaction.py b/tests/unit/topic_delete_transaction_test.py similarity index 100% rename from tests/unit/test_topic_delete_transaction.py rename to tests/unit/topic_delete_transaction_test.py diff --git a/tests/unit/test_topic_id.py b/tests/unit/topic_id_test.py similarity index 100% rename from tests/unit/test_topic_id.py rename to tests/unit/topic_id_test.py diff --git a/tests/unit/test_topic_info_query.py b/tests/unit/topic_info_query_test.py similarity index 59% rename from tests/unit/test_topic_info_query.py rename to tests/unit/topic_info_query_test.py index 1f39bea60..6bae027b2 100644 --- a/tests/unit/test_topic_info_query.py +++ b/tests/unit/topic_info_query_test.py @@ -3,12 +3,14 @@ import pytest from hiero_sdk_python.consensus.topic_info import TopicInfo +from hiero_sdk_python.executable import _ExecutionState from hiero_sdk_python.hapi.services import ( basic_types_pb2, consensus_get_topic_info_pb2, consensus_topic_info_pb2, response_header_pb2, - response_pb2 + response_pb2, + query_pb2, ) from hiero_sdk_python.hapi.services.query_header_pb2 import ResponseType from hiero_sdk_python.query.topic_info_query import TopicInfoQuery @@ -59,6 +61,15 @@ def create_topic_info_response(status_code=ResponseCode.OK, with_info=True): return responses +def _response_with_status(status: ResponseCode) -> response_pb2.Response: + """Create a Response with only the consensusGetTopicInfo header status set.""" + return response_pb2.Response( + consensusGetTopicInfo=consensus_get_topic_info_pb2.ConsensusGetTopicInfoResponse( + header=response_header_pb2.ResponseHeader(nodeTransactionPrecheckCode=status) + ) + ) + + def test_topic_info_query(topic_id): """Test basic functionality of TopicInfoQuery with mock server.""" responses = create_topic_info_response() @@ -96,3 +107,61 @@ def test_topic_info_query_with_empty_topic_id(): query.execute(client) assert "Topic ID must be set" in str(exc_info.value) + + +def test_make_request_builds_expected_protobuf(topic_id): + """ + Covers TopicInfoQuery._make_request() using REAL protobuf classes. + This is fast and deterministic (no network). + """ + query = TopicInfoQuery().set_topic_id(topic_id) + + req = query._make_request() + assert isinstance(req, query_pb2.Query) + + # Ensure the correct oneof field is populated + assert req.HasField("consensusGetTopicInfo") + assert req.consensusGetTopicInfo.HasField("topicID") + + # Compare serialized protobuf to avoid equality quirks + expected_topic_id_proto = topic_id._to_proto() + assert ( + req.consensusGetTopicInfo.topicID.SerializeToString() + == expected_topic_id_proto.SerializeToString() + ) + + +@pytest.mark.parametrize( + "status,expected_state", + [ + (ResponseCode.OK, _ExecutionState.FINISHED), + (ResponseCode.UNKNOWN, _ExecutionState.RETRY), + (ResponseCode.BUSY, _ExecutionState.RETRY), + (ResponseCode.PLATFORM_NOT_ACTIVE, _ExecutionState.RETRY), + (ResponseCode.INVALID_TOPIC_ID, _ExecutionState.ERROR), + ], +) +def test_should_retry_branches(status, expected_state): + """Covers OK / RETRY / ERROR branches of TopicInfoQuery._should_retry().""" + query = TopicInfoQuery() + response = _response_with_status(status) + + assert query._should_retry(response) == expected_state + + +def test_freeze_prevents_set_topic_id_if_available(topic_id): + """ + If TopicInfoQuery implements freeze semantics, ensure it prevents mutation. + This is written to not break the suite if freeze() doesn't exist in your version. + """ + query = TopicInfoQuery() + + if not hasattr(query, "freeze"): + pytest.skip("TopicInfoQuery.freeze() not available in this SDK version") + + query.freeze() + + with pytest.raises(ValueError) as exc_info: + query.set_topic_id(topic_id) + + assert "frozen" in str(exc_info.value).lower() diff --git a/tests/unit/test_topic_info.py b/tests/unit/topic_info_test.py similarity index 100% rename from tests/unit/test_topic_info.py rename to tests/unit/topic_info_test.py diff --git a/tests/unit/test_topic_message_query.py b/tests/unit/topic_message_query_test.py similarity index 100% rename from tests/unit/test_topic_message_query.py rename to tests/unit/topic_message_query_test.py diff --git a/tests/unit/test_topic_message_submit_transaction.py b/tests/unit/topic_message_submit_transaction_test.py similarity index 100% rename from tests/unit/test_topic_message_submit_transaction.py rename to tests/unit/topic_message_submit_transaction_test.py diff --git a/tests/unit/test_topic_update_transaction.py b/tests/unit/topic_update_transaction_test.py similarity index 100% rename from tests/unit/test_topic_update_transaction.py rename to tests/unit/topic_update_transaction_test.py diff --git a/tests/unit/test_transaction_freeze_and_bytes.py b/tests/unit/transaction_freeze_and_bytes_test.py similarity index 100% rename from tests/unit/test_transaction_freeze_and_bytes.py rename to tests/unit/transaction_freeze_and_bytes_test.py diff --git a/tests/unit/test_transaction_nodes.py b/tests/unit/transaction_nodes_test.py similarity index 100% rename from tests/unit/test_transaction_nodes.py rename to tests/unit/transaction_nodes_test.py diff --git a/tests/unit/test_transaction_record_query.py b/tests/unit/transaction_record_query_test.py similarity index 100% rename from tests/unit/test_transaction_record_query.py rename to tests/unit/transaction_record_query_test.py diff --git a/tests/unit/test_transaction_record.py b/tests/unit/transaction_record_test.py similarity index 100% rename from tests/unit/test_transaction_record.py rename to tests/unit/transaction_record_test.py diff --git a/tests/unit/test_transfer_transaction.py b/tests/unit/transfer_transaction_test.py similarity index 100% rename from tests/unit/test_transfer_transaction.py rename to tests/unit/transfer_transaction_test.py